Netbox v2.11.12 (#60)

* Release v2.11.12
This commit is contained in:
Jinal shah 2022-02-08 21:57:31 +05:30 committed by GitHub
parent 684affed71
commit 9279278501
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
462 changed files with 18973 additions and 9368 deletions

1
.github/FUNDING.yml vendored
View File

@ -1 +0,0 @@
github: [jeremystretch]

View File

@ -1,25 +1,29 @@
--- ---
name: 🐛 Bug Report name: 🐛 Bug Report
about: Report a reproducible bug in the current release of NetBox description: Report a reproducible bug in the current release of NetBox
labels: ["type: bug"] labels: ["type: bug"]
body: body:
- type: markdown - type: markdown
attributes: attributes:
value: "**NOTE:** This form is only for reporting _reproducible bugs_ in a value: >
current NetBox installation. If you're having trouble with installation or just **NOTE:** This form is only for reporting _reproducible bugs_ in a current NetBox
looking for assistance with using NetBox, please visit our installation. If you're having trouble with installation or just looking for
[discussion forum](https://github.com/netbox-community/netbox/discussions) instead." assistance with using NetBox, please visit our
[discussion forum](https://github.com/netbox-community/netbox/discussions) instead.
- type: input - type: input
attributes: attributes:
label: NetBox version label: NetBox version
description: "What version of NetBox are you currently running?" description: >
placeholder: v2.10.4 What version of NetBox are you currently running? (If you don't have access to the most
recent NetBox release, consider testing on our [demo instance](https://demo.netbox.dev/)
before opening a bug report to see if your issue has already been addressed.)
placeholder: v2.11.12
validations: validations:
required: true required: true
- type: dropdown - type: dropdown
attributes: attributes:
label: Python version label: Python version
description: "What version of Python are you currently running?" description: What version of Python are you currently running?
options: options:
- 3.6 - 3.6
- 3.7 - 3.7
@ -30,12 +34,14 @@ body:
- type: textarea - type: textarea
attributes: attributes:
label: Steps to Reproduce label: Steps to Reproduce
description: "Describe in detail the exact steps that someone else can take to description: >
Describe in detail the exact steps that someone else can take to
reproduce this bug using the current stable release of NetBox. Begin with the reproduce this bug using the current stable release of NetBox. Begin with the
creation of any necessary database objects and call out every operation being creation of any necessary database objects and call out every operation being
performed explicitly. If reporting a bug in the REST API, be sure to reconstruct performed explicitly. If reporting a bug in the REST API, be sure to reconstruct
the raw HTTP request(s) being made: Don't rely on a client library such as the raw HTTP request(s) being made: Don't rely on a client library such as
pynetbox." pynetbox. Additionally, **do not rely on the demo instance** for reproducing
suspected bugs, as its data is prone to modification or deletion at any time.
placeholder: | placeholder: |
1. Click on "create widget" 1. Click on "create widget"
2. Set foo to 12 and bar to G 2. Set foo to 12 and bar to G
@ -45,19 +51,14 @@ body:
- type: textarea - type: textarea
attributes: attributes:
label: Expected Behavior label: Expected Behavior
description: "What did you expect to happen?" description: What did you expect to happen?
placeholder: "A new widget should have been created with the specified attributes" placeholder: A new widget should have been created with the specified attributes
validations: validations:
required: true required: true
- type: textarea - type: textarea
attributes: attributes:
label: Observed Behavior label: Observed Behavior
description: "What happened instead?" description: What happened instead?
placeholder: "A TypeError exception was raised" placeholder: A TypeError exception was raised
validations: validations:
required: true required: true
- type: markdown
attributes:
value: |
### Additional information
You can use the space below to provide any additional information or to attach files.

View File

@ -3,7 +3,10 @@ blank_issues_enabled: false
contact_links: contact_links:
- name: 📖 Contributing Policy - name: 📖 Contributing Policy
url: https://github.com/netbox-community/netbox/blob/develop/CONTRIBUTING.md url: https://github.com/netbox-community/netbox/blob/develop/CONTRIBUTING.md
about: Please read through our contributing policy before opening an issue or pull request about: "Please read through our contributing policy before opening an issue or pull request"
- name: 💬 Discussion Group - name: ❓ Discussion
url: https://groups.google.com/g/netbox-discuss url: https://github.com/netbox-community/netbox/discussions
about: Join our discussion group for assistance with installation issues and other problems about: "If you're just looking for help, try starting a discussion instead"
- name: 💬 Community Slack
url: https://netdev.chat/
about: "Join #netbox on the NetDev Community Slack for assistance with installation issues and other problems"

View File

@ -1,6 +1,6 @@
--- ---
name: 📖 Documentation Change name: 📖 Documentation Change
about: Suggest an addition or modification to the NetBox documentation description: Suggest an addition or modification to the NetBox documentation
labels: ["type: documentation"] labels: ["type: documentation"]
body: body:
- type: dropdown - type: dropdown
@ -14,25 +14,22 @@ body:
- Cleanup (formatting, typos, etc.) - Cleanup (formatting, typos, etc.)
validations: validations:
required: true required: true
- type: checkboxes - type: dropdown
attributes: attributes:
label: Area label: Area
description: To what section(s) of the documentation does this change pertain? description: To what section of the documentation does this change primarily pertain?
options: options:
- label: Installation instructions - Installation instructions
- label: Configuration parameters - Configuration parameters
- label: Functionality/features - Functionality/features
- label: REST API - REST API
- label: Administration/development - Administration/development
- label: Other - Other
validations:
required: true
- type: textarea - type: textarea
attributes: attributes:
label: Proposed Changes label: Proposed Changes
description: "Describe the proposed changes and why they are necessary" description: Describe the proposed changes and why they are necessary.
validations: validations:
required: true required: true
- type: markdown
attributes:
value: |
### Additional information
You can use the space below to provide any additional information or to attach files.

View File

@ -1,19 +1,20 @@
--- ---
name: ✨ Feature Request name: ✨ Feature Request
about: Propose a new NetBox feature or enhancement description: Propose a new NetBox feature or enhancement
labels: ["type: feature"] labels: ["type: feature"]
body: body:
- type: markdown - type: markdown
attributes: attributes:
value: "**NOTE:** This form is only for submitting well-formed proposals to extend or value: >
modify NetBox in some way. If you're trying to solve a problem but can't figure out how, **NOTE:** This form is only for submitting well-formed proposals to extend or modify
or if you still need time to work on the details of a proposed new feature, please start NetBox in some way. If you're trying to solve a problem but can't figure out how, or if
a [discussion](https://github.com/netbox-community/netbox/discussions) instead." you still need time to work on the details of a proposed new feature, please start a
[discussion](https://github.com/netbox-community/netbox/discussions) instead.
- type: input - type: input
attributes: attributes:
label: NetBox version label: NetBox version
description: "What version of NetBox are you currently running?" description: What version of NetBox are you currently running?
placeholder: v2.10.4 placeholder: v2.11.12
validations: validations:
required: true required: true
- type: dropdown - type: dropdown
@ -28,31 +29,29 @@ body:
- type: textarea - type: textarea
attributes: attributes:
label: Proposed functionality label: Proposed functionality
description: "Describe in detail the new feature or behavior you'd like to propose. description: >
Include any specific changes to work flows, data models, or the user interface." Describe in detail the new feature or behavior you'd like to propose. Include any specific
changes to work flows, data models, or the user interface.
validations: validations:
required: true required: true
- type: textarea - type: textarea
attributes: attributes:
label: Use case label: Use case
description: "Explain how adding this functionality would benefit NetBox users. What description: >
need does it address?" Explain how adding this functionality would benefit NetBox users. What need does it address?
validations: validations:
required: true required: true
- type: textarea - type: textarea
attributes: attributes:
label: Database changes label: Database changes
description: "Note any changes to the database schema necessary to support the new description: >
feature. For example, does the proposal require adding a new model or field? (Not Note any changes to the database schema necessary to support the new feature. For example,
all new features require database changes.)" does the proposal require adding a new model or field? (Not all new features require database
changes.)
- type: textarea - type: textarea
attributes: attributes:
label: External dependencies label: External dependencies
description: "List any new dependencies on external libraries or services that this description: >
new feature would introduce. For example, does the proposal require the installation List any new dependencies on external libraries or services that this new feature would
of a new Python package? (Not all new features introduce new dependencies.)" introduce. For example, does the proposal require the installation of a new Python package?
- type: markdown (Not all new features introduce new dependencies.)
attributes:
value: |
### Additional information
You can use the space below to provide any additional information or to attach files.

View File

@ -1,27 +1,24 @@
--- ---
name: 🏡 Housekeeping name: 🏡 Housekeeping
about: A change pertaining to the codebase itself (developers only) description: A change pertaining to the codebase itself (developers only)
labels: ["type: housekeeping"] labels: ["type: housekeeping"]
body: body:
- type: markdown - type: markdown
attributes: attributes:
value: "**NOTE:** This template is for use by maintainers only. Please do not submit value: >
an issue using this template unless you have been specifically asked to do so." **NOTE:** This template is for use by maintainers only. Please do not submit
an issue using this template unless you have been specifically asked to do so.
- type: textarea - type: textarea
attributes: attributes:
label: Proposed Changes label: Proposed Changes
description: "Describe in detail the new feature or behavior you'd like to propose. description: >
Include any specific changes to work flows, data models, or the user interface." Describe in detail the new feature or behavior you'd like to propose.
Include any specific changes to work flows, data models, or the user interface.
validations: validations:
required: true required: true
- type: textarea - type: textarea
attributes: attributes:
label: Justification label: Justification
description: "Please provide justification for the proposed change(s)." description: Please provide justification for the proposed change(s).
validations: validations:
required: true required: true
- type: markdown
attributes:
value: |
### Additional information
You can use the space below to provide any additional information or to attach files.

30
.github/stale.yml vendored
View File

@ -1,30 +0,0 @@
# Configuration for Stale (https://github.com/apps/stale)
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 45
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 15
# Issues with these labels will never be considered stale
exemptLabels:
- "status: accepted"
- "status: blocked"
- "status: needs milestone"
# Label to use when marking an issue as stale
staleLabel: "pending closure"
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. NetBox
is governed by a small group of core maintainers which means not all opened
issues may receive direct feedback. Please see our [contributing guide](https://github.com/netbox-community/netbox/blob/develop/CONTRIBUTING.md).
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: >
This issue has been automatically closed due to lack of activity. In an
effort to reduce noise, please do not comment any further. Note that the
core maintainers may elect to reopen this issue at a later date if deemed
necessary.

35
.github/workflows/stale.yml vendored Normal file
View File

@ -0,0 +1,35 @@
# close-stale-issues (https://github.com/marketplace/actions/close-stale-issues)
name: 'Close stale issues/PRs'
on:
schedule:
- cron: '0 4 * * *'
jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v4
with:
close-issue-message: >
This issue has been automatically closed due to lack of activity. In an
effort to reduce noise, please do not comment any further. Note that the
core maintainers may elect to reopen this issue at a later date if deemed
necessary.
close-pr-message: >
This PR has been automatically closed due to lack of activity.
days-before-stale: 60
days-before-close: 30
exempt-issue-labels: 'status: accepted,status: blocked,status: needs milestone'
operations-per-run: 100
remove-stale-when-updated: false
stale-issue-label: 'pending closure'
stale-issue-message: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. NetBox
is governed by a small group of core maintainers which means not all opened
issues may receive direct feedback. Please see our [contributing guide](https://github.com/netbox-community/netbox/blob/develop/CONTRIBUTING.md).
stale-pr-label: 'pending closure'
stale-pr-message: >
This PR has been automatically marked as stale because it has not had
recent activity. It will be closed automatically if no further action is
taken.

2
.gitignore vendored
View File

@ -2,6 +2,8 @@
*.swp *.swp
/netbox/netbox/configuration.py /netbox/netbox/configuration.py
/netbox/netbox/ldap_config.py /netbox/netbox/ldap_config.py
/netbox/project-static/.cache
/netbox/project-static/node_modules
/netbox/reports/* /netbox/reports/*
!/netbox/reports/__init__.py !/netbox/reports/__init__.py
/netbox/scripts/* /netbox/scripts/*

View File

@ -13,6 +13,7 @@ pythonPipeline([
'pythonVersion': '3.7', 'pythonVersion': '3.7',
'skipDocs': true, 'skipDocs': true,
'skipLint': true, 'skipLint': true,
'skipUnitTest': true,
'skipIntegrationTest': true, 'skipIntegrationTest': true,
'skipPrivateRepo': true, 'skipPrivateRepo': true,
'podTemplate': """ 'podTemplate': """

View File

@ -25,7 +25,7 @@ discussions.
### Slack ### Slack
For real-time chat, you can join the **#netbox** Slack channel on [NetDev Community](https://join.slack.com/t/netdev-community/shared_invite/zt-mtts8g0n-Sm6Wutn62q_M4OdsaIycrQ). For real-time chat, you can join the **#netbox** Slack channel on [NetDev Community](https://netdev.chat/).
Unfortunately, the Slack channel does not provide long-term retention of chat Unfortunately, the Slack channel does not provide long-term retention of chat
history, so try to avoid it for any discussions would benefit from being history, so try to avoid it for any discussions would benefit from being
preserved for future reference. preserved for future reference.
@ -160,17 +160,20 @@ accumulating a large backlog of work.
The core maintainers group has chosen to make use of GitHub's [Stale bot](https://github.com/apps/stale) The core maintainers group has chosen to make use of GitHub's [Stale bot](https://github.com/apps/stale)
to aid in issue management. to aid in issue management.
* Issues will be marked as stale after 45 days of no activity. * Issues will be marked as stale after 60 days of no activity.
* Then after 15 more days of inactivity, the issue will be closed. * If the stable label is not removed in the following 30 days, the issue will
be closed automatically.
* Any issue bearing one of the following labels will be exempt from all Stale * Any issue bearing one of the following labels will be exempt from all Stale
bot actions: bot actions:
* `status: accepted` * `status: accepted`
* `status: blocked` * `status: blocked`
* `status: needs milestone` * `status: needs milestone`
It is natural that some new issues get more attention than others. Stale bot It is natural that some new issues get more attention than others. The stale
helps bring renewed attention to potentially valuable issues that may have been bot helps bring renewed attention to potentially valuable issues that may have
overlooked. been overlooked. **Do not** comment on an issue that has been marked stale in
an effort to circumvent the bot: Doing so will not remove the stale label.
(Stale labels can be removed only by maintainers.)
## Maintainer Guidance ## Maintainer Guidance

View File

@ -1,7 +1,11 @@
![NetBox](docs/netbox_logo.svg "NetBox logo") <div align="center">
<img src="https://raw.githubusercontent.com/netbox-community/netbox/develop/docs/netbox_logo.svg" width="400" alt="NetBox logo" />
</div>
NetBox is an IP address management (IPAM) and data center infrastructure ![Master branch build status](https://github.com/netbox-community/netbox/workflows/CI/badge.svg?branch=master)
management (DCIM) tool. Initially conceived by the network engineering team at
NetBox is an infrastructure resource modeling (IRM) tool designed to empower
network automation. Initially conceived by the network engineering team at
[DigitalOcean](https://www.digitalocean.com/), NetBox was developed specifically [DigitalOcean](https://www.digitalocean.com/), NetBox was developed specifically
to address the needs of network and infrastructure engineers. It is intended to to address the needs of network and infrastructure engineers. It is intended to
function as a domain-specific source of truth for network operations. function as a domain-specific source of truth for network operations.
@ -10,41 +14,35 @@ NetBox runs as a web application atop the [Django](https://www.djangoproject.com
Python framework with a [PostgreSQL](https://www.postgresql.org/) database. For a Python framework with a [PostgreSQL](https://www.postgresql.org/) database. For a
complete list of requirements, see `requirements.txt`. The code is available [on GitHub](https://github.com/netbox-community/netbox). complete list of requirements, see `requirements.txt`. The code is available [on GitHub](https://github.com/netbox-community/netbox).
The complete documentation for NetBox can be found at [Read the Docs](https://netbox.readthedocs.io/en/stable/). The complete documentation for NetBox can be found at [Read the Docs](https://netbox.readthedocs.io/en/stable/). A public demo instance is available at https://demo.netbox.dev.
<div align="center">
<h4>Thank you to our sponsors!</h4>
[![DigitalOcean](https://raw.githubusercontent.com/wiki/netbox-community/netbox/images/sponsors/digitalocean.png)](https://try.digitalocean.com/developer-cloud)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
[![Equinix Metal](https://raw.githubusercontent.com/wiki/netbox-community/netbox/images/sponsors/equinix.png)](https://metal.equinix.com/)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
[![NS1](https://raw.githubusercontent.com/wiki/netbox-community/netbox/images/sponsors/ns1.png)](https://ns1.com/)
<br />
[![Stellar Technologies](https://raw.githubusercontent.com/wiki/netbox-community/netbox/images/sponsors/stellar.png)](https://stellar.tech/)
</div>
### Discussion ### Discussion
* [GitHub Discussions](https://github.com/netbox-community/netbox/discussions) - Discussion forum hosted by GitHub; ideal for Q&A and other structured discussions * [GitHub Discussions](https://github.com/netbox-community/netbox/discussions) - Discussion forum hosted by GitHub; ideal for Q&A and other structured discussions
* [Slack](https://join.slack.com/t/netdev-community/shared_invite/zt-mtts8g0n-Sm6Wutn62q_M4OdsaIycrQ) - Real-time chat hosted by the NetDev Community; best for unstructured discussion or just hanging out * [Slack](https://netdev.chat/) - Real-time chat hosted by the NetDev Community; best for unstructured discussion or just hanging out
* [Google Group](https://groups.google.com/g/netbox-discuss) - Legacy mailing list; slowly being replaced by GitHub discussions * [Google Group](https://groups.google.com/g/netbox-discuss) - Legacy mailing list; slowly being replaced by GitHub discussions
### Build Status ### Installation
| | status |
|-------------|------------|
| **master** | ![Build status](https://github.com/netbox-community/netbox/workflows/CI/badge.svg?branch=master) |
| **develop** | ![Build status](https://github.com/netbox-community/netbox/workflows/CI/badge.svg?branch=develop) |
### Screenshots
![Screenshot of main page](docs/media/screenshot1.png "Main page")
---
![Screenshot of rack elevation](docs/media/screenshot2.png "Rack elevation")
---
![Screenshot of prefix hierarchy](docs/media/screenshot3.png "Prefix hierarchy")
## Installation
Please see [the documentation](https://netbox.readthedocs.io/en/stable/) for Please see [the documentation](https://netbox.readthedocs.io/en/stable/) for
instructions on installing NetBox. To upgrade NetBox, please download the instructions on installing NetBox. To upgrade NetBox, please download the
[latest release](https://github.com/netbox-community/netbox/releases) and [latest release](https://github.com/netbox-community/netbox/releases) and
run `upgrade.sh`. run `upgrade.sh`.
## Providing Feedback ### Providing Feedback
The best platform for general feedback, assistance, and other discussion is our The best platform for general feedback, assistance, and other discussion is our
[GitHub discussions](https://github.com/netbox-community/netbox/discussions). [GitHub discussions](https://github.com/netbox-community/netbox/discussions).
@ -54,7 +52,15 @@ the [appropriate template](https://github.com/netbox-community/netbox/issues/new
If you are interested in contributing to the development of NetBox, please read If you are interested in contributing to the development of NetBox, please read
our [contributing guide](CONTRIBUTING.md) prior to beginning any work. our [contributing guide](CONTRIBUTING.md) prior to beginning any work.
## Related projects ### Screenshots
![Screenshot of main page](docs/media/screenshot1.png "Main page")
![Screenshot of rack elevation](docs/media/screenshot2.png "Rack elevation")
![Screenshot of prefix hierarchy](docs/media/screenshot3.png "Prefix hierarchy")
### Related projects
Please see [our wiki](https://github.com/netbox-community/netbox/wiki/Community-Contributions) Please see [our wiki](https://github.com/netbox-community/netbox/wiki/Community-Contributions)
for a list of relevant community projects. for a list of relevant community projects.

View File

@ -93,3 +93,7 @@ redis
# SVG image rendering (used for rack elevations) # SVG image rendering (used for rack elevations)
# https://github.com/mozman/svgwrite # https://github.com/mozman/svgwrite
svgwrite svgwrite
# Tabular dataset library (for table-based exports)
# https://github.com/jazzband/tablib
tablib

View File

@ -1,5 +1,5 @@
server { server {
listen 443 ssl; listen [::]:443 ssl ipv6only=off;
# CHANGE THIS TO YOUR SERVER'S NAME # CHANGE THIS TO YOUR SERVER'S NAME
server_name netbox.example.com; server_name netbox.example.com;
@ -23,7 +23,7 @@ server {
server { server {
# Redirect HTTP traffic to HTTPS # Redirect HTTP traffic to HTTPS
listen 80; listen [::]:80 ipv6only=off;
server_name _; server_name _;
return 301 https://$host$request_uri; return 301 https://$host$request_uri;
} }

View File

@ -1,5 +1,5 @@
- facility_id: a.r1.c1.emul8r.000 - facility_id: a.r1.c1.emul8r.000
group: Emulator C1 R1 location: Emulator C1 R1
name: R1 Zone A name: R1 Zone A
role: Customer role: Customer
site: Local Emulator (EMUL8R.000) site: Local Emulator (EMUL8R.000)
@ -7,7 +7,7 @@
u_height: '36' u_height: '36'
width: '19' width: '19'
- facility_id: b.r1.c1.emul8r.000 - facility_id: b.r1.c1.emul8r.000
group: Emulator C1 R1 location: Emulator C1 R1
name: R1 Zone B name: R1 Zone B
role: Utility role: Utility
site: Local Emulator (EMUL8R.000) site: Local Emulator (EMUL8R.000)
@ -15,7 +15,7 @@
u_height: '36' u_height: '36'
width: '19' width: '19'
- facility_id: f.r1.c1.emul8r.000 - facility_id: f.r1.c1.emul8r.000
group: Emulator C1 R1 location: Emulator C1 R1
name: R1 Facility name: R1 Facility
role: Facility role: Facility
site: Local Emulator (EMUL8R.000) site: Local Emulator (EMUL8R.000)
@ -23,7 +23,7 @@
u_height: '36' u_height: '36'
width: '19' width: '19'
- facility_id: a.r2.c1.emul8r.000 - facility_id: a.r2.c1.emul8r.000
group: Emulator C1 R2 location: Emulator C1 R2
name: R2 Zone A name: R2 Zone A
role: Customer role: Customer
site: Local Emulator (EMUL8R.000) site: Local Emulator (EMUL8R.000)
@ -31,7 +31,7 @@
u_height: '36' u_height: '36'
width: '19' width: '19'
- facility_id: b.r2.c1.emul8r.000 - facility_id: b.r2.c1.emul8r.000
group: Emulator C1 R2 location: Emulator C1 R2
name: R2 Zone B name: R2 Zone B
role: Utility role: Utility
site: Local Emulator (EMUL8R.000) site: Local Emulator (EMUL8R.000)
@ -39,7 +39,7 @@
u_height: '36' u_height: '36'
width: '19' width: '19'
- facility_id: f.r2.c1.emul8r.000 - facility_id: f.r2.c1.emul8r.000
group: Emulator C1 R2 location: Emulator C1 R2
name: R2 Facility name: R2 Facility
role: Facility role: Facility
site: Local Emulator (EMUL8R.000) site: Local Emulator (EMUL8R.000)
@ -47,7 +47,7 @@
u_height: '36' u_height: '36'
width: '19' width: '19'
- facility_id: a.r3.c1.emul8r.000 - facility_id: a.r3.c1.emul8r.000
group: Emulator C1 R3 location: Emulator C1 R3
name: R3 Zone A name: R3 Zone A
role: Customer role: Customer
site: Local Emulator (EMUL8R.000) site: Local Emulator (EMUL8R.000)
@ -55,7 +55,7 @@
u_height: '36' u_height: '36'
width: '19' width: '19'
- facility_id: b.r3.c1.emul8r.000 - facility_id: b.r3.c1.emul8r.000
group: Emulator C1 R3 location: Emulator C1 R3
name: R3 Zone B name: R3 Zone B
role: Utility role: Utility
site: Local Emulator (EMUL8R.000) site: Local Emulator (EMUL8R.000)
@ -63,7 +63,7 @@
u_height: '36' u_height: '36'
width: '19' width: '19'
- facility_id: f.r3.c1.emul8r.000 - facility_id: f.r3.c1.emul8r.000
group: Emulator C1 R3 location: Emulator C1 R3
name: R3 Facility name: R3 Facility
role: Facility role: Facility
site: Local Emulator (EMUL8R.000) site: Local Emulator (EMUL8R.000)
@ -71,7 +71,7 @@
u_height: '36' u_height: '36'
width: '19' width: '19'
- facility_id: a.r4.c1.emul8r.000 - facility_id: a.r4.c1.emul8r.000
group: Emulator C1 R4 location: Emulator C1 R4
name: R4 Zone A name: R4 Zone A
role: Customer role: Customer
site: Local Emulator (EMUL8R.000) site: Local Emulator (EMUL8R.000)
@ -79,7 +79,7 @@
u_height: '36' u_height: '36'
width: '19' width: '19'
- facility_id: b.r4.c1.emul8r.000 - facility_id: b.r4.c1.emul8r.000
group: Emulator C1 R4 location: Emulator C1 R4
name: R4 Zone B name: R4 Zone B
role: Utility role: Utility
site: Local Emulator (EMUL8R.000) site: Local Emulator (EMUL8R.000)
@ -87,7 +87,7 @@
u_height: '36' u_height: '36'
width: '19' width: '19'
- facility_id: f.r4.c1.emul8r.000 - facility_id: f.r4.c1.emul8r.000
group: Emulator C1 R4 location: Emulator C1 R4
name: R4 Facility name: R4 Facility
role: Facility role: Facility
site: Local Emulator (EMUL8R.000) site: Local Emulator (EMUL8R.000)
@ -95,7 +95,7 @@
u_height: '36' u_height: '36'
width: '19' width: '19'
- facility_id: a.r5.c1.emul8r.000 - facility_id: a.r5.c1.emul8r.000
group: Emulator C1 R5 location: Emulator C1 R5
name: R5 Zone A name: R5 Zone A
role: Customer role: Customer
site: Local Emulator (EMUL8R.000) site: Local Emulator (EMUL8R.000)
@ -103,7 +103,7 @@
u_height: '36' u_height: '36'
width: '19' width: '19'
- facility_id: b.r5.c1.emul8r.000 - facility_id: b.r5.c1.emul8r.000
group: Emulator C1 R5 location: Emulator C1 R5
name: R5 Zone B name: R5 Zone B
role: Utility role: Utility
site: Local Emulator (EMUL8R.000) site: Local Emulator (EMUL8R.000)
@ -111,7 +111,7 @@
u_height: '36' u_height: '36'
width: '19' width: '19'
- facility_id: f.r5.c1.emul8r.000 - facility_id: f.r5.c1.emul8r.000
group: Emulator C1 R5 location: Emulator C1 R5
name: R5 Facility name: R5 Facility
role: Facility role: Facility
site: Local Emulator (EMUL8R.000) site: Local Emulator (EMUL8R.000)
@ -119,7 +119,7 @@
u_height: '36' u_height: '36'
width: '19' width: '19'
- facility_id: a.r6.c1.emul8r.000 - facility_id: a.r6.c1.emul8r.000
group: Emulator C1 R6 location: Emulator C1 R6
name: R6 Zone A name: R6 Zone A
role: Customer role: Customer
site: Local Emulator (EMUL8R.000) site: Local Emulator (EMUL8R.000)
@ -127,7 +127,7 @@
u_height: '36' u_height: '36'
width: '19' width: '19'
- facility_id: b.r6.c1.emul8r.000 - facility_id: b.r6.c1.emul8r.000
group: Emulator C1 R6 location: Emulator C1 R6
name: R6 Zone B name: R6 Zone B
role: Utility role: Utility
site: Local Emulator (EMUL8R.000) site: Local Emulator (EMUL8R.000)
@ -135,7 +135,7 @@
u_height: '36' u_height: '36'
width: '19' width: '19'
- facility_id: f.r6.c1.emul8r.000 - facility_id: f.r6.c1.emul8r.000
group: Emulator C1 R6 location: Emulator C1 R6
name: R6 Facility name: R6 Facility
role: Facility role: Facility
site: Local Emulator (EMUL8R.000) site: Local Emulator (EMUL8R.000)

View File

@ -1,8 +1,8 @@
from dcim.models import Site,RackGroup from dcim.models import Location, Site
from startup_script_utils import load_yaml from startup_script_utils import load_yaml
import sys import sys
rack_groups = load_yaml('/opt/netbox/initializers/rack_groups.yml') rack_groups = load_yaml('/opt/netbox/initializers/locations.yml')
if rack_groups is None: if rack_groups is None:
sys.exit() sys.exit()
@ -18,7 +18,7 @@ for params in rack_groups:
query = { field: params.pop(assoc) } query = { field: params.pop(assoc) }
params[assoc] = model.objects.get(**query) params[assoc] = model.objects.get(**query)
rack_group, created = RackGroup.objects.get_or_create(**params) location, created = Location.objects.get_or_create(**params)
if created: if created:
print("🎨 Created rack group", rack_group.name) print("🎨 Created rack group", location.name)

View File

@ -1,6 +1,6 @@
import sys import sys
from dcim.models import Site, RackRole, Rack, RackGroup from dcim.models import Site, RackRole, Rack, Location
from startup_script_utils import * from startup_script_utils import *
from tenancy.models import Tenant from tenancy.models import Tenant
@ -16,7 +16,7 @@ required_assocs = {
optional_assocs = { optional_assocs = {
'role': (RackRole, 'name'), 'role': (RackRole, 'name'),
'tenant': (Tenant, 'name'), 'tenant': (Tenant, 'name'),
'group': (RackGroup, 'name') 'location': (Location, 'name')
} }
for params in racks: for params in racks:

View File

@ -24,14 +24,14 @@ switches = get_devices('access-switch')
i = 0 i = 0
for locker in lockers: for locker in lockers:
# break if i is larger than switch interface's size. # break if i is larger than switch interface's size.
if i > len(switches[0].vc_interfaces)-1: if i > len(switches[0].vc_interfaces())-1:
break break
i12 = locker.vc_interfaces[0] i12 = (locker.vc_interfaces())[0]
i34 = locker.vc_interfaces[1] i34 = (locker.vc_interfaces())[1]
efr1 = switches[0].vc_interfaces[i] efr1 = (switches[0].vc_interfaces())[i]
efr2 = switches[1].vc_interfaces[i] efr2 = (switches[1].vc_interfaces())[i]
try: try:
c1 = Cable.objects.create(termination_a=i12, termination_b=efr1) c1 = Cable.objects.create(termination_a=i12, termination_b=efr1)

View File

@ -1,12 +1,15 @@
# Caching # Caching
NetBox supports database query caching using [django-cacheops](https://github.com/Suor/django-cacheops) and Redis. When a query is made, the results are cached in Redis for a short period of time, as defined by the [CACHE_TIMEOUT](../../configuration/optional-settings/#cache_timeout) parameter (15 minutes by default). Within that time, all recurrences of that specific query will return the pre-fetched results from the cache. NetBox supports database query caching using [django-cacheops](https://github.com/Suor/django-cacheops) and Redis. When a query is made, the results are cached in Redis for a short period of time, as defined by the [CACHE_TIMEOUT](../configuration/optional-settings.md#cache_timeout) parameter. Within that time, all recurrences of that specific query will return the pre-fetched results from the cache.
!!! warning
In NetBox v2.11.10 and later queryset caching is disabled by default, and must be configured.
If a change is made to any of the objects returned by the query within that time, or if the timeout expires, the results are automatically invalidated and the next request for those results will be sent to the database. If a change is made to any of the objects returned by the query within that time, or if the timeout expires, the results are automatically invalidated and the next request for those results will be sent to the database.
## Invalidating Cached Data ## Invalidating Cached Data
Although caching is performed automatically and rarely requires administrative intervention, NetBox provides the `invalidate` management command to force invalidation of cached results. This command can reference a specific object my its type and numeric ID: Although caching is performed automatically and rarely requires administrative intervention, NetBox provides the `invalidate` management command to force invalidation of cached results. This command can reference a specific object by its type and numeric ID:
```no-highlight ```no-highlight
$ python netbox/manage.py invalidate dcim.Device.34 $ python netbox/manage.py invalidate dcim.Device.34

View File

@ -1,6 +1,6 @@
# Change Logging # Change Logging
Every time an object in NetBox is created, updated, or deleted, a serialized copy of that object is saved to the database, along with meta data including the current time and the user associated with the change. These records form a persistent record of changes both for each individual object as well as NetBox as a whole. The global change log can be viewed by navigating to Other > Change Log. Every time an object in NetBox is created, updated, or deleted, a serialized copy of that object taken both before and after the change is saved to the database, along with meta data including the current time and the user associated with the change. These records form a persistent record of changes both for each individual object as well as NetBox as a whole. The global change log can be viewed by navigating to Other > Change Log.
A serialized representation of the instance being modified is included in JSON format. This is similar to how objects are conveyed within the REST API, but does not include any nested representations. For instance, the `tenant` field of a site will record only the tenant's ID, not a representation of the tenant. A serialized representation of the instance being modified is included in JSON format. This is similar to how objects are conveyed within the REST API, but does not include any nested representations. For instance, the `tenant` field of a site will record only the tenant's ID, not a representation of the tenant.

View File

@ -16,6 +16,7 @@ Custom fields must be created through the admin UI under Extras > Custom Fields.
* Date: A date in ISO 8601 format (YYYY-MM-DD) * Date: A date in ISO 8601 format (YYYY-MM-DD)
* URL: This will be presented as a link in the web UI * URL: This will be presented as a link in the web UI
* Selection: A selection of one of several pre-defined custom choices * Selection: A selection of one of several pre-defined custom choices
* Multiple selection: A selection field which supports the assignment of multiple values
Each custom field must have a name; this should be a simple database-friendly string, e.g. `tps_report`. You may also assign a corresponding human-friendly label (e.g. "TPS report"); the label will be displayed on web forms. A weight is also required: Higher-weight fields will be ordered lower within a form. (The default weight is 100.) If a description is provided, it will appear beneath the field in a form. Each custom field must have a name; this should be a simple database-friendly string, e.g. `tps_report`. You may also assign a corresponding human-friendly label (e.g. "TPS report"); the label will be displayed on web forms. A weight is also required: Higher-weight fields will be ordered lower within a form. (The default weight is 100.) If a description is provided, it will appear beneath the field in a form.
@ -23,7 +24,7 @@ Marking a field as required will force the user to provide a value for the field
The filter logic controls how values are matched when filtering objects by the custom field. Loose filtering (the default) matches on a partial value, whereas exact matching requires a complete match of the given string to a field's value. For example, exact filtering with the string "red" will only match the exact value "red", whereas loose filtering will match on the values "red", "red-orange", or "bored". Setting the filter logic to "disabled" disables filtering by the field entirely. The filter logic controls how values are matched when filtering objects by the custom field. Loose filtering (the default) matches on a partial value, whereas exact matching requires a complete match of the given string to a field's value. For example, exact filtering with the string "red" will only match the exact value "red", whereas loose filtering will match on the values "red", "red-orange", or "bored". Setting the filter logic to "disabled" disables filtering by the field entirely.
A custom field must be assigned to one or object types, or models, in NetBox. Once created, custom fields will automatically appear as part of these models in the web UI and REST API. Note that not all models support custom fields. A custom field must be assigned to one or more object types, or models, in NetBox. Once created, custom fields will automatically appear as part of these models in the web UI and REST API. Note that not all models support custom fields.
### Custom Field Validation ### Custom Field Validation
@ -37,7 +38,13 @@ NetBox supports limited custom validation for custom field values. Following are
Each custom selection field must have at least two choices. These are specified as a comma-separated list. Choices appear in forms in the order they are listed. Note that choice values are saved exactly as they appear, so it's best to avoid superfluous punctuation or symbols where possible. Each custom selection field must have at least two choices. These are specified as a comma-separated list. Choices appear in forms in the order they are listed. Note that choice values are saved exactly as they appear, so it's best to avoid superfluous punctuation or symbols where possible.
If a default value is specified for a selection field, it must exactly match one of the provided choices. If a default value is specified for a selection field, it must exactly match one of the provided choices. The value of a multiple selection field will always return a list, even if only one value is selected.
## Custom Fields in Templates
Several features within NetBox, such as export templates and webhooks, utilize Jinja2 templating. For convenience, objects which support custom field assignment expose custom field data through the `cf` property. This is a bit cleaner than accessing custom field data through the actual field (`custom_field_data`).
For example, a custom field named `foo123` on the Site model is accessible on an instance as `{{ site.cf.foo123 }}`.
## Custom Fields and the REST API ## Custom Fields and the REST API

View File

@ -17,6 +17,9 @@ When viewing a device named Router4, this link would render as:
Custom links appear as buttons at the top right corner of the page. Numeric weighting can be used to influence the ordering of links. Custom links appear as buttons at the top right corner of the page. Numeric weighting can be used to influence the ordering of links.
!!! warning
Custom links rely on user-created code to generate arbitrary HTML output, which may be dangerous. Only grant permission to create or modify custom links to trusted users.
## Context Data ## Context Data
The following context data is available within the template when rendering a custom link's text or URL. The following context data is available within the template when rendering a custom link's text or URL.

View File

@ -170,18 +170,13 @@ Similar to `ChoiceVar`, but allows for the selection of multiple choices.
A particular object within NetBox. Each ObjectVar must specify a particular model, and allows the user to select one of the available instances. ObjectVar accepts several arguments, listed below. A particular object within NetBox. Each ObjectVar must specify a particular model, and allows the user to select one of the available instances. ObjectVar accepts several arguments, listed below.
* `model` - The model class * `model` - The model class
* `display_field` - The name of the REST API object field to display in the selection list (default: `'name'`) * `display_field` - The name of the REST API object field to display in the selection list (default: `'display'`)
* `query_params` - A dictionary of query parameters to use when retrieving available options (optional) * `query_params` - A dictionary of query parameters to use when retrieving available options (optional)
* `null_option` - A label representing a "null" or empty choice (optional) * `null_option` - A label representing a "null" or empty choice (optional)
The `display_field` argument is useful when referencing a model which does not have a `name` field. For example, when displaying a list of device types, you would likely use the `model` field: !!! warning
The `display_field` parameter is now deprecated, and will be removed in NetBox v3.0. All ObjectVar instances will
```python instead use the new standard `display` field for all serializers (introduced in NetBox v2.11).
device_type = ObjectVar(
model=DeviceType,
display_field='model'
)
```
To limit the selections available within the list, additional query parameters can be passed as the `query_params` dictionary. For example, to show only devices with an "active" status: To limit the selections available within the list, additional query parameters can be passed as the `query_params` dictionary. For example, to show only devices with an "active" status:

View File

@ -2,10 +2,16 @@
NetBox allows users to define custom templates that can be used when exporting objects. To create an export template, navigate to Extras > Export Templates under the admin interface. NetBox allows users to define custom templates that can be used when exporting objects. To create an export template, navigate to Extras > Export Templates under the admin interface.
Each export template is associated with a certain type of object. For instance, if you create an export template for VLANs, your custom template will appear under the "Export" button on the VLANs list. Each export template is associated with a certain type of object. For instance, if you create an export template for VLANs, your custom template will appear under the "Export" button on the VLANs list. Each export template must have a name, and may optionally designate a specific export [MIME type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types) and/or file extension.
Export templates must be written in [Jinja2](https://jinja.palletsprojects.com/). Export templates must be written in [Jinja2](https://jinja.palletsprojects.com/).
!!! note
The name `table` is reserved for internal use.
!!! warning
Export templates are rendered using user-submitted code, which may pose security risks under certain conditions. Only grant permission to create or modify export templates to trusted users.
The list of objects returned from the database when rendering an export template is stored in the `queryset` variable, which you'll typically want to iterate through using a `for` loop. Object properties can be access by name. For example: The list of objects returned from the database when rendering an export template is stored in the `queryset` variable, which you'll typically want to iterate through using a `for` loop. Object properties can be access by name. For example:
```jinja2 ```jinja2
@ -18,6 +24,16 @@ Height: {{ rack.u_height }}U
To access custom fields of an object within a template, use the `cf` attribute. For example, `{{ obj.cf.color }}` will return the value (if any) for a custom field named `color` on `obj`. To access custom fields of an object within a template, use the `cf` attribute. For example, `{{ obj.cf.color }}` will return the value (if any) for a custom field named `color` on `obj`.
If you need to use the config context data in an export template, you'll should use the function `get_config_context` to get all the config context data. For example:
```
{% for server in queryset %}
{% set data = server.get_config_context() %}
{{ data.syslog }}
{% endfor %}
```
The `as_attachment` attribute of an export template controls its behavior when rendered. If true, the rendered content will be returned to the user as a downloadable file. If false, it will be displayed within the browser. (This may be handy e.g. for generating HTML content.)
A MIME type and file extension can optionally be defined for each export template. The default MIME type is `text/plain`. A MIME type and file extension can optionally be defined for each export template. The default MIME type is `text/plain`.
## Example ## Example

View File

@ -0,0 +1,5 @@
# Journaling
All primary objects in NetBox support journaling. A journal is a collection of human-generated notes and comments about an object maintained for historical context. It supplements NetBox's change log to provide additional information about why changes have been made or to convey events which occur outside NetBox. Unlike the change log, in which records typically expire after a configurable period of time, journal entries persist for the life of their associated object.
Each journal entry has a selectable kind (info, success, warning, or danger) and a user-populated `comments` field. Each entry automatically records the date, time, and associated user upon being created.

View File

@ -2,6 +2,13 @@
NetBox supports integration with the [NAPALM automation](https://napalm-automation.net/) library. NAPALM allows NetBox to serve a proxy for operational data, fetching live data from network devices and returning it to a requester via its REST API. Note that NetBox does not store any NAPALM data locally. NetBox supports integration with the [NAPALM automation](https://napalm-automation.net/) library. NAPALM allows NetBox to serve a proxy for operational data, fetching live data from network devices and returning it to a requester via its REST API. Note that NetBox does not store any NAPALM data locally.
The NetBox UI will display tabs for status, LLDP neighbors, and configuration under the device view if the following conditions are met:
* Device status is "Active"
* A primary IP has been assigned to the device
* A platform with a NAPALM driver has been assigned
* The authenticated user has the `dcim.napalm_read_device` permission
!!! note !!! note
To enable this integration, the NAPALM library must be installed. See [installation steps](../../installation/3-netbox/#napalm) for more information. To enable this integration, the NAPALM library must be installed. See [installation steps](../../installation/3-netbox/#napalm) for more information.
@ -22,7 +29,7 @@ GET /api/dcim/devices/1/napalm/?method=get_environment
## Authentication ## Authentication
By default, the [`NAPALM_USERNAME`](../../configuration/optional-settings/#napalm_username) and [`NAPALM_PASSWORD`](../../configuration/optional-settings/#napalm_password) configuration parameters are used for NAPALM authentication. They can be overridden for an individual API call by specifying the `X-NAPALM-Username` and `X-NAPALM-Password` headers. By default, the [`NAPALM_USERNAME`](../configuration/optional-settings.md#napalm_username) and [`NAPALM_PASSWORD`](../configuration/optional-settings.md#napalm_password) configuration parameters are used for NAPALM authentication. They can be overridden for an individual API call by specifying the `X-NAPALM-Username` and `X-NAPALM-Password` headers.
``` ```
$ curl "http://localhost/api/dcim/devices/1/napalm/?method=get_environment" \ $ curl "http://localhost/api/dcim/devices/1/napalm/?method=get_environment" \

View File

@ -12,7 +12,7 @@ A NetBox report is a mechanism for validating the integrity of data within NetBo
## Writing Reports ## Writing Reports
Reports must be saved as files in the [`REPORTS_ROOT`](../../configuration/optional-settings/#reports_root) path (which defaults to `netbox/reports/`). Each file created within this path is considered a separate module. Each module holds one or more reports (Python classes), each of which performs a certain function. The logic of each report is broken into discrete test methods, each of which applies a small portion of the logic comprising the overall test. Reports must be saved as files in the [`REPORTS_ROOT`](../configuration/optional-settings.md#reports_root) path (which defaults to `netbox/reports/`). Each file created within this path is considered a separate module. Each module holds one or more reports (Python classes), each of which performs a certain function. The logic of each report is broken into discrete test methods, each of which applies a small portion of the logic comprising the overall test.
!!! warning !!! warning
The reports path includes a file named `__init__.py`, which registers the path as a Python module. Do not delete this file. The reports path includes a file named `__init__.py`, which registers the path as a Python module. Do not delete this file.
@ -80,7 +80,7 @@ class DeviceConnectionsReport(Report):
self.log_success(device) self.log_success(device)
``` ```
As you can see, reports are completely customizable. Validation logic can be as simple or as complex as needed. As you can see, reports are completely customizable. Validation logic can be as simple or as complex as needed. Also note that the `description` attribute support markdown syntax. It will be rendered in the report list page.
!!! warning !!! warning
Reports should never alter data: If you find yourself using the `create()`, `save()`, `update()`, or `delete()` methods on objects within reports, stop and re-evaluate what you're trying to accomplish. Note that there are no safeguards against the accidental alteration or destruction of data. Reports should never alter data: If you find yourself using the `create()`, `save()`, `update()`, or `delete()` methods on objects within reports, stop and re-evaluate what you're trying to accomplish. Note that there are no safeguards against the accidental alteration or destruction of data.
@ -93,7 +93,7 @@ The following methods are available to log results within a report:
* log_warning(object, message) * log_warning(object, message)
* log_failure(object, message) * log_failure(object, message)
The recording of one or more failure messages will automatically flag a report as failed. It is advised to log a success for each object that is evaluated so that the results will reflect how many objects are being reported on. (The inclusion of a log message is optional for successes.) Messages recorded with `log()` will appear in a report's results but are not associated with a particular object or status. The recording of one or more failure messages will automatically flag a report as failed. It is advised to log a success for each object that is evaluated so that the results will reflect how many objects are being reported on. (The inclusion of a log message is optional for successes.) Messages recorded with `log()` will appear in a report's results but are not associated with a particular object or status. Log messages also support using markdown syntax and will be rendered on the report result page.
To perform additional tasks, such as sending an email or calling a webhook, after a report has been run, extend the `post_run()` method. The status of the report is available as `self.failed` and the results object is `self.result`. To perform additional tasks, such as sending an email or calling a webhook, after a report has been run, extend the `post_run()` method. The status of the report is available as `self.failed` and the results object is `self.result`.

View File

@ -2,6 +2,9 @@
A webhook is a mechanism for conveying to some external system a change that took place in NetBox. For example, you may want to notify a monitoring system whenever the status of a device is updated in NetBox. This can be done by creating a webhook for the device model in NetBox and identifying the webhook receiver. When NetBox detects a change to a device, an HTTP request containing the details of the change and who made it be sent to the specified receiver. Webhooks are configured in the admin UI under Extras > Webhooks. A webhook is a mechanism for conveying to some external system a change that took place in NetBox. For example, you may want to notify a monitoring system whenever the status of a device is updated in NetBox. This can be done by creating a webhook for the device model in NetBox and identifying the webhook receiver. When NetBox detects a change to a device, an HTTP request containing the details of the change and who made it be sent to the specified receiver. Webhooks are configured in the admin UI under Extras > Webhooks.
!!! warning
Webhooks support the inclusion of user-submitted code to generate custom headers and payloads, which may pose security risks under certain conditions. Only grant permission to create or modify webhooks to trusted users.
## Configuration ## Configuration
* **Name** - A unique name for the webhook. The name is not included with outbound messages. * **Name** - A unique name for the webhook. The name is not included with outbound messages.
@ -38,7 +41,8 @@ The following data is available as context for Jinja2 templates:
* `timestamp` - The time at which the event occurred (in [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) format). * `timestamp` - The time at which the event occurred (in [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) format).
* `username` - The name of the user account associated with the change. * `username` - The name of the user account associated with the change.
* `request_id` - The unique request ID. This may be used to correlate multiple changes associated with a single request. * `request_id` - The unique request ID. This may be used to correlate multiple changes associated with a single request.
* `data` - A serialized representation of the object _after_ the change was made. This is typically equivalent to the model's representation in NetBox's REST API. * `data` - A detailed representation of the object in its current state. This is typically equivalent to the model's representation in NetBox's REST API.
* `snapshots` - Minimal "snapshots" of the object state both before and after the change was made; provided ass a dictionary with keys named `prechange` and `postchange`. These are not as extensive as the fully serialized representation, but contain enough information to convey what has changed.
### Default Request Body ### Default Request Body
@ -47,7 +51,7 @@ If no body template is specified, the request body will be populated with a JSON
```no-highlight ```no-highlight
{ {
"event": "created", "event": "created",
"timestamp": "2020-02-25 15:10:26.010582+00:00", "timestamp": "2021-03-09 17:55:33.968016+00:00",
"model": "site", "model": "site",
"username": "jstretch", "username": "jstretch",
"request_id": "fdbca812-3142-4783-b364-2e2bd5c16c6a", "request_id": "fdbca812-3142-4783-b364-2e2bd5c16c6a",
@ -62,6 +66,17 @@ If no body template is specified, the request body will be populated with a JSON
}, },
"region": null, "region": null,
... ...
},
"snapshots": {
"prechange": null,
"postchange": {
"created": "2021-03-09",
"last_updated": "2021-03-09T17:55:33.851Z",
"name": "Site 1",
"slug": "site-1",
"status": "active",
...
}
} }
} }
``` ```

View File

@ -10,7 +10,7 @@ NetBox v2.9 introduced a new object-based permissions framework, which replace's
| ----------- | ----------- | | ----------- | ----------- |
| `{"status": "active"}` | Status is active | | `{"status": "active"}` | Status is active |
| `{"status__in": ["planned", "reserved"]}` | Status is active **OR** reserved | | `{"status__in": ["planned", "reserved"]}` | Status is active **OR** reserved |
| `{"status": "active", "role": "testing"}` | Status is active **OR** role is testing | | `{"status": "active", "role": "testing"}` | Status is active **AND** role is testing |
| `{"name__startswith": "Foo"}` | Name starts with "Foo" (case-sensitive) | | `{"name__startswith": "Foo"}` | Name starts with "Foo" (case-sensitive) |
| `{"name__iendswith": "bar"}` | Name ends with "bar" (case-insensitive) | | `{"name__iendswith": "bar"}` | Name ends with "bar" (case-insensitive) |
| `{"vid__gte": 100, "vid__lt": 200}` | VLAN ID is greater than or equal to 100 **AND** less than 200 | | `{"vid__gte": 100, "vid__lt": 200}` | VLAN ID is greater than or equal to 100 **AND** less than 200 |

View File

@ -12,13 +12,16 @@ NetBox employs a [PostgreSQL](https://www.postgresql.org/) database, so general
Use the `pg_dump` utility to export the entire database to a file: Use the `pg_dump` utility to export the entire database to a file:
```no-highlight ```no-highlight
pg_dump netbox > netbox.sql pg_dump --username netbox --password --host localhost netbox > netbox.sql
``` ```
!!! note
You may need to change the username, host, and/or database in the command above to match your installation.
When replicating a production database for development purposes, you may find it convenient to exclude changelog data, which can easily account for the bulk of a database's size. To do this, exclude the `extras_objectchange` table data from the export. The table will still be included in the output file, but will not be populated with any data. When replicating a production database for development purposes, you may find it convenient to exclude changelog data, which can easily account for the bulk of a database's size. To do this, exclude the `extras_objectchange` table data from the export. The table will still be included in the output file, but will not be populated with any data.
```no-highlight ```no-highlight
pg_dump --exclude-table-data=extras_objectchange netbox > netbox.sql pg_dump ... --exclude-table-data=extras_objectchange netbox > netbox.sql
``` ```
### Load an Exported Database ### Load an Exported Database
@ -41,7 +44,7 @@ Keep in mind that PostgreSQL user accounts and permissions are not included with
If you want to export only the database schema, and not the data itself (e.g. for development reference), do the following: If you want to export only the database schema, and not the data itself (e.g. for development reference), do the following:
```no-highlight ```no-highlight
pg_dump -s netbox > netbox_schema.sql pg_dump --username netbox --password --host localhost -s netbox > netbox_schema.sql
``` ```
--- ---

View File

@ -54,9 +54,9 @@ BASE_PATH = 'netbox/'
## CACHE_TIMEOUT ## CACHE_TIMEOUT
Default: 900 Default: 0 (disabled)
The number of seconds that cache entries will be retained before expiring. The number of seconds that cached database queries will be retained before expiring.
--- ---
@ -257,6 +257,16 @@ LOGGING = {
--- ---
## LOGIN_PERSISTENCE
Default: False
If true, the lifetime of a user's authentication session will be automatically reset upon each valid request. For example, if [`LOGIN_TIMEOUT`](#login_timeout) is configured to 14 days (the default), and a user whose session is due to expire in five days makes a NetBox request (with a valid session cookie), the session's lifetime will be reset to 14 days.
Note that enabling this setting causes NetBox to update a user's session in the database (or file, as configured per [`SESSION_FILE_PATH`](#session_file_path)) with each request, which may introduce significant overhead in very active environments. It also permits an active user to remain authenticated to NetBox indefinitely.
---
## LOGIN_REQUIRED ## LOGIN_REQUIRED
Default: False Default: False
@ -281,6 +291,14 @@ Setting this to True will display a "maintenance mode" banner at the top of ever
--- ---
## MAPS_URL
Default: `https://maps.google.com/?q=` (Google Maps)
This specifies the URL to use when presenting a map of a physical location by street address or GPS coordinates. The URL must accept either a free-form street address or a comma-separated pair of numeric coordinates appended to it.
---
## MAX_PAGE_SIZE ## MAX_PAGE_SIZE
Default: 1000 Default: 1000
@ -301,7 +319,7 @@ The file path to the location where media files (such as image attachments) are
Default: False Default: False
Toggle the availability Prometheus-compatible metrics at `/metrics`. See the [Prometheus Metrics](../../additional-features/prometheus-metrics/) documentation for more details. Toggle the availability Prometheus-compatible metrics at `/metrics`. See the [Prometheus Metrics](../additional-features/prometheus-metrics.md) documentation for more details.
--- ---
@ -507,6 +525,14 @@ The file path to the location where custom scripts will be kept. By default, thi
--- ---
## SESSION_COOKIE_NAME
Default: `sessionid`
The name used for the session cookie. See the [Django documentation](https://docs.djangoproject.com/en/stable/ref/settings/#session-cookie-name) for more detail.
---
## SESSION_FILE_PATH ## SESSION_FILE_PATH
Default: None Default: None

View File

@ -66,6 +66,7 @@ Redis is configured using a configuration setting similar to `DATABASE` and thes
* `PASSWORD` - Redis password (if set) * `PASSWORD` - Redis password (if set)
* `DATABASE` - Numeric database ID * `DATABASE` - Numeric database ID
* `SSL` - Use SSL connection to Redis * `SSL` - Use SSL connection to Redis
* `INSECURE_SKIP_TLS_VERIFY` - Set to `True` to **disable** TLS certificate verification (not recommended)
An example configuration is provided below: An example configuration is provided below:

View File

@ -1,6 +1,7 @@
# Circuits # Circuits
{!docs/models/circuits/provider.md!} {!docs/models/circuits/provider.md!}
{!docs/models/circuits/providernetwork.md!}
--- ---

View File

@ -8,6 +8,8 @@
## Device Components ## Device Components
Device components represent discrete objects within a device which are used to terminate cables, house child devices, or track resources.
{!docs/models/dcim/consoleport.md!} {!docs/models/dcim/consoleport.md!}
{!docs/models/dcim/consoleserverport.md!} {!docs/models/dcim/consoleserverport.md!}
{!docs/models/dcim/powerport.md!} {!docs/models/dcim/powerport.md!}

View File

@ -1,11 +1,12 @@
# Sites and Racks # Sites and Racks
{!docs/models/dcim/site.md!}
{!docs/models/dcim/region.md!} {!docs/models/dcim/region.md!}
{!docs/models/dcim/sitegroup.md!}
{!docs/models/dcim/site.md!}
{!docs/models/dcim/location.md!}
--- ---
{!docs/models/dcim/rack.md!} {!docs/models/dcim/rack.md!}
{!docs/models/dcim/rackgroup.md!}
{!docs/models/dcim/rackrole.md!} {!docs/models/dcim/rackrole.md!}
{!docs/models/dcim/rackreservation.md!} {!docs/models/dcim/rackreservation.md!}

View File

@ -0,0 +1,85 @@
# Adding Models
## 1. Define the model class
Models within each app are stored in either `models.py` or within a submodule under the `models/` directory. When creating a model, be sure to subclass the [appropriate base model](models.md) from `netbox.models`. This will typically be PrimaryModel or OrganizationalModel. Remember to add the model class to the `__all__` listing for the module.
Each model should define, at a minimum:
* A `__str__()` method returning a user-friendly string representation of the instance
* A `get_absolute_url()` method returning an instance's direct URL (using `reverse()`)
* A `Meta` class specifying a deterministic ordering (if ordered by fields other than the primary ID)
## 2. Define field choices
If the model has one or more fields with static choices, define those choices in `choices.py` by subclassing `utilities.choices.ChoiceSet`.
## 3. Generate database migrations
Once your model definition is complete, generate database migrations by running `manage.py -n $NAME --no-header`. Always specify a short unique name when generating migrations.
!!! info
Set `DEVELOPER = True` in your NetBox configuration to enable the creation of new migrations.
## 4. Add all standard views
Most models will need view classes created in `views.py` to serve the following operations:
* List view
* Detail view
* Edit view
* Delete view
* Bulk import
* Bulk edit
* Bulk delete
## 5. Add URL paths
Add the relevant URL path for each view created in the previous step to `urls.py`.
## 6. Create the FilterSet
Each model should have a corresponding FilterSet class defined. This is used to filter UI and API queries. Subclass the appropriate class from `netbox.filtersets` that matches the model's parent class.
Every model FilterSet should define a `q` filter to support general search queries.
## 7. Create the table
Create a table class for the model in `tables.py` by subclassing `utilities.tables.BaseTable`. Under the table's `Meta` class, be sure to list both the fields and default columns.
## 8. Create the object template
Create the HTML template for the object view. (The other views each typically employ a generic template.) This template should extend `generic/object.html`.
## 9. Add the model to the navigation menu
For NetBox releases prior to v3.0, add the relevant link(s) to the navigation menu template. For later releases, add the relevant items in `netbox/netbox/navigation_menu.py`.
## 10. REST API components
Create the following for each model:
* Detailed (full) model serializer in `api/serializers.py`
* Nested serializer in `api/nested_serializers.py`
* API view in `api/views.py`
* Endpoint route in `api/urls.py`
## 11. GraphQL API components (v3.0+)
Create a Graphene object type for the model in `graphql/types.py` by subclassing the appropriate class from `netbox.graphql.types`.
Also extend the schema class defined in `graphql/schema.py` with the individual object and object list fields per the established convention.
## 12. Add tests
Add tests for the following:
* UI views
* API views
* Filter sets
## 13. Documentation
Create a new documentation page for the model in `docs/models/<app_label>/<model_name>.md`. Include this file under the "features" documentation where appropriate.
Also add your model to the index in `docs/development/models.md`.

View File

@ -5,8 +5,8 @@
Getting started with NetBox development is pretty straightforward, and should feel very familiar to anyone with Django development experience. There are a few things you'll need: Getting started with NetBox development is pretty straightforward, and should feel very familiar to anyone with Django development experience. There are a few things you'll need:
* A Linux system or environment * A Linux system or environment
* A PostgreSQL server, which can be installed locally [per the documentation](/installation/1-postgresql/) * A PostgreSQL server, which can be installed locally [per the documentation](../installation/1-postgresql.md)
* A Redis server, which can also be [installed locally](/installation/2-redis/) * A Redis server, which can also be [installed locally](../installation/2-redis.md)
* A supported version of Python * A supported version of Python
### Fork the Repo ### Fork the Repo

View File

@ -8,7 +8,7 @@ There are several official forums for communication among the developers and com
* [GitHub issues](https://github.com/netbox-community/netbox/issues) - All feature requests, bug reports, and other substantial changes to the code base **must** be documented in an issue. * [GitHub issues](https://github.com/netbox-community/netbox/issues) - All feature requests, bug reports, and other substantial changes to the code base **must** be documented in an issue.
* [GitHub Discussions](https://github.com/netbox-community/netbox/discussions) - The preferred forum for general discussion and support issues. Ideal for shaping a feature request prior to submitting an issue. * [GitHub Discussions](https://github.com/netbox-community/netbox/discussions) - The preferred forum for general discussion and support issues. Ideal for shaping a feature request prior to submitting an issue.
* [#netbox on NetDev Community Slack](https://join.slack.com/t/netdev-community/shared_invite/zt-mtts8g0n-Sm6Wutn62q_M4OdsaIycrQ) - Good for quick chats. Avoid any discussion that might need to be referenced later on, as the chat history is not retained long. * [#netbox on NetDev Community Slack](https://netdev.chat/) - Good for quick chats. Avoid any discussion that might need to be referenced later on, as the chat history is not retained long.
* [Google Group](https://groups.google.com/g/netbox-discuss) - Legacy mailing list; slowly being phased out in favor of GitHub discussions. * [Google Group](https://groups.google.com/g/netbox-discuss) - Legacy mailing list; slowly being phased out in favor of GitHub discussions.
## Governance ## Governance

View File

@ -0,0 +1,98 @@
# NetBox Models
## Model Types
A NetBox model represents a discrete object type such as a device or IP address. Each model is defined as a Python class and has its own SQL table. All NetBox data models can be categorized by type.
The Django [content types](https://docs.djangoproject.com/en/stable/ref/contrib/contenttypes/) framework can be used to reference models within the database. A ContentType instance references a model by its `app_label` and `name`: For example, the Site model is referred to as `dcim.site`. The content type combined with an object's primary key form a globally unique identifier for the object (e.g. `dcim.site:123`).
### Features Matrix
* [Change logging](../additional-features/change-logging.md) - Changes to these objects are automatically recorded in the change log
* [Webhooks](../additional-features/webhooks.md) - NetBox is capable of generating outgoing webhooks for these objects
* [Custom fields](../additional-features/custom-fields.md) - These models support the addition of user-defined fields
* [Export templates](../additional-features/export-templates.md) - Users can create custom export templates for these models
* [Tagging](../models/extras/tag.md) - The models can be tagged with user-defined tags
* [Journaling](../additional-features/journaling.md) - These models support persistent historical commentary
* Nesting - These models can be nested recursively to create a hierarchy
| Type | Change Logging | Webhooks | Custom Fields | Export Templates | Tags | Journaling | Nesting |
| ------------------ | ---------------- | ---------------- | ---------------- | ---------------- | ---------------- | ---------------- | ---------------- |
| Primary | :material-check: | :material-check: | :material-check: | :material-check: | :material-check: | :material-check: | |
| Organizational | :material-check: | :material-check: | :material-check: | :material-check: | | | |
| Nested Group | :material-check: | :material-check: | :material-check: | :material-check: | | | :material-check: |
| Component | :material-check: | :material-check: | :material-check: | :material-check: | :material-check: | | |
| Component Template | :material-check: | :material-check: | :material-check: | | | | |
## Models Index
### Primary Models
* [circuits.Circuit](../models/circuits/circuit.md)
* [circuits.Provider](../models/circuits/provider.md)
* [circuits.ProviderNetwork](../models/circuits/providernetwork.md)
* [dcim.Cable](../models/dcim/cable.md)
* [dcim.Device](../models/dcim/device.md)
* [dcim.DeviceType](../models/dcim/devicetype.md)
* [dcim.PowerFeed](../models/dcim/powerfeed.md)
* [dcim.PowerPanel](../models/dcim/powerpanel.md)
* [dcim.Rack](../models/dcim/rack.md)
* [dcim.RackReservation](../models/dcim/rackreservation.md)
* [dcim.Site](../models/dcim/site.md)
* [dcim.VirtualChassis](../models/dcim/virtualchassis.md)
* [ipam.Aggregate](../models/ipam/aggregate.md)
* [ipam.IPAddress](../models/ipam/ipaddress.md)
* [ipam.Prefix](../models/ipam/prefix.md)
* [ipam.RouteTarget](../models/ipam/routetarget.md)
* [ipam.Service](../models/ipam/service.md)
* [ipam.VLAN](../models/ipam/vlan.md)
* [ipam.VRF](../models/ipam/vrf.md)
* [secrets.Secret](../models/secrets/secret.md)
* [tenancy.Tenant](../models/tenancy/tenant.md)
* [virtualization.Cluster](../models/virtualization/cluster.md)
* [virtualization.VirtualMachine](../models/virtualization/virtualmachine.md)
### Organizational Models
* [circuits.CircuitType](../models/circuits/circuittype.md)
* [dcim.DeviceRole](../models/dcim/devicerole.md)
* [dcim.Manufacturer](../models/dcim/manufacturer.md)
* [dcim.Platform](../models/dcim/platform.md)
* [dcim.RackRole](../models/dcim/rackrole.md)
* [ipam.RIR](../models/ipam/rir.md)
* [ipam.Role](../models/ipam/role.md)
* [ipam.VLANGroup](../models/ipam/vlangroup.md)
* [secrets.SecretRole](../models/secrets/secretrole.md)
* [virtualization.ClusterGroup](../models/virtualization/clustergroup.md)
* [virtualization.ClusterType](../models/virtualization/clustertype.md)
### Nested Group Models
* [dcim.Location](../models/dcim/location.md) (formerly RackGroup)
* [dcim.Region](../models/dcim/region.md)
* [dcim.SiteGroup](../models/dcim/sitegroup.md)
* [tenancy.TenantGroup](../models/tenancy/tenantgroup.md)
### Component Models
* [dcim.ConsolePort](../models/dcim/consoleport.md)
* [dcim.ConsoleServerPort](../models/dcim/consoleserverport.md)
* [dcim.DeviceBay](../models/dcim/devicebay.md)
* [dcim.FrontPort](../models/dcim/frontport.md)
* [dcim.Interface](../models/dcim/interface.md)
* [dcim.InventoryItem](../models/dcim/inventoryitem.md)
* [dcim.PowerOutlet](../models/dcim/poweroutlet.md)
* [dcim.PowerPort](../models/dcim/powerport.md)
* [dcim.RearPort](../models/dcim/rearport.md)
* [virtualization.VMInterface](../models/virtualization/vminterface.md)
### Component Template Models
* [dcim.ConsolePortTemplate](../models/dcim/consoleporttemplate.md)
* [dcim.ConsoleServerPortTemplate](../models/dcim/consoleserverporttemplate.md)
* [dcim.DeviceBayTemplate](../models/dcim/devicebaytemplate.md)
* [dcim.FrontPortTemplate](../models/dcim/frontporttemplate.md)
* [dcim.InterfaceTemplate](../models/dcim/interfacetemplate.md)
* [dcim.PowerOutletTemplate](../models/dcim/poweroutlettemplate.md)
* [dcim.PowerPortTemplate](../models/dcim/powerporttemplate.md)
* [dcim.RearPortTemplate](../models/dcim/rearporttemplate.md)

View File

@ -70,7 +70,11 @@ Ensure that continuous integration testing on the `develop` branch is completing
### Update Version and Changelog ### Update Version and Changelog
Update the `VERSION` constant in `settings.py` to the new release version and annotate the current data in the release notes for the new version. Commit these changes to the `develop` branch. * Update the `VERSION` constant in `settings.py` to the new release version.
* Update the example version numbers in the feature request and bug report templates under `.github/ISSUE_TEMPLATES/`.
* Replace the "FUTURE" placeholder in the release notes with the current date.
Commit these changes to the `develop` branch.
### Submit a Pull Request ### Submit a Pull Request

View File

@ -1,8 +1,8 @@
![NetBox](netbox_logo.svg "NetBox logo") ![NetBox](netbox_logo.svg "NetBox logo"){style="height: 100px; margin-bottom: 3em"}
# What is NetBox? # What is NetBox?
NetBox is an open source web application designed to help manage and document computer networks. Initially conceived by the network engineering team at [DigitalOcean](https://www.digitalocean.com/), NetBox was developed specifically to address the needs of network and infrastructure engineers. It encompasses the following aspects of network management: NetBox is an infrastructure resource modeling (IRM) application designed to empower network automation. Initially conceived by the network engineering team at [DigitalOcean](https://www.digitalocean.com/), NetBox was developed specifically to address the needs of network and infrastructure engineers. NetBox is made available as open source under the Apache 2 license. It encompasses the following aspects of network management:
* **IP address management (IPAM)** - IP networks and addresses, VRFs, and VLANs * **IP address management (IPAM)** - IP networks and addresses, VRFs, and VLANs
* **Equipment racks** - Organized by group and site * **Equipment racks** - Organized by group and site

View File

@ -7,24 +7,23 @@ This section entails the installation and configuration of a local PostgreSQL da
## Installation ## Installation
#### Ubuntu === "Ubuntu"
Install the PostgreSQL server and client development libraries using `apt`.
```no-highlight ```no-highlight
sudo apt update sudo apt update
sudo apt install -y postgresql libpq-dev sudo apt install -y postgresql
``` ```
#### CentOS === "CentOS"
PostgreSQL 9.6 and later are available natively on CentOS 8.2. If using an earlier CentOS release, you may need to [install it from an RPM](https://download.postgresql.org/pub/repos/yum/reporpms/EL-7-x86_64/).
```no-highlight ```no-highlight
sudo yum install -y postgresql-server libpq-devel sudo yum install -y postgresql-server
sudo postgresql-setup --initdb sudo postgresql-setup --initdb
``` ```
!!! info
PostgreSQL 9.6 and later are available natively on CentOS 8.2. If using an earlier CentOS release, you may need to [install it from an RPM](https://download.postgresql.org/pub/repos/yum/reporpms/EL-7-x86_64/).
CentOS configures ident host-based authentication for PostgreSQL by default. Because NetBox will need to authenticate using a username and password, modify `/var/lib/pgsql/data/pg_hba.conf` to support MD5 authentication by changing `ident` to `md5` for the lines below: CentOS configures ident host-based authentication for PostgreSQL by default. Because NetBox will need to authenticate using a username and password, modify `/var/lib/pgsql/data/pg_hba.conf` to support MD5 authentication by changing `ident` to `md5` for the lines below:
```no-highlight ```no-highlight
@ -32,7 +31,7 @@ host all all 127.0.0.1/32 md5
host all all ::1/128 md5 host all all ::1/128 md5
``` ```
Then, start the service and enable it to run at boot: Once PostgreSQL has been installed, start the service and enable it to run at boot:
```no-highlight ```no-highlight
sudo systemctl start postgresql sudo systemctl start postgresql

View File

@ -7,13 +7,13 @@
!!! note !!! note
NetBox v2.9.0 and later require Redis v4.0 or higher. If your distribution does not offer a recent enough release, you will need to build Redis from source. Please see [the Redis installation documentation](https://github.com/redis/redis) for further details. NetBox v2.9.0 and later require Redis v4.0 or higher. If your distribution does not offer a recent enough release, you will need to build Redis from source. Please see [the Redis installation documentation](https://github.com/redis/redis) for further details.
### Ubuntu === "Ubuntu"
```no-highlight ```no-highlight
sudo apt install -y redis-server sudo apt install -y redis-server
``` ```
### CentOS === "CentOS"
```no-highlight ```no-highlight
sudo yum install -y redis sudo yum install -y redis

View File

@ -9,16 +9,16 @@ Begin by installing all system packages required by NetBox and its dependencies.
!!! note !!! note
NetBox v2.8.0 and later require Python 3.6, 3.7, or 3.8. NetBox v2.8.0 and later require Python 3.6, 3.7, or 3.8.
### Ubuntu === "Ubuntu"
```no-highlight ```no-highlight
sudo apt install -y python3 python3-pip python3-venv python3-dev build-essential libxml2-dev libxslt1-dev libffi-dev libpq-dev libssl-dev zlib1g-dev sudo apt install -y python3 python3-pip python3-venv python3-dev build-essential libxml2-dev libxslt1-dev libffi-dev libpq-dev libssl-dev zlib1g-dev
``` ```
### CentOS === "CentOS"
```no-highlight ```no-highlight
sudo yum install -y gcc python36 python36-devel python3-pip libxml2-devel libxslt-devel libffi-devel openssl-devel redhat-rpm-config sudo yum install -y gcc python36 python36-devel python3-pip libxml2-devel libxslt-devel libffi-devel libpq-devel openssl-devel redhat-rpm-config
``` ```
Before continuing with either platform, update pip (Python's package management tool) to its latest release: Before continuing with either platform, update pip (Python's package management tool) to its latest release:
@ -57,13 +57,13 @@ sudo mkdir -p /opt/netbox/ && cd /opt/netbox/
If `git` is not already installed, install it: If `git` is not already installed, install it:
#### Ubuntu === "Ubuntu"
```no-highlight ```no-highlight
sudo apt install -y git sudo apt install -y git
``` ```
#### CentOS === "CentOS"
```no-highlight ```no-highlight
sudo yum install -y git sudo yum install -y git
@ -73,6 +73,11 @@ Next, clone the **master** branch of the NetBox GitHub repository into the curre
```no-highlight ```no-highlight
$ sudo git clone -b master https://github.com/netbox-community/netbox.git . $ sudo git clone -b master https://github.com/netbox-community/netbox.git .
```
The screen below should be the result:
```
Cloning into '.'... Cloning into '.'...
remote: Counting objects: 1994, done. remote: Counting objects: 1994, done.
remote: Compressing objects: 100% (150/150), done. remote: Compressing objects: 100% (150/150), done.
@ -89,14 +94,14 @@ Checking connectivity... done.
Create a system user account named `netbox`. We'll configure the WSGI and HTTP services to run under this account. We'll also assign this user ownership of the media directory. This ensures that NetBox will be able to save uploaded files. Create a system user account named `netbox`. We'll configure the WSGI and HTTP services to run under this account. We'll also assign this user ownership of the media directory. This ensures that NetBox will be able to save uploaded files.
#### Ubuntu === "Ubuntu"
``` ```
sudo adduser --system --group netbox sudo adduser --system --group netbox
sudo chown --recursive netbox /opt/netbox/netbox/media/ sudo chown --recursive netbox /opt/netbox/netbox/media/
``` ```
#### CentOS === "CentOS"
``` ```
sudo groupadd --system netbox sudo groupadd --system netbox
@ -113,7 +118,7 @@ cd /opt/netbox/netbox/netbox/
sudo cp configuration.example.py configuration.py sudo cp configuration.example.py configuration.py
``` ```
Open `configuration.py` with your preferred editor to begin configuring NetBox. NetBox offers [many configuration parameters](/configuration/), but only the following four are required for new installations: Open `configuration.py` with your preferred editor to begin configuring NetBox. NetBox offers [many configuration parameters](../configuration/index.md), but only the following four are required for new installations:
* `ALLOWED_HOSTS` * `ALLOWED_HOSTS`
* `DATABASE` * `DATABASE`
@ -136,7 +141,7 @@ ALLOWED_HOSTS = ['*']
### DATABASE ### DATABASE
This parameter holds the database configuration details. You must define the username and password used when you configured PostgreSQL. If the service is running on a remote host, update the `HOST` and `PORT` parameters accordingly. See the [configuration documentation](/configuration/required-settings/#database) for more detail on individual parameters. This parameter holds the database configuration details. You must define the username and password used when you configured PostgreSQL. If the service is running on a remote host, update the `HOST` and `PORT` parameters accordingly. See the [configuration documentation](../configuration/required-settings.md#database) for more detail on individual parameters.
```python ```python
DATABASE = { DATABASE = {
@ -151,7 +156,7 @@ DATABASE = {
### REDIS ### REDIS
Redis is a in-memory key-value store used by NetBox for caching and background task queuing. Redis typically requires minimal configuration; the values below should suffice for most installations. See the [configuration documentation](/configuration/required-settings/#redis) for more detail on individual parameters. Redis is a in-memory key-value store used by NetBox for caching and background task queuing. Redis typically requires minimal configuration; the values below should suffice for most installations. See the [configuration documentation](../configuration/required-settings.md#redis) for more detail on individual parameters.
Note that NetBox requires the specification of two separate Redis databases: `tasks` and `caching`. These may both be provided by the same Redis service, however each should have a unique numeric database ID. Note that NetBox requires the specification of two separate Redis databases: `tasks` and `caching`. These may both be provided by the same Redis service, however each should have a unique numeric database ID.
@ -198,15 +203,15 @@ All Python packages required by NetBox are listed in `requirements.txt` and will
The [NAPALM automation](https://napalm-automation.net/) library allows NetBox to fetch live data from devices and return it to a requester via its REST API. The `NAPALM_USERNAME` and `NAPALM_PASSWORD` configuration parameters define the credentials to be used when connecting to a device. The [NAPALM automation](https://napalm-automation.net/) library allows NetBox to fetch live data from devices and return it to a requester via its REST API. The `NAPALM_USERNAME` and `NAPALM_PASSWORD` configuration parameters define the credentials to be used when connecting to a device.
```no-highlight ```no-highlight
sudo echo napalm >> /opt/netbox/local_requirements.txt sudo sh -c "echo 'napalm' >> /opt/netbox/local_requirements.txt"
``` ```
### Remote File Storage ### Remote File Storage
By default, NetBox will use the local filesystem to store uploaded files. To use a remote filesystem, install the [`django-storages`](https://django-storages.readthedocs.io/en/stable/) library and configure your [desired storage backend](/configuration/optional-settings/#storage_backend) in `configuration.py`. By default, NetBox will use the local filesystem to store uploaded files. To use a remote filesystem, install the [`django-storages`](https://django-storages.readthedocs.io/en/stable/) library and configure your [desired storage backend](../configuration/optional-settings.md#storage_backend) in `configuration.py`.
```no-highlight ```no-highlight
sudo echo django-storages >> /opt/netbox/local_requirements.txt sudo sh -c "echo 'django-storages' >> /opt/netbox/local_requirements.txt"
``` ```
## Run the Upgrade Script ## Run the Upgrade Script
@ -262,9 +267,16 @@ Starting development server at http://0.0.0.0:8000/
Quit the server with CONTROL-C. Quit the server with CONTROL-C.
``` ```
!!! note
By default RHEL based distros will likely block your testing attempts with firewalld. The development server port can be opened with `firewall-cmd` (add `--permanent` if you want the rule to survive server restarts):
```no-highlight
firewall-cmd --zone=public --add-port=8000/tcp
```
Next, connect to the name or IP of the server (as defined in `ALLOWED_HOSTS`) on port 8000; for example, <http://127.0.0.1:8000/>. You should be greeted with the NetBox home page. Next, connect to the name or IP of the server (as defined in `ALLOWED_HOSTS`) on port 8000; for example, <http://127.0.0.1:8000/>. You should be greeted with the NetBox home page.
!!! warning !!! danger
The development server is for development and testing purposes only. It is neither performant nor secure enough for production use. **Do not use it in production.** The development server is for development and testing purposes only. It is neither performant nor secure enough for production use. **Do not use it in production.**
!!! warning !!! warning

View File

@ -30,7 +30,7 @@ pip3 install django-auth-ldap
Once installed, add the package to `local_requirements.txt` to ensure it is re-installed during future rebuilds of the virtual environment: Once installed, add the package to `local_requirements.txt` to ensure it is re-installed during future rebuilds of the virtual environment:
```no-highlight ```no-highlight
sudo echo django-auth-ldap >> /opt/netbox/local_requirements.txt sudo sh -c "echo 'django-auth-ldap' >> /opt/netbox/local_requirements.txt"
``` ```
## Configuration ## Configuration
@ -74,7 +74,7 @@ STARTTLS can be configured by setting `AUTH_LDAP_START_TLS = True` and using the
### User Authentication ### User Authentication
!!! info !!! info
When using Windows Server 2012, `AUTH_LDAP_USER_DN_TEMPLATE` should be set to None. When using Windows Server 2012+, `AUTH_LDAP_USER_DN_TEMPLATE` should be set to None.
```python ```python
from django_auth_ldap.config import LDAPSearch from django_auth_ldap.config import LDAPSearch
@ -142,7 +142,7 @@ AUTH_LDAP_CACHE_TIMEOUT = 3600
`systemctl restart netbox` restarts the NetBox service, and initiates any changes made to `ldap_config.py`. If there are syntax errors present, the NetBox process will not spawn an instance, and errors should be logged to `/var/log/messages`. `systemctl restart netbox` restarts the NetBox service, and initiates any changes made to `ldap_config.py`. If there are syntax errors present, the NetBox process will not spawn an instance, and errors should be logged to `/var/log/messages`.
For troubleshooting LDAP user/group queries, add or merge the following [logging](/configuration/optional-settings.md#logging) configuration to `configuration.py`: For troubleshooting LDAP user/group queries, add or merge the following [logging](../configuration/optional-settings.md#logging) configuration to `configuration.py`:
```python ```python
LOGGING = { LOGGING = {

View File

@ -23,6 +23,9 @@ The video below demonstrates the installation of NetBox v2.10.3 on Ubuntu 20.04
| PostgreSQL | 9.6 | | PostgreSQL | 9.6 |
| Redis | 4.0 | | Redis | 4.0 |
!!! note
Python 3.7 or later will be required in NetBox v3.0. Users are strongly encouraged to install NetBox using Python 3.7 or later for new deployments.
Below is a simplified overview of the NetBox application stack for reference: Below is a simplified overview of the NetBox application stack for reference:
![NetBox UI as seen by a non-authenticated user](../media/installation/netbox_application_stack.png) ![NetBox UI as seen by a non-authenticated user](../media/installation/netbox_application_stack.png)
@ -30,6 +33,3 @@ Below is a simplified overview of the NetBox application stack for reference:
## Upgrading ## Upgrading
If you are upgrading from an existing installation, please consult the [upgrading guide](upgrading.md). If you are upgrading from an existing installation, please consult the [upgrading guide](upgrading.md).
!!! note
Beginning with v2.5.9, the official documentation calls for systemd to be used for managing the WSGI workers in place of supervisord. Please see the instructions for [migrating to systemd](migrating-to-systemd.md) if you are still using supervisord.

View File

@ -2,7 +2,7 @@
## Review the Release Notes ## Review the Release Notes
Prior to upgrading your NetBox instance, be sure to carefully review all [release notes](../../release-notes/) that have been published since your current version was released. Although the upgrade process typically does not involve additional work, certain releases may introduce breaking or backward-incompatible changes. These are called out in the release notes under the release in which the change went into effect. Prior to upgrading your NetBox instance, be sure to carefully review all [release notes](../release-notes/index.md) that have been published since your current version was released. Although the upgrade process typically does not involve additional work, certain releases may introduce breaking or backward-incompatible changes. These are called out in the release notes under the release in which the change went into effect.
## Update Dependencies to Required Versions ## Update Dependencies to Required Versions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 35 KiB

View File

@ -2,9 +2,9 @@
The association of a circuit with a particular site and/or device is modeled separately as a circuit termination. A circuit may have up to two terminations, labeled A and Z. A single-termination circuit can be used when you don't know (or care) about the far end of a circuit (for example, an Internet access circuit which connects to a transit provider). A dual-termination circuit is useful for tracking circuits which connect two sites. The association of a circuit with a particular site and/or device is modeled separately as a circuit termination. A circuit may have up to two terminations, labeled A and Z. A single-termination circuit can be used when you don't know (or care) about the far end of a circuit (for example, an Internet access circuit which connects to a transit provider). A dual-termination circuit is useful for tracking circuits which connect two sites.
Each circuit termination is tied to a site, and may optionally be connected via a cable to a specific device interface or port within that site. Each termination must be assigned a port speed, and can optionally be assigned an upstream speed if it differs from the downstream speed (a common scenario with e.g. DOCSIS cable modems). Fields are also available to track cross-connect and patch panel details. Each circuit termination is attached to either a site or to a provider network. Site terminations may optionally be connected via a cable to a specific device interface or port within that site. Each termination must be assigned a port speed, and can optionally be assigned an upstream speed if it differs from the downstream speed (a common scenario with e.g. DOCSIS cable modems). Fields are also available to track cross-connect and patch panel details.
In adherence with NetBox's philosophy of closely modeling the real world, a circuit may terminate only to a physical interface. For example, circuits may not terminate to LAG interfaces, which are virtual in nature. In such cases, a separate physical circuit is associated with each LAG member interface and each needs to be modeled discretely. In adherence with NetBox's philosophy of closely modeling the real world, a circuit may be connected only to a physical interface. For example, circuits may not terminate to LAG interfaces, which are virtual in nature. In such cases, a separate physical circuit is associated with each LAG member interface and each needs to be modeled discretely.
!!! note !!! note
A circuit in NetBox represents a physical link, and cannot have more than two endpoints. When modeling a multi-point topology, each leg of the topology must be defined as a discrete circuit, with one end terminating within the provider's infrastructure. A circuit in NetBox represents a physical link, and cannot have more than two endpoints. When modeling a multi-point topology, each leg of the topology must be defined as a discrete circuit, with one end terminating within the provider's infrastructure. The provider network model is ideal for representing these networks.

View File

@ -0,0 +1,5 @@
# Provider Networks
This model can be used to represent the boundary of a provider network, the details of which are unknown or unimportant to the NetBox user. For example, it might represent a provider's regional MPLS network to which multiple circuits provide connectivity.
Each provider network must be assigned to a provider. A circuit may terminate to either a provider network or to a site.

View File

@ -8,7 +8,7 @@ A device is said to be full-depth if its installation on one rack face prevents
Each device must be instantiated from a pre-created device type, and its default components (console ports, power ports, interfaces, etc.) will be created automatically. (The device type associated with a device may be changed after its creation, however its components will not be updated retroactively.) Each device must be instantiated from a pre-created device type, and its default components (console ports, power ports, interfaces, etc.) will be created automatically. (The device type associated with a device may be changed after its creation, however its components will not be updated retroactively.)
Each device must be assigned a site, device role, and operational status, and may optionally be assigned to a specific rack within a site. A platform, serial number, and asset tag may optionally be assigned to each device. Each device must be assigned a site, device role, and operational status, and may optionally be assigned to a specific location and/or rack within a site. A platform, serial number, and asset tag may optionally be assigned to each device.
Device names must be unique within a site, unless the device has been assigned to a tenant. Devices may also be unnamed. Device names must be unique within a site, unless the device has been assigned to a tenant. Devices may also be unnamed.

View File

@ -2,11 +2,15 @@
Interfaces in NetBox represent network interfaces used to exchange data with connected devices. On modern networks, these are most commonly Ethernet, but other types are supported as well. Each interface must be assigned a type, and may optionally be assigned a MAC address, MTU, and IEEE 802.1Q mode (tagged or access). Each interface can also be enabled or disabled, and optionally designated as management-only (for out-of-band management). Interfaces in NetBox represent network interfaces used to exchange data with connected devices. On modern networks, these are most commonly Ethernet, but other types are supported as well. Each interface must be assigned a type, and may optionally be assigned a MAC address, MTU, and IEEE 802.1Q mode (tagged or access). Each interface can also be enabled or disabled, and optionally designated as management-only (for out-of-band management).
Interfaces may be physical or virtual in nature, but only physical interfaces may be connected via cables. Cables can connect interfaces to pass-through ports, circuit terminations, or other interfaces. !!! note
Although devices and virtual machines both can have interfaces, a separate model is used for each. Thus, device interfaces have some properties that are not present on virtual machine interfaces and vice versa.
### Interface Types
Interfaces may be physical or virtual in nature, but only physical interfaces may be connected via cables. Cables can connect interfaces to pass-through ports, circuit terminations, or other interfaces. Virtual interfaces, such as 802.1Q-tagged subinterfaces, may be assigned to physical parent interfaces.
Physical interfaces may be arranged into a link aggregation group (LAG) and associated with a parent LAG (virtual) interface. LAG interfaces can be recursively nested to model bonding of trunk groups. Like all virtual interfaces, LAG interfaces cannot be connected physically. Physical interfaces may be arranged into a link aggregation group (LAG) and associated with a parent LAG (virtual) interface. LAG interfaces can be recursively nested to model bonding of trunk groups. Like all virtual interfaces, LAG interfaces cannot be connected physically.
IP addresses can be assigned to interfaces. VLANs can also be assigned to each interface as either tagged or untagged. (An interface may have only one untagged VLAN.) ### IP Address Assignment
!!! note IP addresses can be assigned to interfaces. VLANs can also be assigned to each interface as either tagged or untagged. (An interface may have only one untagged VLAN.)
Although devices and virtual machines both can have interfaces, a separate model is used for each. Thus, device interfaces have some properties that are not present on virtual machine interfaces and vice versa.

View File

@ -0,0 +1,5 @@
# Locations
Racks and devices can be grouped by location within a site. A location may represent a floor, room, cage, or similar organizational unit. Locations can be nested to form a hierarchy. For example, you may have floors within a site, and rooms within a floor.
The name and facility ID of each rack within a location must be unique. (Racks not assigned to the same location may have identical names and/or facility IDs.)

View File

@ -1,6 +1,6 @@
# Power Feed # Power Feed
A power feed represents the distribution of power from a power panel to a particular device, typically a power distribution unit (PDU). The power pot (inlet) on a device can be connected via a cable to a power feed. A power feed may optionally be assigned to a rack to allow more easily tracking the distribution of power among racks. A power feed represents the distribution of power from a power panel to a particular device, typically a power distribution unit (PDU). The power port (inlet) on a device can be connected via a cable to a power feed. A power feed may optionally be assigned to a rack to allow more easily tracking the distribution of power among racks.
Each power feed is assigned an operational type (primary or redundant) and one of the following statuses: Each power feed is assigned an operational type (primary or redundant) and one of the following statuses:

View File

@ -2,7 +2,7 @@
A power panel represents the origin point in NetBox for electrical power being disseminated by one or more power feeds. In a data center environment, one power panel often serves a group of racks, with an individual power feed extending to each rack, though this is not always the case. It is common to have two sets of panels and feeds arranged in parallel to provide redundant power to each rack. A power panel represents the origin point in NetBox for electrical power being disseminated by one or more power feeds. In a data center environment, one power panel often serves a group of racks, with an individual power feed extending to each rack, though this is not always the case. It is common to have two sets of panels and feeds arranged in parallel to provide redundant power to each rack.
Each power panel must be assigned to a site, and may optionally be assigned to a particular rack group. Each power panel must be assigned to a site, and may optionally be assigned to a particular location within that site.
!!! note !!! note
NetBox does not model the mechanism by which power is delivered to a power panel. Power panels define the root level of the power distribution hierarchy in NetBox. NetBox does not model the mechanism by which power is delivered to a power panel. Power panels define the root level of the power distribution hierarchy in NetBox.

View File

@ -1,6 +1,6 @@
# Racks # Racks
The rack model represents a physical two- or four-post equipment rack in which devices can be installed. Each rack must be assigned to a site, and may optionally be assigned to a rack group and/or tenant. Racks can also be organized by user-defined functional roles. The rack model represents a physical two- or four-post equipment rack in which devices can be installed. Each rack must be assigned to a site, and may optionally be assigned to a location and/or tenant. Racks can also be organized by user-defined functional roles.
Rack height is measured in *rack units* (U); racks are commonly between 42U and 48U tall, but NetBox allows you to define racks of arbitrary height. A toggle is provided to indicate whether rack units are in ascending (from the ground up) or descending order. Rack height is measured in *rack units* (U); racks are commonly between 42U and 48U tall, but NetBox allows you to define racks of arbitrary height. A toggle is provided to indicate whether rack units are in ascending (from the ground up) or descending order.

View File

@ -1,7 +0,0 @@
# Rack Groups
Racks can be organized into groups, which can be nested into themselves similar to regions. As with sites, how you choose to designate rack groups will depend on the nature of your organization. For example, if each site represents a campus, each group might represent a building within a campus. If each site represents a building, each rack group might equate to a floor or room.
Each rack group must be assigned to a parent site, and rack groups may optionally be nested within a site to model a multi-level hierarchy. For example, you might have a tier of rooms beneath a tier of floors, all belonging to the same parent building (site).
The name and facility ID of each rack within a group must be unique. (Racks not assigned to the same rack group may have identical names and/or facility IDs.)

View File

@ -0,0 +1,3 @@
# Site Groups
Like regions, site groups can be used to organize sites. Whereas regions are intended to provide geographic organization, site groups can be used to classify sites by role or function. Also like regions, site groups can be nested to form a hierarchy. Sites which belong to a child group are also considered to be members of any of its parent groups.

View File

@ -3,11 +3,13 @@
Sometimes it is desirable to associate additional data with a group of devices or virtual machines to aid in automated configuration. For example, you might want to associate a set of syslog servers for all devices within a particular region. Context data enables the association of extra user-defined data with devices and virtual machines grouped by one or more of the following assignments: Sometimes it is desirable to associate additional data with a group of devices or virtual machines to aid in automated configuration. For example, you might want to associate a set of syslog servers for all devices within a particular region. Context data enables the association of extra user-defined data with devices and virtual machines grouped by one or more of the following assignments:
* Region * Region
* Site group
* Site * Site
* Device type (devices only)
* Role * Role
* Platform * Platform
* Cluster group * Cluster group (VMs only)
* Cluster * Cluster (VMs only)
* Tenant group * Tenant group
* Tenant * Tenant
* Tag * Tag

View File

@ -1,6 +1,6 @@
# VLANs # VLANs
A VLAN represents an isolated layer two domain, identified by a name and a numeric ID (1-4094) as defined in [IEEE 802.1Q](https://en.wikipedia.org/wiki/IEEE_802.1Q). Each VLAN may be assigned to a site, tenant, and/or VLAN group. A VLAN represents an isolated layer two domain, identified by a name and a numeric ID (1-4094) as defined in [IEEE 802.1Q](https://en.wikipedia.org/wiki/IEEE_802.1Q). VLANs are arranged into VLAN groups to define scope and to enforce uniqueness.
Each VLAN must be assigned one of the following operational statuses: Each VLAN must be assigned one of the following operational statuses:

View File

@ -1,5 +1,5 @@
# VLAN Groups # VLAN Groups
VLAN groups can be used to organize VLANs within NetBox. Each group may optionally be assigned to a specific site, but a group cannot belong to multiple sites. VLAN groups can be used to organize VLANs within NetBox. Each VLAN group can be scoped to a particular region, site group, site, location, rack, cluster group, or cluster. Member VLANs will be available for assignment to devices and/or virtual machines within the specified scope.
Groups can also be used to enforce uniqueness: Each VLAN within a group must have a unique ID and name. VLANs which are not assigned to a group may have overlapping names and IDs (including VLANs which belong to a common site). For example, you can create two VLANs with ID 123, but they cannot both be assigned to the same group. Groups can also be used to enforce uniqueness: Each VLAN within a group must have a unique ID and name. VLANs which are not assigned to a group may have overlapping names and IDs (including VLANs which belong to a common site). For example, you can create two VLANs with ID 123, but they cannot both be assigned to the same group.

View File

@ -11,4 +11,6 @@ Like devices, each VM can be assigned a platform and/or functional role, and mus
* Failed * Failed
* Decommissioning * Decommissioning
Additional fields are available for annotating the vCPU count, memory (GB), and disk (GB) allocated to each VM. Each VM may optionally be assigned to a tenant. Virtual machines may have virtual interfaces assigned to them, but do not support any physical component. Additional fields are available for annotating the vCPU count, memory (GB), and disk (GB) allocated to each VM. A VM may be allocated a partial vCPU count (e.g. 1.5 vCPU).
Each VM may optionally be assigned to a tenant. Virtual machines may have virtual interfaces assigned to them, but do not support any physical component.

View File

@ -89,3 +89,58 @@ Restart the WSGI service to load the new plugin:
```no-highlight ```no-highlight
# sudo systemctl restart netbox # 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
```

View File

@ -1 +1 @@
version-2.10.md version-2.11.md

View File

@ -1,5 +1,82 @@
# NetBox v2.10 # NetBox v2.10
## v2.10.10 (2021-04-15)
### Enhancements
* [#5796](https://github.com/netbox-community/netbox/issues/5796) - Add DC terminal power port, outlet types
* [#5980](https://github.com/netbox-community/netbox/issues/5980) - Add Saf-D-Grid power port, outlet types
* [#6157](https://github.com/netbox-community/netbox/issues/6157) - Support Markdown rendering for report logs
* [#6160](https://github.com/netbox-community/netbox/issues/6160) - Add F connector port type
* [#6168](https://github.com/netbox-community/netbox/issues/6168) - Add SFP56 50GE interface type
### Bug Fixes
* [#5419](https://github.com/netbox-community/netbox/issues/5419) - Update parent device/VM when deleting a primary IP
* [#5643](https://github.com/netbox-community/netbox/issues/5643) - Fix VLAN assignment when editing VM interfaces in bulk
* [#5652](https://github.com/netbox-community/netbox/issues/5652) - Update object data when renaming a custom field
* [#6056](https://github.com/netbox-community/netbox/issues/6056) - Optimize change log cleanup
* [#6144](https://github.com/netbox-community/netbox/issues/6144) - Fix MAC address field display in VM interfaces search form
* [#6152](https://github.com/netbox-community/netbox/issues/6152) - Fix custom field filtering for cables, virtual chassis
* [#6162](https://github.com/netbox-community/netbox/issues/6162) - Fix choice field filters (multiple models)
---
## v2.10.9 (2021-04-12)
### Enhancements
* [#5526](https://github.com/netbox-community/netbox/issues/5526) - Add MAC address search field to VM interfaces list
* [#5756](https://github.com/netbox-community/netbox/issues/5756) - Omit child devices from non-racked devices list under rack view
* [#5840](https://github.com/netbox-community/netbox/issues/5840) - Add column to cable termination objects to display cable color
* [#6054](https://github.com/netbox-community/netbox/issues/6054) - Display NAPALM-enabled device tabs only when relevant
* [#6083](https://github.com/netbox-community/netbox/issues/6083) - Support disabling TLS certificate validation for Redis
### Bug Fixes
* [#5805](https://github.com/netbox-community/netbox/issues/5805) - Fix missing custom field filters for cables, rack reservations
* [#6070](https://github.com/netbox-community/netbox/issues/6070) - Add missing `count_ipaddresses` attribute to VMInterface serializer
* [#6073](https://github.com/netbox-community/netbox/issues/6073) - Permit users to manage their own REST API tokens without needing explicit permission
* [#6081](https://github.com/netbox-community/netbox/issues/6081) - Fix interface connections REST API endpoint
* [#6082](https://github.com/netbox-community/netbox/issues/6082) - Support colons in webhook header values
* [#6108](https://github.com/netbox-community/netbox/issues/6108) - Do not infer tenant assignment from parent objects for prefixes, IP addresses
* [#6117](https://github.com/netbox-community/netbox/issues/6117) - Handle exception when attempting to assign an MPTT-enabled model as its own parent
* [#6131](https://github.com/netbox-community/netbox/issues/6131) - Correct handling of boolean fields when cloning objects
---
## v2.10.8 (2021-03-26)
### Bug Fixes
* [#6060](https://github.com/netbox-community/netbox/issues/6060) - Fix exception on cable trace in UI (regression from #5650)
---
## v2.10.7 (2021-03-25)
### Enhancements
* [#5641](https://github.com/netbox-community/netbox/issues/5641) - Allow filtering device components by label
* [#5723](https://github.com/netbox-community/netbox/issues/5723) - Allow customization of the geographic mapping service via `MAPS_URL` config parameter
* [#5736](https://github.com/netbox-community/netbox/issues/5736) - Allow changing site assignment when bulk editing devices
* [#5953](https://github.com/netbox-community/netbox/issues/5953) - Support Markdown rendering for custom script descriptions
* [#6040](https://github.com/netbox-community/netbox/issues/6040) - Add UI search fields for asset tag for devices and racks
### Bug Fixes
* [#5595](https://github.com/netbox-community/netbox/issues/5595) - Restore ability to delete an uploaded device type image
* [#5650](https://github.com/netbox-community/netbox/issues/5650) - Denote when the total length of a cable trace may exceed the indicated value
* [#5962](https://github.com/netbox-community/netbox/issues/5962) - Ensure consistent display of change log action labels
* [#5966](https://github.com/netbox-community/netbox/issues/5966) - Skip Markdown reference link when tabbing through form fields
* [#5977](https://github.com/netbox-community/netbox/issues/5977) - Correct validation of `RELEASE_CHECK_URL` config parameter
* [#6006](https://github.com/netbox-community/netbox/issues/6006) - Fix VLAN group/site association for bulk prefix import
* [#6010](https://github.com/netbox-community/netbox/issues/6010) - Eliminate duplicate virtual chassis search results
* [#6012](https://github.com/netbox-community/netbox/issues/6012) - Pre-populate attributes when creating an available child prefix via the UI
* [#6023](https://github.com/netbox-community/netbox/issues/6023) - Fix display of bottom banner with uBlock Origin enabled
---
## v2.10.6 (2021-03-09) ## v2.10.6 (2021-03-09)
### Enhancements ### Enhancements
@ -19,6 +96,8 @@
* [#5935](https://github.com/netbox-community/netbox/issues/5935) - Fix filtering prefixes list by multiple prefix values * [#5935](https://github.com/netbox-community/netbox/issues/5935) - Fix filtering prefixes list by multiple prefix values
* [#5948](https://github.com/netbox-community/netbox/issues/5948) - Invalidate cached queries when running `renaturalize` * [#5948](https://github.com/netbox-community/netbox/issues/5948) - Invalidate cached queries when running `renaturalize`
---
## v2.10.5 (2021-02-24) ## v2.10.5 (2021-02-24)
### Bug Fixes ### Bug Fixes

View File

@ -0,0 +1,443 @@
# NetBox v2.11
## v2.11.12 (2021-08-23)
### Enhancements
* [#6748](https://github.com/netbox-community/netbox/issues/6748) - Add site group filter to devices list
* [#6790](https://github.com/netbox-community/netbox/issues/6790) - Recognize a /32 IPv4 address as a child of a /32 IPv4 prefix
* [#6872](https://github.com/netbox-community/netbox/issues/6872) - Add table configuration button to child prefixes view
* [#6929](https://github.com/netbox-community/netbox/issues/6929) - Introduce `LOGIN_PERSISTENCE` configuration parameter to persist user sessions
* [#7011](https://github.com/netbox-community/netbox/issues/7011) - Add search field to VM interfaces filter form
### Bug Fixes
* [#5968](https://github.com/netbox-community/netbox/issues/5968) - Model forms should save empty custom field values as null
* [#6326](https://github.com/netbox-community/netbox/issues/6326) - Enable filtering assigned VLANs by group in interface edit form
* [#6686](https://github.com/netbox-community/netbox/issues/6686) - Force assignment of null custom field values to objects
* [#6776](https://github.com/netbox-community/netbox/issues/6776) - Fix erroneous webhook dispatch on failure to save objects
* [#6974](https://github.com/netbox-community/netbox/issues/6974) - Show contextual label for IP address role
* [#7012](https://github.com/netbox-community/netbox/issues/7012) - Fix hidden "add components" dropdown on devices list
---
## v2.11.11 (2021-08-12)
### Enhancements
* [#6883](https://github.com/netbox-community/netbox/issues/6883) - Add C21 & C22 power types
* [#6921](https://github.com/netbox-community/netbox/issues/6921) - Employ a sandbox when rendering Jinja2 code for increased security
### Bug Fixes
* [#6740](https://github.com/netbox-community/netbox/issues/6740) - Add import button to VM interfaces list
* [#6892](https://github.com/netbox-community/netbox/issues/6892) - Fix validation of unit ranges when creating a rack reservation
* [#6896](https://github.com/netbox-community/netbox/issues/6896) - Fix validation of IP address assigned as device/VM primary via NAT relation
* [#6902](https://github.com/netbox-community/netbox/issues/6902) - Populate device field when cloning device components
* [#6908](https://github.com/netbox-community/netbox/issues/6908) - Allow assignment of scope to VLAN groups upon import
* [#6909](https://github.com/netbox-community/netbox/issues/6909) - Remove extraneous `site` column from VLAN group import form
* [#6910](https://github.com/netbox-community/netbox/issues/6910) - Fix exception on invalid CSV import column name
* [#6918](https://github.com/netbox-community/netbox/issues/6918) - Fix return URL persistence when adding multiple objects sequentially
* [#6935](https://github.com/netbox-community/netbox/issues/6935) - Remove extraneous columns from inventory item and device bay tables
* [#6936](https://github.com/netbox-community/netbox/issues/6936) - Add missing `parent` column to inventory item import form
---
## v2.11.10 (2021-07-28)
### Enhancements
* [#6560](https://github.com/netbox-community/netbox/issues/6560) - Enable CSV import via uploaded file
* [#6644](https://github.com/netbox-community/netbox/issues/6644) - Add 6P/4P pass-through port types
* [#6771](https://github.com/netbox-community/netbox/issues/6771) - Add count of inventory items to manufacturer view
* [#6785](https://github.com/netbox-community/netbox/issues/6785) - Add "hardwired" type for power port types
### Bug Fixes
* [#5442](https://github.com/netbox-community/netbox/issues/5442) - Fix assignment of permissions based on LDAP groups
* [#5627](https://github.com/netbox-community/netbox/issues/5627) - Fix filtering of interface connections list
* [#6759](https://github.com/netbox-community/netbox/issues/6759) - Fix assignment of parent interfaces for bulk import
* [#6773](https://github.com/netbox-community/netbox/issues/6773) - Add missing `display` field to rack unit serializer
* [#6774](https://github.com/netbox-community/netbox/issues/6774) - Fix A/Z assignment when swapping circuit terminations
* [#6777](https://github.com/netbox-community/netbox/issues/6777) - Fix default value validation for custom text fields
* [#6778](https://github.com/netbox-community/netbox/issues/6778) - Rack reservation should display rack's location
* [#6780](https://github.com/netbox-community/netbox/issues/6780) - Include rack location in navigation breadcrumbs
* [#6794](https://github.com/netbox-community/netbox/issues/6794) - Fix device name display on device status view
* [#6812](https://github.com/netbox-community/netbox/issues/6812) - Limit reported prefix utilization to 100%
* [#6822](https://github.com/netbox-community/netbox/issues/6822) - Use consistent maximum value for interface MTU
### Other Changes
* [#6781](https://github.com/netbox-community/netbox/issues/6781) - Database query caching is now disabled by default
---
## v2.11.9 (2021-07-08)
### Bug Fixes
* [#6456](https://github.com/netbox-community/netbox/issues/6456) - API schema type should be boolean for `_occupied` on cable termination models
* [#6710](https://github.com/netbox-community/netbox/issues/6710) - Fix assignment of VM interface parent via REST API
* [#6714](https://github.com/netbox-community/netbox/issues/6714) - Fix rendering of device type component creation forms
---
## v2.11.8 (2021-07-06)
### Enhancements
* [#5503](https://github.com/netbox-community/netbox/issues/5503) - Annotate short date & time fields with their longer form
* [#6138](https://github.com/netbox-community/netbox/issues/6138) - Add an `empty` filter modifier for character fields
* [#6200](https://github.com/netbox-community/netbox/issues/6200) - Add rack reservations to global search
* [#6368](https://github.com/netbox-community/netbox/issues/6368) - Enable virtual chassis assignment during bulk import of devices
* [#6620](https://github.com/netbox-community/netbox/issues/6620) - Show assigned VMs count under device role view
* [#6666](https://github.com/netbox-community/netbox/issues/6666) - Show management-only status under interface detail view
* [#6667](https://github.com/netbox-community/netbox/issues/6667) - Display VM memory as GB/TB as appropriate
### Bug Fixes
* [#6626](https://github.com/netbox-community/netbox/issues/6626) - Fix site field on VM search form; add site group
* [#6637](https://github.com/netbox-community/netbox/issues/6637) - Fix group assignment in "available VLANs" link under VLAN group view
* [#6640](https://github.com/netbox-community/netbox/issues/6640) - Disallow numeric values in custom text fields
* [#6652](https://github.com/netbox-community/netbox/issues/6652) - Fix exception when adding components in bulk to multiple devices
* [#6676](https://github.com/netbox-community/netbox/issues/6676) - Fix device/VM counts per cluster under cluster type/group views
* [#6680](https://github.com/netbox-community/netbox/issues/6680) - Allow setting custom field values for VM interfaces on initial creation
* [#6695](https://github.com/netbox-community/netbox/issues/6695) - Fix exception when importing device type with invalid front port definition
---
## v2.11.7 (2021-06-16)
### Enhancements
* [#6455](https://github.com/netbox-community/netbox/issues/6455) - Permit /32 IPv4 and /128 IPv6 prefixes
* [#6493](https://github.com/netbox-community/netbox/issues/6493) - Show change log diff for non-atomic (pre-2.11) changes
* [#6564](https://github.com/netbox-community/netbox/issues/6564) - Add N connector type for pass-through ports
* [#6588](https://github.com/netbox-community/netbox/issues/6588) - Add support for webp files as front/rear device type images
* [#6589](https://github.com/netbox-community/netbox/issues/6589) - Standardize breadcrumb navigation for power panels and feeds
### Bug Fixes
* [#6553](https://github.com/netbox-community/netbox/issues/6553) - ProviderNetwork search should match on name
* [#6562](https://github.com/netbox-community/netbox/issues/6562) - Disable ordering of secrets by assigned object
* [#6563](https://github.com/netbox-community/netbox/issues/6563) - Fix filtering by location for cable connection forms
* [#6584](https://github.com/netbox-community/netbox/issues/6584) - Fix ordering of nested inventory items
* [#6602](https://github.com/netbox-community/netbox/issues/6602) - Fix deletion of devices with cables attached
---
## v2.11.6 (2021-06-04)
### Bug Fixes
* [#6544](https://github.com/netbox-community/netbox/issues/6544) - Fix migration error when upgrading with VRF(s) defined
---
## v2.11.5 (2021-06-04)
**NOTE:** This release includes a database migration that calculates and annotates prefix depth. It may impose a noticeable delay on the upgrade process: Users should anticipate roughly one minute of delay per 100 thousand prefixes being updated.
### Enhancements
* [#6087](https://github.com/netbox-community/netbox/issues/6087) - Improved prefix hierarchy rendering
* [#6487](https://github.com/netbox-community/netbox/issues/6487) - Add location filter to cable connection form
* [#6501](https://github.com/netbox-community/netbox/issues/6501) - Expose prefix depth and children on REST API serializer
* [#6527](https://github.com/netbox-community/netbox/issues/6527) - Support Markdown for report descriptions
* [#6540](https://github.com/netbox-community/netbox/issues/6540) - Add a "flat" column to the prefix table
### Bug Fixes
* [#6064](https://github.com/netbox-community/netbox/issues/6064) - Fix object permission assignments for user and group models
* [#6217](https://github.com/netbox-community/netbox/issues/6217) - Disallow passing of string values for integer custom fields
* [#6284](https://github.com/netbox-community/netbox/issues/6284) - Avoid sending redundant webhooks when adding/removing tags
* [#6492](https://github.com/netbox-community/netbox/issues/6492) - Correct tag population in post-change data resulting from REST API changes
* [#6496](https://github.com/netbox-community/netbox/issues/6496) - Fix upgrade script when Python installed in nonstandard path
* [#6502](https://github.com/netbox-community/netbox/issues/6502) - Correct permissions evaluation for running a report via the REST API
* [#6517](https://github.com/netbox-community/netbox/issues/6517) - Fix assignment of user when creating rack reservations via REST API
* [#6525](https://github.com/netbox-community/netbox/issues/6525) - Paginate related IPs table under IP address view
---
## v2.11.4 (2021-05-25)
### Enhancements
* [#5121](https://github.com/netbox-community/netbox/issues/5121) - Add content type filters for tags
* [#6358](https://github.com/netbox-community/netbox/issues/6358) - Add search field for VLAN groups
* [#6393](https://github.com/netbox-community/netbox/issues/6393) - Add `description` filter for IP addresses
* [#6400](https://github.com/netbox-community/netbox/issues/6400) - Add cyan color choice for plugin buttons
* [#6422](https://github.com/netbox-community/netbox/issues/6422) - Enable filtering users by group under admin UI
* [#6441](https://github.com/netbox-community/netbox/issues/6441) - Improve UI paginator to optimize page object count
### Bug Fixes
* [#6376](https://github.com/netbox-community/netbox/issues/6376) - Fix assignment of VLAN groups to clusters, cluster groups via REST API
* [#6398](https://github.com/netbox-community/netbox/issues/6398) - Avoid exception when deleting device connected to self via circuit
* [#6426](https://github.com/netbox-community/netbox/issues/6426) - Allow assigning virtual chassis member interfaces to LAG on VC master
* [#6438](https://github.com/netbox-community/netbox/issues/6438) - Fix missing descriptions and label for device type imports and exports
* [#6465](https://github.com/netbox-community/netbox/issues/6465) - Fix typo in installed plugins REST API endpoint
* [#6467](https://github.com/netbox-community/netbox/issues/6467) - Fix access to metrics on custom `BASE_PATH` when login is required
* [#6468](https://github.com/netbox-community/netbox/issues/6468) - Disable ordering VLAN groups list by scope object
---
## v2.11.3 (2021-05-07)
### Enhancements
* [#6197](https://github.com/netbox-community/netbox/issues/6197) - Introduced `SESSION_COOKIE_NAME` config parameter
* [#6318](https://github.com/netbox-community/netbox/issues/6318) - Add OM5 MMF cable type
* [#6351](https://github.com/netbox-community/netbox/issues/6351) - Add aggregates count to tenant view
* [#6359](https://github.com/netbox-community/netbox/issues/6359) - Enable custom links for organizational and nested group models
### Bug Fixes
* [#6240](https://github.com/netbox-community/netbox/issues/6240) - Fix display of available VLAN ranges under VLAN group view
* [#6308](https://github.com/netbox-community/netbox/issues/6308) - Fix linking of available VLANs in VLAN group view
* [#6309](https://github.com/netbox-community/netbox/issues/6309) - Restrict parent VM interface assignment to the parent VM
* [#6312](https://github.com/netbox-community/netbox/issues/6312) - Interface device filter should return all virtual chassis interfaces only if device is master
* [#6313](https://github.com/netbox-community/netbox/issues/6313) - Fix device type instance count under manufacturer view
* [#6321](https://github.com/netbox-community/netbox/issues/6321) - Restore "add an IP" button under prefix IPs view
* [#6333](https://github.com/netbox-community/netbox/issues/6333) - Fix filtering of circuit terminations by primary key
* [#6339](https://github.com/netbox-community/netbox/issues/6339) - Improve ordering of interfaces when viewing virtual chassis master
* [#6350](https://github.com/netbox-community/netbox/issues/6350) - Include first & last IP addresses when allocating available IPv6 addresses via the REST API
* [#6355](https://github.com/netbox-community/netbox/issues/6355) - Fix caching error when swapping A/Z circuit terminations
* [#6357](https://github.com/netbox-community/netbox/issues/6357) - Fix ProviderNetwork nested API serializer
* [#6363](https://github.com/netbox-community/netbox/issues/6363) - Correct pre-population of cluster group when creating a cluster
* [#6369](https://github.com/netbox-community/netbox/issues/6369) - Fix interface assignment for VLANs in non-scoped groups
---
## v2.11.2 (2021-04-27)
### Enhancements
* [#6275](https://github.com/netbox-community/netbox/issues/6275) - Linkify rack, device counts on locations list
* [#6278](https://github.com/netbox-community/netbox/issues/6278) - Note device locations on cable traces
* [#6287](https://github.com/netbox-community/netbox/issues/6287) - Add option to clear assigned max length filter on prefixes list
### Bug Fixes
* [#6236](https://github.com/netbox-community/netbox/issues/6236) - Journal entry title should account for configured timezone
* [#6246](https://github.com/netbox-community/netbox/issues/6246) - Permit full-length descriptions when creating device components and VM interfaces
* [#6248](https://github.com/netbox-community/netbox/issues/6248) - Fix table column reconfiguration under Chrome
* [#6252](https://github.com/netbox-community/netbox/issues/6252) - Fix assignment of console port speed values above 19.2kbps
* [#6254](https://github.com/netbox-community/netbox/issues/6254) - Disable ordering of space column in racks table
* [#6258](https://github.com/netbox-community/netbox/issues/6258) - Fix parent assignment for SiteGroup API serializer
* [#6262](https://github.com/netbox-community/netbox/issues/6262) - Support filtering by created/updated time for all relevant objects
* [#6267](https://github.com/netbox-community/netbox/issues/6267) - Fix cable tracing API endpoint for circuit terminations
* [#6289](https://github.com/netbox-community/netbox/issues/6289) - Fix assignment of VC member interfaces to LAG interfaces
---
## v2.11.1 (2021-04-21)
### Enhancements
* [#6161](https://github.com/netbox-community/netbox/issues/6161) - Enable ordering of device component tables
* [#6179](https://github.com/netbox-community/netbox/issues/6179) - Enable natural ordering for virtual machines
* [#6189](https://github.com/netbox-community/netbox/issues/6189) - Add ability to search for locations by name or description
* [#6190](https://github.com/netbox-community/netbox/issues/6190) - Allow filtering devices with no location assigned
* [#6210](https://github.com/netbox-community/netbox/issues/6210) - Include child locations on location view
### Bug Fixes
* [#6184](https://github.com/netbox-community/netbox/issues/6184) - Fix parent object table column in prefix IP addresses list
* [#6188](https://github.com/netbox-community/netbox/issues/6188) - Support custom field filtering for regions, site groups, and locations
* [#6196](https://github.com/netbox-community/netbox/issues/6196) - Fix object list display for users with read-only permissions
* [#6215](https://github.com/netbox-community/netbox/issues/6215) - Restore tenancy section in virtual machine form
---
## v2.11.0 (2021-04-16)
**Note:** NetBox v2.11 is the last major release that will support Python 3.6. Beginning with NetBox v3.0, Python 3.7 or later will be required.
### Breaking Changes
* All objects now use numeric IDs in their UI view URLs instead of slugs. You may need to update external references to NetBox objects. (Note that this does _not_ affect the REST API.)
* The UI now uses numeric IDs when filtering object lists. You may need to update external links to filtered object lists. (Note that the slug- and name-based filters will continue to work, however the filter selection fields within the UI will not be automatically populated.)
* The RackGroup model has been renamed to Location (see [#4971](https://github.com/netbox-community/netbox/issues/4971)). Its REST API endpoint has changed from `/api/dcim/rack-groups/` to `/api/dcim/locations/`.
* The foreign key field `group` on dcim.Rack has been renamed to `location`.
* The foreign key field `site` on ipam.VLANGroup has been replaced with the `scope` generic foreign key (see [#5284](https://github.com/netbox-community/netbox/issues/5284)).
* Custom script ObjectVars no longer support the `queryset` parameter: Use `model` instead (see [#5995](https://github.com/netbox-community/netbox/issues/5995)).
### New Features
#### Journaling Support ([#151](https://github.com/netbox-community/netbox/issues/151))
NetBox now supports journaling for all primary objects. The journal is a collection of human-generated notes and comments about an object maintained for historical context. It supplements NetBox's change log to provide additional information about why changes have been made or to convey events which occur outside NetBox. Unlike the change log, in which records typically expire after some time, journal entries persist for the life of the associated object.
#### Parent Interface Assignments ([#1519](https://github.com/netbox-community/netbox/issues/1519))
Virtual device and VM interfaces can now be assigned to a "parent" interface by setting the `parent` field on the interface object. This is helpful for associating subinterfaces with their physical counterpart. For example, you might assign virtual interfaces Gi0/0.100 and Gi0/0.200 as children of the physical interface Gi0/0.
#### Pre- and Post-Change Snapshots in Webhooks ([#3451](https://github.com/netbox-community/netbox/issues/3451))
In conjunction with the newly improved change logging functionality ([#5913](https://github.com/netbox-community/netbox/issues/5913)), outgoing webhooks now include both pre- and post-change representations of the modified object. These are available in the rendering context as a dictionary named `snapshots` with keys `prechange` and `postchange`. For example, here are the abridged snapshots resulting from renaming a site and changing its status:
```json
"snapshots": {
"prechange": {
"name": "Site 1",
"slug": "site-1",
"status": "active",
...
},
"postchange": {
"name": "Site 2",
"slug": "site-2",
"status": "planned",
...
}
}
```
Note: The pre-change snapshot for a newly created will always be null, as will the post-change snapshot for a deleted object.
#### Mark as Connected Without a Cable ([#3648](https://github.com/netbox-community/netbox/issues/3648))
Cable termination objects (circuit terminations, power feeds, and most device components) can now be marked as "connected" without actually attaching a cable. This helps simplify the process of modeling an infrastructure boundary where we don't necessarily know or care what is connected to an attachment point, but still need to reflect the termination as being occupied.
In addition to the new `mark_connected` boolean field, the REST API representation of these objects now also includes a read-only boolean field named `_occupied`. This conveniently returns true if either a cable is attached or `mark_connected` is true.
#### Allow Assigning Devices to Locations ([#4971](https://github.com/netbox-community/netbox/issues/4971))
Devices can now be assigned to locations (formerly known as rack groups) within a site without needing to be assigned to a particular rack. This is handy for assigning devices to rooms or floors within a building where racks are not used. The `location` foreign key field has been added to the Device model to support this.
#### Dynamic Object Exports ([#4999](https://github.com/netbox-community/netbox/issues/4999))
When exporting a list of objects in NetBox, users now have the option of selecting the "current view". This will render CSV output matching the current configuration of the table being viewed. For example, if you modify the sites list to display only the site name, tenant, and status, the rendered CSV will include only these columns, and they will appear in the order chosen.
The legacy static export behavior has been retained to ensure backward compatibility for dependent integrations. However, users are strongly encouraged to adapt custom export templates where needed as this functionality will be removed in v3.0.
#### Variable Scope Support for VLAN Groups ([#5284](https://github.com/netbox-community/netbox/issues/5284))
In previous releases, VLAN groups could be assigned only to a site. To afford more flexibility in conveying the true scope of an L2 domain, a VLAN group can now be assigned to a region, site group (new in v2.11), site, location, or rack. VLANs assigned to a group will be available only to devices and virtual machines which exist within its scope.
For example, a VLAN within a group assigned to a location will be available only to devices assigned to that location (or one of its child locations), or to a rack within that location.
#### New Site Group Model ([#5892](https://github.com/netbox-community/netbox/issues/5892))
This release introduces the new SiteGroup model, which can be used to organize sites similar to the existing Region model. Whereas regions are intended for geographically arranging sites into countries, states, and so on, the new site group model can be used to organize sites by functional role or other arbitrary classification. Using regions and site groups in conjunction provides two dimensions along which sites can be organized, offering greater flexibility to the user.
#### Improved Change Logging ([#5913](https://github.com/netbox-community/netbox/issues/5913))
The ObjectChange model (which is used to record the creation, modification, and deletion of NetBox objects) now explicitly records the pre-change and post-change state of each object, rather than only the post-change state. This was done to present a more clear depiction of each change being made, and to prevent the erroneous association of a previous unlogged change with its successor.
#### Provider Network Modeling ([#5986](https://github.com/netbox-community/netbox/issues/5986))
A new provider network model has been introduced to represent the boundary of a network that exists outside the scope of NetBox. Each instance of this model must be assigned to a provider, and circuits can now terminate to either provider networks or to sites. The use of this model will likely be extended by future releases to support overlay and virtual circuit modeling.
### Enhancements
* [#4833](https://github.com/netbox-community/netbox/issues/4833) - Allow assigning config contexts by device type
* [#5344](https://github.com/netbox-community/netbox/issues/5344) - Add support for custom fields in tables
* [#5370](https://github.com/netbox-community/netbox/issues/5370) - Extend custom field support to organizational models
* [#5375](https://github.com/netbox-community/netbox/issues/5375) - Add `speed` attribute to console port models
* [#5401](https://github.com/netbox-community/netbox/issues/5401) - Extend custom field support to device component models
* [#5425](https://github.com/netbox-community/netbox/issues/5425) - Create separate tabs for VMs and devices under the cluster view
* [#5451](https://github.com/netbox-community/netbox/issues/5451) - Add support for multiple-selection custom fields
* [#5608](https://github.com/netbox-community/netbox/issues/5608) - Add REST API endpoint for custom links
* [#5610](https://github.com/netbox-community/netbox/issues/5610) - Add REST API endpoint for webhooks
* [#5757](https://github.com/netbox-community/netbox/issues/5757) - Add unique identifier to every object view
* [#5830](https://github.com/netbox-community/netbox/issues/5830) - Add `as_attachment` to ExportTemplate to control download behavior
* [#5848](https://github.com/netbox-community/netbox/issues/5848) - Filter custom fields by content type in format `<app_label>.<model>`
* [#5891](https://github.com/netbox-community/netbox/issues/5891) - Add `display` field to all REST API serializers
* [#5894](https://github.com/netbox-community/netbox/issues/5894) - Use primary keys when filtering object lists by related objects in the UI
* [#5895](https://github.com/netbox-community/netbox/issues/5895) - Rename RackGroup to Location
* [#5901](https://github.com/netbox-community/netbox/issues/5901) - Add `created` and `last_updated` fields to device component models
* [#5971](https://github.com/netbox-community/netbox/issues/5971) - Add dedicated views for organizational models
* [#5972](https://github.com/netbox-community/netbox/issues/5972) - Enable bulk editing for organizational models
* [#5975](https://github.com/netbox-community/netbox/issues/5975) - Allow partial (decimal) vCPU allocations for virtual machines
* [#6001](https://github.com/netbox-community/netbox/issues/6001) - Paginate component tables under device views
* [#6038](https://github.com/netbox-community/netbox/issues/6038) - Include tagged objects list on tag view
* [#6088](https://github.com/netbox-community/netbox/issues/6088) - Improved table configuration form
* [#6097](https://github.com/netbox-community/netbox/issues/6097) - Redirect old slug-based object views
* [#6125](https://github.com/netbox-community/netbox/issues/6125) - Add locations count to home page
* [#6146](https://github.com/netbox-community/netbox/issues/6146) - Add bulk disconnect support for power feeds
* [#6149](https://github.com/netbox-community/netbox/issues/6149) - Support image attachments for locations
### Bug Fixes (from v2.11-beta1)
* [#5583](https://github.com/netbox-community/netbox/issues/5583) - Eliminate redundant change records when adding/removing tags
* [#6100](https://github.com/netbox-community/netbox/issues/6100) - Fix VM interfaces table "add interfaces" link
* [#6104](https://github.com/netbox-community/netbox/issues/6104) - Fix location column on racks table
* [#6105](https://github.com/netbox-community/netbox/issues/6105) - Hide checkboxes for VMs under cluster VMs view
* [#6106](https://github.com/netbox-community/netbox/issues/6106) - Allow assigning a virtual interface as the parent of an existing interface
* [#6107](https://github.com/netbox-community/netbox/issues/6107) - Fix rack selection field on device form
* [#6110](https://github.com/netbox-community/netbox/issues/6110) - Fix handling of TemplateColumn values for table export
* [#6123](https://github.com/netbox-community/netbox/issues/6123) - Prevent device from being assigned to mismatched site and location
* [#6124](https://github.com/netbox-community/netbox/issues/6124) - Location `parent` filter should return all child locations (not just those directly assigned)
* [#6130](https://github.com/netbox-community/netbox/issues/6130) - Improve display of assigned models in custom fields list
* [#6155](https://github.com/netbox-community/netbox/issues/6155) - Fix admin links for plugins, background tasks
* [#6171](https://github.com/netbox-community/netbox/issues/6171) - Fix display of horizontally-scrolling object lists
* [#6173](https://github.com/netbox-community/netbox/issues/6173) - Fix assigned device/VM count when bulk editing/deleting device roles
* [#6176](https://github.com/netbox-community/netbox/issues/6176) - Correct position of MAC address field when creating VM interfaces
* [#6177](https://github.com/netbox-community/netbox/issues/6177) - Prevent VM interface from being assigned as its own parent
### Other Changes
* [#1638](https://github.com/netbox-community/netbox/issues/1638) - Migrate all primary keys to 64-bit integers
* [#5873](https://github.com/netbox-community/netbox/issues/5873) - Use numeric IDs in all object URLs
* [#5938](https://github.com/netbox-community/netbox/issues/5938) - Deprecated support for Python 3.6
* [#5990](https://github.com/netbox-community/netbox/issues/5990) - Deprecated `display_field` parameter for custom script ObjectVar and MultiObjectVar fields
* [#5995](https://github.com/netbox-community/netbox/issues/5995) - Dropped backward compatibility for `queryset` parameter on ObjectVar and MultiObjectVar (use `model` instead)
* [#6014](https://github.com/netbox-community/netbox/issues/6014) - Moved the virtual machine interfaces list to a separate view
* [#6071](https://github.com/netbox-community/netbox/issues/6071) - Cable traces now traverse circuits
### REST API Changes
* All primary keys are now 64-bit integers
* All model serializers now include a `display` field to be used for the presentation of an object to a human user
* All device components
* Added support for custom fields
* Added `created` and `last_updated` fields to track object creation and modification
* All device component templates
* Added `created` and `last_updated` fields to track object creation and modification
* All organizational models
* Added support for custom fields
* All cable termination models (cabled device components, power feeds, and circuit terminations)
* Added `mark_connected` boolean field to force connection status
* Added `_occupied` read-only boolean field as common attribute for determining whether an object is occupied
* Renamed RackGroup to Location
* The `/dcim/rack-groups/` endpoint is now `/dcim/locations/`
* circuits.CircuitTermination
* Added the `provider_network` field
* Removed the `connected_endpoint`, `connected_endpoint_type`, and `connected_endpoint_reachable` fields
* The `trace/` endpoint has been replaced with `paths/`
* circuits.ProviderNetwork
* Added the `/api/circuits/provider-networks/` endpoint
* dcim.Device
* Added the `location` field
* dcim.Interface
* Added the `parent` field
* dcim.PowerPanel
* Renamed `rack_group` field to `location`
* dcim.Rack
* Renamed `group` field to `location`
* dcim.Site
* Added the `group` foreign key field to SiteGroup
* dcim.SiteGroup
* Added the `/api/dcim/site-groups/` endpoint
* extras.ConfigContext
* Added the `site_groups` many-to-many field to track the assignment of ConfigContexts to SiteGroups
* extras.CustomField
* Added new custom field type: `multi-select`
* extras.CustomLink
* Added the `/api/extras/custom-links/` endpoint
* extras.ExportTemplate
* Added the `as_attachment` boolean field
* extras.ObjectChange
* Added the `prechange_data` field
* Renamed `object_data` to `postchange_data`
* extras.Webhook
* Added the `/api/extras/webhooks/` endpoint
* ipam.VLANGroup
* Added the `scope_type`, `scope_id`, and `scope` fields (`scope` is a generic foreign key)
* Dropped the `site` foreign key field
* virtualization.VirtualMachine
* `vcpus` has been changed from an integer to a decimal value
* virtualization.VMInterface
* Added the `parent` field

View File

@ -1,2 +1,2 @@
mkdocs==1.1 mkdocs-material
git+https://github.com/cmacmackin/markdown-include.git git+https://github.com/cmacmackin/markdown-include.git

View File

@ -20,7 +20,7 @@ http://netbox/api/dcim/sites/
} }
``` ```
A token is not required for read-only operations which have been exempted from permissions enforcement (using the [`EXEMPT_VIEW_PERMISSIONS`](../../configuration/optional-settings/#exempt_view_permissions) configuration parameter). However, if a token _is_ required but not present in a request, the API will return a 403 (Forbidden) response: A token is not required for read-only operations which have been exempted from permissions enforcement (using the [`EXEMPT_VIEW_PERMISSIONS`](../configuration/optional-settings.md#exempt_view_permissions) configuration parameter). However, if a token _is_ required but not present in a request, the API will return a 403 (Forbidden) response:
``` ```
$ curl http://netbox/api/dcim/sites/ $ curl http://netbox/api/dcim/sites/

View File

@ -61,27 +61,48 @@ These lookup expressions can be applied by adding a suffix to the desired field'
Numeric based fields (ASN, VLAN ID, etc) support these lookup expressions: Numeric based fields (ASN, VLAN ID, etc) support these lookup expressions:
- `n` - not equal to (negation) | Filter | Description |
- `lt` - less than |--------|-------------|
- `lte` - less than or equal | `n` | Not equal to |
- `gt` - greater than | `lt` | Less than |
- `gte` - greater than or equal | `lte` | Less than or equal to |
| `gt` | Greater than |
| `gte` | Greater than or equal to |
Here is an example of a numeric field lookup expression that will return all VLANs with a VLAN ID greater than 900:
```no-highlight
GET /api/ipam/vlans/?vid__gt=900
```
### String Fields ### String Fields
String based (char) fields (Name, Address, etc) support these lookup expressions: String based (char) fields (Name, Address, etc) support these lookup expressions:
- `n` - not equal to (negation) | Filter | Description |
- `ic` - case insensitive contains |--------|-------------|
- `nic` - negated case insensitive contains | `n` | Not equal to |
- `isw` - case insensitive starts with | `ic` | Contains (case-insensitive) |
- `nisw` - negated case insensitive starts with | `nic` | Does not contain (case-insensitive) |
- `iew` - case insensitive ends with | `isw` | Starts with (case-insensitive) |
- `niew` - negated case insensitive ends with | `nisw` | Does not start with (case-insensitive) |
- `ie` - case insensitive exact match | `iew` | Ends with (case-insensitive) |
- `nie` - negated case insensitive exact match | `niew` | Does not end with (case-insensitive) |
| `ie` | Exact match (case-insensitive) |
| `nie` | Inverse exact match (case-insensitive) |
| `empty` | Is empty (boolean) |
Here is an example of a lookup expression on a string field that will return all devices with `switch` in the name:
```no-highlight
GET /api/dcim/devices/?name__ic=switch
```
### Foreign Keys & Other Fields ### Foreign Keys & Other Fields
Certain other fields, namely foreign key relationships support just the negation Certain other fields, namely foreign key relationships support just the negation
expression: `n`. expression: `n`. Here is an example of a lookup expression on a foreign key, it would return all the VLANs that don't have a VLAN Group ID of 3203:
```no-highlight
GET /api/ipam/vlans/?group_id__n=3203
```

View File

@ -269,7 +269,7 @@ The brief format is supported for both lists and individual objects.
### Excluding Config Contexts ### Excluding Config Contexts
When retrieving devices and virtual machines via the REST API, each will included its rendered [configuration context data](../models/extras/configcontext/) by default. Users with large amounts of context data will likely observe suboptimal performance when returning multiple objects, particularly with very high page sizes. To combat this, context data may be excluded from the response data by attaching the query parameter `?exclude=config_context` to the request. This parameter works for both list and detail views. When retrieving devices and virtual machines via the REST API, each will included its rendered [configuration context data](../models/extras/configcontext.md) by default. Users with large amounts of context data will likely observe suboptimal performance when returning multiple objects, particularly with very high page sizes. To combat this, context data may be excluded from the response data by attaching the query parameter `?exclude=config_context` to the request. This parameter works for both list and detail views.
## Pagination ## Pagination
@ -308,7 +308,7 @@ Vary: Accept
} }
``` ```
The default page is determined by the [`PAGINATE_COUNT`](../../configuration/optional-settings/#paginate_count) configuration parameter, which defaults to 50. However, this can be overridden per request by specifying the desired `offset` and `limit` query parameters. For example, if you wish to retrieve a hundred devices at a time, you would make a request for: The default page is determined by the [`PAGINATE_COUNT`](../configuration/optional-settings.md#paginate_count) configuration parameter, which defaults to 50. However, this can be overridden per request by specifying the desired `offset` and `limit` query parameters. For example, if you wish to retrieve a hundred devices at a time, you would make a request for:
``` ```
http://netbox/api/dcim/devices/?limit=100 http://netbox/api/dcim/devices/?limit=100
@ -325,7 +325,7 @@ The response will return devices 1 through 100. The URL provided in the `next` a
} }
``` ```
The maximum number of objects that can be returned is limited by the [`MAX_PAGE_SIZE`](../../configuration/optional-settings/#max_page_size) configuration parameter, which is 1000 by default. Setting this to `0` or `None` will remove the maximum limit. An API consumer can then pass `?limit=0` to retrieve _all_ matching objects with a single request. The maximum number of objects that can be returned is limited by the [`MAX_PAGE_SIZE`](../configuration/optional-settings.md#max_page_size) configuration parameter, which is 1000 by default. Setting this to `0` or `None` will remove the maximum limit. An API consumer can then pass `?limit=0` to retrieve _all_ matching objects with a single request.
!!! warning !!! warning
Disabling the page size limit introduces a potential for very resource-intensive requests, since one API request can effectively retrieve an entire table from the database. Disabling the page size limit introduces a potential for very resource-intensive requests, since one API request can effectively retrieve an entire table from the database.
@ -387,7 +387,7 @@ curl -s -X GET http://netbox/api/ipam/ip-addresses/5618/ | jq '.'
### Creating a New Object ### Creating a New Object
To create a new object, make a `POST` request to the model's _list_ endpoint with JSON data pertaining to the object being created. Note that a REST API token is required for all write operations; see the [authentication documentation](../authentication/) for more information. Also be sure to set the `Content-Type` HTTP header to `application/json`. To create a new object, make a `POST` request to the model's _list_ endpoint with JSON data pertaining to the object being created. Note that a REST API token is required for all write operations; see the [authentication documentation](authentication.md) for more information. Also be sure to set the `Content-Type` HTTP header to `application/json`.
```no-highlight ```no-highlight
curl -s -X POST \ curl -s -X POST \

View File

@ -4,7 +4,7 @@ As with most other objects, the REST API can be used to view, create, modify, an
## Generating a Session Key ## Generating a Session Key
In order to encrypt or decrypt secret data, a session key must be attached to the API request. To generate a session key, send an authenticated request to the `/api/secrets/get-session-key/` endpoint with the private RSA key which matches your [UserKey](../../core-functionality/secrets/#user-keys). The private key must be POSTed with the name `private_key`. In order to encrypt or decrypt secret data, a session key must be attached to the API request. To generate a session key, send an authenticated request to the `/api/secrets/get-session-key/` endpoint with the private RSA key which matches your [UserKey](../core-functionality/secrets.md#user-keys). The private key must be POSTed with the name `private_key`.
```no-highlight ```no-highlight
$ curl -X POST http://netbox/api/secrets/get-session-key/ \ $ curl -X POST http://netbox/api/secrets/get-session-key/ \

View File

@ -1,18 +1,26 @@
site_name: NetBox Documentation site_name: NetBox Documentation
site_url: https://netbox.readthedocs.io/ site_url: https://netbox.readthedocs.io/
repo_name: netbox-community/netbox
repo_url: https://github.com/netbox-community/netbox repo_url: https://github.com/netbox-community/netbox
python: python:
install: install:
- requirements: docs/requirements.txt - requirements: docs/requirements.txt
theme: theme:
name: readthedocs name: material
navigation_depth: 3 icon:
repo: fontawesome/brands/github
extra_css: extra_css:
- extra.css - extra.css
markdown_extensions: markdown_extensions:
- admonition: - admonition
- attr_list
- markdown_include.include: - markdown_include.include:
headingOffset: 1 headingOffset: 1
- pymdownx.emoji:
emoji_index: !!python/name:materialx.emoji.twemoji
emoji_generator: !!python/name:materialx.emoji.to_svg
- pymdownx.superfences
- pymdownx.tabbed
nav: nav:
- Introduction: 'index.md' - Introduction: 'index.md'
- Installation: - Installation:
@ -49,6 +57,7 @@ nav:
- Custom Links: 'additional-features/custom-links.md' - Custom Links: 'additional-features/custom-links.md'
- Custom Scripts: 'additional-features/custom-scripts.md' - Custom Scripts: 'additional-features/custom-scripts.md'
- Export Templates: 'additional-features/export-templates.md' - Export Templates: 'additional-features/export-templates.md'
- Journaling: 'additional-features/journaling.md'
- NAPALM: 'additional-features/napalm.md' - NAPALM: 'additional-features/napalm.md'
- Prometheus Metrics: 'additional-features/prometheus-metrics.md' - Prometheus Metrics: 'additional-features/prometheus-metrics.md'
- Reports: 'additional-features/reports.md' - Reports: 'additional-features/reports.md'
@ -70,11 +79,14 @@ nav:
- Introduction: 'development/index.md' - Introduction: 'development/index.md'
- Getting Started: 'development/getting-started.md' - Getting Started: 'development/getting-started.md'
- Style Guide: 'development/style-guide.md' - Style Guide: 'development/style-guide.md'
- Models: 'development/models.md'
- Adding Models: 'development/adding-models.md'
- Extending Models: 'development/extending-models.md' - Extending Models: 'development/extending-models.md'
- Application Registry: 'development/application-registry.md' - Application Registry: 'development/application-registry.md'
- User Preferences: 'development/user-preferences.md' - User Preferences: 'development/user-preferences.md'
- Release Checklist: 'development/release-checklist.md' - Release Checklist: 'development/release-checklist.md'
- Release Notes: - Release Notes:
- Version 2.11: 'release-notes/version-2.11.md'
- Version 2.10: 'release-notes/version-2.10.md' - Version 2.10: 'release-notes/version-2.10.md'
- Version 2.9: 'release-notes/version-2.9.md' - Version 2.9: 'release-notes/version-2.9.md'
- Version 2.8: 'release-notes/version-2.8.md' - Version 2.8: 'release-notes/version-2.8.md'

View File

@ -1,16 +1,29 @@
from rest_framework import serializers from rest_framework import serializers
from circuits.models import Circuit, CircuitTermination, CircuitType, Provider from circuits.models import *
from netbox.api import WritableNestedSerializer from netbox.api import WritableNestedSerializer
__all__ = [ __all__ = [
'NestedCircuitSerializer', 'NestedCircuitSerializer',
'NestedCircuitTerminationSerializer', 'NestedCircuitTerminationSerializer',
'NestedCircuitTypeSerializer', 'NestedCircuitTypeSerializer',
'NestedProviderNetworkSerializer',
'NestedProviderSerializer', 'NestedProviderSerializer',
] ]
#
# Provider networks
#
class NestedProviderNetworkSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:providernetwork-detail')
class Meta:
model = ProviderNetwork
fields = ['id', 'url', 'display', 'name']
# #
# Providers # Providers
# #
@ -21,7 +34,7 @@ class NestedProviderSerializer(WritableNestedSerializer):
class Meta: class Meta:
model = Provider model = Provider
fields = ['id', 'url', 'name', 'slug', 'circuit_count'] fields = ['id', 'url', 'display', 'name', 'slug', 'circuit_count']
# #
@ -34,7 +47,7 @@ class NestedCircuitTypeSerializer(WritableNestedSerializer):
class Meta: class Meta:
model = CircuitType model = CircuitType
fields = ['id', 'url', 'name', 'slug', 'circuit_count'] fields = ['id', 'url', 'display', 'name', 'slug', 'circuit_count']
class NestedCircuitSerializer(WritableNestedSerializer): class NestedCircuitSerializer(WritableNestedSerializer):
@ -42,7 +55,7 @@ class NestedCircuitSerializer(WritableNestedSerializer):
class Meta: class Meta:
model = Circuit model = Circuit
fields = ['id', 'url', 'cid'] fields = ['id', 'url', 'display', 'cid']
class NestedCircuitTerminationSerializer(WritableNestedSerializer): class NestedCircuitTerminationSerializer(WritableNestedSerializer):
@ -51,4 +64,4 @@ class NestedCircuitTerminationSerializer(WritableNestedSerializer):
class Meta: class Meta:
model = CircuitTermination model = CircuitTermination
fields = ['id', 'url', 'circuit', 'term_side', 'cable'] fields = ['id', 'url', 'display', 'circuit', 'term_side', 'cable', '_occupied']

View File

@ -1,12 +1,13 @@
from rest_framework import serializers from rest_framework import serializers
from circuits.choices import CircuitStatusChoices from circuits.choices import CircuitStatusChoices
from circuits.models import Provider, Circuit, CircuitTermination, CircuitType from circuits.models import *
from dcim.api.nested_serializers import NestedCableSerializer, NestedInterfaceSerializer, NestedSiteSerializer from dcim.api.nested_serializers import NestedCableSerializer, NestedSiteSerializer
from dcim.api.serializers import CableTerminationSerializer, ConnectedEndpointSerializer from dcim.api.serializers import CableTerminationSerializer, ConnectedEndpointSerializer
from extras.api.customfields import CustomFieldModelSerializer from netbox.api import ChoiceField
from extras.api.serializers import TaggedObjectSerializer from netbox.api.serializers import (
from netbox.api import ChoiceField, ValidatedModelSerializer, WritableNestedSerializer BaseModelSerializer, OrganizationalModelSerializer, PrimaryModelSerializer, WritableNestedSerializer
)
from tenancy.api.nested_serializers import NestedTenantSerializer from tenancy.api.nested_serializers import NestedTenantSerializer
from .nested_serializers import * from .nested_serializers import *
@ -15,15 +16,31 @@ from .nested_serializers import *
# Providers # Providers
# #
class ProviderSerializer(TaggedObjectSerializer, CustomFieldModelSerializer): class ProviderSerializer(PrimaryModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provider-detail') url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provider-detail')
circuit_count = serializers.IntegerField(read_only=True) circuit_count = serializers.IntegerField(read_only=True)
class Meta: class Meta:
model = Provider model = Provider
fields = [ fields = [
'id', 'url', 'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments', 'tags', 'id', 'url', 'display', 'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact',
'custom_fields', 'created', 'last_updated', 'circuit_count', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'circuit_count',
]
#
# Provider networks
#
class ProviderNetworkSerializer(PrimaryModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:providernetwork-detail')
provider = NestedProviderSerializer()
class Meta:
model = ProviderNetwork
fields = [
'id', 'url', 'display', 'provider', 'name', 'description', 'comments', 'tags', 'custom_fields', 'created',
'last_updated',
] ]
@ -31,28 +48,31 @@ class ProviderSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
# Circuits # Circuits
# #
class CircuitTypeSerializer(ValidatedModelSerializer): class CircuitTypeSerializer(OrganizationalModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittype-detail') url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittype-detail')
circuit_count = serializers.IntegerField(read_only=True) circuit_count = serializers.IntegerField(read_only=True)
class Meta: class Meta:
model = CircuitType model = CircuitType
fields = ['id', 'url', 'name', 'slug', 'description', 'circuit_count'] fields = [
'id', 'url', 'display', 'name', 'slug', 'description', 'custom_fields', 'created', 'last_updated',
'circuit_count',
]
class CircuitCircuitTerminationSerializer(WritableNestedSerializer, ConnectedEndpointSerializer): class CircuitCircuitTerminationSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail') url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail')
site = NestedSiteSerializer() site = NestedSiteSerializer()
provider_network = NestedProviderNetworkSerializer()
class Meta: class Meta:
model = CircuitTermination model = CircuitTermination
fields = [ fields = [
'id', 'url', 'site', 'port_speed', 'upstream_speed', 'xconnect_id', 'connected_endpoint', 'id', 'url', 'display', 'site', 'provider_network', 'port_speed', 'upstream_speed', 'xconnect_id',
'connected_endpoint_type', 'connected_endpoint_reachable',
] ]
class CircuitSerializer(TaggedObjectSerializer, CustomFieldModelSerializer): class CircuitSerializer(PrimaryModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuit-detail') url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuit-detail')
provider = NestedProviderSerializer() provider = NestedProviderSerializer()
status = ChoiceField(choices=CircuitStatusChoices, required=False) status = ChoiceField(choices=CircuitStatusChoices, required=False)
@ -64,21 +84,23 @@ class CircuitSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
class Meta: class Meta:
model = Circuit model = Circuit
fields = [ fields = [
'id', 'url', 'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description', 'id', 'url', 'display', 'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate',
'termination_a', 'termination_z', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'description', 'termination_a', 'termination_z', 'comments', 'tags', 'custom_fields', 'created',
'last_updated',
] ]
class CircuitTerminationSerializer(CableTerminationSerializer, ConnectedEndpointSerializer): class CircuitTerminationSerializer(BaseModelSerializer, CableTerminationSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail') url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail')
circuit = NestedCircuitSerializer() circuit = NestedCircuitSerializer()
site = NestedSiteSerializer() site = NestedSiteSerializer(required=False)
provider_network = NestedProviderNetworkSerializer(required=False)
cable = NestedCableSerializer(read_only=True) cable = NestedCableSerializer(read_only=True)
class Meta: class Meta:
model = CircuitTermination model = CircuitTermination
fields = [ fields = [
'id', 'url', 'circuit', 'term_side', 'site', 'port_speed', 'upstream_speed', 'xconnect_id', 'pp_info', 'id', 'url', 'display', 'circuit', 'term_side', 'site', 'provider_network', 'port_speed', 'upstream_speed',
'description', 'cable', 'cable_peer', 'cable_peer_type', 'connected_endpoint', 'connected_endpoint_type', 'xconnect_id', 'pp_info', 'description', 'mark_connected', 'cable', 'cable_peer', 'cable_peer_type',
'connected_endpoint_reachable' '_occupied',
] ]

View File

@ -13,5 +13,8 @@ router.register('circuit-types', views.CircuitTypeViewSet)
router.register('circuits', views.CircuitViewSet) router.register('circuits', views.CircuitViewSet)
router.register('circuit-terminations', views.CircuitTerminationViewSet) router.register('circuit-terminations', views.CircuitTerminationViewSet)
# Provider networks
router.register('provider-networks', views.ProviderNetworkViewSet)
app_name = 'circuits-api' app_name = 'circuits-api'
urlpatterns = router.urls urlpatterns = router.urls

View File

@ -1,9 +1,8 @@
from django.db.models import Prefetch
from rest_framework.routers import APIRootView from rest_framework.routers import APIRootView
from circuits import filters from circuits import filtersets
from circuits.models import Provider, CircuitTermination, CircuitType, Circuit from circuits.models import *
from dcim.api.views import PathEndpointMixin from dcim.api.views import PassThroughPortMixin
from extras.api.views import CustomFieldModelViewSet from extras.api.views import CustomFieldModelViewSet
from netbox.api.views import ModelViewSet from netbox.api.views import ModelViewSet
from utilities.utils import count_related from utilities.utils import count_related
@ -27,19 +26,19 @@ class ProviderViewSet(CustomFieldModelViewSet):
circuit_count=count_related(Circuit, 'provider') circuit_count=count_related(Circuit, 'provider')
) )
serializer_class = serializers.ProviderSerializer serializer_class = serializers.ProviderSerializer
filterset_class = filters.ProviderFilterSet filterset_class = filtersets.ProviderFilterSet
# #
# Circuit Types # Circuit Types
# #
class CircuitTypeViewSet(ModelViewSet): class CircuitTypeViewSet(CustomFieldModelViewSet):
queryset = CircuitType.objects.annotate( queryset = CircuitType.objects.annotate(
circuit_count=count_related(Circuit, 'type') circuit_count=count_related(Circuit, 'type')
) )
serializer_class = serializers.CircuitTypeSerializer serializer_class = serializers.CircuitTypeSerializer
filterset_class = filters.CircuitTypeFilterSet filterset_class = filtersets.CircuitTypeFilterSet
# #
@ -48,21 +47,30 @@ class CircuitTypeViewSet(ModelViewSet):
class CircuitViewSet(CustomFieldModelViewSet): class CircuitViewSet(CustomFieldModelViewSet):
queryset = Circuit.objects.prefetch_related( queryset = Circuit.objects.prefetch_related(
Prefetch('terminations', queryset=CircuitTermination.objects.prefetch_related('site')), 'type', 'tenant', 'provider', 'termination_a', 'termination_z'
'type', 'tenant', 'provider',
).prefetch_related('tags') ).prefetch_related('tags')
serializer_class = serializers.CircuitSerializer serializer_class = serializers.CircuitSerializer
filterset_class = filters.CircuitFilterSet filterset_class = filtersets.CircuitFilterSet
# #
# Circuit Terminations # Circuit Terminations
# #
class CircuitTerminationViewSet(PathEndpointMixin, ModelViewSet): class CircuitTerminationViewSet(PassThroughPortMixin, ModelViewSet):
queryset = CircuitTermination.objects.prefetch_related( queryset = CircuitTermination.objects.prefetch_related(
'circuit', 'site', '_path__destination', 'cable' 'circuit', 'site', 'provider_network', 'cable'
) )
serializer_class = serializers.CircuitTerminationSerializer serializer_class = serializers.CircuitTerminationSerializer
filterset_class = filters.CircuitTerminationFilterSet filterset_class = filtersets.CircuitTerminationFilterSet
brief_prefetch_fields = ['circuit'] brief_prefetch_fields = ['circuit']
#
# Provider networks
#
class ProviderNetworkViewSet(CustomFieldModelViewSet):
queryset = ProviderNetwork.objects.prefetch_related('tags')
serializer_class = serializers.ProviderNetworkSerializer
filterset_class = filtersets.ProviderNetworkFilterSet

View File

@ -1,25 +1,25 @@
import django_filters import django_filters
from django.db.models import Q from django.db.models import Q
from dcim.filters import CableTerminationFilterSet, PathEndpointFilterSet from dcim.filtersets import CableTerminationFilterSet
from dcim.models import Region, Site from dcim.models import Region, Site, SiteGroup
from extras.filters import CustomFieldModelFilterSet, CreatedUpdatedFilterSet from extras.filters import TagFilter
from tenancy.filters import TenancyFilterSet from netbox.filtersets import ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, PrimaryModelFilterSet
from utilities.filters import ( from tenancy.filtersets import TenancyFilterSet
BaseFilterSet, NameSlugSearchFilterSet, TagFilter, TreeNodeMultipleChoiceFilter from utilities.filters import TreeNodeMultipleChoiceFilter
)
from .choices import * from .choices import *
from .models import Circuit, CircuitTermination, CircuitType, Provider from .models import *
__all__ = ( __all__ = (
'CircuitFilterSet', 'CircuitFilterSet',
'CircuitTerminationFilterSet', 'CircuitTerminationFilterSet',
'CircuitTypeFilterSet', 'CircuitTypeFilterSet',
'ProviderNetworkFilterSet',
'ProviderFilterSet', 'ProviderFilterSet',
) )
class ProviderFilterSet(BaseFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet): class ProviderFilterSet(PrimaryModelFilterSet):
q = django_filters.CharFilter( q = django_filters.CharFilter(
method='search', method='search',
label='Search', label='Search',
@ -37,6 +37,19 @@ class ProviderFilterSet(BaseFilterSet, CustomFieldModelFilterSet, CreatedUpdated
to_field_name='slug', to_field_name='slug',
label='Region (slug)', label='Region (slug)',
) )
site_group_id = TreeNodeMultipleChoiceFilter(
queryset=SiteGroup.objects.all(),
field_name='circuits__terminations__site__group',
lookup_expr='in',
label='Site group (ID)',
)
site_group = TreeNodeMultipleChoiceFilter(
queryset=SiteGroup.objects.all(),
field_name='circuits__terminations__site__group',
lookup_expr='in',
to_field_name='slug',
label='Site group (slug)',
)
site_id = django_filters.ModelMultipleChoiceFilter( site_id = django_filters.ModelMultipleChoiceFilter(
field_name='circuits__terminations__site', field_name='circuits__terminations__site',
queryset=Site.objects.all(), queryset=Site.objects.all(),
@ -66,14 +79,7 @@ class ProviderFilterSet(BaseFilterSet, CustomFieldModelFilterSet, CreatedUpdated
) )
class CircuitTypeFilterSet(BaseFilterSet, NameSlugSearchFilterSet): class ProviderNetworkFilterSet(PrimaryModelFilterSet):
class Meta:
model = CircuitType
fields = ['id', 'name', 'slug']
class CircuitFilterSet(BaseFilterSet, CustomFieldModelFilterSet, TenancyFilterSet, CreatedUpdatedFilterSet):
q = django_filters.CharFilter( q = django_filters.CharFilter(
method='search', method='search',
label='Search', label='Search',
@ -88,6 +94,49 @@ class CircuitFilterSet(BaseFilterSet, CustomFieldModelFilterSet, TenancyFilterSe
to_field_name='slug', to_field_name='slug',
label='Provider (slug)', label='Provider (slug)',
) )
tag = TagFilter()
class Meta:
model = ProviderNetwork
fields = ['id', 'name']
def search(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(
Q(name__icontains=value) |
Q(description__icontains=value) |
Q(comments__icontains=value)
).distinct()
class CircuitTypeFilterSet(OrganizationalModelFilterSet):
class Meta:
model = CircuitType
fields = ['id', 'name', 'slug']
class CircuitFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
)
provider_id = django_filters.ModelMultipleChoiceFilter(
queryset=Provider.objects.all(),
label='Provider (ID)',
)
provider = django_filters.ModelMultipleChoiceFilter(
field_name='provider__slug',
queryset=Provider.objects.all(),
to_field_name='slug',
label='Provider (slug)',
)
provider_network_id = django_filters.ModelMultipleChoiceFilter(
field_name='terminations__provider_network',
queryset=ProviderNetwork.objects.all(),
label='ProviderNetwork (ID)',
)
type_id = django_filters.ModelMultipleChoiceFilter( type_id = django_filters.ModelMultipleChoiceFilter(
queryset=CircuitType.objects.all(), queryset=CircuitType.objects.all(),
label='Circuit type (ID)', label='Circuit type (ID)',
@ -102,17 +151,6 @@ class CircuitFilterSet(BaseFilterSet, CustomFieldModelFilterSet, TenancyFilterSe
choices=CircuitStatusChoices, choices=CircuitStatusChoices,
null_value=None null_value=None
) )
site_id = django_filters.ModelMultipleChoiceFilter(
field_name='terminations__site',
queryset=Site.objects.all(),
label='Site (ID)',
)
site = django_filters.ModelMultipleChoiceFilter(
field_name='terminations__site__slug',
queryset=Site.objects.all(),
to_field_name='slug',
label='Site (slug)',
)
region_id = TreeNodeMultipleChoiceFilter( region_id = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(), queryset=Region.objects.all(),
field_name='terminations__site__region', field_name='terminations__site__region',
@ -126,6 +164,30 @@ class CircuitFilterSet(BaseFilterSet, CustomFieldModelFilterSet, TenancyFilterSe
to_field_name='slug', to_field_name='slug',
label='Region (slug)', label='Region (slug)',
) )
site_group_id = TreeNodeMultipleChoiceFilter(
queryset=SiteGroup.objects.all(),
field_name='terminations__site__group',
lookup_expr='in',
label='Site group (ID)',
)
site_group = TreeNodeMultipleChoiceFilter(
queryset=SiteGroup.objects.all(),
field_name='terminations__site__group',
lookup_expr='in',
to_field_name='slug',
label='Site group (slug)',
)
site_id = django_filters.ModelMultipleChoiceFilter(
field_name='terminations__site',
queryset=Site.objects.all(),
label='Site (ID)',
)
site = django_filters.ModelMultipleChoiceFilter(
field_name='terminations__site__slug',
queryset=Site.objects.all(),
to_field_name='slug',
label='Site (slug)',
)
tag = TagFilter() tag = TagFilter()
class Meta: class Meta:
@ -145,7 +207,7 @@ class CircuitFilterSet(BaseFilterSet, CustomFieldModelFilterSet, TenancyFilterSe
).distinct() ).distinct()
class CircuitTerminationFilterSet(BaseFilterSet, CableTerminationFilterSet, PathEndpointFilterSet): class CircuitTerminationFilterSet(ChangeLoggedModelFilterSet, CableTerminationFilterSet):
q = django_filters.CharFilter( q = django_filters.CharFilter(
method='search', method='search',
label='Search', label='Search',
@ -164,10 +226,14 @@ class CircuitTerminationFilterSet(BaseFilterSet, CableTerminationFilterSet, Path
to_field_name='slug', to_field_name='slug',
label='Site (slug)', label='Site (slug)',
) )
provider_network_id = django_filters.ModelMultipleChoiceFilter(
queryset=ProviderNetwork.objects.all(),
label='ProviderNetwork (ID)',
)
class Meta: class Meta:
model = CircuitTermination model = CircuitTermination
fields = ['term_side', 'port_speed', 'upstream_speed', 'xconnect_id'] fields = ['id', 'term_side', 'port_speed', 'upstream_speed', 'xconnect_id']
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():

View File

@ -1,6 +1,7 @@
from django import forms from django import forms
from django.utils.translation import gettext as _
from dcim.models import Region, Site from dcim.models import Region, Site, SiteGroup
from extras.forms import ( from extras.forms import (
AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldFilterForm, CustomFieldModelForm, CustomFieldModelCSVForm, AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldFilterForm, CustomFieldModelForm, CustomFieldModelCSVForm,
) )
@ -8,12 +9,12 @@ from extras.models import Tag
from tenancy.forms import TenancyFilterForm, TenancyForm from tenancy.forms import TenancyFilterForm, TenancyForm
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.forms import ( from utilities.forms import (
add_blank_choice, BootstrapMixin, CommentField, CSVChoiceField, CSVModelChoiceField, CSVModelForm, DatePicker, add_blank_choice, BootstrapMixin, CommentField, CSVChoiceField, CSVModelChoiceField, DatePicker,
DynamicModelChoiceField, DynamicModelMultipleChoiceField, SmallTextarea, SlugField, StaticSelect2, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SelectSpeedWidget, SmallTextarea, SlugField,
StaticSelect2Multiple, TagFilterField, StaticSelect2, StaticSelect2Multiple, TagFilterField,
) )
from .choices import CircuitStatusChoices from .choices import CircuitStatusChoices
from .models import Circuit, CircuitTermination, CircuitType, Provider from .models import *
# #
@ -33,6 +34,10 @@ class ProviderForm(BootstrapMixin, CustomFieldModelForm):
fields = [ fields = [
'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments', 'tags', 'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments', 'tags',
] ]
fieldsets = (
('Provider', ('name', 'slug', 'asn', 'tags')),
('Support Info', ('account', 'portal_url', 'noc_contact', 'admin_contact')),
)
widgets = { widgets = {
'noc_contact': SmallTextarea( 'noc_contact': SmallTextarea(
attrs={'rows': 5} attrs={'rows': 5}
@ -101,24 +106,101 @@ class ProviderFilterForm(BootstrapMixin, CustomFieldFilterForm):
model = Provider model = Provider
q = forms.CharField( q = forms.CharField(
required=False, required=False,
label='Search' label=_('Search')
) )
region = DynamicModelMultipleChoiceField( region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(), queryset=Region.objects.all(),
to_field_name='slug', required=False,
required=False label=_('Region')
) )
site = DynamicModelMultipleChoiceField( site_id = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(), queryset=Site.objects.all(),
to_field_name='slug',
required=False, required=False,
query_params={ query_params={
'region': '$region' 'region_id': '$region_id'
} },
label=_('Site')
) )
asn = forms.IntegerField( asn = forms.IntegerField(
required=False, required=False,
label='ASN' label=_('ASN')
)
tag = TagFilterField(model)
#
# Provider networks
#
class ProviderNetworkForm(BootstrapMixin, CustomFieldModelForm):
provider = DynamicModelChoiceField(
queryset=Provider.objects.all()
)
comments = CommentField()
tags = DynamicModelMultipleChoiceField(
queryset=Tag.objects.all(),
required=False
)
class Meta:
model = ProviderNetwork
fields = [
'provider', 'name', 'description', 'comments', 'tags',
]
fieldsets = (
('Provider Network', ('provider', 'name', 'description', 'tags')),
)
class ProviderNetworkCSVForm(CustomFieldModelCSVForm):
provider = CSVModelChoiceField(
queryset=Provider.objects.all(),
to_field_name='name',
help_text='Assigned provider'
)
class Meta:
model = ProviderNetwork
fields = [
'provider', 'name', 'description', 'comments',
]
class ProviderNetworkBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=ProviderNetwork.objects.all(),
widget=forms.MultipleHiddenInput
)
provider = DynamicModelChoiceField(
queryset=Provider.objects.all(),
required=False
)
description = forms.CharField(
max_length=100,
required=False
)
comments = CommentField(
widget=SmallTextarea,
label='Comments'
)
class Meta:
nullable_fields = [
'description', 'comments',
]
class ProviderNetworkFilterForm(BootstrapMixin, CustomFieldFilterForm):
model = ProviderNetwork
field_order = ['q', 'provider_id']
q = forms.CharField(
required=False,
label=_('Search')
)
provider_id = DynamicModelMultipleChoiceField(
queryset=Provider.objects.all(),
required=False,
label=_('Provider')
) )
tag = TagFilterField(model) tag = TagFilterField(model)
@ -127,7 +209,7 @@ class ProviderFilterForm(BootstrapMixin, CustomFieldFilterForm):
# Circuit types # Circuit types
# #
class CircuitTypeForm(BootstrapMixin, forms.ModelForm): class CircuitTypeForm(BootstrapMixin, CustomFieldModelForm):
slug = SlugField() slug = SlugField()
class Meta: class Meta:
@ -137,7 +219,21 @@ class CircuitTypeForm(BootstrapMixin, forms.ModelForm):
] ]
class CircuitTypeCSVForm(CSVModelForm): class CircuitTypeBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=CircuitType.objects.all(),
widget=forms.MultipleHiddenInput
)
description = forms.CharField(
max_length=200,
required=False
)
class Meta:
nullable_fields = ['description']
class CircuitTypeCSVForm(CustomFieldModelCSVForm):
slug = SlugField() slug = SlugField()
class Meta: class Meta:
@ -171,6 +267,10 @@ class CircuitForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
'cid', 'type', 'provider', 'status', 'install_date', 'commit_rate', 'description', 'tenant_group', 'tenant', 'cid', 'type', 'provider', 'status', 'install_date', 'commit_rate', 'description', 'tenant_group', 'tenant',
'comments', 'tags', 'comments', 'tags',
] ]
fieldsets = (
('Circuit', ('provider', 'cid', 'type', 'status', 'install_date', 'commit_rate', 'description', 'tags')),
('Tenancy', ('tenant_group', 'tenant')),
)
help_texts = { help_texts = {
'cid': "Unique circuit ID", 'cid': "Unique circuit ID",
'commit_rate': "Committed rate", 'commit_rate': "Committed rate",
@ -178,6 +278,7 @@ class CircuitForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
widgets = { widgets = {
'status': StaticSelect2(), 'status': StaticSelect2(),
'install_date': DatePicker(), 'install_date': DatePicker(),
'commit_rate': SelectSpeedWidget(),
} }
@ -256,44 +357,53 @@ class CircuitBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEdit
class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm): class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
model = Circuit model = Circuit
field_order = [ field_order = [
'q', 'type', 'provider', 'status', 'region', 'site', 'tenant_group', 'tenant', 'commit_rate', 'q', 'type_id', 'provider_id', 'provider_network_id', 'status', 'region_id', 'site_id', 'tenant_group_id', 'tenant_id',
'commit_rate',
] ]
q = forms.CharField( q = forms.CharField(
required=False, required=False,
label='Search' label=_('Search')
) )
type = DynamicModelMultipleChoiceField( type_id = DynamicModelMultipleChoiceField(
queryset=CircuitType.objects.all(), queryset=CircuitType.objects.all(),
to_field_name='slug', required=False,
required=False label=_('Type')
) )
provider = DynamicModelMultipleChoiceField( provider_id = DynamicModelMultipleChoiceField(
queryset=Provider.objects.all(), queryset=Provider.objects.all(),
to_field_name='slug', required=False,
required=False label=_('Provider')
)
provider_network_id = DynamicModelMultipleChoiceField(
queryset=ProviderNetwork.objects.all(),
required=False,
query_params={
'provider_id': '$provider_id'
},
label=_('Provider network')
) )
status = forms.MultipleChoiceField( status = forms.MultipleChoiceField(
choices=CircuitStatusChoices, choices=CircuitStatusChoices,
required=False, required=False,
widget=StaticSelect2Multiple() widget=StaticSelect2Multiple()
) )
region = DynamicModelMultipleChoiceField( region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(), queryset=Region.objects.all(),
to_field_name='slug', required=False,
required=False label=_('Region')
) )
site = DynamicModelMultipleChoiceField( site_id = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(), queryset=Site.objects.all(),
to_field_name='slug',
required=False, required=False,
query_params={ query_params={
'region': '$region' 'region_id': '$region_id'
} },
label=_('Site')
) )
commit_rate = forms.IntegerField( commit_rate = forms.IntegerField(
required=False, required=False,
min_value=0, min_value=0,
label='Commit rate (Kbps)' label=_('Commit rate (Kbps)')
) )
tag = TagFilterField(model) tag = TagFilterField(model)
@ -310,17 +420,31 @@ class CircuitTerminationForm(BootstrapMixin, forms.ModelForm):
'sites': '$site' 'sites': '$site'
} }
) )
site_group = DynamicModelChoiceField(
queryset=SiteGroup.objects.all(),
required=False,
initial_params={
'sites': '$site'
}
)
site = DynamicModelChoiceField( site = DynamicModelChoiceField(
queryset=Site.objects.all(), queryset=Site.objects.all(),
query_params={ query_params={
'region_id': '$region' 'region_id': '$region',
} 'group_id': '$site_group',
},
required=False
)
provider_network = DynamicModelChoiceField(
queryset=ProviderNetwork.objects.all(),
required=False
) )
class Meta: class Meta:
model = CircuitTermination model = CircuitTermination
fields = [ fields = [
'term_side', 'region', 'site', 'port_speed', 'upstream_speed', 'xconnect_id', 'pp_info', 'description', 'term_side', 'region', 'site_group', 'site', 'provider_network', 'mark_connected', 'port_speed',
'upstream_speed', 'xconnect_id', 'pp_info', 'description',
] ]
help_texts = { help_texts = {
'port_speed': "Physical circuit speed", 'port_speed': "Physical circuit speed",
@ -329,4 +453,11 @@ class CircuitTerminationForm(BootstrapMixin, forms.ModelForm):
} }
widgets = { widgets = {
'term_side': forms.HiddenInput(), 'term_side': forms.HiddenInput(),
'port_speed': SelectSpeedWidget(),
'upstream_speed': SelectSpeedWidget(),
} }
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['provider_network'].widget.add_query_param('provider_id', self.instance.circuit.provider_id)

View File

@ -0,0 +1,47 @@
import django.core.serializers.json
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('circuits', '0024_standardize_name_length'),
]
operations = [
migrations.AddField(
model_name='circuittype',
name='custom_field_data',
field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder),
),
migrations.AlterField(
model_name='circuit',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='circuittermination',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='circuittype',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='provider',
name='id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AddField(
model_name='circuittermination',
name='created',
field=models.DateField(auto_now_add=True, null=True),
),
migrations.AddField(
model_name='circuittermination',
name='last_updated',
field=models.DateTimeField(auto_now=True, null=True),
),
]

View File

@ -0,0 +1,16 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('circuits', '0025_standardize_models'),
]
operations = [
migrations.AddField(
model_name='circuittermination',
name='mark_connected',
field=models.BooleanField(default=False),
),
]

View File

@ -0,0 +1,65 @@
import django.core.serializers.json
from django.db import migrations, models
import django.db.models.deletion
import taggit.managers
class Migration(migrations.Migration):
dependencies = [
('extras', '0058_journalentry'),
('circuits', '0026_mark_connected'),
]
operations = [
# Create the new ProviderNetwork model
migrations.CreateModel(
name='ProviderNetwork',
fields=[
('created', models.DateField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
('id', models.BigAutoField(primary_key=True, serialize=False)),
('name', models.CharField(max_length=100)),
('description', models.CharField(blank=True, max_length=200)),
('comments', models.TextField(blank=True)),
('provider', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='networks', to='circuits.provider')),
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
],
options={
'ordering': ('provider', 'name'),
},
),
migrations.AddConstraint(
model_name='providernetwork',
constraint=models.UniqueConstraint(fields=('provider', 'name'), name='circuits_providernetwork_provider_name'),
),
migrations.AlterUniqueTogether(
name='providernetwork',
unique_together={('provider', 'name')},
),
# Add ProviderNetwork FK to CircuitTermination
migrations.AddField(
model_name='circuittermination',
name='provider_network',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='circuit_terminations', to='circuits.providernetwork'),
),
migrations.AlterField(
model_name='circuittermination',
name='site',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='circuit_terminations', to='dcim.site'),
),
# Add FKs to CircuitTermination on Circuit
migrations.AddField(
model_name='circuit',
name='termination_a',
field=models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='circuits.circuittermination'),
),
migrations.AddField(
model_name='circuit',
name='termination_z',
field=models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='circuits.circuittermination'),
),
]

View File

@ -0,0 +1,37 @@
import sys
from django.db import migrations
def cache_circuit_terminations(apps, schema_editor):
Circuit = apps.get_model('circuits', 'Circuit')
CircuitTermination = apps.get_model('circuits', 'CircuitTermination')
if 'test' not in sys.argv:
print(f"\n Caching circuit terminations...", flush=True)
a_terminations = {
ct.circuit_id: ct.pk for ct in CircuitTermination.objects.filter(term_side='A')
}
z_terminations = {
ct.circuit_id: ct.pk for ct in CircuitTermination.objects.filter(term_side='Z')
}
for circuit in Circuit.objects.all():
Circuit.objects.filter(pk=circuit.pk).update(
termination_a_id=a_terminations.get(circuit.pk),
termination_z_id=z_terminations.get(circuit.pk),
)
class Migration(migrations.Migration):
dependencies = [
('circuits', '0027_providernetwork'),
]
operations = [
migrations.RunPython(
code=cache_circuit_terminations,
reverse_code=migrations.RunPython.noop
),
]

View File

@ -0,0 +1,32 @@
from django.db import migrations
from django.db.models import Q
def delete_obsolete_cablepaths(apps, schema_editor):
"""
Delete all CablePath instances which originate or terminate at a CircuitTermination.
"""
ContentType = apps.get_model('contenttypes', 'ContentType')
CircuitTermination = apps.get_model('circuits', 'CircuitTermination')
CablePath = apps.get_model('dcim', 'CablePath')
ct = ContentType.objects.get_for_model(CircuitTermination)
CablePath.objects.filter(Q(origin_type=ct) | Q(destination_type=ct)).delete()
class Migration(migrations.Migration):
dependencies = [
('circuits', '0028_cache_circuit_terminations'),
]
operations = [
migrations.RemoveField(
model_name='circuittermination',
name='_path',
),
migrations.RunPython(
code=delete_obsolete_cablepaths,
reverse_code=migrations.RunPython.noop
),
]

View File

@ -1,27 +1,27 @@
from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from django.urls import reverse from django.urls import reverse
from taggit.managers import TaggableManager
from dcim.fields import ASNField from dcim.fields import ASNField
from dcim.models import CableTermination, PathEndpoint from dcim.models import CableTermination, PathEndpoint
from extras.models import ChangeLoggedModel, CustomFieldModel, ObjectChange, TaggedItem from extras.models import ObjectChange
from extras.utils import extras_features from extras.utils import extras_features
from netbox.models import BigIDModel, ChangeLoggedModel, OrganizationalModel, PrimaryModel
from utilities.querysets import RestrictedQuerySet from utilities.querysets import RestrictedQuerySet
from utilities.utils import serialize_object
from .choices import * from .choices import *
from .querysets import CircuitQuerySet
__all__ = ( __all__ = (
'Circuit', 'Circuit',
'CircuitTermination', 'CircuitTermination',
'CircuitType', 'CircuitType',
'ProviderNetwork',
'Provider', 'Provider',
) )
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class Provider(ChangeLoggedModel, CustomFieldModel): class Provider(PrimaryModel):
""" """
Each Circuit belongs to a Provider. This is usually a telecommunications company or similar organization. This model Each Circuit belongs to a Provider. This is usually a telecommunications company or similar organization. This model
stores information pertinent to the user's relationship with the Provider. stores information pertinent to the user's relationship with the Provider.
@ -60,7 +60,6 @@ class Provider(ChangeLoggedModel, CustomFieldModel):
comments = models.TextField( comments = models.TextField(
blank=True blank=True
) )
tags = TaggableManager(through=TaggedItem)
objects = RestrictedQuerySet.as_manager() objects = RestrictedQuerySet.as_manager()
@ -78,7 +77,7 @@ class Provider(ChangeLoggedModel, CustomFieldModel):
return self.name return self.name
def get_absolute_url(self): def get_absolute_url(self):
return reverse('circuits:provider', args=[self.slug]) return reverse('circuits:provider', args=[self.pk])
def to_csv(self): def to_csv(self):
return ( return (
@ -93,7 +92,65 @@ class Provider(ChangeLoggedModel, CustomFieldModel):
) )
class CircuitType(ChangeLoggedModel): #
# Provider networks
#
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class ProviderNetwork(PrimaryModel):
"""
This represents a provider network which exists outside of NetBox, the details of which are unknown or
unimportant to the user.
"""
name = models.CharField(
max_length=100
)
provider = models.ForeignKey(
to='circuits.Provider',
on_delete=models.PROTECT,
related_name='networks'
)
description = models.CharField(
max_length=200,
blank=True
)
comments = models.TextField(
blank=True
)
csv_headers = [
'provider', 'name', 'description', 'comments',
]
objects = RestrictedQuerySet.as_manager()
class Meta:
ordering = ('provider', 'name')
constraints = (
models.UniqueConstraint(
fields=('provider', 'name'),
name='circuits_providernetwork_provider_name'
),
)
unique_together = ('provider', 'name')
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse('circuits:providernetwork', args=[self.pk])
def to_csv(self):
return (
self.provider.name,
self.name,
self.description,
self.comments,
)
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class CircuitType(OrganizationalModel):
""" """
Circuits can be organized by their functional role. For example, a user might wish to define CircuitTypes named Circuits can be organized by their functional role. For example, a user might wish to define CircuitTypes named
"Long Haul," "Metro," or "Out-of-Band". "Long Haul," "Metro," or "Out-of-Band".
@ -122,7 +179,7 @@ class CircuitType(ChangeLoggedModel):
return self.name return self.name
def get_absolute_url(self): def get_absolute_url(self):
return "{}?type={}".format(reverse('circuits:circuit_list'), self.slug) return reverse('circuits:circuittype', args=[self.pk])
def to_csv(self): def to_csv(self):
return ( return (
@ -132,8 +189,8 @@ class CircuitType(ChangeLoggedModel):
) )
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class Circuit(ChangeLoggedModel, CustomFieldModel): class Circuit(PrimaryModel):
""" """
A communications circuit connects two points. Each Circuit belongs to a Provider; Providers may have multiple A communications circuit connects two points. Each Circuit belongs to a Provider; Providers may have multiple
circuits. Each circuit is also assigned a CircuitType and a Site. Circuit port speed and commit rate are measured circuits. Each circuit is also assigned a CircuitType and a Site. Circuit port speed and commit rate are measured
@ -182,8 +239,25 @@ class Circuit(ChangeLoggedModel, CustomFieldModel):
blank=True blank=True
) )
objects = CircuitQuerySet.as_manager() # Cache associated CircuitTerminations
tags = TaggableManager(through=TaggedItem) termination_a = models.ForeignKey(
to='circuits.CircuitTermination',
on_delete=models.SET_NULL,
related_name='+',
editable=False,
blank=True,
null=True
)
termination_z = models.ForeignKey(
to='circuits.CircuitTermination',
on_delete=models.SET_NULL,
related_name='+',
editable=False,
blank=True,
null=True
)
objects = RestrictedQuerySet.as_manager()
csv_headers = [ csv_headers = [
'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description', 'comments', 'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'commit_rate', 'description', 'comments',
@ -218,22 +292,9 @@ class Circuit(ChangeLoggedModel, CustomFieldModel):
def get_status_class(self): def get_status_class(self):
return CircuitStatusChoices.CSS_CLASSES.get(self.status) return CircuitStatusChoices.CSS_CLASSES.get(self.status)
def _get_termination(self, side):
for ct in self.terminations.all():
if ct.term_side == side:
return ct
return None
@property @extras_features('webhooks')
def termination_a(self): class CircuitTermination(ChangeLoggedModel, CableTermination):
return self._get_termination('A')
@property
def termination_z(self):
return self._get_termination('Z')
class CircuitTermination(PathEndpoint, CableTermination):
circuit = models.ForeignKey( circuit = models.ForeignKey(
to='circuits.Circuit', to='circuits.Circuit',
on_delete=models.CASCADE, on_delete=models.CASCADE,
@ -247,7 +308,16 @@ class CircuitTermination(PathEndpoint, CableTermination):
site = models.ForeignKey( site = models.ForeignKey(
to='dcim.Site', to='dcim.Site',
on_delete=models.PROTECT, on_delete=models.PROTECT,
related_name='circuit_terminations' related_name='circuit_terminations',
blank=True,
null=True
)
provider_network = models.ForeignKey(
to=ProviderNetwork,
on_delete=models.PROTECT,
related_name='circuit_terminations',
blank=True,
null=True
) )
port_speed = models.PositiveIntegerField( port_speed = models.PositiveIntegerField(
verbose_name='Port speed (Kbps)', verbose_name='Port speed (Kbps)',
@ -282,26 +352,33 @@ class CircuitTermination(PathEndpoint, CableTermination):
unique_together = ['circuit', 'term_side'] unique_together = ['circuit', 'term_side']
def __str__(self): def __str__(self):
return 'Side {}'.format(self.get_term_side_display()) return f'Termination {self.term_side}: {self.site or self.provider_network}'
def get_absolute_url(self):
if self.site:
return self.site.get_absolute_url()
return self.provider_network.get_absolute_url()
def clean(self):
super().clean()
# 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.")
if self.site and self.provider_network:
raise ValidationError("A circuit termination cannot attach to both a site and a provider network.")
def to_objectchange(self, action): def to_objectchange(self, action):
# Annotate the parent Circuit # Annotate the parent Circuit
try: try:
related_object = self.circuit circuit = self.circuit
except Circuit.DoesNotExist: except Circuit.DoesNotExist:
# Parent circuit has been deleted # Parent circuit has been deleted
related_object = None circuit = None
return super().to_objectchange(action, related_object=circuit)
return ObjectChange(
changed_object=self,
object_repr=str(self),
action=action,
related_object=related_object,
object_data=serialize_object(self)
)
@property @property
def parent(self): def parent_object(self):
return self.circuit return self.circuit
def get_peer_termination(self): def get_peer_termination(self):

View File

@ -1,17 +0,0 @@
from django.db.models import OuterRef, Subquery
from utilities.querysets import RestrictedQuerySet
class CircuitQuerySet(RestrictedQuerySet):
def annotate_sites(self):
"""
Annotate the A and Z termination site names for ordering.
"""
from circuits.models import CircuitTermination
_terminations = CircuitTermination.objects.filter(circuit=OuterRef('pk'))
return self.annotate(
a_side=Subquery(_terminations.filter(term_side='A').values('site__name')[:1]),
z_side=Subquery(_terminations.filter(term_side='Z').values('site__name')[:1]),
)

View File

@ -1,17 +1,26 @@
from django.db.models.signals import post_delete, post_save from django.db.models.signals import post_delete, post_save
from django.dispatch import receiver from django.dispatch import receiver
from django.utils import timezone
from .models import Circuit, CircuitTermination from dcim.signals import rebuild_paths
from .models import CircuitTermination
@receiver(post_save, sender=CircuitTermination)
def update_circuit(instance, **kwargs):
"""
When a CircuitTermination has been modified, update its parent Circuit.
"""
termination_name = f'termination_{instance.term_side.lower()}'
setattr(instance.circuit, termination_name, instance)
instance.circuit.save()
@receiver((post_save, post_delete), sender=CircuitTermination) @receiver((post_save, post_delete), sender=CircuitTermination)
def update_circuit(instance, **kwargs): def rebuild_cablepaths(instance, raw=False, **kwargs):
""" """
When a CircuitTermination has been modified, update the last_updated time of its parent Circuit. Rebuild any CablePaths which traverse the peer CircuitTermination.
""" """
circuits = Circuit.objects.filter(pk=instance.circuit_id) if not raw:
time = timezone.now() peer_termination = instance.get_peer_termination()
for circuit in circuits: if peer_termination:
circuit.last_updated = time rebuild_paths(peer_termination)
circuit.save()

View File

@ -1,9 +1,18 @@
import django_tables2 as tables import django_tables2 as tables
from django_tables2.utils import Accessor from django_tables2.utils import Accessor
from tenancy.tables import COL_TENANT from tenancy.tables import TenantColumn
from utilities.tables import BaseTable, ButtonsColumn, ChoiceFieldColumn, TagColumn, ToggleColumn from utilities.tables import BaseTable, ButtonsColumn, ChoiceFieldColumn, TagColumn, ToggleColumn
from .models import Circuit, CircuitType, Provider from .models import *
CIRCUITTERMINATION_LINK = """
{% if value.site %}
<a href="{{ value.site.get_absolute_url }}">{{ value.site }}</a>
{% elif value.provider_network %}
<a href="{{ value.provider_network.get_absolute_url }}">{{ value.provider_network }}</a>
{% endif %}
"""
# #
@ -12,7 +21,9 @@ from .models import Circuit, CircuitType, Provider
class ProviderTable(BaseTable): class ProviderTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
name = tables.LinkColumn() name = tables.Column(
linkify=True
)
circuit_count = tables.Column( circuit_count = tables.Column(
accessor=Accessor('count_circuits'), accessor=Accessor('count_circuits'),
verbose_name='Circuits' verbose_name='Circuits'
@ -29,17 +40,41 @@ class ProviderTable(BaseTable):
default_columns = ('pk', 'name', 'asn', 'account', 'circuit_count') default_columns = ('pk', 'name', 'asn', 'account', 'circuit_count')
#
# Provider networks
#
class ProviderNetworkTable(BaseTable):
pk = ToggleColumn()
name = tables.Column(
linkify=True
)
provider = tables.Column(
linkify=True
)
tags = TagColumn(
url_name='circuits:providernetwork_list'
)
class Meta(BaseTable.Meta):
model = ProviderNetwork
fields = ('pk', 'name', 'provider', 'description', 'tags')
default_columns = ('pk', 'name', 'provider', 'description')
# #
# Circuit types # Circuit types
# #
class CircuitTypeTable(BaseTable): class CircuitTypeTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
name = tables.LinkColumn() name = tables.Column(
linkify=True
)
circuit_count = tables.Column( circuit_count = tables.Column(
verbose_name='Circuits' verbose_name='Circuits'
) )
actions = ButtonsColumn(CircuitType, pk_field='slug') actions = ButtonsColumn(CircuitType)
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = CircuitType model = CircuitType
@ -53,22 +88,22 @@ class CircuitTypeTable(BaseTable):
class CircuitTable(BaseTable): class CircuitTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
cid = tables.LinkColumn( cid = tables.Column(
linkify=True,
verbose_name='ID' verbose_name='ID'
) )
provider = tables.LinkColumn( provider = tables.Column(
viewname='circuits:provider', linkify=True
args=[Accessor('provider__slug')]
) )
status = ChoiceFieldColumn() status = ChoiceFieldColumn()
tenant = tables.TemplateColumn( tenant = TenantColumn()
template_code=COL_TENANT termination_a = tables.TemplateColumn(
template_code=CIRCUITTERMINATION_LINK,
verbose_name='Side A'
) )
a_side = tables.Column( termination_z = tables.TemplateColumn(
verbose_name='A Side' template_code=CIRCUITTERMINATION_LINK,
) verbose_name='Side Z'
z_side = tables.Column(
verbose_name='Z Side'
) )
tags = TagColumn( tags = TagColumn(
url_name='circuits:circuit_list' url_name='circuits:circuit_list'
@ -77,7 +112,9 @@ class CircuitTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Circuit model = Circuit
fields = ( fields = (
'pk', 'cid', 'provider', 'type', 'status', 'tenant', 'a_side', 'z_side', 'install_date', 'commit_rate', 'pk', 'cid', 'provider', 'type', 'status', 'tenant', 'termination_a', 'termination_z', 'install_date',
'description', 'tags', 'commit_rate', 'description', 'tags',
)
default_columns = (
'pk', 'cid', 'provider', 'type', 'status', 'tenant', 'termination_a', 'termination_z', 'description',
) )
default_columns = ('pk', 'cid', 'provider', 'type', 'status', 'tenant', 'a_side', 'z_side', 'description')

View File

@ -1,7 +1,7 @@
from django.urls import reverse from django.urls import reverse
from circuits.choices import * from circuits.choices import *
from circuits.models import Circuit, CircuitTermination, CircuitType, Provider from circuits.models import *
from dcim.models import Site from dcim.models import Site
from utilities.testing import APITestCase, APIViewTestCases from utilities.testing import APITestCase, APIViewTestCases
@ -17,7 +17,7 @@ class AppTest(APITestCase):
class ProviderTest(APIViewTestCases.APIViewTestCase): class ProviderTest(APIViewTestCases.APIViewTestCase):
model = Provider model = Provider
brief_fields = ['circuit_count', 'id', 'name', 'slug', 'url'] brief_fields = ['circuit_count', 'display', 'id', 'name', 'slug', 'url']
create_data = [ create_data = [
{ {
'name': 'Provider 4', 'name': 'Provider 4',
@ -49,7 +49,7 @@ class ProviderTest(APIViewTestCases.APIViewTestCase):
class CircuitTypeTest(APIViewTestCases.APIViewTestCase): class CircuitTypeTest(APIViewTestCases.APIViewTestCase):
model = CircuitType model = CircuitType
brief_fields = ['circuit_count', 'id', 'name', 'slug', 'url'] brief_fields = ['circuit_count', 'display', 'id', 'name', 'slug', 'url']
create_data = ( create_data = (
{ {
'name': 'Circuit Type 4', 'name': 'Circuit Type 4',
@ -81,7 +81,7 @@ class CircuitTypeTest(APIViewTestCases.APIViewTestCase):
class CircuitTest(APIViewTestCases.APIViewTestCase): class CircuitTest(APIViewTestCases.APIViewTestCase):
model = Circuit model = Circuit
brief_fields = ['cid', 'id', 'url'] brief_fields = ['cid', 'display', 'id', 'url']
bulk_update_data = { bulk_update_data = {
'status': 'planned', 'status': 'planned',
} }
@ -129,7 +129,7 @@ class CircuitTest(APIViewTestCases.APIViewTestCase):
class CircuitTerminationTest(APIViewTestCases.APIViewTestCase): class CircuitTerminationTest(APIViewTestCases.APIViewTestCase):
model = CircuitTermination model = CircuitTermination
brief_fields = ['cable', 'circuit', 'id', 'term_side', 'url'] brief_fields = ['_occupied', 'cable', 'circuit', 'display', 'id', 'term_side', 'url']
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
@ -178,3 +178,43 @@ class CircuitTerminationTest(APIViewTestCases.APIViewTestCase):
cls.bulk_update_data = { cls.bulk_update_data = {
'port_speed': 123456 'port_speed': 123456
} }
class ProviderNetworkTest(APIViewTestCases.APIViewTestCase):
model = ProviderNetwork
brief_fields = ['display', 'id', 'name', 'url']
@classmethod
def setUpTestData(cls):
providers = (
Provider(name='Provider 1', slug='provider-1'),
Provider(name='Provider 2', slug='provider-2'),
)
Provider.objects.bulk_create(providers)
provider_networks = (
ProviderNetwork(name='Provider Network 1', provider=providers[0]),
ProviderNetwork(name='Provider Network 2', provider=providers[0]),
ProviderNetwork(name='Provider Network 3', provider=providers[0]),
)
ProviderNetwork.objects.bulk_create(provider_networks)
cls.create_data = [
{
'name': 'Provider Network 4',
'provider': providers[0].pk,
},
{
'name': 'Provider Network 5',
'provider': providers[0].pk,
},
{
'name': 'Provider Network 6',
'provider': providers[0].pk,
},
]
cls.bulk_update_data = {
'provider': providers[1].pk,
'description': 'New description',
}

View File

@ -1,13 +1,14 @@
from django.test import TestCase from django.test import TestCase
from circuits.choices import * from circuits.choices import *
from circuits.filters import * from circuits.filtersets import *
from circuits.models import Circuit, CircuitTermination, CircuitType, Provider from circuits.models import *
from dcim.models import Cable, Region, Site from dcim.models import Cable, Region, Site, SiteGroup
from tenancy.models import Tenant, TenantGroup from tenancy.models import Tenant, TenantGroup
from utilities.testing import ChangeLoggedFilterSetTests
class ProviderTestCase(TestCase): class ProviderTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = Provider.objects.all() queryset = Provider.objects.all()
filterset = ProviderFilterSet filterset = ProviderFilterSet
@ -27,13 +28,20 @@ class ProviderTestCase(TestCase):
Region(name='Test Region 1', slug='test-region-1'), Region(name='Test Region 1', slug='test-region-1'),
Region(name='Test Region 2', slug='test-region-2'), Region(name='Test Region 2', slug='test-region-2'),
) )
# Can't use bulk_create for models with MPTT fields
for r in regions: for r in regions:
r.save() r.save()
site_groups = (
SiteGroup(name='Site Group 1', slug='site-group-1'),
SiteGroup(name='Site Group 2', slug='site-group-2'),
SiteGroup(name='Site Group 3', slug='site-group-3'),
)
for site_group in site_groups:
site_group.save()
sites = ( sites = (
Site(name='Test Site 1', slug='test-site-1', region=regions[0]), Site(name='Test Site 1', slug='test-site-1', region=regions[0], group=site_groups[0]),
Site(name='Test Site 2', slug='test-site-2', region=regions[1]), Site(name='Test Site 2', slug='test-site-2', region=regions[1], group=site_groups[1]),
) )
Site.objects.bulk_create(sites) Site.objects.bulk_create(sites)
@ -54,10 +62,6 @@ class ProviderTestCase(TestCase):
CircuitTermination(circuit=circuits[1], site=sites[0], term_side='A'), CircuitTermination(circuit=circuits[1], site=sites[0], term_side='A'),
)) ))
def test_id(self):
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_name(self): def test_name(self):
params = {'name': ['Provider 1', 'Provider 2']} params = {'name': ['Provider 1', 'Provider 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@ -74,13 +78,6 @@ class ProviderTestCase(TestCase):
params = {'account': ['1234', '2345']} params = {'account': ['1234', '2345']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_site(self):
sites = Site.objects.all()[:2]
params = {'site_id': [sites[0].pk, sites[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'site': [sites[0].slug, sites[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_region(self): def test_region(self):
regions = Region.objects.all()[:2] regions = Region.objects.all()[:2]
params = {'region_id': [regions[0].pk, regions[1].pk]} params = {'region_id': [regions[0].pk, regions[1].pk]}
@ -88,8 +85,22 @@ class ProviderTestCase(TestCase):
params = {'region': [regions[0].slug, regions[1].slug]} params = {'region': [regions[0].slug, regions[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_site_group(self):
site_groups = SiteGroup.objects.all()[:2]
params = {'site_group_id': [site_groups[0].pk, site_groups[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'site_group': [site_groups[0].slug, site_groups[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
class CircuitTypeTestCase(TestCase): def test_site(self):
sites = Site.objects.all()[:2]
params = {'site_id': [sites[0].pk, sites[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'site': [sites[0].slug, sites[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
class CircuitTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = CircuitType.objects.all() queryset = CircuitType.objects.all()
filterset = CircuitTypeFilterSet filterset = CircuitTypeFilterSet
@ -102,10 +113,6 @@ class CircuitTypeTestCase(TestCase):
CircuitType(name='Circuit Type 3', slug='circuit-type-3'), CircuitType(name='Circuit Type 3', slug='circuit-type-3'),
)) ))
def test_id(self):
params = {'id': [self.queryset.first().pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_name(self): def test_name(self):
params = {'name': ['Circuit Type 1']} params = {'name': ['Circuit Type 1']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
@ -115,7 +122,7 @@ class CircuitTypeTestCase(TestCase):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
class CircuitTestCase(TestCase): class CircuitTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = Circuit.objects.all() queryset = Circuit.objects.all()
filterset = CircuitFilterSet filterset = CircuitFilterSet
@ -127,14 +134,21 @@ class CircuitTestCase(TestCase):
Region(name='Test Region 2', slug='test-region-2'), Region(name='Test Region 2', slug='test-region-2'),
Region(name='Test Region 3', slug='test-region-3'), Region(name='Test Region 3', slug='test-region-3'),
) )
# Can't use bulk_create for models with MPTT fields
for r in regions: for r in regions:
r.save() r.save()
site_groups = (
SiteGroup(name='Site Group 1', slug='site-group-1'),
SiteGroup(name='Site Group 2', slug='site-group-2'),
SiteGroup(name='Site Group 3', slug='site-group-3'),
)
for site_group in site_groups:
site_group.save()
sites = ( sites = (
Site(name='Test Site 1', slug='test-site-1', region=regions[0]), Site(name='Test Site 1', slug='test-site-1', region=regions[0], group=site_groups[0]),
Site(name='Test Site 2', slug='test-site-2', region=regions[1]), Site(name='Test Site 2', slug='test-site-2', region=regions[1], group=site_groups[1]),
Site(name='Test Site 3', slug='test-site-3', region=regions[2]), Site(name='Test Site 3', slug='test-site-3', region=regions[2], group=site_groups[2]),
) )
Site.objects.bulk_create(sites) Site.objects.bulk_create(sites)
@ -165,6 +179,13 @@ class CircuitTestCase(TestCase):
) )
Provider.objects.bulk_create(providers) Provider.objects.bulk_create(providers)
provider_networks = (
ProviderNetwork(name='Provider Network 1', provider=providers[1]),
ProviderNetwork(name='Provider Network 2', provider=providers[1]),
ProviderNetwork(name='Provider Network 3', provider=providers[1]),
)
ProviderNetwork.objects.bulk_create(provider_networks)
circuits = ( circuits = (
Circuit(provider=providers[0], tenant=tenants[0], type=circuit_types[0], cid='Test Circuit 1', install_date='2020-01-01', commit_rate=1000, status=CircuitStatusChoices.STATUS_ACTIVE), Circuit(provider=providers[0], tenant=tenants[0], type=circuit_types[0], cid='Test Circuit 1', install_date='2020-01-01', commit_rate=1000, status=CircuitStatusChoices.STATUS_ACTIVE),
Circuit(provider=providers[0], tenant=tenants[0], type=circuit_types[0], cid='Test Circuit 2', install_date='2020-01-02', commit_rate=2000, status=CircuitStatusChoices.STATUS_ACTIVE), Circuit(provider=providers[0], tenant=tenants[0], type=circuit_types[0], cid='Test Circuit 2', install_date='2020-01-02', commit_rate=2000, status=CircuitStatusChoices.STATUS_ACTIVE),
@ -179,13 +200,12 @@ class CircuitTestCase(TestCase):
CircuitTermination(circuit=circuits[0], site=sites[0], term_side='A'), CircuitTermination(circuit=circuits[0], site=sites[0], term_side='A'),
CircuitTermination(circuit=circuits[1], site=sites[1], term_side='A'), CircuitTermination(circuit=circuits[1], site=sites[1], term_side='A'),
CircuitTermination(circuit=circuits[2], site=sites[2], term_side='A'), CircuitTermination(circuit=circuits[2], site=sites[2], term_side='A'),
CircuitTermination(circuit=circuits[3], provider_network=provider_networks[0], term_side='A'),
CircuitTermination(circuit=circuits[4], provider_network=provider_networks[1], term_side='A'),
CircuitTermination(circuit=circuits[5], provider_network=provider_networks[2], term_side='A'),
)) ))
CircuitTermination.objects.bulk_create(circuit_terminations) CircuitTermination.objects.bulk_create(circuit_terminations)
def test_id(self):
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_cid(self): def test_cid(self):
params = {'cid': ['Test Circuit 1', 'Test Circuit 2']} params = {'cid': ['Test Circuit 1', 'Test Circuit 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@ -205,6 +225,11 @@ class CircuitTestCase(TestCase):
params = {'provider': [provider.slug]} params = {'provider': [provider.slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
def test_provider_network(self):
provider_networks = ProviderNetwork.objects.all()[:2]
params = {'provider_network_id': [provider_networks[0].pk, provider_networks[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_type(self): def test_type(self):
circuit_type = CircuitType.objects.first() circuit_type = CircuitType.objects.first()
params = {'type_id': [circuit_type.pk]} params = {'type_id': [circuit_type.pk]}
@ -223,6 +248,13 @@ class CircuitTestCase(TestCase):
params = {'region': [regions[0].slug, regions[1].slug]} params = {'region': [regions[0].slug, regions[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_site_group(self):
site_groups = SiteGroup.objects.all()[:2]
params = {'site_group_id': [site_groups[0].pk, site_groups[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'site_group': [site_groups[0].slug, site_groups[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_site(self): def test_site(self):
sites = Site.objects.all()[:2] sites = Site.objects.all()[:2]
params = {'site_id': [sites[0].pk, sites[1].pk]} params = {'site_id': [sites[0].pk, sites[1].pk]}
@ -245,7 +277,7 @@ class CircuitTestCase(TestCase):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
class CircuitTerminationTestCase(TestCase): class CircuitTerminationTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = CircuitTermination.objects.all() queryset = CircuitTermination.objects.all()
filterset = CircuitTerminationFilterSet filterset = CircuitTerminationFilterSet
@ -253,14 +285,14 @@ class CircuitTerminationTestCase(TestCase):
def setUpTestData(cls): def setUpTestData(cls):
sites = ( sites = (
Site(name='Test Site 1', slug='test-site-1'), Site(name='Site 1', slug='site-1'),
Site(name='Test Site 2', slug='test-site-2'), Site(name='Site 2', slug='site-2'),
Site(name='Test Site 3', slug='test-site-3'), Site(name='Site 3', slug='site-3'),
) )
Site.objects.bulk_create(sites) Site.objects.bulk_create(sites)
circuit_types = ( circuit_types = (
CircuitType(name='Test Circuit Type 1', slug='test-circuit-type-1'), CircuitType(name='Circuit Type 1', slug='circuit-type-1'),
) )
CircuitType.objects.bulk_create(circuit_types) CircuitType.objects.bulk_create(circuit_types)
@ -269,10 +301,20 @@ class CircuitTerminationTestCase(TestCase):
) )
Provider.objects.bulk_create(providers) Provider.objects.bulk_create(providers)
provider_networks = (
ProviderNetwork(name='Provider Network 1', provider=providers[0]),
ProviderNetwork(name='Provider Network 2', provider=providers[0]),
ProviderNetwork(name='Provider Network 3', provider=providers[0]),
)
ProviderNetwork.objects.bulk_create(provider_networks)
circuits = ( circuits = (
Circuit(provider=providers[0], type=circuit_types[0], cid='Test Circuit 1'), Circuit(provider=providers[0], type=circuit_types[0], cid='Circuit 1'),
Circuit(provider=providers[0], type=circuit_types[0], cid='Test Circuit 2'), Circuit(provider=providers[0], type=circuit_types[0], cid='Circuit 2'),
Circuit(provider=providers[0], type=circuit_types[0], cid='Test Circuit 3'), Circuit(provider=providers[0], type=circuit_types[0], cid='Circuit 3'),
Circuit(provider=providers[0], type=circuit_types[0], cid='Circuit 4'),
Circuit(provider=providers[0], type=circuit_types[0], cid='Circuit 5'),
Circuit(provider=providers[0], type=circuit_types[0], cid='Circuit 6'),
) )
Circuit.objects.bulk_create(circuits) Circuit.objects.bulk_create(circuits)
@ -283,6 +325,9 @@ class CircuitTerminationTestCase(TestCase):
CircuitTermination(circuit=circuits[1], site=sites[2], term_side='Z', port_speed=2000, upstream_speed=2000, xconnect_id='JKL'), CircuitTermination(circuit=circuits[1], site=sites[2], term_side='Z', port_speed=2000, upstream_speed=2000, xconnect_id='JKL'),
CircuitTermination(circuit=circuits[2], site=sites[2], term_side='A', port_speed=3000, upstream_speed=3000, xconnect_id='MNO'), CircuitTermination(circuit=circuits[2], site=sites[2], term_side='A', port_speed=3000, upstream_speed=3000, xconnect_id='MNO'),
CircuitTermination(circuit=circuits[2], site=sites[0], term_side='Z', port_speed=3000, upstream_speed=3000, xconnect_id='PQR'), CircuitTermination(circuit=circuits[2], site=sites[0], term_side='Z', port_speed=3000, upstream_speed=3000, xconnect_id='PQR'),
CircuitTermination(circuit=circuits[3], provider_network=provider_networks[0], term_side='A'),
CircuitTermination(circuit=circuits[4], provider_network=provider_networks[1], term_side='A'),
CircuitTermination(circuit=circuits[5], provider_network=provider_networks[2], term_side='A'),
)) ))
CircuitTermination.objects.bulk_create(circuit_terminations) CircuitTermination.objects.bulk_create(circuit_terminations)
@ -290,7 +335,7 @@ class CircuitTerminationTestCase(TestCase):
def test_term_side(self): def test_term_side(self):
params = {'term_side': 'A'} params = {'term_side': 'A'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
def test_port_speed(self): def test_port_speed(self):
params = {'port_speed': ['1000', '2000']} params = {'port_speed': ['1000', '2000']}
@ -316,12 +361,44 @@ class CircuitTerminationTestCase(TestCase):
params = {'site': [sites[0].slug, sites[1].slug]} params = {'site': [sites[0].slug, sites[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
def test_provider_network(self):
provider_networks = ProviderNetwork.objects.all()[:2]
params = {'provider_network_id': [provider_networks[0].pk, provider_networks[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_cabled(self): def test_cabled(self):
params = {'cabled': True} params = {'cabled': True}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_connected(self):
params = {'connected': True} class ProviderNetworkTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = ProviderNetwork.objects.all()
filterset = ProviderNetworkFilterSet
@classmethod
def setUpTestData(cls):
providers = (
Provider(name='Provider 1', slug='provider-1'),
Provider(name='Provider 2', slug='provider-2'),
Provider(name='Provider 3', slug='provider-3'),
)
Provider.objects.bulk_create(providers)
provider_networks = (
ProviderNetwork(name='Provider Network 1', provider=providers[0]),
ProviderNetwork(name='Provider Network 2', provider=providers[1]),
ProviderNetwork(name='Provider Network 3', provider=providers[2]),
)
ProviderNetwork.objects.bulk_create(provider_networks)
def test_name(self):
params = {'name': ['Provider Network 1', 'Provider Network 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_provider(self):
providers = Provider.objects.all()[:2]
params = {'provider_id': [providers[0].pk, providers[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'provider': [providers[0].slug, providers[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'connected': False}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)

View File

@ -1,8 +1,12 @@
import datetime import datetime
from django.test import override_settings
from django.urls import reverse
from circuits.choices import * from circuits.choices import *
from circuits.models import Circuit, CircuitType, Provider from circuits.models import *
from utilities.testing import ViewTestCases from dcim.models import Cable, Interface, Site
from utilities.testing import ViewTestCases, create_tags, create_test_device
class ProviderTestCase(ViewTestCases.PrimaryObjectViewTestCase): class ProviderTestCase(ViewTestCases.PrimaryObjectViewTestCase):
@ -17,7 +21,7 @@ class ProviderTestCase(ViewTestCases.PrimaryObjectViewTestCase):
Provider(name='Provider 3', slug='provider-3', asn=65003), Provider(name='Provider 3', slug='provider-3', asn=65003),
]) ])
tags = cls.create_tags('Alpha', 'Bravo', 'Charlie') tags = create_tags('Alpha', 'Bravo', 'Charlie')
cls.form_data = { cls.form_data = {
'name': 'Provider X', 'name': 'Provider X',
@ -73,6 +77,10 @@ class CircuitTypeTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
"Circuit Type 6,circuit-type-6", "Circuit Type 6,circuit-type-6",
) )
cls.bulk_edit_data = {
'description': 'Foo',
}
class CircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase): class CircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase):
model = Circuit model = Circuit
@ -98,7 +106,7 @@ class CircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase):
Circuit(cid='Circuit 3', provider=providers[0], type=circuittypes[0]), Circuit(cid='Circuit 3', provider=providers[0], type=circuittypes[0]),
]) ])
tags = cls.create_tags('Alpha', 'Bravo', 'Charlie') tags = create_tags('Alpha', 'Bravo', 'Charlie')
cls.form_data = { cls.form_data = {
'cid': 'Circuit X', 'cid': 'Circuit X',
@ -129,3 +137,99 @@ class CircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase):
'description': 'New description', 'description': 'New description',
'comments': 'New comments', 'comments': 'New comments',
} }
class ProviderNetworkTestCase(ViewTestCases.PrimaryObjectViewTestCase):
model = ProviderNetwork
@classmethod
def setUpTestData(cls):
providers = (
Provider(name='Provider 1', slug='provider-1'),
Provider(name='Provider 2', slug='provider-2'),
)
Provider.objects.bulk_create(providers)
ProviderNetwork.objects.bulk_create([
ProviderNetwork(name='Provider Network 1', provider=providers[0]),
ProviderNetwork(name='Provider Network 2', provider=providers[0]),
ProviderNetwork(name='Provider Network 3', provider=providers[0]),
])
tags = create_tags('Alpha', 'Bravo', 'Charlie')
cls.form_data = {
'name': 'Provider Network X',
'provider': providers[1].pk,
'description': 'A new provider network',
'comments': 'Longer description goes here',
'tags': [t.pk for t in tags],
}
cls.csv_data = (
"name,provider,description",
"Provider Network 4,Provider 1,Foo",
"Provider Network 5,Provider 1,Bar",
"Provider Network 6,Provider 1,Baz",
)
cls.bulk_edit_data = {
'provider': providers[1].pk,
'description': 'New description',
'comments': 'New comments',
}
class CircuitTerminationTestCase(
ViewTestCases.EditObjectViewTestCase,
ViewTestCases.DeleteObjectViewTestCase,
):
model = CircuitTermination
@classmethod
def setUpTestData(cls):
sites = (
Site(name='Site 1', slug='site-1'),
Site(name='Site 2', slug='site-2'),
Site(name='Site 3', slug='site-3'),
)
Site.objects.bulk_create(sites)
provider = Provider.objects.create(name='Provider 1', slug='provider-1')
circuittype = CircuitType.objects.create(name='Circuit Type 1', slug='circuit-type-1')
circuits = (
Circuit(cid='Circuit 1', provider=provider, type=circuittype),
Circuit(cid='Circuit 2', provider=provider, type=circuittype),
Circuit(cid='Circuit 3', provider=provider, type=circuittype),
)
Circuit.objects.bulk_create(circuits)
circuit_terminations = (
CircuitTermination(circuit=circuits[0], term_side='A', site=sites[0]),
CircuitTermination(circuit=circuits[0], term_side='Z', site=sites[1]),
CircuitTermination(circuit=circuits[1], term_side='A', site=sites[0]),
CircuitTermination(circuit=circuits[1], term_side='Z', site=sites[1]),
)
CircuitTermination.objects.bulk_create(circuit_terminations)
cls.form_data = {
'term_side': 'A',
'site': sites[2].pk,
'description': 'New description',
}
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
def test_trace(self):
device = create_test_device('Device 1')
circuittermination = CircuitTermination.objects.first()
interface = Interface.objects.create(
device=device,
name='Interface 1'
)
Cable(termination_a=circuittermination, termination_b=interface).save()
response = self.client.get(reverse('circuits:circuittermination_trace', kwargs={'pk': circuittermination.pk}))
self.assertHttpStatus(response, 200)

View File

@ -1,9 +1,10 @@
from django.urls import path from django.urls import path
from dcim.views import CableCreateView, PathTraceView from dcim.views import CableCreateView, PathTraceView
from extras.views import ObjectChangeLogView from extras.views import ObjectChangeLogView, ObjectJournalView
from utilities.views import SlugRedirectView
from . import views from . import views
from .models import Circuit, CircuitTermination, CircuitType, Provider from .models import *
app_name = 'circuits' app_name = 'circuits'
urlpatterns = [ urlpatterns = [
@ -14,19 +15,35 @@ urlpatterns = [
path('providers/import/', views.ProviderBulkImportView.as_view(), name='provider_import'), path('providers/import/', views.ProviderBulkImportView.as_view(), name='provider_import'),
path('providers/edit/', views.ProviderBulkEditView.as_view(), name='provider_bulk_edit'), path('providers/edit/', views.ProviderBulkEditView.as_view(), name='provider_bulk_edit'),
path('providers/delete/', views.ProviderBulkDeleteView.as_view(), name='provider_bulk_delete'), path('providers/delete/', views.ProviderBulkDeleteView.as_view(), name='provider_bulk_delete'),
path('providers/<slug:slug>/', views.ProviderView.as_view(), name='provider'), path('providers/<int:pk>/', views.ProviderView.as_view(), name='provider'),
path('providers/<slug:slug>/edit/', views.ProviderEditView.as_view(), name='provider_edit'), path('providers/<slug:slug>/', SlugRedirectView.as_view(), kwargs={'model': Provider}),
path('providers/<slug:slug>/delete/', views.ProviderDeleteView.as_view(), name='provider_delete'), path('providers/<int:pk>/edit/', views.ProviderEditView.as_view(), name='provider_edit'),
path('providers/<slug:slug>/changelog/', ObjectChangeLogView.as_view(), name='provider_changelog', kwargs={'model': Provider}), path('providers/<int:pk>/delete/', views.ProviderDeleteView.as_view(), name='provider_delete'),
path('providers/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='provider_changelog', kwargs={'model': Provider}),
path('providers/<int:pk>/journal/', ObjectJournalView.as_view(), name='provider_journal', kwargs={'model': Provider}),
# Provider networks
path('provider-networks/', views.ProviderNetworkListView.as_view(), name='providernetwork_list'),
path('provider-networks/add/', views.ProviderNetworkEditView.as_view(), name='providernetwork_add'),
path('provider-networks/import/', views.ProviderNetworkBulkImportView.as_view(), name='providernetwork_import'),
path('provider-networks/edit/', views.ProviderNetworkBulkEditView.as_view(), name='providernetwork_bulk_edit'),
path('provider-networks/delete/', views.ProviderNetworkBulkDeleteView.as_view(), name='providernetwork_bulk_delete'),
path('provider-networks/<int:pk>/', views.ProviderNetworkView.as_view(), name='providernetwork'),
path('provider-networks/<int:pk>/edit/', views.ProviderNetworkEditView.as_view(), name='providernetwork_edit'),
path('provider-networks/<int:pk>/delete/', views.ProviderNetworkDeleteView.as_view(), name='providernetwork_delete'),
path('provider-networks/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='providernetwork_changelog', kwargs={'model': ProviderNetwork}),
path('provider-networks/<int:pk>/journal/', ObjectJournalView.as_view(), name='providernetwork_journal', kwargs={'model': ProviderNetwork}),
# Circuit types # Circuit types
path('circuit-types/', views.CircuitTypeListView.as_view(), name='circuittype_list'), path('circuit-types/', views.CircuitTypeListView.as_view(), name='circuittype_list'),
path('circuit-types/add/', views.CircuitTypeEditView.as_view(), name='circuittype_add'), path('circuit-types/add/', views.CircuitTypeEditView.as_view(), name='circuittype_add'),
path('circuit-types/import/', views.CircuitTypeBulkImportView.as_view(), name='circuittype_import'), path('circuit-types/import/', views.CircuitTypeBulkImportView.as_view(), name='circuittype_import'),
path('circuit-types/edit/', views.CircuitTypeBulkEditView.as_view(), name='circuittype_bulk_edit'),
path('circuit-types/delete/', views.CircuitTypeBulkDeleteView.as_view(), name='circuittype_bulk_delete'), path('circuit-types/delete/', views.CircuitTypeBulkDeleteView.as_view(), name='circuittype_bulk_delete'),
path('circuit-types/<slug:slug>/edit/', views.CircuitTypeEditView.as_view(), name='circuittype_edit'), path('circuit-types/<int:pk>/', views.CircuitTypeView.as_view(), name='circuittype'),
path('circuit-types/<slug:slug>/delete/', views.CircuitTypeDeleteView.as_view(), name='circuittype_delete'), path('circuit-types/<int:pk>/edit/', views.CircuitTypeEditView.as_view(), name='circuittype_edit'),
path('circuit-types/<slug:slug>/changelog/', ObjectChangeLogView.as_view(), name='circuittype_changelog', kwargs={'model': CircuitType}), path('circuit-types/<int:pk>/delete/', views.CircuitTypeDeleteView.as_view(), name='circuittype_delete'),
path('circuit-types/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='circuittype_changelog', kwargs={'model': CircuitType}),
# Circuits # Circuits
path('circuits/', views.CircuitListView.as_view(), name='circuit_list'), path('circuits/', views.CircuitListView.as_view(), name='circuit_list'),
@ -38,6 +55,7 @@ urlpatterns = [
path('circuits/<int:pk>/edit/', views.CircuitEditView.as_view(), name='circuit_edit'), path('circuits/<int:pk>/edit/', views.CircuitEditView.as_view(), name='circuit_edit'),
path('circuits/<int:pk>/delete/', views.CircuitDeleteView.as_view(), name='circuit_delete'), path('circuits/<int:pk>/delete/', views.CircuitDeleteView.as_view(), name='circuit_delete'),
path('circuits/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='circuit_changelog', kwargs={'model': Circuit}), path('circuits/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='circuit_changelog', kwargs={'model': Circuit}),
path('circuits/<int:pk>/journal/', ObjectJournalView.as_view(), name='circuit_journal', kwargs={'model': Circuit}),
path('circuits/<int:pk>/terminations/swap/', views.CircuitSwapTerminations.as_view(), name='circuit_terminations_swap'), path('circuits/<int:pk>/terminations/swap/', views.CircuitSwapTerminations.as_view(), name='circuit_terminations_swap'),
# Circuit terminations # Circuit terminations

View File

@ -1,15 +1,15 @@
from django.contrib import messages from django.contrib import messages
from django.db import transaction from django.db import transaction
from django.db.models import Q
from django.shortcuts import get_object_or_404, redirect, render from django.shortcuts import get_object_or_404, redirect, render
from django_tables2 import RequestConfig
from netbox.views import generic from netbox.views import generic
from utilities.forms import ConfirmationForm from utilities.forms import ConfirmationForm
from utilities.paginator import EnhancedPaginator, get_paginate_count from utilities.tables import paginate_table
from utilities.utils import count_related from utilities.utils import count_related
from . import filters, forms, tables from . import filtersets, forms, tables
from .choices import CircuitTerminationSideChoices from .choices import CircuitTerminationSideChoices
from .models import Circuit, CircuitTermination, CircuitType, Provider from .models import *
# #
@ -20,7 +20,7 @@ class ProviderListView(generic.ObjectListView):
queryset = Provider.objects.annotate( queryset = Provider.objects.annotate(
count_circuits=count_related(Circuit, 'provider') count_circuits=count_related(Circuit, 'provider')
) )
filterset = filters.ProviderFilterSet filterset = filtersets.ProviderFilterSet
filterset_form = forms.ProviderFilterForm filterset_form = forms.ProviderFilterForm
table = tables.ProviderTable table = tables.ProviderTable
@ -33,16 +33,11 @@ class ProviderView(generic.ObjectView):
provider=instance provider=instance
).prefetch_related( ).prefetch_related(
'type', 'tenant', 'terminations__site' 'type', 'tenant', 'terminations__site'
).annotate_sites() )
circuits_table = tables.CircuitTable(circuits) circuits_table = tables.CircuitTable(circuits)
circuits_table.columns.hide('provider') circuits_table.columns.hide('provider')
paginate_table(circuits_table, request)
paginate = {
'paginator_class': EnhancedPaginator,
'per_page': get_paginate_count(request)
}
RequestConfig(request, paginate).configure(circuits_table)
return { return {
'circuits_table': circuits_table, 'circuits_table': circuits_table,
@ -52,7 +47,6 @@ class ProviderView(generic.ObjectView):
class ProviderEditView(generic.ObjectEditView): class ProviderEditView(generic.ObjectEditView):
queryset = Provider.objects.all() queryset = Provider.objects.all()
model_form = forms.ProviderForm model_form = forms.ProviderForm
template_name = 'circuits/provider_edit.html'
class ProviderDeleteView(generic.ObjectDeleteView): class ProviderDeleteView(generic.ObjectDeleteView):
@ -69,7 +63,7 @@ class ProviderBulkEditView(generic.BulkEditView):
queryset = Provider.objects.annotate( queryset = Provider.objects.annotate(
count_circuits=count_related(Circuit, 'provider') count_circuits=count_related(Circuit, 'provider')
) )
filterset = filters.ProviderFilterSet filterset = filtersets.ProviderFilterSet
table = tables.ProviderTable table = tables.ProviderTable
form = forms.ProviderBulkEditForm form = forms.ProviderBulkEditForm
@ -78,10 +72,70 @@ class ProviderBulkDeleteView(generic.BulkDeleteView):
queryset = Provider.objects.annotate( queryset = Provider.objects.annotate(
count_circuits=count_related(Circuit, 'provider') count_circuits=count_related(Circuit, 'provider')
) )
filterset = filters.ProviderFilterSet filterset = filtersets.ProviderFilterSet
table = tables.ProviderTable table = tables.ProviderTable
#
# Provider networks
#
class ProviderNetworkListView(generic.ObjectListView):
queryset = ProviderNetwork.objects.all()
filterset = filtersets.ProviderNetworkFilterSet
filterset_form = forms.ProviderNetworkFilterForm
table = tables.ProviderNetworkTable
class ProviderNetworkView(generic.ObjectView):
queryset = ProviderNetwork.objects.all()
def get_extra_context(self, request, instance):
circuits = Circuit.objects.restrict(request.user, 'view').filter(
Q(termination_a__provider_network=instance.pk) |
Q(termination_z__provider_network=instance.pk)
).prefetch_related(
'type', 'tenant', 'terminations__site'
)
circuits_table = tables.CircuitTable(circuits)
circuits_table.columns.hide('termination_a')
circuits_table.columns.hide('termination_z')
paginate_table(circuits_table, request)
return {
'circuits_table': circuits_table,
}
class ProviderNetworkEditView(generic.ObjectEditView):
queryset = ProviderNetwork.objects.all()
model_form = forms.ProviderNetworkForm
class ProviderNetworkDeleteView(generic.ObjectDeleteView):
queryset = ProviderNetwork.objects.all()
class ProviderNetworkBulkImportView(generic.BulkImportView):
queryset = ProviderNetwork.objects.all()
model_form = forms.ProviderNetworkCSVForm
table = tables.ProviderNetworkTable
class ProviderNetworkBulkEditView(generic.BulkEditView):
queryset = ProviderNetwork.objects.all()
filterset = filtersets.ProviderNetworkFilterSet
table = tables.ProviderNetworkTable
form = forms.ProviderNetworkBulkEditForm
class ProviderNetworkBulkDeleteView(generic.BulkDeleteView):
queryset = ProviderNetwork.objects.all()
filterset = filtersets.ProviderNetworkFilterSet
table = tables.ProviderNetworkTable
# #
# Circuit Types # Circuit Types
# #
@ -93,6 +147,23 @@ class CircuitTypeListView(generic.ObjectListView):
table = tables.CircuitTypeTable table = tables.CircuitTypeTable
class CircuitTypeView(generic.ObjectView):
queryset = CircuitType.objects.all()
def get_extra_context(self, request, instance):
circuits = Circuit.objects.restrict(request.user, 'view').filter(
type=instance
)
circuits_table = tables.CircuitTable(circuits)
circuits_table.columns.hide('type')
paginate_table(circuits_table, request)
return {
'circuits_table': circuits_table,
}
class CircuitTypeEditView(generic.ObjectEditView): class CircuitTypeEditView(generic.ObjectEditView):
queryset = CircuitType.objects.all() queryset = CircuitType.objects.all()
model_form = forms.CircuitTypeForm model_form = forms.CircuitTypeForm
@ -108,6 +179,15 @@ class CircuitTypeBulkImportView(generic.BulkImportView):
table = tables.CircuitTypeTable table = tables.CircuitTypeTable
class CircuitTypeBulkEditView(generic.BulkEditView):
queryset = CircuitType.objects.annotate(
circuit_count=count_related(Circuit, 'type')
)
filterset = filtersets.CircuitTypeFilterSet
table = tables.CircuitTypeTable
form = forms.CircuitTypeBulkEditForm
class CircuitTypeBulkDeleteView(generic.BulkDeleteView): class CircuitTypeBulkDeleteView(generic.BulkDeleteView):
queryset = CircuitType.objects.annotate( queryset = CircuitType.objects.annotate(
circuit_count=count_related(Circuit, 'type') circuit_count=count_related(Circuit, 'type')
@ -121,9 +201,9 @@ class CircuitTypeBulkDeleteView(generic.BulkDeleteView):
class CircuitListView(generic.ObjectListView): class CircuitListView(generic.ObjectListView):
queryset = Circuit.objects.prefetch_related( queryset = Circuit.objects.prefetch_related(
'provider', 'type', 'tenant', 'terminations' 'provider', 'type', 'tenant', 'termination_a', 'termination_z'
).annotate_sites() )
filterset = filters.CircuitFilterSet filterset = filtersets.CircuitFilterSet
filterset_form = forms.CircuitFilterForm filterset_form = forms.CircuitFilterForm
table = tables.CircuitTable table = tables.CircuitTable
@ -131,36 +211,10 @@ class CircuitListView(generic.ObjectListView):
class CircuitView(generic.ObjectView): class CircuitView(generic.ObjectView):
queryset = Circuit.objects.all() queryset = Circuit.objects.all()
def get_extra_context(self, request, instance):
# A-side termination
termination_a = CircuitTermination.objects.restrict(request.user, 'view').prefetch_related(
'site__region'
).filter(
circuit=instance, term_side=CircuitTerminationSideChoices.SIDE_A
).first()
if termination_a and termination_a.connected_endpoint and hasattr(termination_a.connected_endpoint, 'ip_addresses'):
termination_a.ip_addresses = termination_a.connected_endpoint.ip_addresses.restrict(request.user, 'view')
# Z-side termination
termination_z = CircuitTermination.objects.restrict(request.user, 'view').prefetch_related(
'site__region'
).filter(
circuit=instance, term_side=CircuitTerminationSideChoices.SIDE_Z
).first()
if termination_z and termination_z.connected_endpoint and hasattr(termination_z.connected_endpoint, 'ip_addresses'):
termination_z.ip_addresses = termination_z.connected_endpoint.ip_addresses.restrict(request.user, 'view')
return {
'termination_a': termination_a,
'termination_z': termination_z,
}
class CircuitEditView(generic.ObjectEditView): class CircuitEditView(generic.ObjectEditView):
queryset = Circuit.objects.all() queryset = Circuit.objects.all()
model_form = forms.CircuitForm model_form = forms.CircuitForm
template_name = 'circuits/circuit_edit.html'
class CircuitDeleteView(generic.ObjectDeleteView): class CircuitDeleteView(generic.ObjectDeleteView):
@ -177,7 +231,7 @@ class CircuitBulkEditView(generic.BulkEditView):
queryset = Circuit.objects.prefetch_related( queryset = Circuit.objects.prefetch_related(
'provider', 'type', 'tenant', 'terminations' 'provider', 'type', 'tenant', 'terminations'
) )
filterset = filters.CircuitFilterSet filterset = filtersets.CircuitFilterSet
table = tables.CircuitTable table = tables.CircuitTable
form = forms.CircuitBulkEditForm form = forms.CircuitBulkEditForm
@ -186,7 +240,7 @@ class CircuitBulkDeleteView(generic.BulkDeleteView):
queryset = Circuit.objects.prefetch_related( queryset = Circuit.objects.prefetch_related(
'provider', 'type', 'tenant', 'terminations' 'provider', 'type', 'tenant', 'terminations'
) )
filterset = filters.CircuitFilterSet filterset = filtersets.CircuitFilterSet
table = tables.CircuitTable table = tables.CircuitTable
@ -221,16 +275,11 @@ class CircuitSwapTerminations(generic.ObjectEditView):
if form.is_valid(): if form.is_valid():
termination_a = CircuitTermination.objects.filter( termination_a = CircuitTermination.objects.filter(pk=circuit.termination_a_id).first()
circuit=circuit, term_side=CircuitTerminationSideChoices.SIDE_A termination_z = CircuitTermination.objects.filter(pk=circuit.termination_z_id).first()
).first()
termination_z = CircuitTermination.objects.filter(
circuit=circuit, term_side=CircuitTerminationSideChoices.SIDE_Z
).first()
if termination_a and termination_z: if termination_a and termination_z:
# Use a placeholder to avoid an IntegrityError on the (circuit, term_side) unique constraint # Use a placeholder to avoid an IntegrityError on the (circuit, term_side) unique constraint
print('swapping')
with transaction.atomic(): with transaction.atomic():
termination_a.term_side = '_' termination_a.term_side = '_'
termination_a.save() termination_a.save()
@ -238,14 +287,24 @@ class CircuitSwapTerminations(generic.ObjectEditView):
termination_z.save() termination_z.save()
termination_a.term_side = 'Z' termination_a.term_side = 'Z'
termination_a.save() termination_a.save()
circuit.refresh_from_db()
circuit.termination_a = termination_z
circuit.termination_z = termination_a
circuit.save()
elif termination_a: elif termination_a:
termination_a.term_side = 'Z' termination_a.term_side = 'Z'
termination_a.save() termination_a.save()
circuit.refresh_from_db()
circuit.termination_a = None
circuit.save()
else: else:
termination_z.term_side = 'A' termination_z.term_side = 'A'
termination_z.save() termination_z.save()
circuit.refresh_from_db()
circuit.termination_z = None
circuit.save()
messages.success(request, "Swapped terminations for circuit {}.".format(circuit)) messages.success(request, f"Swapped terminations for circuit {circuit}.")
return redirect('circuits:circuit', pk=circuit.pk) return redirect('circuits:circuit', pk=circuit.pk)
return render(request, 'circuits/circuit_terminations_swap.html', { return render(request, 'circuits/circuit_terminations_swap.html', {

View File

@ -1,7 +1,7 @@
from rest_framework import serializers from rest_framework import serializers
from dcim import models from dcim import models
from netbox.api import WritableNestedSerializer from netbox.api.serializers import BaseModelSerializer, WritableNestedSerializer
__all__ = [ __all__ = [
'NestedCableSerializer', 'NestedCableSerializer',
@ -27,7 +27,7 @@ __all__ = [
'NestedPowerPanelSerializer', 'NestedPowerPanelSerializer',
'NestedPowerPortSerializer', 'NestedPowerPortSerializer',
'NestedPowerPortTemplateSerializer', 'NestedPowerPortTemplateSerializer',
'NestedRackGroupSerializer', 'NestedLocationSerializer',
'NestedRackReservationSerializer', 'NestedRackReservationSerializer',
'NestedRackRoleSerializer', 'NestedRackRoleSerializer',
'NestedRackSerializer', 'NestedRackSerializer',
@ -35,6 +35,7 @@ __all__ = [
'NestedRearPortTemplateSerializer', 'NestedRearPortTemplateSerializer',
'NestedRegionSerializer', 'NestedRegionSerializer',
'NestedSiteSerializer', 'NestedSiteSerializer',
'NestedSiteGroupSerializer',
'NestedVirtualChassisSerializer', 'NestedVirtualChassisSerializer',
] ]
@ -50,7 +51,17 @@ class NestedRegionSerializer(WritableNestedSerializer):
class Meta: class Meta:
model = models.Region model = models.Region
fields = ['id', 'url', 'name', 'slug', 'site_count', '_depth'] fields = ['id', 'url', 'display', 'name', 'slug', 'site_count', '_depth']
class NestedSiteGroupSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:sitegroup-detail')
site_count = serializers.IntegerField(read_only=True)
_depth = serializers.IntegerField(source='level', read_only=True)
class Meta:
model = models.SiteGroup
fields = ['id', 'url', 'display', 'name', 'slug', 'site_count', '_depth']
class NestedSiteSerializer(WritableNestedSerializer): class NestedSiteSerializer(WritableNestedSerializer):
@ -58,21 +69,21 @@ class NestedSiteSerializer(WritableNestedSerializer):
class Meta: class Meta:
model = models.Site model = models.Site
fields = ['id', 'url', 'name', 'slug'] fields = ['id', 'url', 'display', 'name', 'slug']
# #
# Racks # Racks
# #
class NestedRackGroupSerializer(WritableNestedSerializer): class NestedLocationSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackgroup-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:location-detail')
rack_count = serializers.IntegerField(read_only=True) rack_count = serializers.IntegerField(read_only=True)
_depth = serializers.IntegerField(source='level', read_only=True) _depth = serializers.IntegerField(source='level', read_only=True)
class Meta: class Meta:
model = models.RackGroup model = models.Location
fields = ['id', 'url', 'name', 'slug', 'rack_count', '_depth'] fields = ['id', 'url', 'display', 'name', 'slug', 'rack_count', '_depth']
class NestedRackRoleSerializer(WritableNestedSerializer): class NestedRackRoleSerializer(WritableNestedSerializer):
@ -81,7 +92,7 @@ class NestedRackRoleSerializer(WritableNestedSerializer):
class Meta: class Meta:
model = models.RackRole model = models.RackRole
fields = ['id', 'url', 'name', 'slug', 'rack_count'] fields = ['id', 'url', 'display', 'name', 'slug', 'rack_count']
class NestedRackSerializer(WritableNestedSerializer): class NestedRackSerializer(WritableNestedSerializer):
@ -90,7 +101,7 @@ class NestedRackSerializer(WritableNestedSerializer):
class Meta: class Meta:
model = models.Rack model = models.Rack
fields = ['id', 'url', 'name', 'display_name', 'device_count'] fields = ['id', 'url', 'display', 'name', 'display_name', 'device_count']
class NestedRackReservationSerializer(WritableNestedSerializer): class NestedRackReservationSerializer(WritableNestedSerializer):
@ -99,7 +110,7 @@ class NestedRackReservationSerializer(WritableNestedSerializer):
class Meta: class Meta:
model = models.RackReservation model = models.RackReservation
fields = ['id', 'url', 'user', 'units'] fields = ['id', 'url', 'display', 'user', 'units']
def get_user(self, obj): def get_user(self, obj):
return obj.user.username return obj.user.username
@ -115,7 +126,7 @@ class NestedManufacturerSerializer(WritableNestedSerializer):
class Meta: class Meta:
model = models.Manufacturer model = models.Manufacturer
fields = ['id', 'url', 'name', 'slug', 'devicetype_count'] fields = ['id', 'url', 'display', 'name', 'slug', 'devicetype_count']
class NestedDeviceTypeSerializer(WritableNestedSerializer): class NestedDeviceTypeSerializer(WritableNestedSerializer):
@ -125,7 +136,7 @@ class NestedDeviceTypeSerializer(WritableNestedSerializer):
class Meta: class Meta:
model = models.DeviceType model = models.DeviceType
fields = ['id', 'url', 'manufacturer', 'model', 'slug', 'display_name', 'device_count'] fields = ['id', 'url', 'display', 'manufacturer', 'model', 'slug', 'display_name', 'device_count']
class NestedConsolePortTemplateSerializer(WritableNestedSerializer): class NestedConsolePortTemplateSerializer(WritableNestedSerializer):
@ -133,7 +144,7 @@ class NestedConsolePortTemplateSerializer(WritableNestedSerializer):
class Meta: class Meta:
model = models.ConsolePortTemplate model = models.ConsolePortTemplate
fields = ['id', 'url', 'name'] fields = ['id', 'url', 'display', 'name']
class NestedConsoleServerPortTemplateSerializer(WritableNestedSerializer): class NestedConsoleServerPortTemplateSerializer(WritableNestedSerializer):
@ -141,7 +152,7 @@ class NestedConsoleServerPortTemplateSerializer(WritableNestedSerializer):
class Meta: class Meta:
model = models.ConsoleServerPortTemplate model = models.ConsoleServerPortTemplate
fields = ['id', 'url', 'name'] fields = ['id', 'url', 'display', 'name']
class NestedPowerPortTemplateSerializer(WritableNestedSerializer): class NestedPowerPortTemplateSerializer(WritableNestedSerializer):
@ -149,7 +160,7 @@ class NestedPowerPortTemplateSerializer(WritableNestedSerializer):
class Meta: class Meta:
model = models.PowerPortTemplate model = models.PowerPortTemplate
fields = ['id', 'url', 'name'] fields = ['id', 'url', 'display', 'name']
class NestedPowerOutletTemplateSerializer(WritableNestedSerializer): class NestedPowerOutletTemplateSerializer(WritableNestedSerializer):
@ -157,7 +168,7 @@ class NestedPowerOutletTemplateSerializer(WritableNestedSerializer):
class Meta: class Meta:
model = models.PowerOutletTemplate model = models.PowerOutletTemplate
fields = ['id', 'url', 'name'] fields = ['id', 'url', 'display', 'name']
class NestedInterfaceTemplateSerializer(WritableNestedSerializer): class NestedInterfaceTemplateSerializer(WritableNestedSerializer):
@ -165,7 +176,7 @@ class NestedInterfaceTemplateSerializer(WritableNestedSerializer):
class Meta: class Meta:
model = models.InterfaceTemplate model = models.InterfaceTemplate
fields = ['id', 'url', 'name'] fields = ['id', 'url', 'display', 'name']
class NestedRearPortTemplateSerializer(WritableNestedSerializer): class NestedRearPortTemplateSerializer(WritableNestedSerializer):
@ -173,7 +184,7 @@ class NestedRearPortTemplateSerializer(WritableNestedSerializer):
class Meta: class Meta:
model = models.RearPortTemplate model = models.RearPortTemplate
fields = ['id', 'url', 'name'] fields = ['id', 'url', 'display', 'name']
class NestedFrontPortTemplateSerializer(WritableNestedSerializer): class NestedFrontPortTemplateSerializer(WritableNestedSerializer):
@ -181,7 +192,7 @@ class NestedFrontPortTemplateSerializer(WritableNestedSerializer):
class Meta: class Meta:
model = models.FrontPortTemplate model = models.FrontPortTemplate
fields = ['id', 'url', 'name'] fields = ['id', 'url', 'display', 'name']
class NestedDeviceBayTemplateSerializer(WritableNestedSerializer): class NestedDeviceBayTemplateSerializer(WritableNestedSerializer):
@ -189,7 +200,7 @@ class NestedDeviceBayTemplateSerializer(WritableNestedSerializer):
class Meta: class Meta:
model = models.DeviceBayTemplate model = models.DeviceBayTemplate
fields = ['id', 'url', 'name'] fields = ['id', 'url', 'display', 'name']
# #
@ -203,7 +214,7 @@ class NestedDeviceRoleSerializer(WritableNestedSerializer):
class Meta: class Meta:
model = models.DeviceRole model = models.DeviceRole
fields = ['id', 'url', 'name', 'slug', 'device_count', 'virtualmachine_count'] fields = ['id', 'url', 'display', 'name', 'slug', 'device_count', 'virtualmachine_count']
class NestedPlatformSerializer(WritableNestedSerializer): class NestedPlatformSerializer(WritableNestedSerializer):
@ -213,7 +224,7 @@ class NestedPlatformSerializer(WritableNestedSerializer):
class Meta: class Meta:
model = models.Platform model = models.Platform
fields = ['id', 'url', 'name', 'slug', 'device_count', 'virtualmachine_count'] fields = ['id', 'url', 'display', 'name', 'slug', 'device_count', 'virtualmachine_count']
class NestedDeviceSerializer(WritableNestedSerializer): class NestedDeviceSerializer(WritableNestedSerializer):
@ -221,7 +232,7 @@ class NestedDeviceSerializer(WritableNestedSerializer):
class Meta: class Meta:
model = models.Device model = models.Device
fields = ['id', 'url', 'name', 'display_name'] fields = ['id', 'url', 'display', 'name', 'display_name']
class NestedConsoleServerPortSerializer(WritableNestedSerializer): class NestedConsoleServerPortSerializer(WritableNestedSerializer):
@ -230,7 +241,7 @@ class NestedConsoleServerPortSerializer(WritableNestedSerializer):
class Meta: class Meta:
model = models.ConsoleServerPort model = models.ConsoleServerPort
fields = ['id', 'url', 'device', 'name', 'cable'] fields = ['id', 'url', 'display', 'device', 'name', 'cable', '_occupied']
class NestedConsolePortSerializer(WritableNestedSerializer): class NestedConsolePortSerializer(WritableNestedSerializer):
@ -239,7 +250,7 @@ class NestedConsolePortSerializer(WritableNestedSerializer):
class Meta: class Meta:
model = models.ConsolePort model = models.ConsolePort
fields = ['id', 'url', 'device', 'name', 'cable'] fields = ['id', 'url', 'display', 'device', 'name', 'cable', '_occupied']
class NestedPowerOutletSerializer(WritableNestedSerializer): class NestedPowerOutletSerializer(WritableNestedSerializer):
@ -248,7 +259,7 @@ class NestedPowerOutletSerializer(WritableNestedSerializer):
class Meta: class Meta:
model = models.PowerOutlet model = models.PowerOutlet
fields = ['id', 'url', 'device', 'name', 'cable'] fields = ['id', 'url', 'display', 'device', 'name', 'cable', '_occupied']
class NestedPowerPortSerializer(WritableNestedSerializer): class NestedPowerPortSerializer(WritableNestedSerializer):
@ -257,7 +268,7 @@ class NestedPowerPortSerializer(WritableNestedSerializer):
class Meta: class Meta:
model = models.PowerPort model = models.PowerPort
fields = ['id', 'url', 'device', 'name', 'cable'] fields = ['id', 'url', 'display', 'device', 'name', 'cable', '_occupied']
class NestedInterfaceSerializer(WritableNestedSerializer): class NestedInterfaceSerializer(WritableNestedSerializer):
@ -266,7 +277,7 @@ class NestedInterfaceSerializer(WritableNestedSerializer):
class Meta: class Meta:
model = models.Interface model = models.Interface
fields = ['id', 'url', 'device', 'name', 'cable'] fields = ['id', 'url', 'display', 'device', 'name', 'cable', '_occupied']
class NestedRearPortSerializer(WritableNestedSerializer): class NestedRearPortSerializer(WritableNestedSerializer):
@ -275,7 +286,7 @@ class NestedRearPortSerializer(WritableNestedSerializer):
class Meta: class Meta:
model = models.RearPort model = models.RearPort
fields = ['id', 'url', 'device', 'name', 'cable'] fields = ['id', 'url', 'display', 'device', 'name', 'cable', '_occupied']
class NestedFrontPortSerializer(WritableNestedSerializer): class NestedFrontPortSerializer(WritableNestedSerializer):
@ -284,7 +295,7 @@ class NestedFrontPortSerializer(WritableNestedSerializer):
class Meta: class Meta:
model = models.FrontPort model = models.FrontPort
fields = ['id', 'url', 'device', 'name', 'cable'] fields = ['id', 'url', 'display', 'device', 'name', 'cable', '_occupied']
class NestedDeviceBaySerializer(WritableNestedSerializer): class NestedDeviceBaySerializer(WritableNestedSerializer):
@ -293,7 +304,7 @@ class NestedDeviceBaySerializer(WritableNestedSerializer):
class Meta: class Meta:
model = models.DeviceBay model = models.DeviceBay
fields = ['id', 'url', 'device', 'name'] fields = ['id', 'url', 'display', 'device', 'name']
class NestedInventoryItemSerializer(WritableNestedSerializer): class NestedInventoryItemSerializer(WritableNestedSerializer):
@ -303,19 +314,19 @@ class NestedInventoryItemSerializer(WritableNestedSerializer):
class Meta: class Meta:
model = models.InventoryItem model = models.InventoryItem
fields = ['id', 'url', 'device', 'name', '_depth'] fields = ['id', 'url', 'display', 'device', 'name', '_depth']
# #
# Cables # Cables
# #
class NestedCableSerializer(serializers.ModelSerializer): class NestedCableSerializer(BaseModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:cable-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:cable-detail')
class Meta: class Meta:
model = models.Cable model = models.Cable
fields = ['id', 'url', 'label'] fields = ['id', 'url', 'display', 'label']
# #
@ -342,7 +353,7 @@ class NestedPowerPanelSerializer(WritableNestedSerializer):
class Meta: class Meta:
model = models.PowerPanel model = models.PowerPanel
fields = ['id', 'url', 'name', 'powerfeed_count'] fields = ['id', 'url', 'display', 'name', 'powerfeed_count']
class NestedPowerFeedSerializer(WritableNestedSerializer): class NestedPowerFeedSerializer(WritableNestedSerializer):
@ -350,4 +361,4 @@ class NestedPowerFeedSerializer(WritableNestedSerializer):
class Meta: class Meta:
model = models.PowerFeed model = models.PowerFeed
fields = ['id', 'url', 'name', 'cable'] fields = ['id', 'url', 'display', 'name', 'cable', '_occupied']

View File

@ -3,23 +3,16 @@ from django.contrib.contenttypes.models import ContentType
from drf_yasg.utils import swagger_serializer_method from drf_yasg.utils import swagger_serializer_method
from rest_framework import serializers from rest_framework import serializers
from rest_framework.validators import UniqueTogetherValidator from rest_framework.validators import UniqueTogetherValidator
from timezone_field.rest_framework import TimeZoneSerializerField
from dcim.choices import * from dcim.choices import *
from dcim.constants import * from dcim.constants import *
from dcim.models import ( from dcim.models import *
Cable, CablePath, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBayTemplate, DeviceType, DeviceRole, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate,
Manufacturer, InventoryItem, Platform, PowerFeed, PowerOutlet, PowerOutletTemplate, PowerPanel, PowerPort,
PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site,
VirtualChassis,
)
from dcim.utils import decompile_path_node
from extras.api.customfields import CustomFieldModelSerializer
from extras.api.serializers import TaggedObjectSerializer
from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedVLANSerializer from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedVLANSerializer
from ipam.models import VLAN from ipam.models import VLAN
from netbox.api import ( from netbox.api import ChoiceField, ContentTypeField, SerializedPKRelatedField
ChoiceField, ContentTypeField, SerializedPKRelatedField, TimeZoneField, ValidatedModelSerializer, from netbox.api.serializers import (
NestedGroupModelSerializer, OrganizationalModelSerializer, PrimaryModelSerializer, ValidatedModelSerializer,
WritableNestedSerializer, WritableNestedSerializer,
) )
from tenancy.api.nested_serializers import NestedTenantSerializer from tenancy.api.nested_serializers import NestedTenantSerializer
@ -32,6 +25,7 @@ from .nested_serializers import *
class CableTerminationSerializer(serializers.ModelSerializer): class CableTerminationSerializer(serializers.ModelSerializer):
cable_peer_type = serializers.SerializerMethodField(read_only=True) cable_peer_type = serializers.SerializerMethodField(read_only=True)
cable_peer = serializers.SerializerMethodField(read_only=True) cable_peer = serializers.SerializerMethodField(read_only=True)
_occupied = serializers.SerializerMethodField(read_only=True)
def get_cable_peer_type(self, obj): def get_cable_peer_type(self, obj):
if obj._cable_peer is not None: if obj._cable_peer is not None:
@ -49,8 +43,12 @@ class CableTerminationSerializer(serializers.ModelSerializer):
return serializer(obj._cable_peer, context=context).data return serializer(obj._cable_peer, context=context).data
return None return None
@swagger_serializer_method(serializer_or_field=serializers.BooleanField)
def get__occupied(self, obj):
return obj._occupied
class ConnectedEndpointSerializer(ValidatedModelSerializer):
class ConnectedEndpointSerializer(serializers.ModelSerializer):
connected_endpoint_type = serializers.SerializerMethodField(read_only=True) connected_endpoint_type = serializers.SerializerMethodField(read_only=True)
connected_endpoint = serializers.SerializerMethodField(read_only=True) connected_endpoint = serializers.SerializerMethodField(read_only=True)
connected_endpoint_reachable = serializers.SerializerMethodField(read_only=True) connected_endpoint_reachable = serializers.SerializerMethodField(read_only=True)
@ -82,23 +80,39 @@ class ConnectedEndpointSerializer(ValidatedModelSerializer):
# Regions/sites # Regions/sites
# #
class RegionSerializer(CustomFieldModelSerializer): class RegionSerializer(NestedGroupModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:region-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:region-detail')
parent = NestedRegionSerializer(required=False, allow_null=True) parent = NestedRegionSerializer(required=False, allow_null=True)
site_count = serializers.IntegerField(read_only=True) site_count = serializers.IntegerField(read_only=True)
_depth = serializers.IntegerField(source='level', read_only=True)
class Meta: class Meta:
model = Region model = Region
fields = ['id', 'url', 'name', 'slug', 'parent', 'description', 'site_count', 'custom_fields', '_depth'] fields = [
'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'custom_fields', 'created', 'last_updated',
'site_count', '_depth',
]
class SiteSerializer(TaggedObjectSerializer, CustomFieldModelSerializer): class SiteGroupSerializer(NestedGroupModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:sitegroup-detail')
parent = NestedSiteGroupSerializer(required=False, allow_null=True)
site_count = serializers.IntegerField(read_only=True)
class Meta:
model = SiteGroup
fields = [
'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'custom_fields', 'created', 'last_updated',
'site_count', '_depth',
]
class SiteSerializer(PrimaryModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:site-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:site-detail')
status = ChoiceField(choices=SiteStatusChoices, required=False) status = ChoiceField(choices=SiteStatusChoices, required=False)
region = NestedRegionSerializer(required=False, allow_null=True) region = NestedRegionSerializer(required=False, allow_null=True)
group = NestedSiteGroupSerializer(required=False, allow_null=True)
tenant = NestedTenantSerializer(required=False, allow_null=True) tenant = NestedTenantSerializer(required=False, allow_null=True)
time_zone = TimeZoneField(required=False) time_zone = TimeZoneSerializerField(required=False)
circuit_count = serializers.IntegerField(read_only=True) circuit_count = serializers.IntegerField(read_only=True)
device_count = serializers.IntegerField(read_only=True) device_count = serializers.IntegerField(read_only=True)
prefix_count = serializers.IntegerField(read_only=True) prefix_count = serializers.IntegerField(read_only=True)
@ -109,10 +123,10 @@ class SiteSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
class Meta: class Meta:
model = Site model = Site
fields = [ fields = [
'id', 'url', 'name', 'slug', 'status', 'region', 'tenant', 'facility', 'asn', 'time_zone', 'description', 'id', 'url', 'display', 'name', 'slug', 'status', 'region', 'group', 'tenant', 'facility', 'asn',
'physical_address', 'shipping_address', 'latitude', 'longitude', 'contact_name', 'contact_phone', 'time_zone', 'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'contact_name',
'contact_email', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'circuit_count', 'contact_phone', 'contact_email', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
'device_count', 'prefix_count', 'rack_count', 'virtualmachine_count', 'vlan_count', 'circuit_count', 'device_count', 'prefix_count', 'rack_count', 'virtualmachine_count', 'vlan_count',
] ]
@ -120,31 +134,37 @@ class SiteSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
# Racks # Racks
# #
class RackGroupSerializer(ValidatedModelSerializer): class LocationSerializer(NestedGroupModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackgroup-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:location-detail')
site = NestedSiteSerializer() site = NestedSiteSerializer()
parent = NestedRackGroupSerializer(required=False, allow_null=True) parent = NestedLocationSerializer(required=False, allow_null=True)
rack_count = serializers.IntegerField(read_only=True) rack_count = serializers.IntegerField(read_only=True)
_depth = serializers.IntegerField(source='level', read_only=True) device_count = serializers.IntegerField(read_only=True)
class Meta: class Meta:
model = RackGroup model = Location
fields = ['id', 'url', 'name', 'slug', 'site', 'parent', 'description', 'rack_count', '_depth'] fields = [
'id', 'url', 'display', 'name', 'slug', 'site', 'parent', 'description', 'custom_fields', 'created',
'last_updated', 'rack_count', 'device_count', '_depth',
]
class RackRoleSerializer(ValidatedModelSerializer): class RackRoleSerializer(OrganizationalModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackrole-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackrole-detail')
rack_count = serializers.IntegerField(read_only=True) rack_count = serializers.IntegerField(read_only=True)
class Meta: class Meta:
model = RackRole model = RackRole
fields = ['id', 'url', 'name', 'slug', 'color', 'description', 'rack_count'] fields = [
'id', 'url', 'display', 'name', 'slug', 'color', 'description', 'custom_fields', 'created', 'last_updated',
'rack_count',
]
class RackSerializer(TaggedObjectSerializer, CustomFieldModelSerializer): class RackSerializer(PrimaryModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rack-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rack-detail')
site = NestedSiteSerializer() site = NestedSiteSerializer()
group = NestedRackGroupSerializer(required=False, allow_null=True, default=None) location = NestedLocationSerializer(required=False, allow_null=True, default=None)
tenant = NestedTenantSerializer(required=False, allow_null=True) tenant = NestedTenantSerializer(required=False, allow_null=True)
status = ChoiceField(choices=RackStatusChoices, required=False) status = ChoiceField(choices=RackStatusChoices, required=False)
role = NestedRackRoleSerializer(required=False, allow_null=True) role = NestedRackRoleSerializer(required=False, allow_null=True)
@ -157,21 +177,22 @@ class RackSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
class Meta: class Meta:
model = Rack model = Rack
fields = [ fields = [
'id', 'url', 'name', 'facility_id', 'display_name', 'site', 'group', 'tenant', 'status', 'role', 'serial', 'id', 'url', 'display', 'name', 'facility_id', 'display_name', 'site', 'location', 'tenant', 'status',
'asset_tag', 'type', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'role', 'serial', 'asset_tag', 'type', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth',
'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'device_count', 'powerfeed_count', 'outer_unit', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'device_count',
'powerfeed_count',
] ]
# Omit the UniqueTogetherValidator that would be automatically added to validate (group, facility_id). This # Omit the UniqueTogetherValidator that would be automatically added to validate (location, facility_id). This
# prevents facility_id from being interpreted as a required field. # prevents facility_id from being interpreted as a required field.
validators = [ validators = [
UniqueTogetherValidator(queryset=Rack.objects.all(), fields=('group', 'name')) UniqueTogetherValidator(queryset=Rack.objects.all(), fields=('location', 'name'))
] ]
def validate(self, data): def validate(self, data):
# Validate uniqueness of (group, facility_id) since we omitted the automatically-created validator from Meta. # Validate uniqueness of (location, facility_id) since we omitted the automatically-created validator from Meta.
if data.get('facility_id', None): if data.get('facility_id', None):
validator = UniqueTogetherValidator(queryset=Rack.objects.all(), fields=('group', 'facility_id')) validator = UniqueTogetherValidator(queryset=Rack.objects.all(), fields=('location', 'facility_id'))
validator(data, self) validator(data, self)
# Enforce model validation # Enforce model validation
@ -189,9 +210,13 @@ class RackUnitSerializer(serializers.Serializer):
face = ChoiceField(choices=DeviceFaceChoices, read_only=True) face = ChoiceField(choices=DeviceFaceChoices, read_only=True)
device = NestedDeviceSerializer(read_only=True) device = NestedDeviceSerializer(read_only=True)
occupied = serializers.BooleanField(read_only=True) occupied = serializers.BooleanField(read_only=True)
display = serializers.SerializerMethodField(read_only=True)
def get_display(self, obj):
return obj['name']
class RackReservationSerializer(TaggedObjectSerializer, CustomFieldModelSerializer): class RackReservationSerializer(PrimaryModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackreservation-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackreservation-detail')
rack = NestedRackSerializer() rack = NestedRackSerializer()
user = NestedUserSerializer() user = NestedUserSerializer()
@ -199,7 +224,10 @@ class RackReservationSerializer(TaggedObjectSerializer, CustomFieldModelSerializ
class Meta: class Meta:
model = RackReservation model = RackReservation
fields = ['id', 'url', 'rack', 'units', 'created', 'user', 'tenant', 'description', 'tags', 'custom_fields'] fields = [
'id', 'url', 'display', 'rack', 'units', 'created', 'user', 'tenant', 'description', 'tags',
'custom_fields',
]
class RackElevationDetailFilterSerializer(serializers.Serializer): class RackElevationDetailFilterSerializer(serializers.Serializer):
@ -242,7 +270,7 @@ class RackElevationDetailFilterSerializer(serializers.Serializer):
# Device types # Device types
# #
class ManufacturerSerializer(ValidatedModelSerializer): class ManufacturerSerializer(OrganizationalModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:manufacturer-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:manufacturer-detail')
devicetype_count = serializers.IntegerField(read_only=True) devicetype_count = serializers.IntegerField(read_only=True)
inventoryitem_count = serializers.IntegerField(read_only=True) inventoryitem_count = serializers.IntegerField(read_only=True)
@ -251,11 +279,12 @@ class ManufacturerSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = Manufacturer model = Manufacturer
fields = [ fields = [
'id', 'url', 'name', 'slug', 'description', 'devicetype_count', 'inventoryitem_count', 'platform_count', 'id', 'url', 'display', 'name', 'slug', 'description', 'custom_fields', 'created', 'last_updated',
'devicetype_count', 'inventoryitem_count', 'platform_count',
] ]
class DeviceTypeSerializer(TaggedObjectSerializer, CustomFieldModelSerializer): class DeviceTypeSerializer(PrimaryModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicetype-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicetype-detail')
manufacturer = NestedManufacturerSerializer() manufacturer = NestedManufacturerSerializer()
subdevice_role = ChoiceField(choices=SubdeviceRoleChoices, allow_blank=True, required=False) subdevice_role = ChoiceField(choices=SubdeviceRoleChoices, allow_blank=True, required=False)
@ -264,9 +293,9 @@ class DeviceTypeSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
class Meta: class Meta:
model = DeviceType model = DeviceType
fields = [ fields = [
'id', 'url', 'manufacturer', 'model', 'slug', 'display_name', 'part_number', 'u_height', 'is_full_depth', 'id', 'url', 'display', 'manufacturer', 'model', 'slug', 'display_name', 'part_number', 'u_height',
'subdevice_role', 'front_image', 'rear_image', 'comments', 'tags', 'custom_fields', 'created', 'is_full_depth', 'subdevice_role', 'front_image', 'rear_image', 'comments', 'tags', 'custom_fields',
'last_updated', 'device_count', 'created', 'last_updated', 'device_count',
] ]
@ -281,7 +310,9 @@ class ConsolePortTemplateSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = ConsolePortTemplate model = ConsolePortTemplate
fields = ['id', 'url', 'device_type', 'name', 'label', 'type', 'description'] fields = [
'id', 'url', 'display', 'device_type', 'name', 'label', 'type', 'description', 'created', 'last_updated',
]
class ConsoleServerPortTemplateSerializer(ValidatedModelSerializer): class ConsoleServerPortTemplateSerializer(ValidatedModelSerializer):
@ -295,7 +326,9 @@ class ConsoleServerPortTemplateSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = ConsoleServerPortTemplate model = ConsoleServerPortTemplate
fields = ['id', 'url', 'device_type', 'name', 'label', 'type', 'description'] fields = [
'id', 'url', 'display', 'device_type', 'name', 'label', 'type', 'description', 'created', 'last_updated',
]
class PowerPortTemplateSerializer(ValidatedModelSerializer): class PowerPortTemplateSerializer(ValidatedModelSerializer):
@ -309,7 +342,10 @@ class PowerPortTemplateSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = PowerPortTemplate model = PowerPortTemplate
fields = ['id', 'url', 'device_type', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description'] fields = [
'id', 'url', 'display', 'device_type', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw',
'description', 'created', 'last_updated',
]
class PowerOutletTemplateSerializer(ValidatedModelSerializer): class PowerOutletTemplateSerializer(ValidatedModelSerializer):
@ -331,7 +367,10 @@ class PowerOutletTemplateSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = PowerOutletTemplate model = PowerOutletTemplate
fields = ['id', 'url', 'device_type', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description'] fields = [
'id', 'url', 'display', 'device_type', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description',
'created', 'last_updated',
]
class InterfaceTemplateSerializer(ValidatedModelSerializer): class InterfaceTemplateSerializer(ValidatedModelSerializer):
@ -341,7 +380,10 @@ class InterfaceTemplateSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = InterfaceTemplate model = InterfaceTemplate
fields = ['id', 'url', 'device_type', 'name', 'label', 'type', 'mgmt_only', 'description'] fields = [
'id', 'url', 'display', 'device_type', 'name', 'label', 'type', 'mgmt_only', 'description', 'created',
'last_updated',
]
class RearPortTemplateSerializer(ValidatedModelSerializer): class RearPortTemplateSerializer(ValidatedModelSerializer):
@ -351,7 +393,10 @@ class RearPortTemplateSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = RearPortTemplate model = RearPortTemplate
fields = ['id', 'url', 'device_type', 'name', 'label', 'type', 'positions', 'description'] fields = [
'id', 'url', 'display', 'device_type', 'name', 'label', 'type', 'positions', 'description', 'created',
'last_updated',
]
class FrontPortTemplateSerializer(ValidatedModelSerializer): class FrontPortTemplateSerializer(ValidatedModelSerializer):
@ -362,7 +407,10 @@ class FrontPortTemplateSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = FrontPortTemplate model = FrontPortTemplate
fields = ['id', 'url', 'device_type', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description'] fields = [
'id', 'url', 'display', 'device_type', 'name', 'label', 'type', 'rear_port', 'rear_port_position',
'description', 'created', 'last_updated',
]
class DeviceBayTemplateSerializer(ValidatedModelSerializer): class DeviceBayTemplateSerializer(ValidatedModelSerializer):
@ -371,14 +419,14 @@ class DeviceBayTemplateSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = DeviceBayTemplate model = DeviceBayTemplate
fields = ['id', 'url', 'device_type', 'name', 'label', 'description'] fields = ['id', 'url', 'display', 'device_type', 'name', 'label', 'description', 'created', 'last_updated']
# #
# Devices # Devices
# #
class DeviceRoleSerializer(ValidatedModelSerializer): class DeviceRoleSerializer(OrganizationalModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicerole-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicerole-detail')
device_count = serializers.IntegerField(read_only=True) device_count = serializers.IntegerField(read_only=True)
virtualmachine_count = serializers.IntegerField(read_only=True) virtualmachine_count = serializers.IntegerField(read_only=True)
@ -386,11 +434,12 @@ class DeviceRoleSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = DeviceRole model = DeviceRole
fields = [ fields = [
'id', 'url', 'name', 'slug', 'color', 'vm_role', 'description', 'device_count', 'virtualmachine_count', 'id', 'url', 'display', 'name', 'slug', 'color', 'vm_role', 'description', 'custom_fields', 'created',
'last_updated', 'device_count', 'virtualmachine_count',
] ]
class PlatformSerializer(ValidatedModelSerializer): class PlatformSerializer(OrganizationalModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:platform-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:platform-detail')
manufacturer = NestedManufacturerSerializer(required=False, allow_null=True) manufacturer = NestedManufacturerSerializer(required=False, allow_null=True)
device_count = serializers.IntegerField(read_only=True) device_count = serializers.IntegerField(read_only=True)
@ -399,18 +448,19 @@ class PlatformSerializer(ValidatedModelSerializer):
class Meta: class Meta:
model = Platform model = Platform
fields = [ fields = [
'id', 'url', 'name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args', 'description', 'device_count', 'id', 'url', 'display', 'name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args', 'description',
'virtualmachine_count', 'custom_fields', 'created', 'last_updated', 'device_count', 'virtualmachine_count',
] ]
class DeviceSerializer(TaggedObjectSerializer, CustomFieldModelSerializer): class DeviceSerializer(PrimaryModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:device-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:device-detail')
device_type = NestedDeviceTypeSerializer() device_type = NestedDeviceTypeSerializer()
device_role = NestedDeviceRoleSerializer() device_role = NestedDeviceRoleSerializer()
tenant = NestedTenantSerializer(required=False, allow_null=True) tenant = NestedTenantSerializer(required=False, allow_null=True)
platform = NestedPlatformSerializer(required=False, allow_null=True) platform = NestedPlatformSerializer(required=False, allow_null=True)
site = NestedSiteSerializer() site = NestedSiteSerializer()
location = NestedLocationSerializer(required=False, allow_null=True, default=None)
rack = NestedRackSerializer(required=False, allow_null=True) rack = NestedRackSerializer(required=False, allow_null=True)
face = ChoiceField(choices=DeviceFaceChoices, allow_blank=True, required=False) face = ChoiceField(choices=DeviceFaceChoices, allow_blank=True, required=False)
status = ChoiceField(choices=DeviceStatusChoices, required=False) status = ChoiceField(choices=DeviceStatusChoices, required=False)
@ -424,10 +474,10 @@ class DeviceSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
class Meta: class Meta:
model = Device model = Device
fields = [ fields = [
'id', 'url', 'name', 'display_name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'id', 'url', 'display', 'name', 'display_name', 'device_type', 'device_role', 'tenant', 'platform',
'asset_tag', 'site', 'rack', 'position', 'face', 'parent_device', 'status', 'primary_ip', 'primary_ip4', 'serial', 'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'parent_device', 'status',
'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'local_context_data', 'primary_ip', 'primary_ip4', 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority',
'tags', 'custom_fields', 'created', 'last_updated', 'comments', 'local_context_data', 'tags', 'custom_fields', 'created', 'last_updated',
] ]
validators = [] validators = []
@ -460,10 +510,10 @@ class DeviceWithConfigContextSerializer(DeviceSerializer):
class Meta(DeviceSerializer.Meta): class Meta(DeviceSerializer.Meta):
fields = [ fields = [
'id', 'url', 'name', 'display_name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'id', 'url', 'display', 'name', 'display_name', 'device_type', 'device_role', 'tenant', 'platform',
'asset_tag', 'site', 'rack', 'position', 'face', 'parent_device', 'status', 'primary_ip', 'primary_ip4', 'serial', 'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'parent_device', 'status',
'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'local_context_data', 'primary_ip', 'primary_ip4', 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority',
'tags', 'custom_fields', 'config_context', 'created', 'last_updated', 'comments', 'local_context_data', 'tags', 'custom_fields', 'config_context', 'created', 'last_updated',
] ]
@swagger_serializer_method(serializer_or_field=serializers.DictField) @swagger_serializer_method(serializer_or_field=serializers.DictField)
@ -475,7 +525,11 @@ class DeviceNAPALMSerializer(serializers.Serializer):
method = serializers.DictField() method = serializers.DictField()
class ConsoleServerPortSerializer(TaggedObjectSerializer, CableTerminationSerializer, ConnectedEndpointSerializer): #
# Device components
#
class ConsoleServerPortSerializer(PrimaryModelSerializer, CableTerminationSerializer, ConnectedEndpointSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverport-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverport-detail')
device = NestedDeviceSerializer() device = NestedDeviceSerializer()
type = ChoiceField( type = ChoiceField(
@ -483,17 +537,23 @@ class ConsoleServerPortSerializer(TaggedObjectSerializer, CableTerminationSerial
allow_blank=True, allow_blank=True,
required=False required=False
) )
speed = ChoiceField(
choices=ConsolePortSpeedChoices,
allow_blank=True,
required=False
)
cable = NestedCableSerializer(read_only=True) cable = NestedCableSerializer(read_only=True)
class Meta: class Meta:
model = ConsoleServerPort model = ConsoleServerPort
fields = [ fields = [
'id', 'url', 'device', 'name', 'label', 'type', 'description', 'cable', 'cable_peer', 'cable_peer_type', 'id', 'url', 'display', 'device', 'name', 'label', 'type', 'speed', 'description', 'mark_connected',
'connected_endpoint', 'connected_endpoint_type', 'connected_endpoint_reachable', 'tags', 'cable', 'cable_peer', 'cable_peer_type', 'connected_endpoint', 'connected_endpoint_type',
'connected_endpoint_reachable', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
] ]
class ConsolePortSerializer(TaggedObjectSerializer, CableTerminationSerializer, ConnectedEndpointSerializer): class ConsolePortSerializer(PrimaryModelSerializer, CableTerminationSerializer, ConnectedEndpointSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleport-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleport-detail')
device = NestedDeviceSerializer() device = NestedDeviceSerializer()
type = ChoiceField( type = ChoiceField(
@ -501,17 +561,23 @@ class ConsolePortSerializer(TaggedObjectSerializer, CableTerminationSerializer,
allow_blank=True, allow_blank=True,
required=False required=False
) )
speed = ChoiceField(
choices=ConsolePortSpeedChoices,
allow_blank=True,
required=False
)
cable = NestedCableSerializer(read_only=True) cable = NestedCableSerializer(read_only=True)
class Meta: class Meta:
model = ConsolePort model = ConsolePort
fields = [ fields = [
'id', 'url', 'device', 'name', 'label', 'type', 'description', 'cable', 'cable_peer', 'cable_peer_type', 'id', 'url', 'display', 'device', 'name', 'label', 'type', 'speed', 'description', 'mark_connected',
'connected_endpoint', 'connected_endpoint_type', 'connected_endpoint_reachable', 'tags', 'cable', 'cable_peer', 'cable_peer_type', 'connected_endpoint', 'connected_endpoint_type',
'connected_endpoint_reachable', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
] ]
class PowerOutletSerializer(TaggedObjectSerializer, CableTerminationSerializer, ConnectedEndpointSerializer): class PowerOutletSerializer(PrimaryModelSerializer, CableTerminationSerializer, ConnectedEndpointSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlet-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlet-detail')
device = NestedDeviceSerializer() device = NestedDeviceSerializer()
type = ChoiceField( type = ChoiceField(
@ -534,13 +600,13 @@ class PowerOutletSerializer(TaggedObjectSerializer, CableTerminationSerializer,
class Meta: class Meta:
model = PowerOutlet model = PowerOutlet
fields = [ fields = [
'id', 'url', 'device', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description', 'cable', 'id', 'url', 'display', 'device', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description',
'cable_peer', 'cable_peer_type', 'connected_endpoint', 'connected_endpoint_type', 'mark_connected', 'cable', 'cable_peer', 'cable_peer_type', 'connected_endpoint', 'connected_endpoint_type',
'connected_endpoint_reachable', 'tags', 'connected_endpoint_reachable', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
] ]
class PowerPortSerializer(TaggedObjectSerializer, CableTerminationSerializer, ConnectedEndpointSerializer): class PowerPortSerializer(PrimaryModelSerializer, CableTerminationSerializer, ConnectedEndpointSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerport-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerport-detail')
device = NestedDeviceSerializer() device = NestedDeviceSerializer()
type = ChoiceField( type = ChoiceField(
@ -553,16 +619,17 @@ class PowerPortSerializer(TaggedObjectSerializer, CableTerminationSerializer, Co
class Meta: class Meta:
model = PowerPort model = PowerPort
fields = [ fields = [
'id', 'url', 'device', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', 'cable', 'id', 'url', 'display', 'device', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description',
'cable_peer', 'cable_peer_type', 'connected_endpoint', 'connected_endpoint_type', 'mark_connected', 'cable', 'cable_peer', 'cable_peer_type', 'connected_endpoint', 'connected_endpoint_type',
'connected_endpoint_reachable', 'tags', 'connected_endpoint_reachable', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
] ]
class InterfaceSerializer(TaggedObjectSerializer, CableTerminationSerializer, ConnectedEndpointSerializer): class InterfaceSerializer(PrimaryModelSerializer, CableTerminationSerializer, ConnectedEndpointSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail')
device = NestedDeviceSerializer() device = NestedDeviceSerializer()
type = ChoiceField(choices=InterfaceTypeChoices) type = ChoiceField(choices=InterfaceTypeChoices)
parent = NestedInterfaceSerializer(required=False, allow_null=True)
lag = NestedInterfaceSerializer(required=False, allow_null=True) lag = NestedInterfaceSerializer(required=False, allow_null=True)
mode = ChoiceField(choices=InterfaceModeChoices, allow_blank=True, required=False) mode = ChoiceField(choices=InterfaceModeChoices, allow_blank=True, required=False)
untagged_vlan = NestedVLANSerializer(required=False, allow_null=True) untagged_vlan = NestedVLANSerializer(required=False, allow_null=True)
@ -578,10 +645,11 @@ class InterfaceSerializer(TaggedObjectSerializer, CableTerminationSerializer, Co
class Meta: class Meta:
model = Interface model = Interface
fields = [ fields = [
'id', 'url', 'device', 'name', 'label', 'type', 'enabled', 'lag', 'mtu', 'mac_address', 'mgmt_only', 'id', 'url', 'display', 'device', 'name', 'label', 'type', 'enabled', 'parent', 'lag', 'mtu', 'mac_address',
'description', 'mode', 'untagged_vlan', 'tagged_vlans', 'cable', 'cable_peer', 'cable_peer_type', 'mgmt_only', 'description', 'mode', 'untagged_vlan', 'tagged_vlans', 'mark_connected', 'cable',
'connected_endpoint', 'connected_endpoint_type', 'connected_endpoint_reachable', 'tags', 'cable_peer', 'cable_peer_type', 'connected_endpoint', 'connected_endpoint_type',
'count_ipaddresses', 'connected_endpoint_reachable', 'tags', 'custom_fields', 'created', 'last_updated', 'count_ipaddresses',
'_occupied',
] ]
def validate(self, data): def validate(self, data):
@ -598,7 +666,7 @@ class InterfaceSerializer(TaggedObjectSerializer, CableTerminationSerializer, Co
return super().validate(data) return super().validate(data)
class RearPortSerializer(TaggedObjectSerializer, CableTerminationSerializer, ValidatedModelSerializer): class RearPortSerializer(PrimaryModelSerializer, CableTerminationSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearport-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearport-detail')
device = NestedDeviceSerializer() device = NestedDeviceSerializer()
type = ChoiceField(choices=PortTypeChoices) type = ChoiceField(choices=PortTypeChoices)
@ -607,8 +675,8 @@ class RearPortSerializer(TaggedObjectSerializer, CableTerminationSerializer, Val
class Meta: class Meta:
model = RearPort model = RearPort
fields = [ fields = [
'id', 'url', 'device', 'name', 'label', 'type', 'positions', 'description', 'cable', 'cable_peer', 'id', 'url', 'display', 'device', 'name', 'label', 'type', 'positions', 'description', 'mark_connected',
'cable_peer_type', 'tags', 'cable', 'cable_peer', 'cable_peer_type', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
] ]
@ -620,10 +688,10 @@ class FrontPortRearPortSerializer(WritableNestedSerializer):
class Meta: class Meta:
model = RearPort model = RearPort
fields = ['id', 'url', 'name', 'label'] fields = ['id', 'url', 'display', 'name', 'label']
class FrontPortSerializer(TaggedObjectSerializer, CableTerminationSerializer, ValidatedModelSerializer): class FrontPortSerializer(PrimaryModelSerializer, CableTerminationSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:frontport-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:frontport-detail')
device = NestedDeviceSerializer() device = NestedDeviceSerializer()
type = ChoiceField(choices=PortTypeChoices) type = ChoiceField(choices=PortTypeChoices)
@ -633,26 +701,30 @@ class FrontPortSerializer(TaggedObjectSerializer, CableTerminationSerializer, Va
class Meta: class Meta:
model = FrontPort model = FrontPort
fields = [ fields = [
'id', 'url', 'device', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description', 'cable', 'id', 'url', 'display', 'device', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description',
'cable_peer', 'cable_peer_type', 'tags', 'mark_connected', 'cable', 'cable_peer', 'cable_peer_type', 'tags', 'custom_fields', 'created',
'last_updated', '_occupied',
] ]
class DeviceBaySerializer(TaggedObjectSerializer, ValidatedModelSerializer): class DeviceBaySerializer(PrimaryModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebay-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebay-detail')
device = NestedDeviceSerializer() device = NestedDeviceSerializer()
installed_device = NestedDeviceSerializer(required=False, allow_null=True) installed_device = NestedDeviceSerializer(required=False, allow_null=True)
class Meta: class Meta:
model = DeviceBay model = DeviceBay
fields = ['id', 'url', 'device', 'name', 'label', 'description', 'installed_device', 'tags'] fields = [
'id', 'url', 'display', 'device', 'name', 'label', 'description', 'installed_device', 'tags',
'custom_fields', 'created', 'last_updated',
]
# #
# Inventory items # Inventory items
# #
class InventoryItemSerializer(TaggedObjectSerializer, ValidatedModelSerializer): class InventoryItemSerializer(PrimaryModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitem-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitem-detail')
device = NestedDeviceSerializer() device = NestedDeviceSerializer()
# Provide a default value to satisfy UniqueTogetherValidator # Provide a default value to satisfy UniqueTogetherValidator
@ -663,8 +735,8 @@ class InventoryItemSerializer(TaggedObjectSerializer, ValidatedModelSerializer):
class Meta: class Meta:
model = InventoryItem model = InventoryItem
fields = [ fields = [
'id', 'url', 'device', 'parent', 'name', 'label', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'id', 'url', 'display', 'device', 'parent', 'name', 'label', 'manufacturer', 'part_id', 'serial',
'discovered', 'description', 'tags', '_depth', 'asset_tag', 'discovered', 'description', 'tags', 'custom_fields', 'created', 'last_updated', '_depth',
] ]
@ -672,7 +744,7 @@ class InventoryItemSerializer(TaggedObjectSerializer, ValidatedModelSerializer):
# Cables # Cables
# #
class CableSerializer(TaggedObjectSerializer, CustomFieldModelSerializer): class CableSerializer(PrimaryModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:cable-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:cable-detail')
termination_a_type = ContentTypeField( termination_a_type = ContentTypeField(
queryset=ContentType.objects.filter(CABLE_TERMINATION_MODELS) queryset=ContentType.objects.filter(CABLE_TERMINATION_MODELS)
@ -688,7 +760,7 @@ class CableSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
class Meta: class Meta:
model = Cable model = Cable
fields = [ fields = [
'id', 'url', 'termination_a_type', 'termination_a_id', 'termination_a', 'termination_b_type', 'id', 'url', 'display', 'termination_a_type', 'termination_a_id', 'termination_a', 'termination_b_type',
'termination_b_id', 'termination_b', 'type', 'status', 'label', 'color', 'length', 'length_unit', 'tags', 'termination_b_id', 'termination_b', 'type', 'status', 'label', 'color', 'length', 'length_unit', 'tags',
'custom_fields', 'custom_fields',
] ]
@ -779,7 +851,7 @@ class CablePathSerializer(serializers.ModelSerializer):
class InterfaceConnectionSerializer(ValidatedModelSerializer): class InterfaceConnectionSerializer(ValidatedModelSerializer):
interface_a = serializers.SerializerMethodField() interface_a = serializers.SerializerMethodField()
interface_b = NestedInterfaceSerializer(source='connected_endpoint') interface_b = NestedInterfaceSerializer(source='_path.destination')
connected_endpoint_reachable = serializers.SerializerMethodField(read_only=True) connected_endpoint_reachable = serializers.SerializerMethodField(read_only=True)
class Meta: class Meta:
@ -802,24 +874,24 @@ class InterfaceConnectionSerializer(ValidatedModelSerializer):
# Virtual chassis # Virtual chassis
# #
class VirtualChassisSerializer(TaggedObjectSerializer, CustomFieldModelSerializer): class VirtualChassisSerializer(PrimaryModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualchassis-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualchassis-detail')
master = NestedDeviceSerializer(required=False) master = NestedDeviceSerializer(required=False)
member_count = serializers.IntegerField(read_only=True) member_count = serializers.IntegerField(read_only=True)
class Meta: class Meta:
model = VirtualChassis model = VirtualChassis
fields = ['id', 'url', 'name', 'domain', 'master', 'tags', 'custom_fields', 'member_count'] fields = ['id', 'url', 'display', 'name', 'domain', 'master', 'tags', 'custom_fields', 'member_count']
# #
# Power panels # Power panels
# #
class PowerPanelSerializer(TaggedObjectSerializer, CustomFieldModelSerializer): class PowerPanelSerializer(PrimaryModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerpanel-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerpanel-detail')
site = NestedSiteSerializer() site = NestedSiteSerializer()
rack_group = NestedRackGroupSerializer( location = NestedLocationSerializer(
required=False, required=False,
allow_null=True, allow_null=True,
default=None default=None
@ -828,15 +900,10 @@ class PowerPanelSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
class Meta: class Meta:
model = PowerPanel model = PowerPanel
fields = ['id', 'url', 'site', 'rack_group', 'name', 'tags', 'custom_fields', 'powerfeed_count'] fields = ['id', 'url', 'display', 'site', 'location', 'name', 'tags', 'custom_fields', 'powerfeed_count']
class PowerFeedSerializer( class PowerFeedSerializer(PrimaryModelSerializer, CableTerminationSerializer, ConnectedEndpointSerializer):
TaggedObjectSerializer,
CableTerminationSerializer,
ConnectedEndpointSerializer,
CustomFieldModelSerializer
):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerfeed-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerfeed-detail')
power_panel = NestedPowerPanelSerializer() power_panel = NestedPowerPanelSerializer()
rack = NestedRackSerializer( rack = NestedRackSerializer(
@ -865,8 +932,8 @@ class PowerFeedSerializer(
class Meta: class Meta:
model = PowerFeed model = PowerFeed
fields = [ fields = [
'id', 'url', 'power_panel', 'rack', 'name', 'status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'id', 'url', 'display', 'power_panel', 'rack', 'name', 'status', 'type', 'supply', 'phase', 'voltage',
'max_utilization', 'comments', 'cable', 'cable_peer', 'cable_peer_type', 'connected_endpoint', 'amperage', 'max_utilization', 'comments', 'mark_connected', 'cable', 'cable_peer', 'cable_peer_type',
'connected_endpoint_type', 'connected_endpoint_reachable', 'tags', 'custom_fields', 'created', 'connected_endpoint', 'connected_endpoint_type', 'connected_endpoint_reachable', 'tags', 'custom_fields',
'last_updated', 'created', 'last_updated', '_occupied',
] ]

View File

@ -7,10 +7,11 @@ router.APIRootView = views.DCIMRootView
# Sites # Sites
router.register('regions', views.RegionViewSet) router.register('regions', views.RegionViewSet)
router.register('site-groups', views.SiteGroupViewSet)
router.register('sites', views.SiteViewSet) router.register('sites', views.SiteViewSet)
# Racks # Racks
router.register('rack-groups', views.RackGroupViewSet) router.register('locations', views.LocationViewSet)
router.register('rack-roles', views.RackRoleViewSet) router.register('rack-roles', views.RackRoleViewSet)
router.register('racks', views.RackViewSet) router.register('racks', views.RackViewSet)
router.register('rack-reservations', views.RackReservationViewSet) router.register('rack-reservations', views.RackReservationViewSet)

View File

@ -2,6 +2,7 @@ import socket
from collections import OrderedDict from collections import OrderedDict
from django.conf import settings from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.db.models import F from django.db.models import F
from django.http import HttpResponseForbidden, HttpResponse from django.http import HttpResponseForbidden, HttpResponse
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
@ -15,14 +16,8 @@ from rest_framework.routers import APIRootView
from rest_framework.viewsets import GenericViewSet, ViewSet from rest_framework.viewsets import GenericViewSet, ViewSet
from circuits.models import Circuit from circuits.models import Circuit
from dcim import filters from dcim import filtersets
from dcim.models import ( from dcim.models import *
Cable, CablePath, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate,
Manufacturer, InventoryItem, Platform, PowerFeed, PowerOutlet, PowerOutletTemplate, PowerPanel, PowerPort,
PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site,
VirtualChassis,
)
from extras.api.views import ConfigContextQuerySetMixin, CustomFieldModelViewSet from extras.api.views import ConfigContextQuerySetMixin, CustomFieldModelViewSet
from ipam.models import Prefix, VLAN from ipam.models import Prefix, VLAN
from netbox.api.views import ModelViewSet from netbox.api.views import ModelViewSet
@ -108,7 +103,23 @@ class RegionViewSet(CustomFieldModelViewSet):
cumulative=True cumulative=True
) )
serializer_class = serializers.RegionSerializer serializer_class = serializers.RegionSerializer
filterset_class = filters.RegionFilterSet filterset_class = filtersets.RegionFilterSet
#
# Site groups
#
class SiteGroupViewSet(CustomFieldModelViewSet):
queryset = SiteGroup.objects.add_related_count(
SiteGroup.objects.all(),
Site,
'group',
'site_count',
cumulative=True
)
serializer_class = serializers.SiteGroupSerializer
filterset_class = filtersets.SiteGroupFilterSet
# #
@ -127,35 +138,41 @@ class SiteViewSet(CustomFieldModelViewSet):
virtualmachine_count=count_related(VirtualMachine, 'cluster__site') virtualmachine_count=count_related(VirtualMachine, 'cluster__site')
) )
serializer_class = serializers.SiteSerializer serializer_class = serializers.SiteSerializer
filterset_class = filters.SiteFilterSet filterset_class = filtersets.SiteFilterSet
# #
# Rack groups # Locations
# #
class RackGroupViewSet(ModelViewSet): class LocationViewSet(CustomFieldModelViewSet):
queryset = RackGroup.objects.add_related_count( queryset = Location.objects.add_related_count(
RackGroup.objects.all(), Location.objects.add_related_count(
Location.objects.all(),
Device,
'location',
'device_count',
cumulative=True
),
Rack, Rack,
'group', 'location',
'rack_count', 'rack_count',
cumulative=True cumulative=True
).prefetch_related('site') ).prefetch_related('site')
serializer_class = serializers.RackGroupSerializer serializer_class = serializers.LocationSerializer
filterset_class = filters.RackGroupFilterSet filterset_class = filtersets.LocationFilterSet
# #
# Rack roles # Rack roles
# #
class RackRoleViewSet(ModelViewSet): class RackRoleViewSet(CustomFieldModelViewSet):
queryset = RackRole.objects.annotate( queryset = RackRole.objects.annotate(
rack_count=count_related(Rack, 'role') rack_count=count_related(Rack, 'role')
) )
serializer_class = serializers.RackRoleSerializer serializer_class = serializers.RackRoleSerializer
filterset_class = filters.RackRoleFilterSet filterset_class = filtersets.RackRoleFilterSet
# #
@ -164,13 +181,13 @@ class RackRoleViewSet(ModelViewSet):
class RackViewSet(CustomFieldModelViewSet): class RackViewSet(CustomFieldModelViewSet):
queryset = Rack.objects.prefetch_related( queryset = Rack.objects.prefetch_related(
'site', 'group__site', 'role', 'tenant', 'tags' 'site', 'location', 'role', 'tenant', 'tags'
).annotate( ).annotate(
device_count=count_related(Device, 'rack'), device_count=count_related(Device, 'rack'),
powerfeed_count=count_related(PowerFeed, 'rack') powerfeed_count=count_related(PowerFeed, 'rack')
) )
serializer_class = serializers.RackSerializer serializer_class = serializers.RackSerializer
filterset_class = filters.RackFilterSet filterset_class = filtersets.RackFilterSet
@swagger_auto_schema( @swagger_auto_schema(
responses={200: serializers.RackUnitSerializer(many=True)}, responses={200: serializers.RackUnitSerializer(many=True)},
@ -227,25 +244,21 @@ class RackViewSet(CustomFieldModelViewSet):
class RackReservationViewSet(ModelViewSet): class RackReservationViewSet(ModelViewSet):
queryset = RackReservation.objects.prefetch_related('rack', 'user', 'tenant') queryset = RackReservation.objects.prefetch_related('rack', 'user', 'tenant')
serializer_class = serializers.RackReservationSerializer serializer_class = serializers.RackReservationSerializer
filterset_class = filters.RackReservationFilterSet filterset_class = filtersets.RackReservationFilterSet
# Assign user from request
def perform_create(self, serializer):
serializer.save(user=self.request.user)
# #
# Manufacturers # Manufacturers
# #
class ManufacturerViewSet(ModelViewSet): class ManufacturerViewSet(CustomFieldModelViewSet):
queryset = Manufacturer.objects.annotate( queryset = Manufacturer.objects.annotate(
devicetype_count=count_related(DeviceType, 'manufacturer'), devicetype_count=count_related(DeviceType, 'manufacturer'),
inventoryitem_count=count_related(InventoryItem, 'manufacturer'), inventoryitem_count=count_related(InventoryItem, 'manufacturer'),
platform_count=count_related(Platform, 'manufacturer') platform_count=count_related(Platform, 'manufacturer')
) )
serializer_class = serializers.ManufacturerSerializer serializer_class = serializers.ManufacturerSerializer
filterset_class = filters.ManufacturerFilterSet filterset_class = filtersets.ManufacturerFilterSet
# #
@ -257,7 +270,7 @@ class DeviceTypeViewSet(CustomFieldModelViewSet):
device_count=count_related(Device, 'device_type') device_count=count_related(Device, 'device_type')
) )
serializer_class = serializers.DeviceTypeSerializer serializer_class = serializers.DeviceTypeSerializer
filterset_class = filters.DeviceTypeFilterSet filterset_class = filtersets.DeviceTypeFilterSet
brief_prefetch_fields = ['manufacturer'] brief_prefetch_fields = ['manufacturer']
@ -268,75 +281,75 @@ class DeviceTypeViewSet(CustomFieldModelViewSet):
class ConsolePortTemplateViewSet(ModelViewSet): class ConsolePortTemplateViewSet(ModelViewSet):
queryset = ConsolePortTemplate.objects.prefetch_related('device_type__manufacturer') queryset = ConsolePortTemplate.objects.prefetch_related('device_type__manufacturer')
serializer_class = serializers.ConsolePortTemplateSerializer serializer_class = serializers.ConsolePortTemplateSerializer
filterset_class = filters.ConsolePortTemplateFilterSet filterset_class = filtersets.ConsolePortTemplateFilterSet
class ConsoleServerPortTemplateViewSet(ModelViewSet): class ConsoleServerPortTemplateViewSet(ModelViewSet):
queryset = ConsoleServerPortTemplate.objects.prefetch_related('device_type__manufacturer') queryset = ConsoleServerPortTemplate.objects.prefetch_related('device_type__manufacturer')
serializer_class = serializers.ConsoleServerPortTemplateSerializer serializer_class = serializers.ConsoleServerPortTemplateSerializer
filterset_class = filters.ConsoleServerPortTemplateFilterSet filterset_class = filtersets.ConsoleServerPortTemplateFilterSet
class PowerPortTemplateViewSet(ModelViewSet): class PowerPortTemplateViewSet(ModelViewSet):
queryset = PowerPortTemplate.objects.prefetch_related('device_type__manufacturer') queryset = PowerPortTemplate.objects.prefetch_related('device_type__manufacturer')
serializer_class = serializers.PowerPortTemplateSerializer serializer_class = serializers.PowerPortTemplateSerializer
filterset_class = filters.PowerPortTemplateFilterSet filterset_class = filtersets.PowerPortTemplateFilterSet
class PowerOutletTemplateViewSet(ModelViewSet): class PowerOutletTemplateViewSet(ModelViewSet):
queryset = PowerOutletTemplate.objects.prefetch_related('device_type__manufacturer') queryset = PowerOutletTemplate.objects.prefetch_related('device_type__manufacturer')
serializer_class = serializers.PowerOutletTemplateSerializer serializer_class = serializers.PowerOutletTemplateSerializer
filterset_class = filters.PowerOutletTemplateFilterSet filterset_class = filtersets.PowerOutletTemplateFilterSet
class InterfaceTemplateViewSet(ModelViewSet): class InterfaceTemplateViewSet(ModelViewSet):
queryset = InterfaceTemplate.objects.prefetch_related('device_type__manufacturer') queryset = InterfaceTemplate.objects.prefetch_related('device_type__manufacturer')
serializer_class = serializers.InterfaceTemplateSerializer serializer_class = serializers.InterfaceTemplateSerializer
filterset_class = filters.InterfaceTemplateFilterSet filterset_class = filtersets.InterfaceTemplateFilterSet
class FrontPortTemplateViewSet(ModelViewSet): class FrontPortTemplateViewSet(ModelViewSet):
queryset = FrontPortTemplate.objects.prefetch_related('device_type__manufacturer') queryset = FrontPortTemplate.objects.prefetch_related('device_type__manufacturer')
serializer_class = serializers.FrontPortTemplateSerializer serializer_class = serializers.FrontPortTemplateSerializer
filterset_class = filters.FrontPortTemplateFilterSet filterset_class = filtersets.FrontPortTemplateFilterSet
class RearPortTemplateViewSet(ModelViewSet): class RearPortTemplateViewSet(ModelViewSet):
queryset = RearPortTemplate.objects.prefetch_related('device_type__manufacturer') queryset = RearPortTemplate.objects.prefetch_related('device_type__manufacturer')
serializer_class = serializers.RearPortTemplateSerializer serializer_class = serializers.RearPortTemplateSerializer
filterset_class = filters.RearPortTemplateFilterSet filterset_class = filtersets.RearPortTemplateFilterSet
class DeviceBayTemplateViewSet(ModelViewSet): class DeviceBayTemplateViewSet(ModelViewSet):
queryset = DeviceBayTemplate.objects.prefetch_related('device_type__manufacturer') queryset = DeviceBayTemplate.objects.prefetch_related('device_type__manufacturer')
serializer_class = serializers.DeviceBayTemplateSerializer serializer_class = serializers.DeviceBayTemplateSerializer
filterset_class = filters.DeviceBayTemplateFilterSet filterset_class = filtersets.DeviceBayTemplateFilterSet
# #
# Device roles # Device roles
# #
class DeviceRoleViewSet(ModelViewSet): class DeviceRoleViewSet(CustomFieldModelViewSet):
queryset = DeviceRole.objects.annotate( queryset = DeviceRole.objects.annotate(
device_count=count_related(Device, 'device_role'), device_count=count_related(Device, 'device_role'),
virtualmachine_count=count_related(VirtualMachine, 'role') virtualmachine_count=count_related(VirtualMachine, 'role')
) )
serializer_class = serializers.DeviceRoleSerializer serializer_class = serializers.DeviceRoleSerializer
filterset_class = filters.DeviceRoleFilterSet filterset_class = filtersets.DeviceRoleFilterSet
# #
# Platforms # Platforms
# #
class PlatformViewSet(ModelViewSet): class PlatformViewSet(CustomFieldModelViewSet):
queryset = Platform.objects.annotate( queryset = Platform.objects.annotate(
device_count=count_related(Device, 'platform'), device_count=count_related(Device, 'platform'),
virtualmachine_count=count_related(VirtualMachine, 'platform') virtualmachine_count=count_related(VirtualMachine, 'platform')
) )
serializer_class = serializers.PlatformSerializer serializer_class = serializers.PlatformSerializer
filterset_class = filters.PlatformFilterSet filterset_class = filtersets.PlatformFilterSet
# #
@ -345,10 +358,10 @@ class PlatformViewSet(ModelViewSet):
class DeviceViewSet(ConfigContextQuerySetMixin, CustomFieldModelViewSet): class DeviceViewSet(ConfigContextQuerySetMixin, CustomFieldModelViewSet):
queryset = Device.objects.prefetch_related( queryset = Device.objects.prefetch_related(
'device_type__manufacturer', 'device_role', 'tenant', 'platform', 'site', 'rack', 'parent_bay', 'device_type__manufacturer', 'device_role', 'tenant', 'platform', 'site', 'location', 'rack', 'parent_bay',
'virtual_chassis__master', 'primary_ip4__nat_outside', 'primary_ip6__nat_outside', 'tags', 'virtual_chassis__master', 'primary_ip4__nat_outside', 'primary_ip6__nat_outside', 'tags',
) )
filterset_class = filters.DeviceFilterSet filterset_class = filtersets.DeviceFilterSet
def get_serializer_class(self): def get_serializer_class(self):
""" """
@ -493,7 +506,7 @@ class DeviceViewSet(ConfigContextQuerySetMixin, CustomFieldModelViewSet):
class ConsolePortViewSet(PathEndpointMixin, ModelViewSet): class ConsolePortViewSet(PathEndpointMixin, ModelViewSet):
queryset = ConsolePort.objects.prefetch_related('device', '_path__destination', 'cable', '_cable_peer', 'tags') queryset = ConsolePort.objects.prefetch_related('device', '_path__destination', 'cable', '_cable_peer', 'tags')
serializer_class = serializers.ConsolePortSerializer serializer_class = serializers.ConsolePortSerializer
filterset_class = filters.ConsolePortFilterSet filterset_class = filtersets.ConsolePortFilterSet
brief_prefetch_fields = ['device'] brief_prefetch_fields = ['device']
@ -502,58 +515,58 @@ class ConsoleServerPortViewSet(PathEndpointMixin, ModelViewSet):
'device', '_path__destination', 'cable', '_cable_peer', 'tags' 'device', '_path__destination', 'cable', '_cable_peer', 'tags'
) )
serializer_class = serializers.ConsoleServerPortSerializer serializer_class = serializers.ConsoleServerPortSerializer
filterset_class = filters.ConsoleServerPortFilterSet filterset_class = filtersets.ConsoleServerPortFilterSet
brief_prefetch_fields = ['device'] brief_prefetch_fields = ['device']
class PowerPortViewSet(PathEndpointMixin, ModelViewSet): class PowerPortViewSet(PathEndpointMixin, ModelViewSet):
queryset = PowerPort.objects.prefetch_related('device', '_path__destination', 'cable', '_cable_peer', 'tags') queryset = PowerPort.objects.prefetch_related('device', '_path__destination', 'cable', '_cable_peer', 'tags')
serializer_class = serializers.PowerPortSerializer serializer_class = serializers.PowerPortSerializer
filterset_class = filters.PowerPortFilterSet filterset_class = filtersets.PowerPortFilterSet
brief_prefetch_fields = ['device'] brief_prefetch_fields = ['device']
class PowerOutletViewSet(PathEndpointMixin, ModelViewSet): class PowerOutletViewSet(PathEndpointMixin, ModelViewSet):
queryset = PowerOutlet.objects.prefetch_related('device', '_path__destination', 'cable', '_cable_peer', 'tags') queryset = PowerOutlet.objects.prefetch_related('device', '_path__destination', 'cable', '_cable_peer', 'tags')
serializer_class = serializers.PowerOutletSerializer serializer_class = serializers.PowerOutletSerializer
filterset_class = filters.PowerOutletFilterSet filterset_class = filtersets.PowerOutletFilterSet
brief_prefetch_fields = ['device'] brief_prefetch_fields = ['device']
class InterfaceViewSet(PathEndpointMixin, ModelViewSet): class InterfaceViewSet(PathEndpointMixin, ModelViewSet):
queryset = Interface.objects.prefetch_related( queryset = Interface.objects.prefetch_related(
'device', '_path__destination', 'cable', '_cable_peer', 'ip_addresses', 'tags' 'device', 'parent', 'lag', '_path__destination', 'cable', '_cable_peer', 'ip_addresses', 'tags'
) )
serializer_class = serializers.InterfaceSerializer serializer_class = serializers.InterfaceSerializer
filterset_class = filters.InterfaceFilterSet filterset_class = filtersets.InterfaceFilterSet
brief_prefetch_fields = ['device'] brief_prefetch_fields = ['device']
class FrontPortViewSet(PassThroughPortMixin, ModelViewSet): class FrontPortViewSet(PassThroughPortMixin, ModelViewSet):
queryset = FrontPort.objects.prefetch_related('device__device_type__manufacturer', 'rear_port', 'cable', 'tags') queryset = FrontPort.objects.prefetch_related('device__device_type__manufacturer', 'rear_port', 'cable', 'tags')
serializer_class = serializers.FrontPortSerializer serializer_class = serializers.FrontPortSerializer
filterset_class = filters.FrontPortFilterSet filterset_class = filtersets.FrontPortFilterSet
brief_prefetch_fields = ['device'] brief_prefetch_fields = ['device']
class RearPortViewSet(PassThroughPortMixin, ModelViewSet): class RearPortViewSet(PassThroughPortMixin, ModelViewSet):
queryset = RearPort.objects.prefetch_related('device__device_type__manufacturer', 'cable', 'tags') queryset = RearPort.objects.prefetch_related('device__device_type__manufacturer', 'cable', 'tags')
serializer_class = serializers.RearPortSerializer serializer_class = serializers.RearPortSerializer
filterset_class = filters.RearPortFilterSet filterset_class = filtersets.RearPortFilterSet
brief_prefetch_fields = ['device'] brief_prefetch_fields = ['device']
class DeviceBayViewSet(ModelViewSet): class DeviceBayViewSet(ModelViewSet):
queryset = DeviceBay.objects.prefetch_related('installed_device').prefetch_related('tags') queryset = DeviceBay.objects.prefetch_related('installed_device').prefetch_related('tags')
serializer_class = serializers.DeviceBaySerializer serializer_class = serializers.DeviceBaySerializer
filterset_class = filters.DeviceBayFilterSet filterset_class = filtersets.DeviceBayFilterSet
brief_prefetch_fields = ['device'] brief_prefetch_fields = ['device']
class InventoryItemViewSet(ModelViewSet): class InventoryItemViewSet(ModelViewSet):
queryset = InventoryItem.objects.prefetch_related('device', 'manufacturer').prefetch_related('tags') queryset = InventoryItem.objects.prefetch_related('device', 'manufacturer').prefetch_related('tags')
serializer_class = serializers.InventoryItemSerializer serializer_class = serializers.InventoryItemSerializer
filterset_class = filters.InventoryItemFilterSet filterset_class = filtersets.InventoryItemFilterSet
brief_prefetch_fields = ['device'] brief_prefetch_fields = ['device']
@ -566,7 +579,7 @@ class ConsoleConnectionViewSet(ListModelMixin, GenericViewSet):
_path__destination_id__isnull=False _path__destination_id__isnull=False
) )
serializer_class = serializers.ConsolePortSerializer serializer_class = serializers.ConsolePortSerializer
filterset_class = filters.ConsoleConnectionFilterSet filterset_class = filtersets.ConsoleConnectionFilterSet
class PowerConnectionViewSet(ListModelMixin, GenericViewSet): class PowerConnectionViewSet(ListModelMixin, GenericViewSet):
@ -574,17 +587,17 @@ class PowerConnectionViewSet(ListModelMixin, GenericViewSet):
_path__destination_id__isnull=False _path__destination_id__isnull=False
) )
serializer_class = serializers.PowerPortSerializer serializer_class = serializers.PowerPortSerializer
filterset_class = filters.PowerConnectionFilterSet filterset_class = filtersets.PowerConnectionFilterSet
class InterfaceConnectionViewSet(ListModelMixin, GenericViewSet): class InterfaceConnectionViewSet(ListModelMixin, GenericViewSet):
queryset = Interface.objects.prefetch_related('device', '_path').filter( queryset = Interface.objects.prefetch_related('device', '_path').filter(
# Avoid duplicate connections by only selecting the lower PK in a connected pair _path__destination_type__app_label='dcim',
_path__destination_id__isnull=False, _path__destination_type__model='interface',
pk__lt=F('_path__destination_id') _path__destination_id__isnull=False
) )
serializer_class = serializers.InterfaceConnectionSerializer serializer_class = serializers.InterfaceConnectionSerializer
filterset_class = filters.InterfaceConnectionFilterSet filterset_class = filtersets.InterfaceConnectionFilterSet
# #
@ -597,7 +610,7 @@ class CableViewSet(ModelViewSet):
'termination_a', 'termination_b' 'termination_a', 'termination_b'
) )
serializer_class = serializers.CableSerializer serializer_class = serializers.CableSerializer
filterset_class = filters.CableFilterSet filterset_class = filtersets.CableFilterSet
# #
@ -609,7 +622,7 @@ class VirtualChassisViewSet(ModelViewSet):
member_count=count_related(Device, 'virtual_chassis') member_count=count_related(Device, 'virtual_chassis')
) )
serializer_class = serializers.VirtualChassisSerializer serializer_class = serializers.VirtualChassisSerializer
filterset_class = filters.VirtualChassisFilterSet filterset_class = filtersets.VirtualChassisFilterSet
brief_prefetch_fields = ['master'] brief_prefetch_fields = ['master']
@ -619,12 +632,12 @@ class VirtualChassisViewSet(ModelViewSet):
class PowerPanelViewSet(ModelViewSet): class PowerPanelViewSet(ModelViewSet):
queryset = PowerPanel.objects.prefetch_related( queryset = PowerPanel.objects.prefetch_related(
'site', 'rack_group' 'site', 'location'
).annotate( ).annotate(
powerfeed_count=count_related(PowerFeed, 'power_panel') powerfeed_count=count_related(PowerFeed, 'power_panel')
) )
serializer_class = serializers.PowerPanelSerializer serializer_class = serializers.PowerPanelSerializer
filterset_class = filters.PowerPanelFilterSet filterset_class = filtersets.PowerPanelFilterSet
# #
@ -636,7 +649,7 @@ class PowerFeedViewSet(PathEndpointMixin, CustomFieldModelViewSet):
'power_panel', 'rack', '_path__destination', 'cable', '_cable_peer', 'tags' 'power_panel', 'rack', '_path__destination', 'cable', '_cable_peer', 'tags'
) )
serializer_class = serializers.PowerFeedSerializer serializer_class = serializers.PowerFeedSerializer
filterset_class = filters.PowerFeedFilterSet filterset_class = filtersets.PowerFeedFilterSet
# #

View File

@ -221,6 +221,29 @@ class ConsolePortTypeChoices(ChoiceSet):
) )
class ConsolePortSpeedChoices(ChoiceSet):
SPEED_1200 = 1200
SPEED_2400 = 2400
SPEED_4800 = 4800
SPEED_9600 = 9600
SPEED_19200 = 19200
SPEED_38400 = 38400
SPEED_57600 = 57600
SPEED_115200 = 115200
CHOICES = (
(SPEED_1200, '1200 bps'),
(SPEED_2400, '2400 bps'),
(SPEED_4800, '4800 bps'),
(SPEED_9600, '9600 bps'),
(SPEED_19200, '19.2 kbps'),
(SPEED_38400, '38.4 kbps'),
(SPEED_57600, '57.6 kbps'),
(SPEED_115200, '115.2 kbps'),
)
# #
# PowerPorts # PowerPorts
# #
@ -233,6 +256,7 @@ class PowerPortTypeChoices(ChoiceSet):
TYPE_IEC_C14 = 'iec-60320-c14' TYPE_IEC_C14 = 'iec-60320-c14'
TYPE_IEC_C16 = 'iec-60320-c16' TYPE_IEC_C16 = 'iec-60320-c16'
TYPE_IEC_C20 = 'iec-60320-c20' TYPE_IEC_C20 = 'iec-60320-c20'
TYPE_IEC_C22 = 'iec-60320-c22'
# IEC 60309 # IEC 60309
TYPE_IEC_PNE4H = 'iec-60309-p-n-e-4h' TYPE_IEC_PNE4H = 'iec-60309-p-n-e-4h'
TYPE_IEC_PNE6H = 'iec-60309-p-n-e-6h' TYPE_IEC_PNE6H = 'iec-60309-p-n-e-6h'
@ -318,6 +342,12 @@ class PowerPortTypeChoices(ChoiceSet):
TYPE_USB_MICRO_B = 'usb-micro-b' TYPE_USB_MICRO_B = 'usb-micro-b'
TYPE_USB_3_B = 'usb-3-b' TYPE_USB_3_B = 'usb-3-b'
TYPE_USB_3_MICROB = 'usb-3-micro-b' TYPE_USB_3_MICROB = 'usb-3-micro-b'
# Direct current (DC)
TYPE_DC = 'dc-terminal'
# Proprietary
TYPE_SAF_D_GRID = 'saf-d-grid'
# Other
TYPE_HARDWIRED = 'hardwired'
CHOICES = ( CHOICES = (
('IEC 60320', ( ('IEC 60320', (
@ -326,6 +356,7 @@ class PowerPortTypeChoices(ChoiceSet):
(TYPE_IEC_C14, 'C14'), (TYPE_IEC_C14, 'C14'),
(TYPE_IEC_C16, 'C16'), (TYPE_IEC_C16, 'C16'),
(TYPE_IEC_C20, 'C20'), (TYPE_IEC_C20, 'C20'),
(TYPE_IEC_C22, 'C22'),
)), )),
('IEC 60309', ( ('IEC 60309', (
(TYPE_IEC_PNE4H, 'P+N+E 4H'), (TYPE_IEC_PNE4H, 'P+N+E 4H'),
@ -418,6 +449,15 @@ class PowerPortTypeChoices(ChoiceSet):
(TYPE_USB_3_B, 'USB 3.0 Type B'), (TYPE_USB_3_B, 'USB 3.0 Type B'),
(TYPE_USB_3_MICROB, 'USB 3.0 Micro B'), (TYPE_USB_3_MICROB, 'USB 3.0 Micro B'),
)), )),
('DC', (
(TYPE_DC, 'DC Terminal'),
)),
('Proprietary', (
(TYPE_SAF_D_GRID, 'Saf-D-Grid'),
)),
('Other', (
(TYPE_HARDWIRED, 'Hardwired'),
)),
) )
@ -433,6 +473,7 @@ class PowerOutletTypeChoices(ChoiceSet):
TYPE_IEC_C13 = 'iec-60320-c13' TYPE_IEC_C13 = 'iec-60320-c13'
TYPE_IEC_C15 = 'iec-60320-c15' TYPE_IEC_C15 = 'iec-60320-c15'
TYPE_IEC_C19 = 'iec-60320-c19' TYPE_IEC_C19 = 'iec-60320-c19'
TYPE_IEC_C21 = 'iec-60320-c21'
# IEC 60309 # IEC 60309
TYPE_IEC_PNE4H = 'iec-60309-p-n-e-4h' TYPE_IEC_PNE4H = 'iec-60309-p-n-e-4h'
TYPE_IEC_PNE6H = 'iec-60309-p-n-e-6h' TYPE_IEC_PNE6H = 'iec-60309-p-n-e-6h'
@ -511,8 +552,13 @@ class PowerOutletTypeChoices(ChoiceSet):
TYPE_USB_A = 'usb-a' TYPE_USB_A = 'usb-a'
TYPE_USB_MICROB = 'usb-micro-b' TYPE_USB_MICROB = 'usb-micro-b'
TYPE_USB_C = 'usb-c' TYPE_USB_C = 'usb-c'
# Direct current (DC)
TYPE_DC = 'dc-terminal'
# Proprietary # Proprietary
TYPE_HDOT_CX = 'hdot-cx' TYPE_HDOT_CX = 'hdot-cx'
TYPE_SAF_D_GRID = 'saf-d-grid'
# Other
TYPE_HARDWIRED = 'hardwired'
CHOICES = ( CHOICES = (
('IEC 60320', ( ('IEC 60320', (
@ -521,6 +567,7 @@ class PowerOutletTypeChoices(ChoiceSet):
(TYPE_IEC_C13, 'C13'), (TYPE_IEC_C13, 'C13'),
(TYPE_IEC_C15, 'C15'), (TYPE_IEC_C15, 'C15'),
(TYPE_IEC_C19, 'C19'), (TYPE_IEC_C19, 'C19'),
(TYPE_IEC_C21, 'C21'),
)), )),
('IEC 60309', ( ('IEC 60309', (
(TYPE_IEC_PNE4H, 'P+N+E 4H'), (TYPE_IEC_PNE4H, 'P+N+E 4H'),
@ -606,8 +653,15 @@ class PowerOutletTypeChoices(ChoiceSet):
(TYPE_USB_MICROB, 'USB Micro B'), (TYPE_USB_MICROB, 'USB Micro B'),
(TYPE_USB_C, 'USB Type C'), (TYPE_USB_C, 'USB Type C'),
)), )),
('DC', (
(TYPE_DC, 'DC Terminal'),
)),
('Proprietary', ( ('Proprietary', (
(TYPE_HDOT_CX, 'HDOT Cx'), (TYPE_HDOT_CX, 'HDOT Cx'),
(TYPE_SAF_D_GRID, 'Saf-D-Grid'),
)),
('Other', (
(TYPE_HARDWIRED, 'Hardwired'),
)), )),
) )
@ -649,6 +703,7 @@ class InterfaceTypeChoices(ChoiceSet):
TYPE_10GE_XENPAK = '10gbase-x-xenpak' TYPE_10GE_XENPAK = '10gbase-x-xenpak'
TYPE_10GE_X2 = '10gbase-x-x2' TYPE_10GE_X2 = '10gbase-x-x2'
TYPE_25GE_SFP28 = '25gbase-x-sfp28' TYPE_25GE_SFP28 = '25gbase-x-sfp28'
TYPE_50GE_SFP56 = '50gbase-x-sfp56'
TYPE_40GE_QSFP_PLUS = '40gbase-x-qsfpp' TYPE_40GE_QSFP_PLUS = '40gbase-x-qsfpp'
TYPE_50GE_QSFP28 = '50gbase-x-sfp28' TYPE_50GE_QSFP28 = '50gbase-x-sfp28'
TYPE_100GE_CFP = '100gbase-x-cfp' TYPE_100GE_CFP = '100gbase-x-cfp'
@ -754,6 +809,7 @@ class InterfaceTypeChoices(ChoiceSet):
(TYPE_10GE_XENPAK, 'XENPAK (10GE)'), (TYPE_10GE_XENPAK, 'XENPAK (10GE)'),
(TYPE_10GE_X2, 'X2 (10GE)'), (TYPE_10GE_X2, 'X2 (10GE)'),
(TYPE_25GE_SFP28, 'SFP28 (25GE)'), (TYPE_25GE_SFP28, 'SFP28 (25GE)'),
(TYPE_50GE_SFP56, 'SFP56 (50GE)'),
(TYPE_40GE_QSFP_PLUS, 'QSFP+ (40GE)'), (TYPE_40GE_QSFP_PLUS, 'QSFP+ (40GE)'),
(TYPE_50GE_QSFP28, 'QSFP28 (50GE)'), (TYPE_50GE_QSFP28, 'QSFP28 (50GE)'),
(TYPE_100GE_CFP, 'CFP (100GE)'), (TYPE_100GE_CFP, 'CFP (100GE)'),
@ -881,12 +937,19 @@ class PortTypeChoices(ChoiceSet):
TYPE_8P6C = '8p6c' TYPE_8P6C = '8p6c'
TYPE_8P4C = '8p4c' TYPE_8P4C = '8p4c'
TYPE_8P2C = '8p2c' TYPE_8P2C = '8p2c'
TYPE_6P6C = '6p6c'
TYPE_6P4C = '6p4c'
TYPE_6P2C = '6p2c'
TYPE_4P4C = '4p4c'
TYPE_4P2C = '4p2c'
TYPE_GG45 = 'gg45' TYPE_GG45 = 'gg45'
TYPE_TERA4P = 'tera-4p' TYPE_TERA4P = 'tera-4p'
TYPE_TERA2P = 'tera-2p' TYPE_TERA2P = 'tera-2p'
TYPE_TERA1P = 'tera-1p' TYPE_TERA1P = 'tera-1p'
TYPE_110_PUNCH = '110-punch' TYPE_110_PUNCH = '110-punch'
TYPE_BNC = 'bnc' TYPE_BNC = 'bnc'
TYPE_F = 'f'
TYPE_N = 'n'
TYPE_MRJ21 = 'mrj21' TYPE_MRJ21 = 'mrj21'
TYPE_ST = 'st' TYPE_ST = 'st'
TYPE_SC = 'sc' TYPE_SC = 'sc'
@ -910,12 +973,19 @@ class PortTypeChoices(ChoiceSet):
(TYPE_8P6C, '8P6C'), (TYPE_8P6C, '8P6C'),
(TYPE_8P4C, '8P4C'), (TYPE_8P4C, '8P4C'),
(TYPE_8P2C, '8P2C'), (TYPE_8P2C, '8P2C'),
(TYPE_6P6C, '6P6C'),
(TYPE_6P4C, '6P4C'),
(TYPE_6P2C, '6P2C'),
(TYPE_4P4C, '4P4C'),
(TYPE_4P2C, '4P2C'),
(TYPE_GG45, 'GG45'), (TYPE_GG45, 'GG45'),
(TYPE_TERA4P, 'TERA 4P'), (TYPE_TERA4P, 'TERA 4P'),
(TYPE_TERA2P, 'TERA 2P'), (TYPE_TERA2P, 'TERA 2P'),
(TYPE_TERA1P, 'TERA 1P'), (TYPE_TERA1P, 'TERA 1P'),
(TYPE_110_PUNCH, '110 Punch'), (TYPE_110_PUNCH, '110 Punch'),
(TYPE_BNC, 'BNC'), (TYPE_BNC, 'BNC'),
(TYPE_F, 'F Connector'),
(TYPE_N, 'N Connector'),
(TYPE_MRJ21, 'MRJ21'), (TYPE_MRJ21, 'MRJ21'),
), ),
), ),
@ -963,6 +1033,7 @@ class CableTypeChoices(ChoiceSet):
TYPE_MMF_OM2 = 'mmf-om2' TYPE_MMF_OM2 = 'mmf-om2'
TYPE_MMF_OM3 = 'mmf-om3' TYPE_MMF_OM3 = 'mmf-om3'
TYPE_MMF_OM4 = 'mmf-om4' TYPE_MMF_OM4 = 'mmf-om4'
TYPE_MMF_OM5 = 'mmf-om5'
TYPE_SMF = 'smf' TYPE_SMF = 'smf'
TYPE_SMF_OS1 = 'smf-os1' TYPE_SMF_OS1 = 'smf-os1'
TYPE_SMF_OS2 = 'smf-os2' TYPE_SMF_OS2 = 'smf-os2'
@ -993,6 +1064,7 @@ class CableTypeChoices(ChoiceSet):
(TYPE_MMF_OM2, 'Multimode Fiber (OM2)'), (TYPE_MMF_OM2, 'Multimode Fiber (OM2)'),
(TYPE_MMF_OM3, 'Multimode Fiber (OM3)'), (TYPE_MMF_OM3, 'Multimode Fiber (OM3)'),
(TYPE_MMF_OM4, 'Multimode Fiber (OM4)'), (TYPE_MMF_OM4, 'Multimode Fiber (OM4)'),
(TYPE_MMF_OM5, 'Multimode Fiber (OM5)'),
(TYPE_SMF, 'Singlemode Fiber'), (TYPE_SMF, 'Singlemode Fiber'),
(TYPE_SMF_OS1, 'Singlemode Fiber (OS1)'), (TYPE_SMF_OS1, 'Singlemode Fiber (OS1)'),
(TYPE_SMF_OS2, 'Singlemode Fiber (OS2)'), (TYPE_SMF_OS2, 'Singlemode Fiber (OS2)'),

View File

@ -2,6 +2,9 @@ from django.db.models import Q
from .choices import InterfaceTypeChoices from .choices import InterfaceTypeChoices
# Exclude SVG images (unsupported by PIL)
DEVICETYPE_IMAGE_FORMATS = 'image/bmp,image/gif,image/jpeg,image/png,image/tiff,image/webp'
# #
# Racks # Racks
@ -26,7 +29,7 @@ REARPORT_POSITIONS_MAX = 1024
# #
INTERFACE_MTU_MIN = 1 INTERFACE_MTU_MIN = 1
INTERFACE_MTU_MAX = 32767 # Max value of a signed 16-bit integer INTERFACE_MTU_MAX = 65536
VIRTUAL_IFACE_TYPES = [ VIRTUAL_IFACE_TYPES = [
InterfaceTypeChoices.TYPE_VIRTUAL, InterfaceTypeChoices.TYPE_VIRTUAL,

View File

@ -1,25 +1,21 @@
import django_filters import django_filters
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.db.models import Count
from extras.filters import CustomFieldModelFilterSet, LocalConfigContextFilterSet, CreatedUpdatedFilterSet from extras.filters import TagFilter
from tenancy.filters import TenancyFilterSet from extras.filtersets import LocalConfigContextFilterSet
from netbox.filtersets import (
BaseFilterSet, ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, PrimaryModelFilterSet,
)
from tenancy.filtersets import TenancyFilterSet
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.choices import ColorChoices from utilities.choices import ColorChoices
from utilities.filters import ( from utilities.filters import (
BaseFilterSet, MultiValueCharFilter, MultiValueMACAddressFilter, MultiValueNumberFilter, MultiValueCharFilter, MultiValueMACAddressFilter, MultiValueNumberFilter, TreeNodeMultipleChoiceFilter,
NameSlugSearchFilterSet, TagFilter, TreeNodeMultipleChoiceFilter,
) )
from virtualization.models import Cluster from virtualization.models import Cluster
from .choices import * from .choices import *
from .constants import * from .constants import *
from .models import ( from .models import *
Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate,
InventoryItem, Manufacturer, Platform, PowerFeed, PowerOutlet, PowerOutletTemplate, PowerPanel, PowerPort,
PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site,
VirtualChassis,
)
__all__ = ( __all__ = (
@ -41,6 +37,7 @@ __all__ = (
'InterfaceFilterSet', 'InterfaceFilterSet',
'InterfaceTemplateFilterSet', 'InterfaceTemplateFilterSet',
'InventoryItemFilterSet', 'InventoryItemFilterSet',
'LocationFilterSet',
'ManufacturerFilterSet', 'ManufacturerFilterSet',
'PathEndpointFilterSet', 'PathEndpointFilterSet',
'PlatformFilterSet', 'PlatformFilterSet',
@ -52,18 +49,18 @@ __all__ = (
'PowerPortFilterSet', 'PowerPortFilterSet',
'PowerPortTemplateFilterSet', 'PowerPortTemplateFilterSet',
'RackFilterSet', 'RackFilterSet',
'RackGroupFilterSet',
'RackReservationFilterSet', 'RackReservationFilterSet',
'RackRoleFilterSet', 'RackRoleFilterSet',
'RearPortFilterSet', 'RearPortFilterSet',
'RearPortTemplateFilterSet', 'RearPortTemplateFilterSet',
'RegionFilterSet', 'RegionFilterSet',
'SiteFilterSet', 'SiteFilterSet',
'SiteGroupFilterSet',
'VirtualChassisFilterSet', 'VirtualChassisFilterSet',
) )
class RegionFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CustomFieldModelFilterSet): class RegionFilterSet(OrganizationalModelFilterSet):
parent_id = django_filters.ModelMultipleChoiceFilter( parent_id = django_filters.ModelMultipleChoiceFilter(
queryset=Region.objects.all(), queryset=Region.objects.all(),
label='Parent region (ID)', label='Parent region (ID)',
@ -80,7 +77,24 @@ class RegionFilterSet(BaseFilterSet, NameSlugSearchFilterSet, CustomFieldModelFi
fields = ['id', 'name', 'slug', 'description'] fields = ['id', 'name', 'slug', 'description']
class SiteFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet): class SiteGroupFilterSet(OrganizationalModelFilterSet):
parent_id = django_filters.ModelMultipleChoiceFilter(
queryset=SiteGroup.objects.all(),
label='Parent site group (ID)',
)
parent = django_filters.ModelMultipleChoiceFilter(
field_name='parent__slug',
queryset=SiteGroup.objects.all(),
to_field_name='slug',
label='Parent site group (slug)',
)
class Meta:
model = SiteGroup
fields = ['id', 'name', 'slug', 'description']
class SiteFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
q = django_filters.CharFilter( q = django_filters.CharFilter(
method='search', method='search',
label='Search', label='Search',
@ -97,11 +111,22 @@ class SiteFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet,
) )
region = TreeNodeMultipleChoiceFilter( region = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(), queryset=Region.objects.all(),
field_name='region',
lookup_expr='in', lookup_expr='in',
to_field_name='slug', to_field_name='slug',
label='Region (slug)', label='Region (slug)',
) )
group_id = TreeNodeMultipleChoiceFilter(
queryset=SiteGroup.objects.all(),
field_name='group',
lookup_expr='in',
label='Group (ID)',
)
group = TreeNodeMultipleChoiceFilter(
queryset=SiteGroup.objects.all(),
lookup_expr='in',
to_field_name='slug',
label='Group (slug)',
)
tag = TagFilter() tag = TagFilter()
class Meta: class Meta:
@ -132,7 +157,7 @@ class SiteFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet,
return queryset.filter(qs_filter) return queryset.filter(qs_filter)
class RackGroupFilterSet(BaseFilterSet, NameSlugSearchFilterSet): class LocationFilterSet(OrganizationalModelFilterSet):
region_id = TreeNodeMultipleChoiceFilter( region_id = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(), queryset=Region.objects.all(),
field_name='site__region', field_name='site__region',
@ -146,6 +171,19 @@ class RackGroupFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
to_field_name='slug', to_field_name='slug',
label='Region (slug)', label='Region (slug)',
) )
site_group_id = TreeNodeMultipleChoiceFilter(
queryset=SiteGroup.objects.all(),
field_name='site__group',
lookup_expr='in',
label='Site group (ID)',
)
site_group = TreeNodeMultipleChoiceFilter(
queryset=SiteGroup.objects.all(),
field_name='site__group',
lookup_expr='in',
to_field_name='slug',
label='Site group (slug)',
)
site_id = django_filters.ModelMultipleChoiceFilter( site_id = django_filters.ModelMultipleChoiceFilter(
queryset=Site.objects.all(), queryset=Site.objects.all(),
label='Site (ID)', label='Site (ID)',
@ -156,30 +194,41 @@ class RackGroupFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
to_field_name='slug', to_field_name='slug',
label='Site (slug)', label='Site (slug)',
) )
parent_id = django_filters.ModelMultipleChoiceFilter( parent_id = TreeNodeMultipleChoiceFilter(
queryset=RackGroup.objects.all(), queryset=Location.objects.all(),
label='Rack group (ID)', field_name='parent',
lookup_expr='in',
label='Location (ID)',
) )
parent = django_filters.ModelMultipleChoiceFilter( parent = TreeNodeMultipleChoiceFilter(
field_name='parent__slug', queryset=Location.objects.all(),
queryset=RackGroup.objects.all(), field_name='parent',
lookup_expr='in',
to_field_name='slug', to_field_name='slug',
label='Rack group (slug)', label='Location (slug)',
) )
class Meta: class Meta:
model = RackGroup model = Location
fields = ['id', 'name', 'slug', 'description'] 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)
)
class RackRoleFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
class RackRoleFilterSet(OrganizationalModelFilterSet):
class Meta: class Meta:
model = RackRole model = RackRole
fields = ['id', 'name', 'slug', 'color'] fields = ['id', 'name', 'slug', 'color']
class RackFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet): class RackFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
q = django_filters.CharFilter( q = django_filters.CharFilter(
method='search', method='search',
label='Search', label='Search',
@ -197,6 +246,19 @@ class RackFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet,
to_field_name='slug', to_field_name='slug',
label='Region (slug)', label='Region (slug)',
) )
site_group_id = TreeNodeMultipleChoiceFilter(
queryset=SiteGroup.objects.all(),
field_name='site__group',
lookup_expr='in',
label='Site group (ID)',
)
site_group = TreeNodeMultipleChoiceFilter(
queryset=SiteGroup.objects.all(),
field_name='site__group',
lookup_expr='in',
to_field_name='slug',
label='Site group (slug)',
)
site_id = django_filters.ModelMultipleChoiceFilter( site_id = django_filters.ModelMultipleChoiceFilter(
queryset=Site.objects.all(), queryset=Site.objects.all(),
label='Site (ID)', label='Site (ID)',
@ -207,18 +269,18 @@ class RackFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet,
to_field_name='slug', to_field_name='slug',
label='Site (slug)', label='Site (slug)',
) )
group_id = TreeNodeMultipleChoiceFilter( location_id = TreeNodeMultipleChoiceFilter(
queryset=RackGroup.objects.all(), queryset=Location.objects.all(),
field_name='group', field_name='location',
lookup_expr='in', lookup_expr='in',
label='Rack group (ID)', label='Location (ID)',
) )
group = TreeNodeMultipleChoiceFilter( location = TreeNodeMultipleChoiceFilter(
queryset=RackGroup.objects.all(), queryset=Location.objects.all(),
field_name='group', field_name='location',
lookup_expr='in', lookup_expr='in',
to_field_name='slug', to_field_name='slug',
label='Rack group (slug)', label='Location (slug)',
) )
status = django_filters.MultipleChoiceFilter( status = django_filters.MultipleChoiceFilter(
choices=RackStatusChoices, choices=RackStatusChoices,
@ -264,7 +326,7 @@ class RackFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet,
) )
class RackReservationFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet): class RackReservationFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
q = django_filters.CharFilter( q = django_filters.CharFilter(
method='search', method='search',
label='Search', label='Search',
@ -284,18 +346,18 @@ class RackReservationFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModel
to_field_name='slug', to_field_name='slug',
label='Site (slug)', label='Site (slug)',
) )
group_id = TreeNodeMultipleChoiceFilter( location_id = TreeNodeMultipleChoiceFilter(
queryset=RackGroup.objects.all(), queryset=Location.objects.all(),
field_name='rack__group', field_name='rack__location',
lookup_expr='in', lookup_expr='in',
label='Rack group (ID)', label='Location (ID)',
) )
group = TreeNodeMultipleChoiceFilter( location = TreeNodeMultipleChoiceFilter(
queryset=RackGroup.objects.all(), queryset=Location.objects.all(),
field_name='rack__group', field_name='rack__location',
lookup_expr='in', lookup_expr='in',
to_field_name='slug', to_field_name='slug',
label='Rack group (slug)', label='Location (slug)',
) )
user_id = django_filters.ModelMultipleChoiceFilter( user_id = django_filters.ModelMultipleChoiceFilter(
queryset=User.objects.all(), queryset=User.objects.all(),
@ -324,14 +386,14 @@ class RackReservationFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModel
) )
class ManufacturerFilterSet(BaseFilterSet, NameSlugSearchFilterSet): class ManufacturerFilterSet(OrganizationalModelFilterSet):
class Meta: class Meta:
model = Manufacturer model = Manufacturer
fields = ['id', 'name', 'slug', 'description'] fields = ['id', 'name', 'slug', 'description']
class DeviceTypeFilterSet(BaseFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet): class DeviceTypeFilterSet(PrimaryModelFilterSet):
q = django_filters.CharFilter( q = django_filters.CharFilter(
method='search', method='search',
label='Search', label='Search',
@ -417,7 +479,7 @@ class DeviceTypeFilterSet(BaseFilterSet, CustomFieldModelFilterSet, CreatedUpdat
return queryset.exclude(devicebaytemplates__isnull=value) return queryset.exclude(devicebaytemplates__isnull=value)
class DeviceTypeComponentFilterSet(NameSlugSearchFilterSet): class DeviceTypeComponentFilterSet(django_filters.FilterSet):
devicetype_id = django_filters.ModelMultipleChoiceFilter( devicetype_id = django_filters.ModelMultipleChoiceFilter(
queryset=DeviceType.objects.all(), queryset=DeviceType.objects.all(),
field_name='device_type_id', field_name='device_type_id',
@ -425,70 +487,86 @@ class DeviceTypeComponentFilterSet(NameSlugSearchFilterSet):
) )
class ConsolePortTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet): class ConsolePortTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
class Meta: class Meta:
model = ConsolePortTemplate model = ConsolePortTemplate
fields = ['id', 'name', 'type'] fields = ['id', 'name', 'type']
class ConsoleServerPortTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet): class ConsoleServerPortTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
class Meta: class Meta:
model = ConsoleServerPortTemplate model = ConsoleServerPortTemplate
fields = ['id', 'name', 'type'] fields = ['id', 'name', 'type']
class PowerPortTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet): class PowerPortTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
class Meta: class Meta:
model = PowerPortTemplate model = PowerPortTemplate
fields = ['id', 'name', 'type', 'maximum_draw', 'allocated_draw'] fields = ['id', 'name', 'type', 'maximum_draw', 'allocated_draw']
class PowerOutletTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet): class PowerOutletTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
feed_leg = django_filters.MultipleChoiceFilter(
choices=PowerOutletFeedLegChoices,
null_value=None
)
class Meta: class Meta:
model = PowerOutletTemplate model = PowerOutletTemplate
fields = ['id', 'name', 'type', 'feed_leg'] fields = ['id', 'name', 'type', 'feed_leg']
class InterfaceTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet): class InterfaceTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
type = django_filters.MultipleChoiceFilter(
choices=InterfaceTypeChoices,
null_value=None
)
class Meta: class Meta:
model = InterfaceTemplate model = InterfaceTemplate
fields = ['id', 'name', 'type', 'mgmt_only'] fields = ['id', 'name', 'type', 'mgmt_only']
class FrontPortTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet): class FrontPortTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
type = django_filters.MultipleChoiceFilter(
choices=PortTypeChoices,
null_value=None
)
class Meta: class Meta:
model = FrontPortTemplate model = FrontPortTemplate
fields = ['id', 'name', 'type'] fields = ['id', 'name', 'type']
class RearPortTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet): class RearPortTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
type = django_filters.MultipleChoiceFilter(
choices=PortTypeChoices,
null_value=None
)
class Meta: class Meta:
model = RearPortTemplate model = RearPortTemplate
fields = ['id', 'name', 'type', 'positions'] fields = ['id', 'name', 'type', 'positions']
class DeviceBayTemplateFilterSet(BaseFilterSet, DeviceTypeComponentFilterSet): class DeviceBayTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
class Meta: class Meta:
model = DeviceBayTemplate model = DeviceBayTemplate
fields = ['id', 'name'] fields = ['id', 'name']
class DeviceRoleFilterSet(BaseFilterSet, NameSlugSearchFilterSet): class DeviceRoleFilterSet(OrganizationalModelFilterSet):
class Meta: class Meta:
model = DeviceRole model = DeviceRole
fields = ['id', 'name', 'slug', 'color', 'vm_role'] fields = ['id', 'name', 'slug', 'color', 'vm_role']
class PlatformFilterSet(BaseFilterSet, NameSlugSearchFilterSet): class PlatformFilterSet(OrganizationalModelFilterSet):
manufacturer_id = django_filters.ModelMultipleChoiceFilter( manufacturer_id = django_filters.ModelMultipleChoiceFilter(
field_name='manufacturer', field_name='manufacturer',
queryset=Manufacturer.objects.all(), queryset=Manufacturer.objects.all(),
@ -506,13 +584,7 @@ class PlatformFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
fields = ['id', 'name', 'slug', 'napalm_driver', 'description'] fields = ['id', 'name', 'slug', 'napalm_driver', 'description']
class DeviceFilterSet( class DeviceFilterSet(PrimaryModelFilterSet, TenancyFilterSet, LocalConfigContextFilterSet):
BaseFilterSet,
TenancyFilterSet,
LocalConfigContextFilterSet,
CustomFieldModelFilterSet,
CreatedUpdatedFilterSet
):
q = django_filters.CharFilter( q = django_filters.CharFilter(
method='search', method='search',
label='Search', label='Search',
@ -566,6 +638,19 @@ class DeviceFilterSet(
to_field_name='slug', to_field_name='slug',
label='Region (slug)', label='Region (slug)',
) )
site_group_id = TreeNodeMultipleChoiceFilter(
queryset=SiteGroup.objects.all(),
field_name='site__group',
lookup_expr='in',
label='Site group (ID)',
)
site_group = TreeNodeMultipleChoiceFilter(
queryset=SiteGroup.objects.all(),
field_name='site__group',
lookup_expr='in',
to_field_name='slug',
label='Site group (slug)',
)
site_id = django_filters.ModelMultipleChoiceFilter( site_id = django_filters.ModelMultipleChoiceFilter(
queryset=Site.objects.all(), queryset=Site.objects.all(),
label='Site (ID)', label='Site (ID)',
@ -576,11 +661,11 @@ class DeviceFilterSet(
to_field_name='slug', to_field_name='slug',
label='Site name (slug)', label='Site name (slug)',
) )
rack_group_id = TreeNodeMultipleChoiceFilter( location_id = TreeNodeMultipleChoiceFilter(
queryset=RackGroup.objects.all(), queryset=Location.objects.all(),
field_name='rack__group', field_name='location',
lookup_expr='in', lookup_expr='in',
label='Rack group (ID)', label='Location (ID)',
) )
rack_id = django_filters.ModelMultipleChoiceFilter( rack_id = django_filters.ModelMultipleChoiceFilter(
field_name='rack', field_name='rack',
@ -722,6 +807,19 @@ class DeviceComponentFilterSet(django_filters.FilterSet):
to_field_name='slug', to_field_name='slug',
label='Region (slug)', label='Region (slug)',
) )
site_group_id = TreeNodeMultipleChoiceFilter(
queryset=SiteGroup.objects.all(),
field_name='device__site__group',
lookup_expr='in',
label='Site group (ID)',
)
site_group = TreeNodeMultipleChoiceFilter(
queryset=SiteGroup.objects.all(),
field_name='device__site__group',
lookup_expr='in',
to_field_name='slug',
label='Site group (slug)',
)
site_id = django_filters.ModelMultipleChoiceFilter( site_id = django_filters.ModelMultipleChoiceFilter(
field_name='device__site', field_name='device__site',
queryset=Site.objects.all(), queryset=Site.objects.all(),
@ -775,7 +873,7 @@ class PathEndpointFilterSet(django_filters.FilterSet):
return queryset.filter(Q(_path__isnull=True) | Q(_path__is_active=False)) return queryset.filter(Q(_path__isnull=True) | Q(_path__is_active=False))
class ConsolePortFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet): class ConsolePortFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet):
type = django_filters.MultipleChoiceFilter( type = django_filters.MultipleChoiceFilter(
choices=ConsolePortTypeChoices, choices=ConsolePortTypeChoices,
null_value=None null_value=None
@ -783,15 +881,10 @@ class ConsolePortFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTermina
class Meta: class Meta:
model = ConsolePort model = ConsolePort
fields = ['id', 'name', 'description'] fields = ['id', 'name', 'label', 'description']
class ConsoleServerPortFilterSet( class ConsoleServerPortFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet):
BaseFilterSet,
DeviceComponentFilterSet,
CableTerminationFilterSet,
PathEndpointFilterSet
):
type = django_filters.MultipleChoiceFilter( type = django_filters.MultipleChoiceFilter(
choices=ConsolePortTypeChoices, choices=ConsolePortTypeChoices,
null_value=None null_value=None
@ -799,10 +892,10 @@ class ConsoleServerPortFilterSet(
class Meta: class Meta:
model = ConsoleServerPort model = ConsoleServerPort
fields = ['id', 'name', 'description'] fields = ['id', 'name', 'label', 'description']
class PowerPortFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet): class PowerPortFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet):
type = django_filters.MultipleChoiceFilter( type = django_filters.MultipleChoiceFilter(
choices=PowerPortTypeChoices, choices=PowerPortTypeChoices,
null_value=None null_value=None
@ -810,21 +903,25 @@ class PowerPortFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminati
class Meta: class Meta:
model = PowerPort model = PowerPort
fields = ['id', 'name', 'maximum_draw', 'allocated_draw', 'description'] fields = ['id', 'name', 'label', 'maximum_draw', 'allocated_draw', 'description']
class PowerOutletFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet): class PowerOutletFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet):
type = django_filters.MultipleChoiceFilter( type = django_filters.MultipleChoiceFilter(
choices=PowerOutletTypeChoices, choices=PowerOutletTypeChoices,
null_value=None null_value=None
) )
feed_leg = django_filters.MultipleChoiceFilter(
choices=PowerOutletFeedLegChoices,
null_value=None
)
class Meta: class Meta:
model = PowerOutlet model = PowerOutlet
fields = ['id', 'name', 'feed_leg', 'description'] fields = ['id', 'name', 'label', 'feed_leg', 'description']
class InterfaceFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet): class InterfaceFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet):
q = django_filters.CharFilter( q = django_filters.CharFilter(
method='search', method='search',
label='Search', label='Search',
@ -845,6 +942,11 @@ class InterfaceFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminati
method='filter_kind', method='filter_kind',
label='Kind of interface', label='Kind of interface',
) )
parent_id = django_filters.ModelMultipleChoiceFilter(
field_name='parent',
queryset=Interface.objects.all(),
label='Parent interface (ID)',
)
lag_id = django_filters.ModelMultipleChoiceFilter( lag_id = django_filters.ModelMultipleChoiceFilter(
field_name='lag', field_name='lag',
queryset=Interface.objects.all(), queryset=Interface.objects.all(),
@ -867,14 +969,14 @@ class InterfaceFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminati
class Meta: class Meta:
model = Interface model = Interface
fields = ['id', 'name', 'type', 'enabled', 'mtu', 'mgmt_only', 'mode', 'description'] fields = ['id', 'name', 'label', 'type', 'enabled', 'mtu', 'mgmt_only', 'mode', 'description']
def filter_device(self, queryset, name, value): def filter_device(self, queryset, name, value):
try: try:
devices = Device.objects.filter(**{'{}__in'.format(name): value}) devices = Device.objects.filter(**{'{}__in'.format(name): value})
vc_interface_ids = [] vc_interface_ids = []
for device in devices: for device in devices:
vc_interface_ids.extend(device.vc_interfaces.values_list('id', flat=True)) vc_interface_ids.extend(device.vc_interfaces().values_list('id', flat=True))
return queryset.filter(pk__in=vc_interface_ids) return queryset.filter(pk__in=vc_interface_ids)
except Device.DoesNotExist: except Device.DoesNotExist:
return queryset.none() return queryset.none()
@ -885,7 +987,7 @@ class InterfaceFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminati
try: try:
devices = Device.objects.filter(pk__in=id_list) devices = Device.objects.filter(pk__in=id_list)
for device in devices: for device in devices:
vc_interface_ids += device.vc_interfaces.values_list('id', flat=True) vc_interface_ids += device.vc_interfaces().values_list('id', flat=True)
return queryset.filter(pk__in=vc_interface_ids) return queryset.filter(pk__in=vc_interface_ids)
except Device.DoesNotExist: except Device.DoesNotExist:
return queryset.none() return queryset.none()
@ -917,28 +1019,36 @@ class InterfaceFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminati
}.get(value, queryset.none()) }.get(value, queryset.none())
class FrontPortFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet): class FrontPortFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet):
type = django_filters.MultipleChoiceFilter(
choices=PortTypeChoices,
null_value=None
)
class Meta: class Meta:
model = FrontPort model = FrontPort
fields = ['id', 'name', 'type', 'description'] fields = ['id', 'name', 'label', 'type', 'description']
class RearPortFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet): class RearPortFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet):
type = django_filters.MultipleChoiceFilter(
choices=PortTypeChoices,
null_value=None
)
class Meta: class Meta:
model = RearPort model = RearPort
fields = ['id', 'name', 'type', 'positions', 'description'] fields = ['id', 'name', 'label', 'type', 'positions', 'description']
class DeviceBayFilterSet(BaseFilterSet, DeviceComponentFilterSet): class DeviceBayFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet):
class Meta: class Meta:
model = DeviceBay model = DeviceBay
fields = ['id', 'name', 'description'] fields = ['id', 'name', 'label', 'description']
class InventoryItemFilterSet(BaseFilterSet, DeviceComponentFilterSet): class InventoryItemFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet):
q = django_filters.CharFilter( q = django_filters.CharFilter(
method='search', method='search',
label='Search', label='Search',
@ -996,7 +1106,7 @@ class InventoryItemFilterSet(BaseFilterSet, DeviceComponentFilterSet):
class Meta: class Meta:
model = InventoryItem model = InventoryItem
fields = ['id', 'name', 'part_id', 'asset_tag', 'discovered'] fields = ['id', 'name', 'label', 'part_id', 'asset_tag', 'discovered']
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -1011,7 +1121,7 @@ class InventoryItemFilterSet(BaseFilterSet, DeviceComponentFilterSet):
return queryset.filter(qs_filter) return queryset.filter(qs_filter)
class VirtualChassisFilterSet(BaseFilterSet): class VirtualChassisFilterSet(PrimaryModelFilterSet):
q = django_filters.CharFilter( q = django_filters.CharFilter(
method='search', method='search',
label='Search', label='Search',
@ -1039,6 +1149,19 @@ class VirtualChassisFilterSet(BaseFilterSet):
to_field_name='slug', to_field_name='slug',
label='Region (slug)', label='Region (slug)',
) )
site_group_id = TreeNodeMultipleChoiceFilter(
queryset=SiteGroup.objects.all(),
field_name='master__site__group',
lookup_expr='in',
label='Site group (ID)',
)
site_group = TreeNodeMultipleChoiceFilter(
queryset=SiteGroup.objects.all(),
field_name='master__site__group',
lookup_expr='in',
to_field_name='slug',
label='Site group (slug)',
)
site_id = django_filters.ModelMultipleChoiceFilter( site_id = django_filters.ModelMultipleChoiceFilter(
field_name='master__site', field_name='master__site',
queryset=Site.objects.all(), queryset=Site.objects.all(),
@ -1075,10 +1198,10 @@ class VirtualChassisFilterSet(BaseFilterSet):
Q(members__name__icontains=value) | Q(members__name__icontains=value) |
Q(domain__icontains=value) Q(domain__icontains=value)
) )
return queryset.filter(qs_filter) return queryset.filter(qs_filter).distinct()
class CableFilterSet(BaseFilterSet): class CableFilterSet(PrimaryModelFilterSet):
q = django_filters.CharFilter( q = django_filters.CharFilter(
method='search', method='search',
label='Search', label='Search',
@ -1142,7 +1265,7 @@ class CableFilterSet(BaseFilterSet):
return queryset return queryset
class ConnectionFilterSet: class ConnectionFilterSet(BaseFilterSet):
def filter_site(self, queryset, name, value): def filter_site(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -1155,7 +1278,7 @@ class ConnectionFilterSet:
return queryset.filter(**{f'{name}__in': value}) return queryset.filter(**{f'{name}__in': value})
class ConsoleConnectionFilterSet(ConnectionFilterSet, BaseFilterSet): class ConsoleConnectionFilterSet(ConnectionFilterSet):
site = django_filters.CharFilter( site = django_filters.CharFilter(
method='filter_site', method='filter_site',
label='Site (slug)', label='Site (slug)',
@ -1173,7 +1296,7 @@ class ConsoleConnectionFilterSet(ConnectionFilterSet, BaseFilterSet):
fields = ['name'] fields = ['name']
class PowerConnectionFilterSet(ConnectionFilterSet, BaseFilterSet): class PowerConnectionFilterSet(ConnectionFilterSet):
site = django_filters.CharFilter( site = django_filters.CharFilter(
method='filter_site', method='filter_site',
label='Site (slug)', label='Site (slug)',
@ -1191,7 +1314,7 @@ class PowerConnectionFilterSet(ConnectionFilterSet, BaseFilterSet):
fields = ['name'] fields = ['name']
class InterfaceConnectionFilterSet(ConnectionFilterSet, BaseFilterSet): class InterfaceConnectionFilterSet(ConnectionFilterSet):
site = django_filters.CharFilter( site = django_filters.CharFilter(
method='filter_site', method='filter_site',
label='Site (slug)', label='Site (slug)',
@ -1209,7 +1332,7 @@ class InterfaceConnectionFilterSet(ConnectionFilterSet, BaseFilterSet):
fields = [] fields = []
class PowerPanelFilterSet(BaseFilterSet): class PowerPanelFilterSet(PrimaryModelFilterSet):
q = django_filters.CharFilter( q = django_filters.CharFilter(
method='search', method='search',
label='Search', label='Search',
@ -1227,6 +1350,19 @@ class PowerPanelFilterSet(BaseFilterSet):
to_field_name='slug', to_field_name='slug',
label='Region (slug)', label='Region (slug)',
) )
site_group_id = TreeNodeMultipleChoiceFilter(
queryset=SiteGroup.objects.all(),
field_name='site__group',
lookup_expr='in',
label='Site group (ID)',
)
site_group = TreeNodeMultipleChoiceFilter(
queryset=SiteGroup.objects.all(),
field_name='site__group',
lookup_expr='in',
to_field_name='slug',
label='Site group (slug)',
)
site_id = django_filters.ModelMultipleChoiceFilter( site_id = django_filters.ModelMultipleChoiceFilter(
queryset=Site.objects.all(), queryset=Site.objects.all(),
label='Site (ID)', label='Site (ID)',
@ -1237,11 +1373,11 @@ class PowerPanelFilterSet(BaseFilterSet):
to_field_name='slug', to_field_name='slug',
label='Site name (slug)', label='Site name (slug)',
) )
rack_group_id = TreeNodeMultipleChoiceFilter( location_id = TreeNodeMultipleChoiceFilter(
queryset=RackGroup.objects.all(), queryset=Location.objects.all(),
field_name='rack_group', field_name='location',
lookup_expr='in', lookup_expr='in',
label='Rack group (ID)', label='Location (ID)',
) )
tag = TagFilter() tag = TagFilter()
@ -1258,13 +1394,7 @@ class PowerPanelFilterSet(BaseFilterSet):
return queryset.filter(qs_filter) return queryset.filter(qs_filter)
class PowerFeedFilterSet( class PowerFeedFilterSet(PrimaryModelFilterSet, CableTerminationFilterSet, PathEndpointFilterSet):
BaseFilterSet,
CableTerminationFilterSet,
PathEndpointFilterSet,
CustomFieldModelFilterSet,
CreatedUpdatedFilterSet
):
q = django_filters.CharFilter( q = django_filters.CharFilter(
method='search', method='search',
label='Search', label='Search',
@ -1282,6 +1412,19 @@ class PowerFeedFilterSet(
to_field_name='slug', to_field_name='slug',
label='Region (slug)', label='Region (slug)',
) )
site_group_id = TreeNodeMultipleChoiceFilter(
queryset=SiteGroup.objects.all(),
field_name='power_panel__site__group',
lookup_expr='in',
label='Site group (ID)',
)
site_group = TreeNodeMultipleChoiceFilter(
queryset=SiteGroup.objects.all(),
field_name='power_panel__site__group',
lookup_expr='in',
to_field_name='slug',
label='Site group (slug)',
)
site_id = django_filters.ModelMultipleChoiceFilter( site_id = django_filters.ModelMultipleChoiceFilter(
field_name='power_panel__site', field_name='power_panel__site',
queryset=Site.objects.all(), queryset=Site.objects.all(),
@ -1302,6 +1445,10 @@ class PowerFeedFilterSet(
queryset=Rack.objects.all(), queryset=Rack.objects.all(),
label='Rack (ID)', label='Rack (ID)',
) )
status = django_filters.MultipleChoiceFilter(
choices=PowerFeedStatusChoices,
null_value=None
)
tag = TagFilter() tag = TagFilter()
class Meta: class Meta:

Some files were not shown because too many files have changed in this diff Show More