mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-16 12:12:53 -06:00
commit
41d653738a
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@ -14,7 +14,7 @@ body:
|
|||||||
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: v3.3.1
|
placeholder: v3.3.2
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
|
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
@ -14,7 +14,7 @@ body:
|
|||||||
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: v3.3.1
|
placeholder: v3.3.2
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
|
19
.github/PULL_REQUEST_TEMPLATE.md
vendored
19
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -1,15 +1,16 @@
|
|||||||
<!--
|
<!--
|
||||||
Thank you for your interest in contributing to NetBox! Please note
|
Thank you for your interest in contributing to NetBox! Please note that
|
||||||
that our contribution policy requires that a feature request or bug
|
our contribution policy requires that a feature request or bug report be
|
||||||
report be opened for approval prior to filing a pull request. This
|
approved and assigned prior to filing a pull request. This helps avoid
|
||||||
helps avoid wasting time and effort on something that we might not
|
wasting time and effort on something that we might not be able to accept.
|
||||||
be able to accept.
|
|
||||||
|
|
||||||
Please indicate the relevant feature request or bug report below.
|
IF YOUR PULL REQUEST DOES NOT REFERENCE AN ISSUE WHICH HAS BEEN ASSIGNED
|
||||||
IF YOUR PULL REQUEST DOES NOT REFERENCE AN ACCEPTED BUG REPORT OR
|
TO YOU, IT WE BE CLOSED AUTOMATICALLY.
|
||||||
FEATURE REQUEST, IT WILL BE MARKED AS INVALID AND CLOSED.
|
|
||||||
|
Specify your assigned issue number on the line below.
|
||||||
-->
|
-->
|
||||||
### Fixes: <ISSUE NUMBER GOES HERE>
|
### Fixes: #1234
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
Please include a summary of the proposed changes below.
|
Please include a summary of the proposed changes below.
|
||||||
-->
|
-->
|
||||||
|
@ -102,23 +102,28 @@ appropriate labels will be applied for categorization.
|
|||||||
[getting started](https://docs.netbox.dev/en/stable/development/getting-started/)
|
[getting started](https://docs.netbox.dev/en/stable/development/getting-started/)
|
||||||
documentation for tips on setting up your development environment.
|
documentation for tips on setting up your development environment.
|
||||||
|
|
||||||
* Be sure to open an issue **before** starting work on a pull request, and
|
* Be sure to open an issue and wait for it to be assigned to you **before**
|
||||||
discuss your idea with the NetBox maintainers before beginning work. This will
|
starting work on a pull request, and discuss your idea with the NetBox
|
||||||
help prevent wasting time on something that might we might not be able to
|
maintainers before beginning work. This will help prevent wasting time on
|
||||||
implement. When suggesting a new feature, also make sure it won't conflict with
|
proposed changes that we might not be able to accept. When suggesting a new
|
||||||
any work that's already in progress.
|
feature, also make sure it won't conflict with any work that's already in
|
||||||
|
progress.
|
||||||
|
|
||||||
* Once you've opened or identified an issue you'd like to work on, ask that it
|
* Once you've opened or identified an issue you'd like to work on, ask that it
|
||||||
be assigned to you so that others are aware it's being worked on. A maintainer
|
be assigned to you so that others are aware it's being worked on. If it meets
|
||||||
will then mark the issue as "accepted."
|
the acceptance criteria, a maintainer will then mark the issue as "accepted"
|
||||||
|
and assign it to you. (Note that GitHub requires that a user first comment on
|
||||||
|
an issue before it can be assigned to that user.)
|
||||||
|
|
||||||
* Any pull request which does _not_ relate to an **accepted** issue will be closed.
|
* Any pull request which does not relate to an **assigned** issue will be
|
||||||
|
closed.
|
||||||
|
|
||||||
* All new functionality must include relevant tests where applicable.
|
* All new functionality must include relevant tests where applicable.
|
||||||
|
|
||||||
* When submitting a pull request, please be sure to work off of the `develop`
|
* When submitting a pull request, please be sure to work off of the `develop`
|
||||||
branch, rather than `master`. The `develop` branch is used for ongoing
|
branch, rather than `master`. The `develop` branch is used for ongoing
|
||||||
development, while `master` is used for tagging stable releases.
|
development, while `master` is used for tagging stable releases. (If you're
|
||||||
|
developing for the next minor release, use `feature` instead.)
|
||||||
|
|
||||||
* In most cases, it is not necessary to add a changelog entry: A maintainer will
|
* In most cases, it is not necessary to add a changelog entry: A maintainer will
|
||||||
take care of this when the PR is merged. (This helps avoid merge conflicts
|
take care of this when the PR is merged. (This helps avoid merge conflicts
|
||||||
@ -136,8 +141,10 @@ these checks):
|
|||||||
|
|
||||||
Only comment on an issue if you are sharing a relevant idea or constructive
|
Only comment on an issue if you are sharing a relevant idea or constructive
|
||||||
feedback. **Do not** comment on an issue just to show your support (give the
|
feedback. **Do not** comment on an issue just to show your support (give the
|
||||||
top post a :+1: instead) or ask for an ETA. These comments will be deleted to
|
top post a :+1: instead) or to ask for an update. Doing so generates
|
||||||
reduce noise in the discussion.
|
unnecessary noise in the discussion, and is especially annoying for people who
|
||||||
|
have subscribed to updates for the issue. Any comments without substance
|
||||||
|
relevant to the discussion will be deleted.
|
||||||
|
|
||||||
## Issue Lifecycle
|
## Issue Lifecycle
|
||||||
|
|
||||||
|
25
README.md
25
README.md
@ -4,12 +4,14 @@
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
NetBox is an infrastructure resource modeling (IRM) tool designed to empower
|
NetBox is the leading solution for modeling and documenting modern networks. By
|
||||||
network automation, used by thousands of organizations around the world.
|
combining the traditional disciplines of IP address management (IPAM) and
|
||||||
Initially conceived by the network engineering team at
|
datacenter infrastructure management (DCIM) with powerful APIs and extensions,
|
||||||
[DigitalOcean](https://www.digitalocean.com/), NetBox was developed specifically
|
NetBox provides the ideal "source of truth" to power network automation.
|
||||||
to address the needs of network and infrastructure engineers. It is intended to
|
Available as open source software under the Apache 2.0 license, NetBox is
|
||||||
function as a domain-specific source of truth for network operations.
|
employed by thousands of organizations around the world.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
Myriad infrastructure components can be modeled in NetBox, including:
|
Myriad infrastructure components can be modeled in NetBox, including:
|
||||||
|
|
||||||
@ -21,6 +23,7 @@ Myriad infrastructure components can be modeled in NetBox, including:
|
|||||||
* Virtual machines and clusters
|
* Virtual machines and clusters
|
||||||
* IP prefixes, ranges, and addresses
|
* IP prefixes, ranges, and addresses
|
||||||
* VRFs and route targets
|
* VRFs and route targets
|
||||||
|
* L2VPN and overlays
|
||||||
* FHRP groups (VRRP, HSRP, etc.)
|
* FHRP groups (VRRP, HSRP, etc.)
|
||||||
* AS numbers
|
* AS numbers
|
||||||
* VLANs and scoped VLAN groups
|
* VLANs and scoped VLAN groups
|
||||||
@ -45,11 +48,13 @@ customized and extended through the use of:
|
|||||||
NetBox also features a complete REST API as well as a GraphQL API for easily
|
NetBox also features a complete REST API as well as a GraphQL API for easily
|
||||||
integrating with other tools and systems.
|
integrating with other tools and systems.
|
||||||
|
|
||||||
|
The complete documentation for NetBox can be found at [docs.netbox.dev](https://docs.netbox.dev/).
|
||||||
|
A public demo instance is available at [demo.netbox.dev](https://demo.netbox.dev).
|
||||||
|
|
||||||
NetBox runs as a web application atop the [Django](https://www.djangoproject.com/)
|
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 [docs.netbox.dev](https://docs.netbox.dev/). A public demo instance is available at https://demo.netbox.dev.
|
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<h4>Thank you to our sponsors!</h4>
|
<h4>Thank you to our sponsors!</h4>
|
||||||
@ -90,8 +95,6 @@ our [contributing guide](CONTRIBUTING.md) prior to beginning any work.
|
|||||||
|
|
||||||
### Screenshots
|
### Screenshots
|
||||||
|
|
||||||
")
|
|
||||||
|
|
||||||
")
|
")
|
||||||
|
|
||||||

|

|
||||||
|
388
docs/development/git-cheat-sheet.md
Normal file
388
docs/development/git-cheat-sheet.md
Normal file
@ -0,0 +1,388 @@
|
|||||||
|
# git Cheat Sheet
|
||||||
|
|
||||||
|
This cheat sheet serves as a convenient reference for NetBox contributors who already somewhat familiar with using git. For a general introduction to the tooling and workflows involved, please see GitHub's guide [Getting started with git](https://docs.github.com/en/get-started/getting-started-with-git/setting-your-username-in-git).
|
||||||
|
|
||||||
|
## Common Operations
|
||||||
|
|
||||||
|
### Clone a Repo
|
||||||
|
|
||||||
|
This copies a remote git repository (e.g. from GitHub) to your local workstation. It will create a new directory bearing the repo's name in the current path.
|
||||||
|
|
||||||
|
``` title="Command"
|
||||||
|
git clone https://github.com/$org-name/$repo-name
|
||||||
|
```
|
||||||
|
|
||||||
|
``` title="Example"
|
||||||
|
$ git clone https://github.com/netbox-community/netbox
|
||||||
|
Cloning into 'netbox'...
|
||||||
|
remote: Enumerating objects: 95112, done.
|
||||||
|
remote: Counting objects: 100% (682/682), done.
|
||||||
|
remote: Compressing objects: 100% (246/246), done.
|
||||||
|
remote: Total 95112 (delta 448), reused 637 (delta 436), pack-reused 94430
|
||||||
|
Receiving objects: 100% (95112/95112), 60.40 MiB | 45.82 MiB/s, done.
|
||||||
|
Resolving deltas: 100% (74979/74979), done.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pull New Commits
|
||||||
|
|
||||||
|
To update your local branch with any recent upstream commits, run `git pull`.
|
||||||
|
|
||||||
|
``` title="Command"
|
||||||
|
git pull
|
||||||
|
```
|
||||||
|
|
||||||
|
``` title="Example"
|
||||||
|
$ git pull
|
||||||
|
remote: Enumerating objects: 1, done.
|
||||||
|
remote: Counting objects: 100% (1/1), done.
|
||||||
|
remote: Total 1 (delta 0), reused 0 (delta 0), pack-reused 0
|
||||||
|
Unpacking objects: 100% (1/1), done.
|
||||||
|
From https://github.com/netbox-community/netbox
|
||||||
|
28bc76695..e0741cc9a develop -> origin/develop
|
||||||
|
Updating 28bc76695..e0741cc9a
|
||||||
|
Fast-forward
|
||||||
|
docs/release-notes/version-3.3.md | 1 +
|
||||||
|
netbox/netbox/settings.py | 1 +
|
||||||
|
2 files changed, 2 insertions(+)
|
||||||
|
```
|
||||||
|
|
||||||
|
### List Branches
|
||||||
|
|
||||||
|
`git branch` lists all local branches. Appending `-a` to this command will list both local (green) and remote (red) branches.
|
||||||
|
|
||||||
|
``` title="Command"
|
||||||
|
git branch -a
|
||||||
|
```
|
||||||
|
|
||||||
|
``` title="Example"
|
||||||
|
$ git branch -a
|
||||||
|
* develop
|
||||||
|
remotes/origin/10170-changelog
|
||||||
|
remotes/origin/HEAD -> origin/develop
|
||||||
|
remotes/origin/develop
|
||||||
|
remotes/origin/feature
|
||||||
|
remotes/origin/master
|
||||||
|
```
|
||||||
|
|
||||||
|
### Switch Branches
|
||||||
|
|
||||||
|
To switch to a different branch, use the `checkout` command.
|
||||||
|
|
||||||
|
``` title="Command"
|
||||||
|
git checkout $branchname
|
||||||
|
```
|
||||||
|
|
||||||
|
``` title="Example"
|
||||||
|
$ git checkout feature
|
||||||
|
Branch 'feature' set up to track remote branch 'feature' from 'origin'.
|
||||||
|
Switched to a new branch 'feature'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Create a New Branch
|
||||||
|
|
||||||
|
Use the `-b` argument with `checkout` to create a new _local_ branch from the current branch.
|
||||||
|
|
||||||
|
``` title="Command"
|
||||||
|
git checkout -b $newbranch
|
||||||
|
```
|
||||||
|
|
||||||
|
``` title="Example"
|
||||||
|
$ git checkout -b 123-fix-foo
|
||||||
|
Switched to a new branch '123-fix-foo'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rename a Branch
|
||||||
|
|
||||||
|
To rename the current branch, use the `git branch` command with the `-m` argument (for "modify").
|
||||||
|
|
||||||
|
``` title="Command"
|
||||||
|
git branch -m $newname
|
||||||
|
```
|
||||||
|
|
||||||
|
``` title="Example"
|
||||||
|
$ git branch -m jstretch-testing
|
||||||
|
$ git branch
|
||||||
|
develop
|
||||||
|
feature
|
||||||
|
* jstretch-testing
|
||||||
|
```
|
||||||
|
|
||||||
|
### Merge a Branch
|
||||||
|
|
||||||
|
To merge one branch into another, use the `git merge` command. Start by checking out the _destination_ branch, and merge the _source_ branch into it.
|
||||||
|
|
||||||
|
``` title="Command"
|
||||||
|
git merge $sourcebranch
|
||||||
|
```
|
||||||
|
|
||||||
|
``` title="Example"
|
||||||
|
$ git checkout testing
|
||||||
|
Switched to branch 'testing'
|
||||||
|
Your branch is up to date with 'origin/testing'.
|
||||||
|
$ git merge branch2
|
||||||
|
Updating 9a12b5b5f..8ee42390b
|
||||||
|
Fast-forward
|
||||||
|
newfile.py | 0
|
||||||
|
1 file changed, 0 insertions(+), 0 deletions(-)
|
||||||
|
create mode 100644 newfile.py
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! warning "Avoid Merging Remote Branches"
|
||||||
|
You generally want to avoid merging branches that exist on the remote (upstream) repository, such as `develop` and `feature`: Merges into these branches should be done via a pull request on GitHub. Only merge branches when it is necessary to consolidate work you've done locally.
|
||||||
|
|
||||||
|
### Show Pending Changes
|
||||||
|
|
||||||
|
After making changes to files in the repo, `git status` will display a summary of created, modified, and deleted files.
|
||||||
|
|
||||||
|
``` title="Command"
|
||||||
|
git status
|
||||||
|
```
|
||||||
|
|
||||||
|
``` title="Example"
|
||||||
|
$ git status
|
||||||
|
On branch 123-fix-foo
|
||||||
|
Changes not staged for commit:
|
||||||
|
(use "git add <file>..." to update what will be committed)
|
||||||
|
(use "git checkout -- <file>..." to discard changes in working directory)
|
||||||
|
|
||||||
|
modified: README.md
|
||||||
|
|
||||||
|
Untracked files:
|
||||||
|
(use "git add <file>..." to include in what will be committed)
|
||||||
|
|
||||||
|
foo.py
|
||||||
|
|
||||||
|
no changes added to commit (use "git add" and/or "git commit -a")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Stage Changed Files
|
||||||
|
|
||||||
|
Before creating a new commit, modified files must be staged. This is typically done with the `git add` command. You can specify a particular path, or just append `-A` to automatically staged _all_ changed files within the current directory. Run `git status` again to verify what files have been staged.
|
||||||
|
|
||||||
|
``` title="Command"
|
||||||
|
git add -A
|
||||||
|
```
|
||||||
|
|
||||||
|
``` title="Example"
|
||||||
|
$ git add -A
|
||||||
|
$ git status
|
||||||
|
On branch 123-fix-foo
|
||||||
|
Changes to be committed:
|
||||||
|
(use "git reset HEAD <file>..." to unstage)
|
||||||
|
|
||||||
|
modified: README.md
|
||||||
|
new file: foo.py
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### Review Staged Files
|
||||||
|
|
||||||
|
It's a good idea to thoroughly review all staged changes immediately prior to creating a new commit. This can be done using the `git diff` command. Appending the `--staged` argument will show staged changes; omitting it will show changes that have not yet been staged.
|
||||||
|
|
||||||
|
``` title="Command"
|
||||||
|
git diff --staged
|
||||||
|
```
|
||||||
|
|
||||||
|
``` title="Example"
|
||||||
|
$ git diff --staged
|
||||||
|
diff --git a/README.md b/README.md
|
||||||
|
index 93e125079..4344fb514 100644
|
||||||
|
--- a/README.md
|
||||||
|
+++ b/README.md
|
||||||
|
@@ -1,3 +1,8 @@
|
||||||
|
+
|
||||||
|
+Added some lines here
|
||||||
|
+and here
|
||||||
|
+and here too
|
||||||
|
+
|
||||||
|
<div align="center">
|
||||||
|
<img src="https://raw.githubusercontent.com/netbox-community/netbox/develop/docs/netbox_logo.svg" width="400" alt="NetBox logo" />
|
||||||
|
</div>
|
||||||
|
diff --git a/foo.py b/foo.py
|
||||||
|
new file mode 100644
|
||||||
|
index 000000000..e69de29bb
|
||||||
|
```
|
||||||
|
|
||||||
|
### Create a New Commit
|
||||||
|
|
||||||
|
The `git commit` command records your changes to the current branch. Specify a commit message with the `-m` argument. (If omitted, a file editor will be opened to provide a message.
|
||||||
|
|
||||||
|
``` title="Command"
|
||||||
|
git commit -m "Fixes #123: Fixed the thing that was broken"
|
||||||
|
```
|
||||||
|
|
||||||
|
``` title="Example"
|
||||||
|
$ git commit -m "Fixes #123: Fixed the thing that was broken"
|
||||||
|
[123-fix-foo 9a12b5b5f] Fixes #123: Fixed the thing that was broken
|
||||||
|
2 files changed, 5 insertions(+)
|
||||||
|
create mode 100644 foo.py
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! tip "Automatically Closing Issues"
|
||||||
|
GitHub will [automatically close](https://github.blog/2013-01-22-closing-issues-via-commit-messages/) any issues referenced in a commit message by `Fixes:` or `Closes:` when the commit is merged into the repository's default branch. Contributors are strongly encouraged to follow this convention when forming commit messages. (Use "Closes" for feature requests and "Fixes" for bugs.)
|
||||||
|
|
||||||
|
### Push a Commit Upstream
|
||||||
|
|
||||||
|
Once you've made a commit locally, it needs to be pushed upstream to the _remote_ repository (typically called "origin"). This is done with the `git push` command. If this is a new branch that doesn't yet exist on the remote repository, you'll need to set the upstream for it when pushing.
|
||||||
|
|
||||||
|
``` title="Command"
|
||||||
|
git push -u origin $branchname
|
||||||
|
```
|
||||||
|
|
||||||
|
``` title="Example"
|
||||||
|
$ git push -u origin testing
|
||||||
|
Counting objects: 3, done.
|
||||||
|
Delta compression using up to 16 threads.
|
||||||
|
Compressing objects: 100% (3/3), done.
|
||||||
|
Writing objects: 100% (3/3), 377 bytes | 377.00 KiB/s, done.
|
||||||
|
Total 3 (delta 2), reused 0 (delta 0)
|
||||||
|
remote: Resolving deltas: 100% (2/2), completed with 2 local objects.
|
||||||
|
remote:
|
||||||
|
remote: Create a pull request for 'testing' on GitHub by visiting:
|
||||||
|
remote: https://github.com/netbox-community/netbox/pull/new/testing
|
||||||
|
remote:
|
||||||
|
To https://github.com/netbox-community/netbox
|
||||||
|
* [new branch] testing -> testing
|
||||||
|
Branch 'testing' set up to track remote branch 'testing' from 'origin'.
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! tip
|
||||||
|
You can apply the following git configuration to automatically set the upstream for all new branches. This obviates the need to specify `-u origin`.
|
||||||
|
|
||||||
|
```
|
||||||
|
git config --global push.default current
|
||||||
|
```
|
||||||
|
|
||||||
|
## The GitHub CLI Client
|
||||||
|
|
||||||
|
GitHub provides a [free CLI client](https://cli.github.com/) to simplify many aspects of interacting with GitHub repositories. Note that this utility is separate from `git`, and must be [installed separately](https://github.com/cli/cli#installation).
|
||||||
|
|
||||||
|
This guide provides some examples of common operations, but be sure to check out the [GitHub CLI manual](https://cli.github.com/manual/) for a complete accounting of available commands.
|
||||||
|
|
||||||
|
### List Open Pull Requests
|
||||||
|
|
||||||
|
``` title="Command"
|
||||||
|
gh pr list
|
||||||
|
```
|
||||||
|
|
||||||
|
``` title="Example"
|
||||||
|
$ gh pr list
|
||||||
|
|
||||||
|
Showing 3 of 3 open pull requests in netbox-community/netbox
|
||||||
|
|
||||||
|
#10223 #7503 API Bulk-Create of Devices does not check Rack-Space 7503-bulkdevice about 17 hours ago
|
||||||
|
#9716 Closes #9599: Add cursor pagination mode lyuyangh:cursor-pagination about 1 month ago
|
||||||
|
#9498 Adds replication and adoption for module import sleepinggenius2:issue_9361 about 2 months ago
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check Out a PR
|
||||||
|
|
||||||
|
This command will automatically check out the remote branch associated with an open pull request.
|
||||||
|
|
||||||
|
``` title="Command"
|
||||||
|
gh pr checkout $number
|
||||||
|
```
|
||||||
|
|
||||||
|
``` title="Example"
|
||||||
|
$ gh pr checkout 10223
|
||||||
|
Branch '7503-bulkdevice' set up to track remote branch '7503-bulkdevice' from 'origin'.
|
||||||
|
Switched to a new branch '7503-bulkdevice'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fixing Mistakes
|
||||||
|
|
||||||
|
### Modify the Previous Commit
|
||||||
|
|
||||||
|
Sometimes you'll find that you've overlooked a necessary change and need to commit again. If you haven't pushed your most recent commit and just need to make a small tweak or two, you can _amend_ your most recent commit instead of creating a new one.
|
||||||
|
|
||||||
|
First, stage the desired files with `git add` and verify the changes, the issue the `git commit` command with the `--amend` argument. You can also append the `--no-edit` argument if you would like to keep the previous commit message.
|
||||||
|
|
||||||
|
``` title="Command"
|
||||||
|
git commit --amend --no-edit
|
||||||
|
```
|
||||||
|
|
||||||
|
``` title="Example"
|
||||||
|
$ git add -A
|
||||||
|
$ git diff --staged
|
||||||
|
$ git commit --amend --no-edit
|
||||||
|
[testing 239b16921] Added a new file
|
||||||
|
Date: Fri Aug 26 16:30:05 2022 -0400
|
||||||
|
2 files changed, 1 insertion(+)
|
||||||
|
create mode 100644 newfile.py
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! danger "Don't Amend After Pushing"
|
||||||
|
Never amend a commit you've already pushed upstream unless you're **certain** no one else is working on the same branch. Force-pushing will overwrite the change history, which will break any commits from other contributors. When in doubt, create a new commit instead.
|
||||||
|
|
||||||
|
### Undo the Last Commit
|
||||||
|
|
||||||
|
The `git reset` command can be used to undo the most recent commit. (`HEAD~` is equivalent to `HEAD~1` and references the commit prior to the current HEAD.) After making and staging your changes, commit using `-c ORIG_HEAD` to replace the erroneous commit.
|
||||||
|
|
||||||
|
``` title="Command"
|
||||||
|
git reset HEAD~
|
||||||
|
```
|
||||||
|
|
||||||
|
``` title="Example"
|
||||||
|
$ git add -A
|
||||||
|
$ git commit -m "Erroneous commit"
|
||||||
|
[testing 09ce06736] Erroneous commit
|
||||||
|
Date: Mon Aug 29 15:20:04 2022 -0400
|
||||||
|
1 file changed, 1 insertion(+)
|
||||||
|
create mode 100644 BADCHANGE
|
||||||
|
$ git reset HEAD~
|
||||||
|
$ rm BADFILE
|
||||||
|
$ git add -A
|
||||||
|
$ git commit -m "Fixed commit"
|
||||||
|
[testing c585709f3] Fixed commit
|
||||||
|
Date: Mon Aug 29 15:22:38 2022 -0400
|
||||||
|
1 file changed, 65 insertions(+), 20 deletions(-)
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! danger "Don't Reset After Pushing"
|
||||||
|
Resetting only works until you've pushed your local changes upstream. If you've already pushed upstream, use `git revert` instead. This will create a _new_ commit that reverts the erroneous one, but ensures that the git history remains intact.
|
||||||
|
|
||||||
|
### Rebase from Upstream
|
||||||
|
|
||||||
|
If a change has been pushed to the upstream branch since you most recently pulled it, attempting to push a new local commit will fail:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ git push
|
||||||
|
To https://github.com/netbox-community/netbox.git
|
||||||
|
! [rejected] develop -> develop (fetch first)
|
||||||
|
error: failed to push some refs to 'https://github.com/netbox-community/netbox.git'
|
||||||
|
hint: Updates were rejected because the remote contains work that you do
|
||||||
|
hint: not have locally. This is usually caused by another repository pushing
|
||||||
|
hint: to the same ref. You may want to first integrate the remote changes
|
||||||
|
hint: (e.g., 'git pull ...') before pushing again.
|
||||||
|
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
|
||||||
|
```
|
||||||
|
|
||||||
|
To resolve this, first fetch the upstream branch to update your local copy, and then [rebase](https://git-scm.com/book/en/v2/Git-Branching-Rebasing) your local branch to include the new changes. Once the rebase has completed, you can push your local commits upstream.
|
||||||
|
|
||||||
|
``` title="Commands"
|
||||||
|
git fetch
|
||||||
|
git rebase origin/$branchname
|
||||||
|
```
|
||||||
|
|
||||||
|
``` title="Example"
|
||||||
|
$ git fetch
|
||||||
|
remote: Enumerating objects: 1, done.
|
||||||
|
remote: Counting objects: 100% (1/1), done.
|
||||||
|
remote: Total 1 (delta 0), reused 0 (delta 0), pack-reused 0
|
||||||
|
Unpacking objects: 100% (1/1), done.
|
||||||
|
From https://github.com/netbox-community/netbox
|
||||||
|
815b2d8a2..8c35ebbb7 develop -> origin/develop
|
||||||
|
$ git rebase origin/develop
|
||||||
|
First, rewinding head to replay your work on top of it...
|
||||||
|
Applying: Further tweaks to the PR template
|
||||||
|
Applying: Changelog for #10176, #10217
|
||||||
|
$ git push
|
||||||
|
Counting objects: 9, done.
|
||||||
|
Delta compression using up to 16 threads.
|
||||||
|
Compressing objects: 100% (9/9), done.
|
||||||
|
Writing objects: 100% (9/9), 1.02 KiB | 1.02 MiB/s, done.
|
||||||
|
Total 9 (delta 6), reused 0 (delta 0)
|
||||||
|
remote: Resolving deltas: 100% (6/6), completed with 5 local objects.
|
||||||
|
To https://github.com/netbox-community/netbox.git
|
||||||
|
8c35ebbb7..ada745324 develop -> develop
|
||||||
|
```
|
@ -4,6 +4,8 @@
|
|||||||
|
|
||||||
NetBox is the leading solution for modeling and documenting modern networks. By combining the traditional disciplines of IP address management (IPAM) and datacenter infrastructure management (DCIM) with powerful APIs and extensions, NetBox provides the ideal "source of truth" to power network automation. Read on to discover why thousands of organizations worldwide put NetBox at the heart of their infrastructure.
|
NetBox is the leading solution for modeling and documenting modern networks. By combining the traditional disciplines of IP address management (IPAM) and datacenter infrastructure management (DCIM) with powerful APIs and extensions, NetBox provides the ideal "source of truth" to power network automation. Read on to discover why thousands of organizations worldwide put NetBox at the heart of their infrastructure.
|
||||||
|
|
||||||
|
[](./media/screenshots/netbox-ui.png)
|
||||||
|
|
||||||
## :material-server-network: Built for Networks
|
## :material-server-network: Built for Networks
|
||||||
|
|
||||||
Unlike general-purpose CMDBs, NetBox has curated a data model which caters specifically to the needs of network engineers and operators. It delivers a wide assortment of object types carefully crafted to best serve the needs of infrastructure design and documentation. These cover all facets of network technology, from IP address managements to cabling to overlays and more:
|
Unlike general-purpose CMDBs, NetBox has curated a data model which caters specifically to the needs of network engineers and operators. It delivers a wide assortment of object types carefully crafted to best serve the needs of infrastructure design and documentation. These cover all facets of network technology, from IP address managements to cabling to overlays and more:
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 174 KiB |
Binary file not shown.
Before Width: | Height: | Size: 96 KiB |
BIN
docs/media/screenshots/netbox-ui.png
Normal file
BIN
docs/media/screenshots/netbox-ui.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 173 KiB |
@ -34,12 +34,12 @@ To utilize a filter set in a subclass of one of NetBox's generic views (such as
|
|||||||
```python
|
```python
|
||||||
# views.py
|
# views.py
|
||||||
from netbox.views.generic import ObjectListView
|
from netbox.views.generic import ObjectListView
|
||||||
from .filtersets import MyModelFitlerSet
|
from .filtersets import MyModelFilterSet
|
||||||
from .models import MyModel
|
from .models import MyModel
|
||||||
|
|
||||||
class MyModelListView(ObjectListView):
|
class MyModelListView(ObjectListView):
|
||||||
queryset = MyModel.objects.all()
|
queryset = MyModel.objects.all()
|
||||||
filterset = MyModelFitlerSet
|
filterset = MyModelFilterSet
|
||||||
```
|
```
|
||||||
|
|
||||||
To enable a filter set on a REST API endpoint, set the `filterset_class` attribute on the API view:
|
To enable a filter set on a REST API endpoint, set the `filterset_class` attribute on the API view:
|
||||||
|
@ -1,5 +1,39 @@
|
|||||||
# NetBox v3.3
|
# NetBox v3.3
|
||||||
|
|
||||||
|
## v3.3.2 (2022-09-02)
|
||||||
|
|
||||||
|
### Enhancements
|
||||||
|
|
||||||
|
* [#9477](https://github.com/netbox-community/netbox/issues/9477) - Enable clearing applied table column ordering
|
||||||
|
* [#10034](https://github.com/netbox-community/netbox/issues/10034) - Add L2VPN column to interface and VLAN tables
|
||||||
|
* [#10043](https://github.com/netbox-community/netbox/issues/10043) - Add support for `limit` query parameter to available VLANs API endpoint
|
||||||
|
* [#10060](https://github.com/netbox-community/netbox/issues/10060) - Add journal entries to global search
|
||||||
|
* [#10195](https://github.com/netbox-community/netbox/issues/10195) - Enable filtering of device components by rack
|
||||||
|
* [#10233](https://github.com/netbox-community/netbox/issues/10233) - Enable sorting rack elevations by facility ID
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* [#9328](https://github.com/netbox-community/netbox/issues/9328) - Hide available IPs when non-default ordering is applied
|
||||||
|
* [#9481](https://github.com/netbox-community/netbox/issues/9481) - Update child device location when parent location changes
|
||||||
|
* [#9832](https://github.com/netbox-community/netbox/issues/9832) - Improve error message when validating rack reservation units
|
||||||
|
* [#9895](https://github.com/netbox-community/netbox/issues/9895) - Various corrections to OpenAPI spec
|
||||||
|
* [#9962](https://github.com/netbox-community/netbox/issues/9962) - SSO login should respect `next` URL query parameter
|
||||||
|
* [#9963](https://github.com/netbox-community/netbox/issues/9963) - Fix support for custom `CSRF_COOKIE_NAME` value
|
||||||
|
* [#10155](https://github.com/netbox-community/netbox/issues/10155) - Fix rear port display when editing front port template for module type
|
||||||
|
* [#10156](https://github.com/netbox-community/netbox/issues/10156) - Avoid forcing SVG image links to open in a new window
|
||||||
|
* [#10161](https://github.com/netbox-community/netbox/issues/10161) - Restore "set null" option for custom fields during bulk edit
|
||||||
|
* [#10176](https://github.com/netbox-community/netbox/issues/10176) - Correct utilization display for empty racks
|
||||||
|
* [#10177](https://github.com/netbox-community/netbox/issues/10177) - Correct display of custom fields when editing VM interfaces
|
||||||
|
* [#10178](https://github.com/netbox-community/netbox/issues/10178) - Display manufacturer name alongside device type under device view
|
||||||
|
* [#10181](https://github.com/netbox-community/netbox/issues/10181) - Restore MultiPartParser (regression from #10031)
|
||||||
|
* [#10184](https://github.com/netbox-community/netbox/issues/10184) - Fix vertical alignment when displaying object attributes with buttons
|
||||||
|
* [#10208](https://github.com/netbox-community/netbox/issues/10208) - Fix permissions evaluation for interface actions dropdown menu
|
||||||
|
* [#10217](https://github.com/netbox-community/netbox/issues/10217) - Handle exception when trace splits to multiple rear ports
|
||||||
|
* [#10220](https://github.com/netbox-community/netbox/issues/10220) - Validate IP version when assigning primary IPs to a virtual machine
|
||||||
|
* [#10231](https://github.com/netbox-community/netbox/issues/10231) - Correct API schema definition for several serializer fields
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## v3.3.1 (2022-08-25)
|
## v3.3.1 (2022-08-25)
|
||||||
|
|
||||||
### Enhancements
|
### Enhancements
|
||||||
|
@ -249,6 +249,7 @@ nav:
|
|||||||
- User Preferences: 'development/user-preferences.md'
|
- User Preferences: 'development/user-preferences.md'
|
||||||
- Web UI: 'development/web-ui.md'
|
- Web UI: 'development/web-ui.md'
|
||||||
- Release Checklist: 'development/release-checklist.md'
|
- Release Checklist: 'development/release-checklist.md'
|
||||||
|
- git Cheat Sheet: 'development/git-cheat-sheet.md'
|
||||||
- Release Notes:
|
- Release Notes:
|
||||||
- Summary: 'release-notes/index.md'
|
- Summary: 'release-notes/index.md'
|
||||||
- Version 3.3: 'release-notes/version-3.3.md'
|
- Version 3.3: 'release-notes/version-3.3.md'
|
||||||
|
@ -316,6 +316,7 @@ class NestedModuleSerializer(WritableNestedSerializer):
|
|||||||
class NestedConsoleServerPortSerializer(WritableNestedSerializer):
|
class NestedConsoleServerPortSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverport-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverport-detail')
|
||||||
device = NestedDeviceSerializer(read_only=True)
|
device = NestedDeviceSerializer(read_only=True)
|
||||||
|
_occupied = serializers.BooleanField(required=False, read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.ConsoleServerPort
|
model = models.ConsoleServerPort
|
||||||
@ -325,6 +326,7 @@ class NestedConsoleServerPortSerializer(WritableNestedSerializer):
|
|||||||
class NestedConsolePortSerializer(WritableNestedSerializer):
|
class NestedConsolePortSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleport-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleport-detail')
|
||||||
device = NestedDeviceSerializer(read_only=True)
|
device = NestedDeviceSerializer(read_only=True)
|
||||||
|
_occupied = serializers.BooleanField(required=False, read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.ConsolePort
|
model = models.ConsolePort
|
||||||
@ -334,6 +336,7 @@ class NestedConsolePortSerializer(WritableNestedSerializer):
|
|||||||
class NestedPowerOutletSerializer(WritableNestedSerializer):
|
class NestedPowerOutletSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlet-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlet-detail')
|
||||||
device = NestedDeviceSerializer(read_only=True)
|
device = NestedDeviceSerializer(read_only=True)
|
||||||
|
_occupied = serializers.BooleanField(required=False, read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.PowerOutlet
|
model = models.PowerOutlet
|
||||||
@ -343,6 +346,7 @@ class NestedPowerOutletSerializer(WritableNestedSerializer):
|
|||||||
class NestedPowerPortSerializer(WritableNestedSerializer):
|
class NestedPowerPortSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerport-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerport-detail')
|
||||||
device = NestedDeviceSerializer(read_only=True)
|
device = NestedDeviceSerializer(read_only=True)
|
||||||
|
_occupied = serializers.BooleanField(required=False, read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.PowerPort
|
model = models.PowerPort
|
||||||
@ -352,6 +356,7 @@ class NestedPowerPortSerializer(WritableNestedSerializer):
|
|||||||
class NestedInterfaceSerializer(WritableNestedSerializer):
|
class NestedInterfaceSerializer(WritableNestedSerializer):
|
||||||
device = NestedDeviceSerializer(read_only=True)
|
device = NestedDeviceSerializer(read_only=True)
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail')
|
||||||
|
_occupied = serializers.BooleanField(required=False, read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Interface
|
model = models.Interface
|
||||||
@ -361,6 +366,7 @@ class NestedInterfaceSerializer(WritableNestedSerializer):
|
|||||||
class NestedRearPortSerializer(WritableNestedSerializer):
|
class NestedRearPortSerializer(WritableNestedSerializer):
|
||||||
device = NestedDeviceSerializer(read_only=True)
|
device = NestedDeviceSerializer(read_only=True)
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearport-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearport-detail')
|
||||||
|
_occupied = serializers.BooleanField(required=False, read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.RearPort
|
model = models.RearPort
|
||||||
@ -370,6 +376,7 @@ class NestedRearPortSerializer(WritableNestedSerializer):
|
|||||||
class NestedFrontPortSerializer(WritableNestedSerializer):
|
class NestedFrontPortSerializer(WritableNestedSerializer):
|
||||||
device = NestedDeviceSerializer(read_only=True)
|
device = NestedDeviceSerializer(read_only=True)
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:frontport-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:frontport-detail')
|
||||||
|
_occupied = serializers.BooleanField(required=False, read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.FrontPort
|
model = models.FrontPort
|
||||||
@ -454,6 +461,7 @@ class NestedPowerPanelSerializer(WritableNestedSerializer):
|
|||||||
|
|
||||||
class NestedPowerFeedSerializer(WritableNestedSerializer):
|
class NestedPowerFeedSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerfeed-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerfeed-detail')
|
||||||
|
_occupied = serializers.BooleanField(required=False, read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.PowerFeed
|
model = models.PowerFeed
|
||||||
|
@ -579,7 +579,7 @@ class InventoryItemTemplateSerializer(ValidatedModelSerializer):
|
|||||||
'description', 'component_type', 'component_id', 'component', 'created', 'last_updated', '_depth',
|
'description', 'component_type', 'component_id', 'component', 'created', 'last_updated', '_depth',
|
||||||
]
|
]
|
||||||
|
|
||||||
@swagger_serializer_method(serializer_or_field=serializers.DictField)
|
@swagger_serializer_method(serializer_or_field=serializers.JSONField)
|
||||||
def get_component(self, obj):
|
def get_component(self, obj):
|
||||||
if obj.component is None:
|
if obj.component is None:
|
||||||
return None
|
return None
|
||||||
@ -693,13 +693,13 @@ class DeviceWithConfigContextSerializer(DeviceSerializer):
|
|||||||
'local_context_data', 'tags', 'custom_fields', 'config_context', 'created', 'last_updated',
|
'local_context_data', 'tags', 'custom_fields', 'config_context', 'created', 'last_updated',
|
||||||
]
|
]
|
||||||
|
|
||||||
@swagger_serializer_method(serializer_or_field=serializers.DictField)
|
@swagger_serializer_method(serializer_or_field=serializers.JSONField)
|
||||||
def get_config_context(self, obj):
|
def get_config_context(self, obj):
|
||||||
return obj.get_config_context()
|
return obj.get_config_context()
|
||||||
|
|
||||||
|
|
||||||
class DeviceNAPALMSerializer(serializers.Serializer):
|
class DeviceNAPALMSerializer(serializers.Serializer):
|
||||||
method = serializers.DictField()
|
method = serializers.JSONField()
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -975,7 +975,7 @@ class InventoryItemSerializer(NetBoxModelSerializer):
|
|||||||
'custom_fields', 'created', 'last_updated', '_depth',
|
'custom_fields', 'created', 'last_updated', '_depth',
|
||||||
]
|
]
|
||||||
|
|
||||||
@swagger_serializer_method(serializer_or_field=serializers.DictField)
|
@swagger_serializer_method(serializer_or_field=serializers.JSONField)
|
||||||
def get_component(self, obj):
|
def get_component(self, obj):
|
||||||
if obj.component is None:
|
if obj.component is None:
|
||||||
return None
|
return None
|
||||||
@ -1046,7 +1046,7 @@ class CableTerminationSerializer(NetBoxModelSerializer):
|
|||||||
'id', 'url', 'display', 'cable', 'cable_end', 'termination_type', 'termination_id', 'termination'
|
'id', 'url', 'display', 'cable', 'cable_end', 'termination_type', 'termination_id', 'termination'
|
||||||
]
|
]
|
||||||
|
|
||||||
@swagger_serializer_method(serializer_or_field=serializers.DictField)
|
@swagger_serializer_method(serializer_or_field=serializers.JSONField)
|
||||||
def get_termination(self, obj):
|
def get_termination(self, obj):
|
||||||
serializer = get_serializer_for_model(obj.termination, prefix=NESTED_SERIALIZER_PREFIX)
|
serializer = get_serializer_for_model(obj.termination, prefix=NESTED_SERIALIZER_PREFIX)
|
||||||
context = {'request': self.context['request']}
|
context = {'request': self.context['request']}
|
||||||
|
@ -1084,6 +1084,17 @@ class DeviceComponentFilterSet(django_filters.FilterSet):
|
|||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Location (slug)',
|
label='Location (slug)',
|
||||||
)
|
)
|
||||||
|
rack_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
field_name='device__rack',
|
||||||
|
queryset=Rack.objects.all(),
|
||||||
|
label='Rack (ID)',
|
||||||
|
)
|
||||||
|
rack = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
field_name='device__rack__name',
|
||||||
|
queryset=Rack.objects.all(),
|
||||||
|
to_field_name='name',
|
||||||
|
label='Rack (name)',
|
||||||
|
)
|
||||||
device_id = django_filters.ModelMultipleChoiceFilter(
|
device_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=Device.objects.all(),
|
queryset=Device.objects.all(),
|
||||||
label='Device (ID)',
|
label='Device (ID)',
|
||||||
|
@ -87,6 +87,15 @@ class DeviceComponentFilterForm(NetBoxModelFilterSetForm):
|
|||||||
},
|
},
|
||||||
label=_('Location')
|
label=_('Location')
|
||||||
)
|
)
|
||||||
|
rack_id = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=Rack.objects.all(),
|
||||||
|
required=False,
|
||||||
|
query_params={
|
||||||
|
'site_id': '$site_id',
|
||||||
|
'location_id': '$location_id',
|
||||||
|
},
|
||||||
|
label=_('Rack')
|
||||||
|
)
|
||||||
virtual_chassis_id = DynamicModelMultipleChoiceField(
|
virtual_chassis_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=VirtualChassis.objects.all(),
|
queryset=VirtualChassis.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
@ -932,7 +941,7 @@ class ConsolePortFilterForm(DeviceComponentFilterForm):
|
|||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'tag')),
|
(None, ('q', 'tag')),
|
||||||
('Attributes', ('name', 'label', 'type', 'speed')),
|
('Attributes', ('name', 'label', 'type', 'speed')),
|
||||||
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id')),
|
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
|
||||||
)
|
)
|
||||||
type = MultipleChoiceField(
|
type = MultipleChoiceField(
|
||||||
choices=ConsolePortTypeChoices,
|
choices=ConsolePortTypeChoices,
|
||||||
@ -950,7 +959,7 @@ class ConsoleServerPortFilterForm(DeviceComponentFilterForm):
|
|||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'tag')),
|
(None, ('q', 'tag')),
|
||||||
('Attributes', ('name', 'label', 'type', 'speed')),
|
('Attributes', ('name', 'label', 'type', 'speed')),
|
||||||
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id')),
|
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
|
||||||
)
|
)
|
||||||
type = MultipleChoiceField(
|
type = MultipleChoiceField(
|
||||||
choices=ConsolePortTypeChoices,
|
choices=ConsolePortTypeChoices,
|
||||||
@ -968,7 +977,7 @@ class PowerPortFilterForm(DeviceComponentFilterForm):
|
|||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'tag')),
|
(None, ('q', 'tag')),
|
||||||
('Attributes', ('name', 'label', 'type')),
|
('Attributes', ('name', 'label', 'type')),
|
||||||
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id')),
|
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
|
||||||
)
|
)
|
||||||
type = MultipleChoiceField(
|
type = MultipleChoiceField(
|
||||||
choices=PowerPortTypeChoices,
|
choices=PowerPortTypeChoices,
|
||||||
@ -982,7 +991,7 @@ class PowerOutletFilterForm(DeviceComponentFilterForm):
|
|||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'tag')),
|
(None, ('q', 'tag')),
|
||||||
('Attributes', ('name', 'label', 'type')),
|
('Attributes', ('name', 'label', 'type')),
|
||||||
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id')),
|
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
|
||||||
)
|
)
|
||||||
type = MultipleChoiceField(
|
type = MultipleChoiceField(
|
||||||
choices=PowerOutletTypeChoices,
|
choices=PowerOutletTypeChoices,
|
||||||
@ -999,7 +1008,7 @@ class InterfaceFilterForm(DeviceComponentFilterForm):
|
|||||||
('Addressing', ('vrf_id', 'mac_address', 'wwn')),
|
('Addressing', ('vrf_id', 'mac_address', 'wwn')),
|
||||||
('PoE', ('poe_mode', 'poe_type')),
|
('PoE', ('poe_mode', 'poe_type')),
|
||||||
('Wireless', ('rf_role', 'rf_channel', 'rf_channel_width', 'tx_power')),
|
('Wireless', ('rf_role', 'rf_channel', 'rf_channel_width', 'tx_power')),
|
||||||
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id')),
|
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
|
||||||
)
|
)
|
||||||
kind = MultipleChoiceField(
|
kind = MultipleChoiceField(
|
||||||
choices=InterfaceKindChoices,
|
choices=InterfaceKindChoices,
|
||||||
@ -1084,7 +1093,7 @@ class FrontPortFilterForm(DeviceComponentFilterForm):
|
|||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'tag')),
|
(None, ('q', 'tag')),
|
||||||
('Attributes', ('name', 'label', 'type', 'color')),
|
('Attributes', ('name', 'label', 'type', 'color')),
|
||||||
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id')),
|
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
|
||||||
)
|
)
|
||||||
model = FrontPort
|
model = FrontPort
|
||||||
type = MultipleChoiceField(
|
type = MultipleChoiceField(
|
||||||
@ -1102,7 +1111,7 @@ class RearPortFilterForm(DeviceComponentFilterForm):
|
|||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'tag')),
|
(None, ('q', 'tag')),
|
||||||
('Attributes', ('name', 'label', 'type', 'color')),
|
('Attributes', ('name', 'label', 'type', 'color')),
|
||||||
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id')),
|
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
|
||||||
)
|
)
|
||||||
type = MultipleChoiceField(
|
type = MultipleChoiceField(
|
||||||
choices=PortTypeChoices,
|
choices=PortTypeChoices,
|
||||||
@ -1119,7 +1128,7 @@ class ModuleBayFilterForm(DeviceComponentFilterForm):
|
|||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'tag')),
|
(None, ('q', 'tag')),
|
||||||
('Attributes', ('name', 'label', 'position')),
|
('Attributes', ('name', 'label', 'position')),
|
||||||
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id')),
|
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
|
||||||
)
|
)
|
||||||
tag = TagFilterField(model)
|
tag = TagFilterField(model)
|
||||||
position = forms.CharField(
|
position = forms.CharField(
|
||||||
@ -1132,7 +1141,7 @@ class DeviceBayFilterForm(DeviceComponentFilterForm):
|
|||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'tag')),
|
(None, ('q', 'tag')),
|
||||||
('Attributes', ('name', 'label')),
|
('Attributes', ('name', 'label')),
|
||||||
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id')),
|
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
|
||||||
)
|
)
|
||||||
tag = TagFilterField(model)
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
@ -1142,7 +1151,7 @@ class InventoryItemFilterForm(DeviceComponentFilterForm):
|
|||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'tag')),
|
(None, ('q', 'tag')),
|
||||||
('Attributes', ('name', 'label', 'role_id', 'manufacturer_id', 'serial', 'asset_tag', 'discovered')),
|
('Attributes', ('name', 'label', 'role_id', 'manufacturer_id', 'serial', 'asset_tag', 'discovered')),
|
||||||
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id')),
|
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
|
||||||
)
|
)
|
||||||
role_id = DynamicModelMultipleChoiceField(
|
role_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=InventoryItemRole.objects.all(),
|
queryset=InventoryItemRole.objects.all(),
|
||||||
|
@ -1069,6 +1069,7 @@ class FrontPortTemplateForm(BootstrapMixin, forms.ModelForm):
|
|||||||
required=False,
|
required=False,
|
||||||
query_params={
|
query_params={
|
||||||
'devicetype_id': '$device_type',
|
'devicetype_id': '$device_type',
|
||||||
|
'moduletype_id': '$module_type',
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -677,6 +677,12 @@ class CablePath(models.Model):
|
|||||||
"""
|
"""
|
||||||
Return all available next segments in a split cable path.
|
Return all available next segments in a split cable path.
|
||||||
"""
|
"""
|
||||||
rearports = self.path_objects[-1]
|
nodes = self.path_objects[-1]
|
||||||
|
|
||||||
return FrontPort.objects.filter(rear_port__in=rearports)
|
# RearPort splitting to multiple FrontPorts with no stack position
|
||||||
|
if type(nodes[0]) is RearPort:
|
||||||
|
return FrontPort.objects.filter(rear_port__in=nodes)
|
||||||
|
# Cable terminating to multiple FrontPorts mapped to different
|
||||||
|
# RearPorts connected to different cables
|
||||||
|
elif type(nodes[0]) is FrontPort:
|
||||||
|
return RearPort.objects.filter(pk__in=[fp.rear_port_id for fp in nodes])
|
||||||
|
@ -168,6 +168,10 @@ class DeviceType(NetBoxModel):
|
|||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('dcim:devicetype', args=[self.pk])
|
return reverse('dcim:devicetype', args=[self.pk])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def get_full_name(self):
|
||||||
|
return f"{ self.manufacturer } { self.model }"
|
||||||
|
|
||||||
def to_yaml(self):
|
def to_yaml(self):
|
||||||
data = {
|
data = {
|
||||||
'manufacturer': self.manufacturer.name,
|
'manufacturer': self.manufacturer.name,
|
||||||
@ -864,6 +868,7 @@ class Device(NetBoxModel, ConfigContextModel):
|
|||||||
for device in devices:
|
for device in devices:
|
||||||
device.site = self.site
|
device.site = self.site
|
||||||
device.rack = self.rack
|
device.rack = self.rack
|
||||||
|
device.location = self.location
|
||||||
device.save()
|
device.save()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -350,7 +350,7 @@ class Rack(NetBoxModel):
|
|||||||
# Remove units without enough space above them to accommodate a device of the specified height
|
# Remove units without enough space above them to accommodate a device of the specified height
|
||||||
available_units = []
|
available_units = []
|
||||||
for u in units:
|
for u in units:
|
||||||
if set(drange(u, u + u_height, 0.5)).issubset(units):
|
if set(drange(u, u + decimal.Decimal(u_height), 0.5)).issubset(units):
|
||||||
available_units.append(u)
|
available_units.append(u)
|
||||||
|
|
||||||
return list(reversed(available_units))
|
return list(reversed(available_units))
|
||||||
@ -415,12 +415,13 @@ class Rack(NetBoxModel):
|
|||||||
"""
|
"""
|
||||||
# Determine unoccupied units
|
# Determine unoccupied units
|
||||||
total_units = len(list(self.units))
|
total_units = len(list(self.units))
|
||||||
available_units = self.get_available_units()
|
available_units = self.get_available_units(u_height=0.5)
|
||||||
|
|
||||||
# Remove reserved units
|
# Remove reserved units
|
||||||
for u in self.get_reserved_units():
|
for ru in self.get_reserved_units():
|
||||||
if u in available_units:
|
for u in drange(ru, ru + 1, 0.5):
|
||||||
available_units.remove(u)
|
if u in available_units:
|
||||||
|
available_units.remove(u)
|
||||||
|
|
||||||
occupied_unit_count = total_units - len(available_units)
|
occupied_unit_count = total_units - len(available_units)
|
||||||
percentage = float(occupied_unit_count) / total_units * 100
|
percentage = float(occupied_unit_count) / total_units * 100
|
||||||
|
@ -94,7 +94,7 @@ class Connector(Group):
|
|||||||
self.add(cable)
|
self.add(cable)
|
||||||
|
|
||||||
# Add link
|
# Add link
|
||||||
link = Hyperlink(href=url, target='_blank')
|
link = Hyperlink(href=url, target='_parent')
|
||||||
|
|
||||||
# Add text label(s)
|
# Add text label(s)
|
||||||
cursor = start[1]
|
cursor = start[1]
|
||||||
@ -281,7 +281,7 @@ class CableTraceSVG:
|
|||||||
self.cursor += PADDING * 2
|
self.cursor += PADDING * 2
|
||||||
|
|
||||||
# Add link
|
# Add link
|
||||||
link = Hyperlink(href=f'{self.base_url}{wirelesslink.get_absolute_url()}', target='_blank')
|
link = Hyperlink(href=f'{self.base_url}{wirelesslink.get_absolute_url()}', target='_parent')
|
||||||
|
|
||||||
# Add text label(s)
|
# Add text label(s)
|
||||||
for i, label in enumerate(labels):
|
for i, label in enumerate(labels):
|
||||||
|
@ -151,7 +151,7 @@ class RackElevationSVG:
|
|||||||
css_extra = ' shaded' if is_shaded else ''
|
css_extra = ' shaded' if is_shaded else ''
|
||||||
|
|
||||||
# Create hyperlink element
|
# Create hyperlink element
|
||||||
link = Hyperlink(href=f'{self.base_url}{device.get_absolute_url()}', target='_blank')
|
link = Hyperlink(href=f'{self.base_url}{device.get_absolute_url()}', target="_parent")
|
||||||
link.set_desc(description)
|
link.set_desc(description)
|
||||||
|
|
||||||
# Add rect element to hyperlink
|
# Add rect element to hyperlink
|
||||||
@ -235,10 +235,7 @@ class RackElevationSVG:
|
|||||||
self.margin_width,
|
self.margin_width,
|
||||||
u_height * self.unit_height
|
u_height * self.unit_height
|
||||||
)
|
)
|
||||||
link = Hyperlink(
|
link = Hyperlink(href=f'{self.base_url}{reservation.get_absolute_url()}', target='_parent')
|
||||||
href='{}{}'.format(self.base_url, reservation.get_absolute_url()),
|
|
||||||
target='_blank'
|
|
||||||
)
|
|
||||||
link.set_desc(f'Reservation #{reservation.pk}: {reservation.description}')
|
link.set_desc(f'Reservation #{reservation.pk}: {reservation.description}')
|
||||||
link.add(
|
link.add(
|
||||||
Rect(coords, size, class_='reservation')
|
Rect(coords, size, class_='reservation')
|
||||||
@ -268,7 +265,7 @@ class RackElevationSVG:
|
|||||||
y_offset + self.unit_height / 2
|
y_offset + self.unit_height / 2
|
||||||
)
|
)
|
||||||
|
|
||||||
link = Hyperlink(href=url_string.format(unit), target='_blank')
|
link = Hyperlink(href=url_string.format(unit), target='_parent')
|
||||||
link.add(Rect((x_offset, y_offset), (self.unit_width, self.unit_height), class_='slot'))
|
link.add(Rect((x_offset, y_offset), (self.unit_width, self.unit_height), class_='slot'))
|
||||||
link.add(Text('add device', insert=text_coords, class_='add-device'))
|
link.add(Text('add device', insert=text_coords, class_='add-device'))
|
||||||
|
|
||||||
|
@ -483,6 +483,12 @@ class BaseInterfaceTable(NetBoxTable):
|
|||||||
orderable=False,
|
orderable=False,
|
||||||
verbose_name='FHRP Groups'
|
verbose_name='FHRP Groups'
|
||||||
)
|
)
|
||||||
|
l2vpn = tables.Column(
|
||||||
|
accessor=tables.A('l2vpn_termination__l2vpn'),
|
||||||
|
linkify=True,
|
||||||
|
orderable=False,
|
||||||
|
verbose_name='L2VPN'
|
||||||
|
)
|
||||||
untagged_vlan = tables.Column(linkify=True)
|
untagged_vlan = tables.Column(linkify=True)
|
||||||
tagged_vlans = columns.TemplateColumn(
|
tagged_vlans = columns.TemplateColumn(
|
||||||
template_code=INTERFACE_TAGGED_VLANS,
|
template_code=INTERFACE_TAGGED_VLANS,
|
||||||
@ -520,8 +526,8 @@ class InterfaceTable(ModularDeviceComponentTable, BaseInterfaceTable, PathEndpoi
|
|||||||
'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'enabled', 'type', 'mgmt_only', 'mtu',
|
'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'enabled', 'type', 'mgmt_only', 'mtu',
|
||||||
'speed', 'duplex', 'mode', 'mac_address', 'wwn', 'poe_mode', 'poe_type', 'rf_role', 'rf_channel',
|
'speed', 'duplex', 'mode', 'mac_address', 'wwn', 'poe_mode', 'poe_type', 'rf_role', 'rf_channel',
|
||||||
'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'description', 'mark_connected', 'cable',
|
'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'description', 'mark_connected', 'cable',
|
||||||
'cable_color', 'wireless_link', 'wireless_lans', 'link_peer', 'connection', 'tags', 'vrf', 'ip_addresses',
|
'cable_color', 'wireless_link', 'wireless_lans', 'link_peer', 'connection', 'tags', 'vrf', 'l2vpn',
|
||||||
'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'created', 'last_updated',
|
'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'created', 'last_updated',
|
||||||
)
|
)
|
||||||
default_columns = ('pk', 'name', 'device', 'label', 'enabled', 'type', 'description')
|
default_columns = ('pk', 'name', 'device', 'label', 'enabled', 'type', 'description')
|
||||||
|
|
||||||
@ -554,8 +560,8 @@ class DeviceInterfaceTable(InterfaceTable):
|
|||||||
'pk', 'id', 'name', 'module_bay', 'module', 'label', 'enabled', 'type', 'parent', 'bridge', 'lag',
|
'pk', 'id', 'name', 'module_bay', 'module', 'label', 'enabled', 'type', 'parent', 'bridge', 'lag',
|
||||||
'mgmt_only', 'mtu', 'mode', 'mac_address', 'wwn', 'rf_role', 'rf_channel', 'rf_channel_frequency',
|
'mgmt_only', 'mtu', 'mode', 'mac_address', 'wwn', 'rf_role', 'rf_channel', 'rf_channel_frequency',
|
||||||
'rf_channel_width', 'tx_power', 'description', 'mark_connected', 'cable', 'cable_color', 'wireless_link',
|
'rf_channel_width', 'tx_power', 'description', 'mark_connected', 'cable', 'cable_color', 'wireless_link',
|
||||||
'wireless_lans', 'link_peer', 'connection', 'tags', 'ip_addresses', 'fhrp_groups', 'untagged_vlan',
|
'wireless_lans', 'link_peer', 'connection', 'tags', 'vrf', 'l2vpn', 'ip_addresses', 'fhrp_groups',
|
||||||
'tagged_vlans', 'actions',
|
'untagged_vlan', 'tagged_vlans', 'actions',
|
||||||
)
|
)
|
||||||
order_by = ('name',)
|
order_by = ('name',)
|
||||||
default_columns = (
|
default_columns = (
|
||||||
|
@ -51,7 +51,7 @@ class RackTable(TenancyColumnsMixin, NetBoxTable):
|
|||||||
status = columns.ChoiceFieldColumn()
|
status = columns.ChoiceFieldColumn()
|
||||||
role = columns.ColoredLabelColumn()
|
role = columns.ColoredLabelColumn()
|
||||||
u_height = tables.TemplateColumn(
|
u_height = tables.TemplateColumn(
|
||||||
template_code="{{ record.u_height }}U",
|
template_code="{{ value }}U",
|
||||||
verbose_name='Height'
|
verbose_name='Height'
|
||||||
)
|
)
|
||||||
comments = columns.MarkdownColumn()
|
comments = columns.MarkdownColumn()
|
||||||
|
@ -4,7 +4,7 @@ LINKTERMINATION = """
|
|||||||
<a href="{{ termination.parent_object.get_absolute_url }}">{{ termination.parent_object }}</a>
|
<a href="{{ termination.parent_object.get_absolute_url }}">{{ termination.parent_object }}</a>
|
||||||
<i class="mdi mdi-chevron-right"></i>
|
<i class="mdi mdi-chevron-right"></i>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a href="{{ termination.get_absolute_url }}">{{ termination }}</a>{% if not forloop.last %},{% endif %}
|
<a href="{{ termination.get_absolute_url }}">{{ termination }}</a>{% if not forloop.last %}<br />{% endif %}
|
||||||
{% empty %}
|
{% empty %}
|
||||||
{{ ''|placeholder }}
|
{{ ''|placeholder }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@ -33,7 +33,7 @@ DEVICEBAY_STATUS = """
|
|||||||
|
|
||||||
INTERFACE_IPADDRESSES = """
|
INTERFACE_IPADDRESSES = """
|
||||||
<div class="table-badge-group">
|
<div class="table-badge-group">
|
||||||
{% for ip in record.ip_addresses.all %}
|
{% for ip in value.all %}
|
||||||
{% if ip.status != 'active' %}
|
{% if ip.status != 'active' %}
|
||||||
<a href="{{ ip.get_absolute_url }}" class="table-badge badge bg-{{ ip.get_status_color }}" data-bs-toggle="tooltip" data-bs-placement="left" title="{{ ip.get_status_display }}">{{ ip }}</a>
|
<a href="{{ ip.get_absolute_url }}" class="table-badge badge bg-{{ ip.get_status_color }}" data-bs-toggle="tooltip" data-bs-placement="left" title="{{ ip.get_status_display }}">{{ ip }}</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
@ -53,7 +53,7 @@ INTERFACE_FHRPGROUPS = """
|
|||||||
|
|
||||||
INTERFACE_TAGGED_VLANS = """
|
INTERFACE_TAGGED_VLANS = """
|
||||||
{% if record.mode == 'tagged' %}
|
{% if record.mode == 'tagged' %}
|
||||||
{% for vlan in record.tagged_vlans.all %}
|
{% for vlan in value.all %}
|
||||||
<a href="{{ vlan.get_absolute_url }}">{{ vlan }}</a><br />
|
<a href="{{ vlan.get_absolute_url }}">{{ vlan }}</a><br />
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% elif record.mode == 'tagged-all' %}
|
{% elif record.mode == 'tagged-all' %}
|
||||||
@ -62,7 +62,7 @@ INTERFACE_TAGGED_VLANS = """
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
INTERFACE_WIRELESS_LANS = """
|
INTERFACE_WIRELESS_LANS = """
|
||||||
{% for wlan in record.wireless_lans.all %}
|
{% for wlan in value.all %}
|
||||||
<a href="{{ wlan.get_absolute_url }}">{{ wlan }}</a><br />
|
<a href="{{ wlan.get_absolute_url }}">{{ wlan }}</a><br />
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
"""
|
"""
|
||||||
@ -226,7 +226,7 @@ POWEROUTLET_BUTTONS = """
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
INTERFACE_BUTTONS = """
|
INTERFACE_BUTTONS = """
|
||||||
{% if perms.dcim.edit_interface %}
|
{% if perms.dcim.change_interface %}
|
||||||
<span class="dropdown">
|
<span class="dropdown">
|
||||||
<button type="button" class="btn btn-primary btn-sm dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false" title="Add">
|
<button type="button" class="btn btn-primary btn-sm dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false" title="Add">
|
||||||
<span class="mdi mdi-plus-thick" aria-hidden="true"></span>
|
<span class="mdi mdi-plus-thick" aria-hidden="true"></span>
|
||||||
|
@ -1924,10 +1924,17 @@ class ConsolePortTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
for location in locations:
|
for location in locations:
|
||||||
location.save()
|
location.save()
|
||||||
|
|
||||||
|
racks = (
|
||||||
|
Rack(name='Rack 1', site=sites[0]),
|
||||||
|
Rack(name='Rack 2', site=sites[1]),
|
||||||
|
Rack(name='Rack 3', site=sites[2]),
|
||||||
|
)
|
||||||
|
Rack.objects.bulk_create(racks)
|
||||||
|
|
||||||
devices = (
|
devices = (
|
||||||
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0]),
|
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0], rack=racks[0]),
|
||||||
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1]),
|
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1], rack=racks[1]),
|
||||||
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2]),
|
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2], rack=racks[2]),
|
||||||
Device(name=None, device_type=device_type, device_role=device_role, site=sites[3]), # For cable connections
|
Device(name=None, device_type=device_type, device_role=device_role, site=sites[3]), # For cable connections
|
||||||
)
|
)
|
||||||
Device.objects.bulk_create(devices)
|
Device.objects.bulk_create(devices)
|
||||||
@ -2003,6 +2010,20 @@ class ConsolePortTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
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(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_location(self):
|
||||||
|
locations = Location.objects.all()[:2]
|
||||||
|
params = {'location_id': [locations[0].pk, locations[1].pk]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
params = {'location': [locations[0].slug, locations[1].slug]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_rack(self):
|
||||||
|
racks = Rack.objects.all()[:2]
|
||||||
|
params = {'rack_id': [racks[0].pk, racks[1].pk]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
params = {'rack': [racks[0].name, racks[1].name]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
def test_device(self):
|
def test_device(self):
|
||||||
devices = Device.objects.all()[:2]
|
devices = Device.objects.all()[:2]
|
||||||
params = {'device_id': [devices[0].pk, devices[1].pk]}
|
params = {'device_id': [devices[0].pk, devices[1].pk]}
|
||||||
@ -2015,13 +2036,6 @@ class ConsolePortTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
params = {'module_id': [modules[0].pk, modules[1].pk]}
|
params = {'module_id': [modules[0].pk, modules[1].pk]}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
def test_location(self):
|
|
||||||
locations = Location.objects.all()[:2]
|
|
||||||
params = {'location_id': [locations[0].pk, locations[1].pk]}
|
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
|
||||||
params = {'location': [locations[0].slug, locations[1].slug]}
|
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
|
||||||
|
|
||||||
def test_cabled(self):
|
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)
|
||||||
@ -2071,10 +2085,17 @@ class ConsoleServerPortTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
for location in locations:
|
for location in locations:
|
||||||
location.save()
|
location.save()
|
||||||
|
|
||||||
|
racks = (
|
||||||
|
Rack(name='Rack 1', site=sites[0]),
|
||||||
|
Rack(name='Rack 2', site=sites[1]),
|
||||||
|
Rack(name='Rack 3', site=sites[2]),
|
||||||
|
)
|
||||||
|
Rack.objects.bulk_create(racks)
|
||||||
|
|
||||||
devices = (
|
devices = (
|
||||||
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0]),
|
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0], rack=racks[0]),
|
||||||
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1]),
|
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1], rack=racks[1]),
|
||||||
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2]),
|
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2], rack=racks[2]),
|
||||||
Device(name=None, device_type=device_type, device_role=device_role, site=sites[3]), # For cable connections
|
Device(name=None, device_type=device_type, device_role=device_role, site=sites[3]), # For cable connections
|
||||||
)
|
)
|
||||||
Device.objects.bulk_create(devices)
|
Device.objects.bulk_create(devices)
|
||||||
@ -2157,6 +2178,13 @@ class ConsoleServerPortTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
params = {'location': [locations[0].slug, locations[1].slug]}
|
params = {'location': [locations[0].slug, locations[1].slug]}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_rack(self):
|
||||||
|
racks = Rack.objects.all()[:2]
|
||||||
|
params = {'rack_id': [racks[0].pk, racks[1].pk]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
params = {'rack': [racks[0].name, racks[1].name]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
def test_device(self):
|
def test_device(self):
|
||||||
devices = Device.objects.all()[:2]
|
devices = Device.objects.all()[:2]
|
||||||
params = {'device_id': [devices[0].pk, devices[1].pk]}
|
params = {'device_id': [devices[0].pk, devices[1].pk]}
|
||||||
@ -2218,10 +2246,17 @@ class PowerPortTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
for location in locations:
|
for location in locations:
|
||||||
location.save()
|
location.save()
|
||||||
|
|
||||||
|
racks = (
|
||||||
|
Rack(name='Rack 1', site=sites[0]),
|
||||||
|
Rack(name='Rack 2', site=sites[1]),
|
||||||
|
Rack(name='Rack 3', site=sites[2]),
|
||||||
|
)
|
||||||
|
Rack.objects.bulk_create(racks)
|
||||||
|
|
||||||
devices = (
|
devices = (
|
||||||
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0]),
|
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0], rack=racks[0]),
|
||||||
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1]),
|
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1], rack=racks[1]),
|
||||||
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2]),
|
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2], rack=racks[2]),
|
||||||
Device(name=None, device_type=device_type, device_role=device_role, site=sites[3]), # For cable connections
|
Device(name=None, device_type=device_type, device_role=device_role, site=sites[3]), # For cable connections
|
||||||
)
|
)
|
||||||
Device.objects.bulk_create(devices)
|
Device.objects.bulk_create(devices)
|
||||||
@ -2312,6 +2347,13 @@ class PowerPortTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
params = {'location': [locations[0].slug, locations[1].slug]}
|
params = {'location': [locations[0].slug, locations[1].slug]}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_rack(self):
|
||||||
|
racks = Rack.objects.all()[:2]
|
||||||
|
params = {'rack_id': [racks[0].pk, racks[1].pk]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
params = {'rack': [racks[0].name, racks[1].name]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
def test_device(self):
|
def test_device(self):
|
||||||
devices = Device.objects.all()[:2]
|
devices = Device.objects.all()[:2]
|
||||||
params = {'device_id': [devices[0].pk, devices[1].pk]}
|
params = {'device_id': [devices[0].pk, devices[1].pk]}
|
||||||
@ -2373,10 +2415,17 @@ class PowerOutletTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
for location in locations:
|
for location in locations:
|
||||||
location.save()
|
location.save()
|
||||||
|
|
||||||
|
racks = (
|
||||||
|
Rack(name='Rack 1', site=sites[0]),
|
||||||
|
Rack(name='Rack 2', site=sites[1]),
|
||||||
|
Rack(name='Rack 3', site=sites[2]),
|
||||||
|
)
|
||||||
|
Rack.objects.bulk_create(racks)
|
||||||
|
|
||||||
devices = (
|
devices = (
|
||||||
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0]),
|
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0], rack=racks[0]),
|
||||||
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1]),
|
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1], rack=racks[1]),
|
||||||
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2]),
|
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2], rack=racks[2]),
|
||||||
Device(name=None, device_type=device_type, device_role=device_role, site=sites[3]), # For cable connections
|
Device(name=None, device_type=device_type, device_role=device_role, site=sites[3]), # For cable connections
|
||||||
)
|
)
|
||||||
Device.objects.bulk_create(devices)
|
Device.objects.bulk_create(devices)
|
||||||
@ -2463,6 +2512,13 @@ class PowerOutletTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
params = {'location': [locations[0].slug, locations[1].slug]}
|
params = {'location': [locations[0].slug, locations[1].slug]}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_rack(self):
|
||||||
|
racks = Rack.objects.all()[:2]
|
||||||
|
params = {'rack_id': [racks[0].pk, racks[1].pk]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
params = {'rack': [racks[0].name, racks[1].name]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
def test_device(self):
|
def test_device(self):
|
||||||
devices = Device.objects.all()[:2]
|
devices = Device.objects.all()[:2]
|
||||||
params = {'device_id': [devices[0].pk, devices[1].pk]}
|
params = {'device_id': [devices[0].pk, devices[1].pk]}
|
||||||
@ -2524,10 +2580,17 @@ class InterfaceTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
for location in locations:
|
for location in locations:
|
||||||
location.save()
|
location.save()
|
||||||
|
|
||||||
|
racks = (
|
||||||
|
Rack(name='Rack 1', site=sites[0]),
|
||||||
|
Rack(name='Rack 2', site=sites[1]),
|
||||||
|
Rack(name='Rack 3', site=sites[2]),
|
||||||
|
)
|
||||||
|
Rack.objects.bulk_create(racks)
|
||||||
|
|
||||||
devices = (
|
devices = (
|
||||||
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0]),
|
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0], rack=racks[0]),
|
||||||
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1]),
|
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1], rack=racks[1]),
|
||||||
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2]),
|
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2], rack=racks[2]),
|
||||||
Device(name=None, device_type=device_type, device_role=device_role, site=sites[3]), # For cable connections
|
Device(name=None, device_type=device_type, device_role=device_role, site=sites[3]), # For cable connections
|
||||||
)
|
)
|
||||||
Device.objects.bulk_create(devices)
|
Device.objects.bulk_create(devices)
|
||||||
@ -2793,6 +2856,13 @@ class InterfaceTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
params = {'location': [locations[0].slug, locations[1].slug]}
|
params = {'location': [locations[0].slug, locations[1].slug]}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_rack(self):
|
||||||
|
racks = Rack.objects.all()[:2]
|
||||||
|
params = {'rack_id': [racks[0].pk, racks[1].pk]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
params = {'rack': [racks[0].name, racks[1].name]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
def test_virtual_chassis_id(self):
|
def test_virtual_chassis_id(self):
|
||||||
params = {'virtual_chassis_id': [VirtualChassis.objects.first().pk]}
|
params = {'virtual_chassis_id': [VirtualChassis.objects.first().pk]}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
@ -2899,10 +2969,17 @@ class FrontPortTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
for location in locations:
|
for location in locations:
|
||||||
location.save()
|
location.save()
|
||||||
|
|
||||||
|
racks = (
|
||||||
|
Rack(name='Rack 1', site=sites[0]),
|
||||||
|
Rack(name='Rack 2', site=sites[1]),
|
||||||
|
Rack(name='Rack 3', site=sites[2]),
|
||||||
|
)
|
||||||
|
Rack.objects.bulk_create(racks)
|
||||||
|
|
||||||
devices = (
|
devices = (
|
||||||
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0]),
|
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0], rack=racks[0]),
|
||||||
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1]),
|
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1], rack=racks[1]),
|
||||||
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2]),
|
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2], rack=racks[2]),
|
||||||
Device(name=None, device_type=device_type, device_role=device_role, site=sites[3]), # For cable connections
|
Device(name=None, device_type=device_type, device_role=device_role, site=sites[3]), # For cable connections
|
||||||
)
|
)
|
||||||
Device.objects.bulk_create(devices)
|
Device.objects.bulk_create(devices)
|
||||||
@ -2994,6 +3071,13 @@ class FrontPortTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
params = {'location': [locations[0].slug, locations[1].slug]}
|
params = {'location': [locations[0].slug, locations[1].slug]}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_rack(self):
|
||||||
|
racks = Rack.objects.all()[:2]
|
||||||
|
params = {'rack_id': [racks[0].pk, racks[1].pk]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
params = {'rack': [racks[0].name, racks[1].name]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
def test_device(self):
|
def test_device(self):
|
||||||
devices = Device.objects.all()[:2]
|
devices = Device.objects.all()[:2]
|
||||||
params = {'device_id': [devices[0].pk, devices[1].pk]}
|
params = {'device_id': [devices[0].pk, devices[1].pk]}
|
||||||
@ -3055,10 +3139,17 @@ class RearPortTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
for location in locations:
|
for location in locations:
|
||||||
location.save()
|
location.save()
|
||||||
|
|
||||||
|
racks = (
|
||||||
|
Rack(name='Rack 1', site=sites[0]),
|
||||||
|
Rack(name='Rack 2', site=sites[1]),
|
||||||
|
Rack(name='Rack 3', site=sites[2]),
|
||||||
|
)
|
||||||
|
Rack.objects.bulk_create(racks)
|
||||||
|
|
||||||
devices = (
|
devices = (
|
||||||
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0]),
|
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0], rack=racks[0]),
|
||||||
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1]),
|
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1], rack=racks[1]),
|
||||||
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2]),
|
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2], rack=racks[2]),
|
||||||
Device(name=None, device_type=device_type, device_role=device_role, site=sites[3]), # For cable connections
|
Device(name=None, device_type=device_type, device_role=device_role, site=sites[3]), # For cable connections
|
||||||
)
|
)
|
||||||
Device.objects.bulk_create(devices)
|
Device.objects.bulk_create(devices)
|
||||||
@ -3144,6 +3235,13 @@ class RearPortTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
params = {'location': [locations[0].slug, locations[1].slug]}
|
params = {'location': [locations[0].slug, locations[1].slug]}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_rack(self):
|
||||||
|
racks = Rack.objects.all()[:2]
|
||||||
|
params = {'rack_id': [racks[0].pk, racks[1].pk]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
params = {'rack': [racks[0].name, racks[1].name]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
def test_device(self):
|
def test_device(self):
|
||||||
devices = Device.objects.all()[:2]
|
devices = Device.objects.all()[:2]
|
||||||
params = {'device_id': [devices[0].pk, devices[1].pk]}
|
params = {'device_id': [devices[0].pk, devices[1].pk]}
|
||||||
@ -3204,10 +3302,17 @@ class ModuleBayTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
for location in locations:
|
for location in locations:
|
||||||
location.save()
|
location.save()
|
||||||
|
|
||||||
|
racks = (
|
||||||
|
Rack(name='Rack 1', site=sites[0]),
|
||||||
|
Rack(name='Rack 2', site=sites[1]),
|
||||||
|
Rack(name='Rack 3', site=sites[2]),
|
||||||
|
)
|
||||||
|
Rack.objects.bulk_create(racks)
|
||||||
|
|
||||||
devices = (
|
devices = (
|
||||||
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0]),
|
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0], rack=racks[0]),
|
||||||
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1]),
|
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1], rack=racks[1]),
|
||||||
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2]),
|
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2], rack=racks[2]),
|
||||||
)
|
)
|
||||||
Device.objects.bulk_create(devices)
|
Device.objects.bulk_create(devices)
|
||||||
|
|
||||||
@ -3258,6 +3363,13 @@ class ModuleBayTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
params = {'location': [locations[0].slug, locations[1].slug]}
|
params = {'location': [locations[0].slug, locations[1].slug]}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_rack(self):
|
||||||
|
racks = Rack.objects.all()[:2]
|
||||||
|
params = {'rack_id': [racks[0].pk, racks[1].pk]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
params = {'rack': [racks[0].name, racks[1].name]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
def test_device(self):
|
def test_device(self):
|
||||||
devices = Device.objects.all()[:2]
|
devices = Device.objects.all()[:2]
|
||||||
params = {'device_id': [devices[0].pk, devices[1].pk]}
|
params = {'device_id': [devices[0].pk, devices[1].pk]}
|
||||||
@ -3307,10 +3419,17 @@ class DeviceBayTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
for location in locations:
|
for location in locations:
|
||||||
location.save()
|
location.save()
|
||||||
|
|
||||||
|
racks = (
|
||||||
|
Rack(name='Rack 1', site=sites[0]),
|
||||||
|
Rack(name='Rack 2', site=sites[1]),
|
||||||
|
Rack(name='Rack 3', site=sites[2]),
|
||||||
|
)
|
||||||
|
Rack.objects.bulk_create(racks)
|
||||||
|
|
||||||
devices = (
|
devices = (
|
||||||
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0]),
|
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0], rack=racks[0]),
|
||||||
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1]),
|
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1], rack=racks[1]),
|
||||||
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2]),
|
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2], rack=racks[2]),
|
||||||
)
|
)
|
||||||
Device.objects.bulk_create(devices)
|
Device.objects.bulk_create(devices)
|
||||||
|
|
||||||
@ -3361,6 +3480,13 @@ class DeviceBayTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
params = {'location': [locations[0].slug, locations[1].slug]}
|
params = {'location': [locations[0].slug, locations[1].slug]}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_rack(self):
|
||||||
|
racks = Rack.objects.all()[:2]
|
||||||
|
params = {'rack_id': [racks[0].pk, racks[1].pk]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
params = {'rack': [racks[0].name, racks[1].name]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
def test_device(self):
|
def test_device(self):
|
||||||
devices = Device.objects.all()[:2]
|
devices = Device.objects.all()[:2]
|
||||||
params = {'device_id': [devices[0].pk, devices[1].pk]}
|
params = {'device_id': [devices[0].pk, devices[1].pk]}
|
||||||
@ -3416,10 +3542,17 @@ class InventoryItemTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
for location in locations:
|
for location in locations:
|
||||||
location.save()
|
location.save()
|
||||||
|
|
||||||
|
racks = (
|
||||||
|
Rack(name='Rack 1', site=sites[0]),
|
||||||
|
Rack(name='Rack 2', site=sites[1]),
|
||||||
|
Rack(name='Rack 3', site=sites[2]),
|
||||||
|
)
|
||||||
|
Rack.objects.bulk_create(racks)
|
||||||
|
|
||||||
devices = (
|
devices = (
|
||||||
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0]),
|
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], location=locations[0], rack=racks[0]),
|
||||||
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1]),
|
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[1], location=locations[1], rack=racks[1]),
|
||||||
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2]),
|
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[2], location=locations[2], rack=racks[2]),
|
||||||
)
|
)
|
||||||
Device.objects.bulk_create(devices)
|
Device.objects.bulk_create(devices)
|
||||||
|
|
||||||
@ -3503,6 +3636,13 @@ class InventoryItemTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
params = {'location': [locations[0].slug, locations[1].slug]}
|
params = {'location': [locations[0].slug, locations[1].slug]}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||||
|
|
||||||
|
def test_rack(self):
|
||||||
|
racks = Rack.objects.all()[:2]
|
||||||
|
params = {'rack_id': [racks[0].pk, racks[1].pk]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||||
|
params = {'rack': [racks[0].name, racks[1].name]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||||
|
|
||||||
def test_device(self):
|
def test_device(self):
|
||||||
devices = Device.objects.all()[:2]
|
devices = Device.objects.all()[:2]
|
||||||
params = {'device_id': [devices[0].pk, devices[1].pk]}
|
params = {'device_id': [devices[0].pk, devices[1].pk]}
|
||||||
|
@ -589,10 +589,17 @@ class RackElevationListView(generic.ObjectListView):
|
|||||||
racks = filtersets.RackFilterSet(request.GET, self.queryset).qs
|
racks = filtersets.RackFilterSet(request.GET, self.queryset).qs
|
||||||
total_count = racks.count()
|
total_count = racks.count()
|
||||||
|
|
||||||
# Determine ordering
|
ORDERING_CHOICES = {
|
||||||
reverse = bool(request.GET.get('reverse', False))
|
'name': 'Name (A-Z)',
|
||||||
if reverse:
|
'-name': 'Name (Z-A)',
|
||||||
racks = racks.reverse()
|
'facility_id': 'Facility ID (A-Z)',
|
||||||
|
'-facility_id': 'Facility ID (Z-A)',
|
||||||
|
}
|
||||||
|
sort = request.GET.get('sort', "name")
|
||||||
|
if sort not in ORDERING_CHOICES:
|
||||||
|
sort = 'name'
|
||||||
|
|
||||||
|
racks = racks.order_by(sort)
|
||||||
|
|
||||||
# Pagination
|
# Pagination
|
||||||
per_page = get_paginate_count(request)
|
per_page = get_paginate_count(request)
|
||||||
@ -614,7 +621,9 @@ class RackElevationListView(generic.ObjectListView):
|
|||||||
'paginator': paginator,
|
'paginator': paginator,
|
||||||
'page': page,
|
'page': page,
|
||||||
'total_count': total_count,
|
'total_count': total_count,
|
||||||
'reverse': reverse,
|
'sort': sort,
|
||||||
|
'sort_display_name': ORDERING_CHOICES[sort],
|
||||||
|
'sort_choices': ORDERING_CHOICES,
|
||||||
'rack_face': rack_face,
|
'rack_face': rack_face,
|
||||||
'filter_form': forms.RackElevationFilterForm(request.GET),
|
'filter_form': forms.RackElevationFilterForm(request.GET),
|
||||||
})
|
})
|
||||||
|
@ -192,7 +192,7 @@ class ImageAttachmentSerializer(ValidatedModelSerializer):
|
|||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
@swagger_serializer_method(serializer_or_field=serializers.DictField)
|
@swagger_serializer_method(serializer_or_field=serializers.JSONField)
|
||||||
def get_parent(self, obj):
|
def get_parent(self, obj):
|
||||||
serializer = get_serializer_for_model(obj.parent, prefix=NESTED_SERIALIZER_PREFIX)
|
serializer = get_serializer_for_model(obj.parent, prefix=NESTED_SERIALIZER_PREFIX)
|
||||||
return serializer(obj.parent, context={'request': self.context['request']}).data
|
return serializer(obj.parent, context={'request': self.context['request']}).data
|
||||||
@ -242,7 +242,7 @@ class JournalEntrySerializer(NetBoxModelSerializer):
|
|||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
@swagger_serializer_method(serializer_or_field=serializers.DictField)
|
@swagger_serializer_method(serializer_or_field=serializers.JSONField)
|
||||||
def get_assigned_object(self, instance):
|
def get_assigned_object(self, instance):
|
||||||
serializer = get_serializer_for_model(instance.assigned_object_type.model_class(), prefix=NESTED_SERIALIZER_PREFIX)
|
serializer = get_serializer_for_model(instance.assigned_object_type.model_class(), prefix=NESTED_SERIALIZER_PREFIX)
|
||||||
context = {'request': self.context['request']}
|
context = {'request': self.context['request']}
|
||||||
@ -403,6 +403,7 @@ class ScriptSerializer(serializers.Serializer):
|
|||||||
vars = serializers.SerializerMethodField(read_only=True)
|
vars = serializers.SerializerMethodField(read_only=True)
|
||||||
result = NestedJobResultSerializer()
|
result = NestedJobResultSerializer()
|
||||||
|
|
||||||
|
@swagger_serializer_method(serializer_or_field=serializers.JSONField)
|
||||||
def get_vars(self, instance):
|
def get_vars(self, instance):
|
||||||
return {
|
return {
|
||||||
k: v.__class__.__name__ for k, v in instance._get_vars().items()
|
k: v.__class__.__name__ for k, v in instance._get_vars().items()
|
||||||
@ -461,7 +462,7 @@ class ObjectChangeSerializer(BaseModelSerializer):
|
|||||||
'changed_object_id', 'changed_object', 'prechange_data', 'postchange_data',
|
'changed_object_id', 'changed_object', 'prechange_data', 'postchange_data',
|
||||||
]
|
]
|
||||||
|
|
||||||
@swagger_serializer_method(serializer_or_field=serializers.DictField)
|
@swagger_serializer_method(serializer_or_field=serializers.JSONField)
|
||||||
def get_changed_object(self, obj):
|
def get_changed_object(self, obj):
|
||||||
"""
|
"""
|
||||||
Serialize a nested representation of the changed object.
|
Serialize a nested representation of the changed object.
|
||||||
|
@ -183,6 +183,7 @@ class ObjectChangeTable(NetBoxTable):
|
|||||||
verbose_name='Username'
|
verbose_name='Username'
|
||||||
)
|
)
|
||||||
full_name = tables.TemplateColumn(
|
full_name = tables.TemplateColumn(
|
||||||
|
accessor=tables.A('user'),
|
||||||
template_code=OBJECTCHANGE_FULL_NAME,
|
template_code=OBJECTCHANGE_FULL_NAME,
|
||||||
verbose_name='Full Name',
|
verbose_name='Full Name',
|
||||||
orderable=False
|
orderable=False
|
||||||
@ -192,6 +193,7 @@ class ObjectChangeTable(NetBoxTable):
|
|||||||
verbose_name='Type'
|
verbose_name='Type'
|
||||||
)
|
)
|
||||||
object_repr = tables.TemplateColumn(
|
object_repr = tables.TemplateColumn(
|
||||||
|
accessor=tables.A('changed_object'),
|
||||||
template_code=OBJECTCHANGE_OBJECT,
|
template_code=OBJECTCHANGE_OBJECT,
|
||||||
verbose_name='Object'
|
verbose_name='Object'
|
||||||
)
|
)
|
||||||
|
@ -9,12 +9,12 @@ CONFIGCONTEXT_ACTIONS = """
|
|||||||
|
|
||||||
OBJECTCHANGE_FULL_NAME = """
|
OBJECTCHANGE_FULL_NAME = """
|
||||||
{% load helpers %}
|
{% load helpers %}
|
||||||
{{ record.user.get_full_name|placeholder }}
|
{{ value.get_full_name|placeholder }}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
OBJECTCHANGE_OBJECT = """
|
OBJECTCHANGE_OBJECT = """
|
||||||
{% if record.changed_object and record.changed_object.get_absolute_url %}
|
{% if value and value.get_absolute_url %}
|
||||||
<a href="{{ record.changed_object.get_absolute_url }}">{{ record.object_repr }}</a>
|
<a href="{{ value.get_absolute_url }}">{{ record.object_repr }}</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ record.object_repr }}
|
{{ record.object_repr }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -143,7 +143,7 @@ class FHRPGroupAssignmentSerializer(NetBoxModelSerializer):
|
|||||||
'last_updated',
|
'last_updated',
|
||||||
]
|
]
|
||||||
|
|
||||||
@swagger_serializer_method(serializer_or_field=serializers.DictField)
|
@swagger_serializer_method(serializer_or_field=serializers.JSONField)
|
||||||
def get_interface(self, obj):
|
def get_interface(self, obj):
|
||||||
if obj.interface is None:
|
if obj.interface is None:
|
||||||
return None
|
return None
|
||||||
@ -190,6 +190,7 @@ class VLANGroupSerializer(NetBoxModelSerializer):
|
|||||||
]
|
]
|
||||||
validators = []
|
validators = []
|
||||||
|
|
||||||
|
@swagger_serializer_method(serializer_or_field=serializers.JSONField)
|
||||||
def get_scope(self, obj):
|
def get_scope(self, obj):
|
||||||
if obj.scope_id is None:
|
if obj.scope_id is None:
|
||||||
return None
|
return None
|
||||||
@ -373,7 +374,7 @@ class IPAddressSerializer(NetBoxModelSerializer):
|
|||||||
'custom_fields', 'created', 'last_updated',
|
'custom_fields', 'created', 'last_updated',
|
||||||
]
|
]
|
||||||
|
|
||||||
@swagger_serializer_method(serializer_or_field=serializers.DictField)
|
@swagger_serializer_method(serializer_or_field=serializers.JSONField)
|
||||||
def get_assigned_object(self, obj):
|
def get_assigned_object(self, obj):
|
||||||
if obj.assigned_object is None:
|
if obj.assigned_object is None:
|
||||||
return None
|
return None
|
||||||
@ -482,7 +483,7 @@ class L2VPNTerminationSerializer(NetBoxModelSerializer):
|
|||||||
'assigned_object', 'tags', 'custom_fields', 'created', 'last_updated'
|
'assigned_object', 'tags', 'custom_fields', 'created', 'last_updated'
|
||||||
]
|
]
|
||||||
|
|
||||||
@swagger_serializer_method(serializer_or_field=serializers.DictField)
|
@swagger_serializer_method(serializer_or_field=serializers.JSONField)
|
||||||
def get_assigned_object(self, instance):
|
def get_assigned_object(self, instance):
|
||||||
serializer = get_serializer_for_model(instance.assigned_object, prefix=NESTED_SERIALIZER_PREFIX)
|
serializer = get_serializer_for_model(instance.assigned_object, prefix=NESTED_SERIALIZER_PREFIX)
|
||||||
context = {'request': self.context['request']}
|
context = {'request': self.context['request']}
|
||||||
|
@ -174,6 +174,21 @@ class L2VPNTerminationViewSet(NetBoxModelViewSet):
|
|||||||
# Views
|
# Views
|
||||||
#
|
#
|
||||||
|
|
||||||
|
def get_results_limit(request):
|
||||||
|
"""
|
||||||
|
Return the lesser of the specified limit (if any) and the configured MAX_PAGE_SIZE.
|
||||||
|
"""
|
||||||
|
config = get_config()
|
||||||
|
try:
|
||||||
|
limit = int(request.query_params.get('limit', config.PAGINATE_COUNT)) or config.MAX_PAGE_SIZE
|
||||||
|
except ValueError:
|
||||||
|
limit = config.PAGINATE_COUNT
|
||||||
|
if config.MAX_PAGE_SIZE:
|
||||||
|
limit = min(limit, config.MAX_PAGE_SIZE)
|
||||||
|
|
||||||
|
return limit
|
||||||
|
|
||||||
|
|
||||||
class AvailablePrefixesView(ObjectValidationMixin, APIView):
|
class AvailablePrefixesView(ObjectValidationMixin, APIView):
|
||||||
queryset = Prefix.objects.all()
|
queryset = Prefix.objects.all()
|
||||||
|
|
||||||
@ -265,16 +280,7 @@ class AvailableIPAddressesView(ObjectValidationMixin, APIView):
|
|||||||
@swagger_auto_schema(responses={200: serializers.AvailableIPSerializer(many=True)})
|
@swagger_auto_schema(responses={200: serializers.AvailableIPSerializer(many=True)})
|
||||||
def get(self, request, pk):
|
def get(self, request, pk):
|
||||||
parent = self.get_parent(request, pk)
|
parent = self.get_parent(request, pk)
|
||||||
config = get_config()
|
limit = get_results_limit(request)
|
||||||
PAGINATE_COUNT = config.PAGINATE_COUNT
|
|
||||||
MAX_PAGE_SIZE = config.MAX_PAGE_SIZE
|
|
||||||
|
|
||||||
try:
|
|
||||||
limit = int(request.query_params.get('limit', PAGINATE_COUNT))
|
|
||||||
except ValueError:
|
|
||||||
limit = PAGINATE_COUNT
|
|
||||||
if MAX_PAGE_SIZE:
|
|
||||||
limit = min(limit, MAX_PAGE_SIZE)
|
|
||||||
|
|
||||||
# Calculate available IPs within the parent
|
# Calculate available IPs within the parent
|
||||||
ip_list = []
|
ip_list = []
|
||||||
@ -357,8 +363,9 @@ class AvailableVLANsView(ObjectValidationMixin, APIView):
|
|||||||
@swagger_auto_schema(responses={200: serializers.AvailableVLANSerializer(many=True)})
|
@swagger_auto_schema(responses={200: serializers.AvailableVLANSerializer(many=True)})
|
||||||
def get(self, request, pk):
|
def get(self, request, pk):
|
||||||
vlangroup = get_object_or_404(VLANGroup.objects.restrict(request.user), pk=pk)
|
vlangroup = get_object_or_404(VLANGroup.objects.restrict(request.user), pk=pk)
|
||||||
available_vlans = vlangroup.get_available_vids()
|
limit = get_results_limit(request)
|
||||||
|
|
||||||
|
available_vlans = vlangroup.get_available_vids()[:limit]
|
||||||
serializer = serializers.AvailableVLANSerializer(available_vlans, many=True, context={
|
serializer = serializers.AvailableVLANSerializer(available_vlans, many=True, context={
|
||||||
'request': request,
|
'request': request,
|
||||||
'group': vlangroup,
|
'group': vlangroup,
|
||||||
|
@ -10,7 +10,7 @@ __all__ = (
|
|||||||
|
|
||||||
|
|
||||||
IPADDRESSES = """
|
IPADDRESSES = """
|
||||||
{% for ip in record.ip_addresses.all %}
|
{% for ip in value.all %}
|
||||||
<a href="{{ ip.get_absolute_url }}">{{ ip }}</a><br />
|
<a href="{{ ip.get_absolute_url }}">{{ ip }}</a><br />
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
"""
|
"""
|
||||||
|
@ -47,7 +47,7 @@ IPADDRESS_ASSIGN_LINK = """
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
VRF_LINK = """
|
VRF_LINK = """
|
||||||
{% if record.vrf %}
|
{% if value %}
|
||||||
<a href="{{ record.vrf.get_absolute_url }}">{{ record.vrf }}</a>
|
<a href="{{ record.vrf.get_absolute_url }}">{{ record.vrf }}</a>
|
||||||
{% elif object.vrf %}
|
{% elif object.vrf %}
|
||||||
<a href="{{ object.vrf.get_absolute_url }}">{{ object.vrf }}</a>
|
<a href="{{ object.vrf.get_absolute_url }}">{{ object.vrf }}</a>
|
||||||
|
@ -30,7 +30,7 @@ VLAN_LINK = """
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
VLAN_PREFIXES = """
|
VLAN_PREFIXES = """
|
||||||
{% for prefix in record.prefixes.all %}
|
{% for prefix in value.all %}
|
||||||
<a href="{% url 'ipam:prefix' pk=prefix.pk %}">{{ prefix }}</a>{% if not forloop.last %}<br />{% endif %}
|
<a href="{% url 'ipam:prefix' pk=prefix.pk %}">{{ prefix }}</a>{% if not forloop.last %}<br />{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
"""
|
"""
|
||||||
@ -110,6 +110,12 @@ class VLANTable(TenancyColumnsMixin, NetBoxTable):
|
|||||||
role = tables.Column(
|
role = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
|
l2vpn = tables.Column(
|
||||||
|
accessor=tables.A('l2vpn_termination__l2vpn'),
|
||||||
|
linkify=True,
|
||||||
|
orderable=False,
|
||||||
|
verbose_name='L2VPN'
|
||||||
|
)
|
||||||
prefixes = columns.TemplateColumn(
|
prefixes = columns.TemplateColumn(
|
||||||
template_code=VLAN_PREFIXES,
|
template_code=VLAN_PREFIXES,
|
||||||
orderable=False,
|
orderable=False,
|
||||||
@ -122,8 +128,8 @@ class VLANTable(TenancyColumnsMixin, NetBoxTable):
|
|||||||
class Meta(NetBoxTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = VLAN
|
model = VLAN
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'vid', 'name', 'site', 'group', 'prefixes', 'tenant', 'tenant_group', 'status', 'role', 'description', 'tags',
|
'pk', 'id', 'vid', 'name', 'site', 'group', 'prefixes', 'tenant', 'tenant_group', 'status', 'role',
|
||||||
'created', 'last_updated',
|
'description', 'tags', 'l2vpn', 'created', 'last_updated',
|
||||||
)
|
)
|
||||||
default_columns = ('pk', 'vid', 'name', 'site', 'group', 'prefixes', 'tenant', 'status', 'role', 'description')
|
default_columns = ('pk', 'vid', 'name', 'site', 'group', 'prefixes', 'tenant', 'status', 'role', 'description')
|
||||||
row_attrs = {
|
row_attrs = {
|
||||||
|
@ -699,9 +699,18 @@ class VLANGroupTest(APIViewTestCases.APIViewTestCase):
|
|||||||
"""
|
"""
|
||||||
Test retrieval of all available VLANs within a group.
|
Test retrieval of all available VLANs within a group.
|
||||||
"""
|
"""
|
||||||
self.add_permissions('ipam.view_vlangroup', 'ipam.view_vlan')
|
MIN_VID = 100
|
||||||
vlangroup = VLANGroup.objects.first()
|
MAX_VID = 199
|
||||||
|
|
||||||
|
self.add_permissions('ipam.view_vlangroup', 'ipam.view_vlan')
|
||||||
|
vlangroup = VLANGroup.objects.create(
|
||||||
|
name='VLAN Group X',
|
||||||
|
slug='vlan-group-x',
|
||||||
|
min_vid=MIN_VID,
|
||||||
|
max_vid=MAX_VID
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create a set of VLANs within the group
|
||||||
vlans = (
|
vlans = (
|
||||||
VLAN(vid=10, name='VLAN 10', group=vlangroup),
|
VLAN(vid=10, name='VLAN 10', group=vlangroup),
|
||||||
VLAN(vid=20, name='VLAN 20', group=vlangroup),
|
VLAN(vid=20, name='VLAN 20', group=vlangroup),
|
||||||
@ -711,13 +720,17 @@ class VLANGroupTest(APIViewTestCases.APIViewTestCase):
|
|||||||
|
|
||||||
# Retrieve all available VLANs
|
# Retrieve all available VLANs
|
||||||
url = reverse('ipam-api:vlangroup-available-vlans', kwargs={'pk': vlangroup.pk})
|
url = reverse('ipam-api:vlangroup-available-vlans', kwargs={'pk': vlangroup.pk})
|
||||||
response = self.client.get(url, **self.header)
|
response = self.client.get(f'{url}?limit=0', **self.header)
|
||||||
|
self.assertEqual(len(response.data), MAX_VID - MIN_VID + 1)
|
||||||
self.assertEqual(len(response.data), 4094 - len(vlans))
|
|
||||||
available_vlans = {vlan['vid'] for vlan in response.data}
|
available_vlans = {vlan['vid'] for vlan in response.data}
|
||||||
for vlan in vlans:
|
for vlan in vlans:
|
||||||
self.assertNotIn(vlan.vid, available_vlans)
|
self.assertNotIn(vlan.vid, available_vlans)
|
||||||
|
|
||||||
|
# Retrieve a maximum number of available VLANs
|
||||||
|
url = reverse('ipam-api:vlangroup-available-vlans', kwargs={'pk': vlangroup.pk})
|
||||||
|
response = self.client.get(f'{url}?limit=10', **self.header)
|
||||||
|
self.assertEqual(len(response.data), 10)
|
||||||
|
|
||||||
def test_create_single_available_vlan(self):
|
def test_create_single_available_vlan(self):
|
||||||
"""
|
"""
|
||||||
Test the creation of a single available VLAN.
|
Test the creation of a single available VLAN.
|
||||||
|
@ -526,9 +526,8 @@ class PrefixIPAddressesView(generic.ObjectChildrenView):
|
|||||||
return parent.get_child_ips().restrict(request.user, 'view').prefetch_related('vrf', 'tenant', 'tenant__group')
|
return parent.get_child_ips().restrict(request.user, 'view').prefetch_related('vrf', 'tenant', 'tenant__group')
|
||||||
|
|
||||||
def prep_table_data(self, request, queryset, parent):
|
def prep_table_data(self, request, queryset, parent):
|
||||||
if not request.GET.get('q'):
|
if not request.GET.get('q') and not request.GET.get('sort'):
|
||||||
return add_available_ipaddresses(parent.prefix, queryset, parent.is_pool)
|
return add_available_ipaddresses(parent.prefix, queryset, parent.is_pool)
|
||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
def get_extra_context(self, request, instance):
|
def get_extra_context(self, request, instance):
|
||||||
|
@ -38,7 +38,7 @@ class GenericObjectSerializer(serializers.Serializer):
|
|||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
@swagger_serializer_method(serializer_or_field=serializers.DictField)
|
@swagger_serializer_method(serializer_or_field=serializers.JSONField)
|
||||||
def get_object(self, obj):
|
def get_object(self, obj):
|
||||||
serializer = get_serializer_for_model(obj, prefix=NESTED_SERIALIZER_PREFIX)
|
serializer = get_serializer_for_model(obj, prefix=NESTED_SERIALIZER_PREFIX)
|
||||||
# context = {'request': self.context['request']}
|
# context = {'request': self.context['request']}
|
||||||
|
@ -2,6 +2,8 @@ import circuits.filtersets
|
|||||||
import circuits.tables
|
import circuits.tables
|
||||||
import dcim.filtersets
|
import dcim.filtersets
|
||||||
import dcim.tables
|
import dcim.tables
|
||||||
|
import extras.filtersets
|
||||||
|
import extras.tables
|
||||||
import ipam.filtersets
|
import ipam.filtersets
|
||||||
import ipam.tables
|
import ipam.tables
|
||||||
import tenancy.filtersets
|
import tenancy.filtersets
|
||||||
@ -15,6 +17,7 @@ from dcim.models import (
|
|||||||
Cable, Device, DeviceType, Interface, Location, Module, ModuleType, PowerFeed, Rack, RackReservation, Site,
|
Cable, Device, DeviceType, Interface, Location, Module, ModuleType, PowerFeed, Rack, RackReservation, Site,
|
||||||
VirtualChassis,
|
VirtualChassis,
|
||||||
)
|
)
|
||||||
|
from extras.models import JournalEntry
|
||||||
from ipam.models import Aggregate, ASN, IPAddress, Prefix, Service, VLAN, VRF
|
from ipam.models import Aggregate, ASN, IPAddress, Prefix, Service, VLAN, VRF
|
||||||
from tenancy.models import Contact, Tenant, ContactAssignment
|
from tenancy.models import Contact, Tenant, ContactAssignment
|
||||||
from utilities.utils import count_related
|
from utilities.utils import count_related
|
||||||
@ -238,6 +241,15 @@ WIRELESS_TYPES = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
JOURNAL_TYPES = {
|
||||||
|
'journalentry': {
|
||||||
|
'queryset': JournalEntry.objects.prefetch_related('assigned_object', 'created_by'),
|
||||||
|
'filterset': extras.filtersets.JournalEntryFilterSet,
|
||||||
|
'table': extras.tables.JournalEntryTable,
|
||||||
|
'url': 'extras:journalentry_list',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
SEARCH_TYPE_HIERARCHY = {
|
SEARCH_TYPE_HIERARCHY = {
|
||||||
'Circuits': CIRCUIT_TYPES,
|
'Circuits': CIRCUIT_TYPES,
|
||||||
'DCIM': DCIM_TYPES,
|
'DCIM': DCIM_TYPES,
|
||||||
@ -245,6 +257,7 @@ SEARCH_TYPE_HIERARCHY = {
|
|||||||
'Tenancy': TENANCY_TYPES,
|
'Tenancy': TENANCY_TYPES,
|
||||||
'Virtualization': VIRTUALIZATION_TYPES,
|
'Virtualization': VIRTUALIZATION_TYPES,
|
||||||
'Wireless': WIRELESS_TYPES,
|
'Wireless': WIRELESS_TYPES,
|
||||||
|
'Journal': JOURNAL_TYPES,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -29,7 +29,7 @@ django.utils.encoding.force_text = force_str
|
|||||||
# Environment setup
|
# Environment setup
|
||||||
#
|
#
|
||||||
|
|
||||||
VERSION = '3.3.1'
|
VERSION = '3.3.2'
|
||||||
|
|
||||||
# Hostname
|
# Hostname
|
||||||
HOSTNAME = platform.node()
|
HOSTNAME = platform.node()
|
||||||
@ -535,6 +535,7 @@ REST_FRAMEWORK = {
|
|||||||
'DEFAULT_PAGINATION_CLASS': 'netbox.api.pagination.OptionalLimitOffsetPagination',
|
'DEFAULT_PAGINATION_CLASS': 'netbox.api.pagination.OptionalLimitOffsetPagination',
|
||||||
'DEFAULT_PARSER_CLASSES': (
|
'DEFAULT_PARSER_CLASSES': (
|
||||||
'rest_framework.parsers.JSONParser',
|
'rest_framework.parsers.JSONParser',
|
||||||
|
'rest_framework.parsers.MultiPartParser',
|
||||||
),
|
),
|
||||||
'DEFAULT_PERMISSION_CLASSES': (
|
'DEFAULT_PERMISSION_CLASSES': (
|
||||||
'netbox.api.authentication.TokenPermissions',
|
'netbox.api.authentication.TokenPermissions',
|
||||||
@ -575,7 +576,6 @@ SWAGGER_SETTINGS = {
|
|||||||
'DEFAULT_AUTO_SCHEMA_CLASS': 'utilities.custom_inspectors.NetBoxSwaggerAutoSchema',
|
'DEFAULT_AUTO_SCHEMA_CLASS': 'utilities.custom_inspectors.NetBoxSwaggerAutoSchema',
|
||||||
'DEFAULT_FIELD_INSPECTORS': [
|
'DEFAULT_FIELD_INSPECTORS': [
|
||||||
'utilities.custom_inspectors.CustomFieldsDataFieldInspector',
|
'utilities.custom_inspectors.CustomFieldsDataFieldInspector',
|
||||||
'utilities.custom_inspectors.JSONFieldInspector',
|
|
||||||
'utilities.custom_inspectors.NullableBooleanFieldInspector',
|
'utilities.custom_inspectors.NullableBooleanFieldInspector',
|
||||||
'utilities.custom_inspectors.ChoiceFieldInspector',
|
'utilities.custom_inspectors.ChoiceFieldInspector',
|
||||||
'utilities.custom_inspectors.SerializedPKRelatedFieldInspector',
|
'utilities.custom_inspectors.SerializedPKRelatedFieldInspector',
|
||||||
@ -585,6 +585,7 @@ SWAGGER_SETTINGS = {
|
|||||||
'drf_yasg.inspectors.ChoiceFieldInspector',
|
'drf_yasg.inspectors.ChoiceFieldInspector',
|
||||||
'drf_yasg.inspectors.FileFieldInspector',
|
'drf_yasg.inspectors.FileFieldInspector',
|
||||||
'drf_yasg.inspectors.DictFieldInspector',
|
'drf_yasg.inspectors.DictFieldInspector',
|
||||||
|
'drf_yasg.inspectors.JSONFieldInspector',
|
||||||
'drf_yasg.inspectors.SerializerMethodFieldInspector',
|
'drf_yasg.inspectors.SerializerMethodFieldInspector',
|
||||||
'drf_yasg.inspectors.SimpleFieldInspector',
|
'drf_yasg.inspectors.SimpleFieldInspector',
|
||||||
'drf_yasg.inspectors.StringDefaultFieldInspector',
|
'drf_yasg.inspectors.StringDefaultFieldInspector',
|
||||||
|
BIN
netbox/project-static/dist/config.js
vendored
BIN
netbox/project-static/dist/config.js
vendored
Binary file not shown.
BIN
netbox/project-static/dist/config.js.map
vendored
BIN
netbox/project-static/dist/config.js.map
vendored
Binary file not shown.
BIN
netbox/project-static/dist/lldp.js
vendored
BIN
netbox/project-static/dist/lldp.js
vendored
Binary file not shown.
BIN
netbox/project-static/dist/lldp.js.map
vendored
BIN
netbox/project-static/dist/lldp.js.map
vendored
Binary file not shown.
BIN
netbox/project-static/dist/netbox-dark.css
vendored
BIN
netbox/project-static/dist/netbox-dark.css
vendored
Binary file not shown.
BIN
netbox/project-static/dist/netbox-light.css
vendored
BIN
netbox/project-static/dist/netbox-light.css
vendored
Binary file not shown.
BIN
netbox/project-static/dist/netbox-print.css
vendored
BIN
netbox/project-static/dist/netbox-print.css
vendored
Binary file not shown.
BIN
netbox/project-static/dist/netbox.js
vendored
BIN
netbox/project-static/dist/netbox.js
vendored
Binary file not shown.
BIN
netbox/project-static/dist/netbox.js.map
vendored
BIN
netbox/project-static/dist/netbox.js.map
vendored
Binary file not shown.
BIN
netbox/project-static/dist/status.js
vendored
BIN
netbox/project-static/dist/status.js
vendored
Binary file not shown.
BIN
netbox/project-static/dist/status.js.map
vendored
BIN
netbox/project-static/dist/status.js.map
vendored
Binary file not shown.
@ -27,7 +27,6 @@
|
|||||||
"bootstrap": "~5.0.2",
|
"bootstrap": "~5.0.2",
|
||||||
"clipboard": "^2.0.8",
|
"clipboard": "^2.0.8",
|
||||||
"color2k": "^1.2.4",
|
"color2k": "^1.2.4",
|
||||||
"cookie": "^0.4.1",
|
|
||||||
"dayjs": "^1.10.4",
|
"dayjs": "^1.10.4",
|
||||||
"flatpickr": "4.6.3",
|
"flatpickr": "4.6.3",
|
||||||
"htmx.org": "^1.6.1",
|
"htmx.org": "^1.6.1",
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
import Cookie from 'cookie';
|
|
||||||
|
|
||||||
type Method = 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE';
|
type Method = 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE';
|
||||||
type ReqData = URLSearchParams | Dict | undefined | unknown;
|
type ReqData = URLSearchParams | Dict | undefined | unknown;
|
||||||
type SelectedOption = { name: string; options: string[] };
|
type SelectedOption = { name: string; options: string[] };
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window { CSRF_TOKEN: any; }
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Infer valid HTMLElement props based on element name.
|
* Infer valid HTMLElement props based on element name.
|
||||||
*/
|
*/
|
||||||
@ -93,23 +95,12 @@ export function isElement(obj: Element | null | undefined): obj is Element {
|
|||||||
return typeof obj !== null && typeof obj !== 'undefined';
|
return typeof obj !== null && typeof obj !== 'undefined';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve the CSRF token from cookie storage.
|
|
||||||
*/
|
|
||||||
function getCsrfToken(): string {
|
|
||||||
const { csrftoken: csrfToken } = Cookie.parse(document.cookie);
|
|
||||||
if (typeof csrfToken === 'undefined') {
|
|
||||||
throw new Error('Invalid or missing CSRF token');
|
|
||||||
}
|
|
||||||
return csrfToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function apiRequest<R extends Dict, D extends ReqData = undefined>(
|
export async function apiRequest<R extends Dict, D extends ReqData = undefined>(
|
||||||
url: string,
|
url: string,
|
||||||
method: Method,
|
method: Method,
|
||||||
data?: D,
|
data?: D,
|
||||||
): Promise<APIResponse<R>> {
|
): Promise<APIResponse<R>> {
|
||||||
const token = getCsrfToken();
|
const token = window.CSRF_TOKEN;
|
||||||
const headers = new Headers({ 'X-CSRFToken': token });
|
const headers = new Headers({ 'X-CSRFToken': token });
|
||||||
|
|
||||||
let body;
|
let body;
|
||||||
|
@ -235,12 +235,12 @@ table {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
th.asc a::after {
|
th.asc > a::after {
|
||||||
content: "\f0140";
|
content: "\f0140";
|
||||||
font-family: 'Material Design Icons';
|
font-family: 'Material Design Icons';
|
||||||
}
|
}
|
||||||
|
|
||||||
th.desc a::after {
|
th.desc > a::after {
|
||||||
content: "\f0143";
|
content: "\f0143";
|
||||||
font-family: 'Material Design Icons';
|
font-family: 'Material Design Icons';
|
||||||
}
|
}
|
||||||
|
@ -737,11 +737,6 @@ configstore@^3.0.0:
|
|||||||
write-file-atomic "^2.0.0"
|
write-file-atomic "^2.0.0"
|
||||||
xdg-basedir "^3.0.0"
|
xdg-basedir "^3.0.0"
|
||||||
|
|
||||||
cookie@^0.4.1:
|
|
||||||
version "0.4.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.1.tgz#afd713fe26ebd21ba95ceb61f9a8116e50a537d1"
|
|
||||||
integrity sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==
|
|
||||||
|
|
||||||
copy-to-clipboard@^3.2.0:
|
copy-to-clipboard@^3.2.0:
|
||||||
version "3.3.1"
|
version "3.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/copy-to-clipboard/-/copy-to-clipboard-3.3.1.tgz#115aa1a9998ffab6196f93076ad6da3b913662ae"
|
resolved "https://registry.yarnpkg.com/copy-to-clipboard/-/copy-to-clipboard-3.3.1.tgz#115aa1a9998ffab6196f93076ad6da3b913662ae"
|
||||||
|
@ -99,6 +99,7 @@
|
|||||||
}
|
}
|
||||||
return setMode("light", true);
|
return setMode("light", true);
|
||||||
})();
|
})();
|
||||||
|
window.CSRF_TOKEN = "{{ csrf_token }}";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{# Static resources #}
|
{# Static resources #}
|
||||||
|
@ -28,7 +28,7 @@
|
|||||||
<div class="sidenav-inner h-100 mb-auto">
|
<div class="sidenav-inner h-100 mb-auto">
|
||||||
|
|
||||||
{# Collapse #}
|
{# Collapse #}
|
||||||
<div class="collapse sidenav-collapse">
|
<div class="collapse sidenav-collapse pb-4">
|
||||||
|
|
||||||
{# Nav Items #}
|
{# Nav Items #}
|
||||||
{% nav %}
|
{% nav %}
|
||||||
|
@ -1,18 +1,8 @@
|
|||||||
{% extends 'base/layout.html' %}
|
{% extends 'generic/object_edit.html' %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
{% load helpers %}
|
{% load helpers %}
|
||||||
{% load form_helpers %}
|
{% load form_helpers %}
|
||||||
|
|
||||||
{% block title %}Connect Cable{% endblock %}
|
|
||||||
|
|
||||||
{% block tabs %}
|
|
||||||
<ul class="nav nav-tabs px-3">
|
|
||||||
<li class="nav-item" role="presentation">
|
|
||||||
<a href="#" role="tab" data-bs-toggle="tab" class="nav-link active">Connect Cable</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block content-wrapper %}
|
{% block content-wrapper %}
|
||||||
<div class="tab-content">
|
<div class="tab-content">
|
||||||
{% render_errors form %}
|
{% render_errors form %}
|
||||||
@ -116,8 +106,12 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="row my-3">
|
<div class="row my-3">
|
||||||
<div class="col col-md-12 text-center">
|
<div class="col col-md-12 text-center">
|
||||||
|
{% if object.pk %}
|
||||||
|
<button type="submit" name="_update" class="btn btn-primary">Save</button>
|
||||||
|
{% else %}
|
||||||
|
<button type="submit" name="_create" class="btn btn-primary">Create</button>
|
||||||
|
{% endif %}
|
||||||
<a href="{{ return_url }}" class="btn btn-outline-danger">Cancel</a>
|
<a href="{{ return_url }}" class="btn btn-outline-danger">Cancel</a>
|
||||||
<button type="submit" name="_update" class="btn btn-primary">Connect</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -22,16 +22,18 @@
|
|||||||
<h3 class="text-danger">Path split!</h3>
|
<h3 class="text-danger">Path split!</h3>
|
||||||
<p>Select a node below to continue:</p>
|
<p>Select a node below to continue:</p>
|
||||||
<ul class="text-start">
|
<ul class="text-start">
|
||||||
{% for next_node in path.get_split_nodes %}
|
{% for next_node in path.get_split_nodes %}
|
||||||
{% if next_node.cable %}
|
{% if next_node.cable %}
|
||||||
<li>
|
{% with viewname=next_node|viewname:"trace" %}
|
||||||
<a href="{% url 'dcim:frontport_trace' pk=next_node.pk %}">{{ next_node }}</a>
|
<li>
|
||||||
(Cable {{ next_node.cable|linkify }})
|
<a href="{% url viewname pk=next_node.pk %}">{{ next_node|meta:"verbose_name"|bettertitle }} {{ next_node }}</a>
|
||||||
</li>
|
(Cable {{ next_node.cable|linkify }})
|
||||||
{% else %}
|
</li>
|
||||||
<li class="text-muted">{{ next_node }}</li>
|
{% endwith %}
|
||||||
{% endif %}
|
{% else %}
|
||||||
{% endfor %}
|
<li class="text-muted">{{ next_node }}</li>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
{% else %}
|
{% else %}
|
||||||
<h3 class="text-center text-success">Trace Completed</h3>
|
<h3 class="text-center text-success">Trace Completed</h3>
|
||||||
|
@ -46,10 +46,10 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Rack</th>
|
<th scope="row">Rack</th>
|
||||||
<td>
|
<td class="position-relative">
|
||||||
{% if object.rack %}
|
{% if object.rack %}
|
||||||
{{ object.rack|linkify }}
|
{{ object.rack|linkify }}
|
||||||
<div class="float-end noprint">
|
<div class="position-absolute top-50 end-0 translate-middle-y noprint">
|
||||||
<a href="{{ object.rack.get_absolute_url }}?device={{ object.pk }}" class="btn btn-primary btn-sm" title="Highlight device">
|
<a href="{{ object.rack.get_absolute_url }}?device={{ object.pk }}" class="btn btn-primary btn-sm" title="Highlight device">
|
||||||
<i class="mdi mdi-view-day-outline"></i>
|
<i class="mdi mdi-view-day-outline"></i>
|
||||||
</a>
|
</a>
|
||||||
@ -90,7 +90,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Device Type</th>
|
<th scope="row">Device Type</th>
|
||||||
<td>
|
<td>
|
||||||
{{ object.device_type|linkify }} ({{ object.device_type.u_height }}U)
|
{{ object.device_type|linkify:"get_full_name" }} ({{ object.device_type.u_height }}U)
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -58,9 +58,9 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Racks</th>
|
<th scope="row">Racks</th>
|
||||||
<td>
|
<td class="position-relative">
|
||||||
{% if rack_count %}
|
{% if rack_count %}
|
||||||
<div class="float-end noprint">
|
<div class="position-absolute top-50 end-0 translate-middle-y noprint">
|
||||||
<a href="{% url 'dcim:rack_elevation_list' %}?location_id={{ object.pk }}" class="btn btn-sm btn-primary" title="View elevations">
|
<a href="{% url 'dcim:rack_elevation_list' %}?location_id={{ object.pk }}" class="btn btn-sm btn-primary" title="View elevations">
|
||||||
<i class="mdi mdi-server"></i>
|
<i class="mdi mdi-server"></i>
|
||||||
</a>
|
</a>
|
||||||
|
@ -18,9 +18,15 @@
|
|||||||
<a href="{% url 'dcim:rack_elevation_list' %}{% querystring request face='front' %}" class="btn btn-outline-secondary{% if rack_face == 'front' %} active{% endif %}">Front</a>
|
<a href="{% url 'dcim:rack_elevation_list' %}{% querystring request face='front' %}" class="btn btn-outline-secondary{% if rack_face == 'front' %} active{% endif %}">Front</a>
|
||||||
<a href="{% url 'dcim:rack_elevation_list' %}{% querystring request face='rear' %}" class="btn btn-outline-secondary{% if rack_face == 'rear' %} active{% endif %}">Rear</a>
|
<a href="{% url 'dcim:rack_elevation_list' %}{% querystring request face='rear' %}" class="btn btn-outline-secondary{% if rack_face == 'rear' %} active{% endif %}">Rear</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="btn-group btn-group-sm" role="group">
|
<div class="dropdown">
|
||||||
<a href="{% url 'dcim:rack_elevation_list' %}{% querystring request reverse=None %}" class="btn btn-outline-secondary{% if not reverse %} active{% endif %}">Normal</a>
|
<button type="button" class="btn btn-sm btn-outline-secondary dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||||
<a href="{% url 'dcim:rack_elevation_list' %}{% querystring request reverse='true' %}" class="btn btn-outline-secondary{% if reverse %} active{% endif %}">Reversed</a>
|
<i class="mdi mdi-sort"></i> Sort By {{ sort_display_name }}
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu dropdown-menu-end">
|
||||||
|
{% for sort_key, sort_display_name in sort_choices.items %}
|
||||||
|
<li><a class="dropdown-item{% if sort == sort_key %} active{% endif %}" href="{% url 'dcim:rack_elevation_list' %}{% querystring request sort=sort_key %}">{{ sort_display_name }}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -85,11 +85,11 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Physical Address</th>
|
<th scope="row">Physical Address</th>
|
||||||
<td>
|
<td class="position-relative">
|
||||||
{% if object.physical_address %}
|
{% if object.physical_address %}
|
||||||
<div class="float-end noprint">
|
<div class="position-absolute top-50 end-0 translate-middle-y noprint">
|
||||||
<a href="{{ config.MAPS_URL }}{{ object.physical_address|urlencode }}" target="_blank" class="btn btn-primary btn-sm">
|
<a href="{{ config.MAPS_URL }}{{ object.physical_address|urlencode }}" target="_blank" class="btn btn-primary btn-sm">
|
||||||
<i class="mdi mdi-map-marker"></i> Map It
|
<i class="mdi mdi-map-marker"></i> Map
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<span>{{ object.physical_address|linebreaksbr }}</span>
|
<span>{{ object.physical_address|linebreaksbr }}</span>
|
||||||
@ -104,9 +104,9 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">GPS Coordinates</th>
|
<th scope="row">GPS Coordinates</th>
|
||||||
<td>
|
<td class="position-relative">
|
||||||
{% if object.latitude and object.longitude %}
|
{% if object.latitude and object.longitude %}
|
||||||
<div class="float-end noprint">
|
<div class="position-absolute top-50 end-0 translate-middle-y noprint">
|
||||||
<a href="{{ config.MAPS_URL }}{{ object.latitude }},{{ object.longitude }}" target="_blank" class="btn btn-primary btn-sm">
|
<a href="{{ config.MAPS_URL }}{{ object.latitude }},{{ object.longitude }}" target="_blank" class="btn btn-primary btn-sm">
|
||||||
<i class="mdi mdi-map-marker"></i> Map It
|
<i class="mdi mdi-map-marker"></i> Map It
|
||||||
</a>
|
</a>
|
||||||
|
@ -8,6 +8,16 @@
|
|||||||
{% for column in table.columns %}
|
{% for column in table.columns %}
|
||||||
{% if column.orderable %}
|
{% if column.orderable %}
|
||||||
<th {{ column.attrs.th.as_html }}>
|
<th {{ column.attrs.th.as_html }}>
|
||||||
|
{% if column.is_ordered %}
|
||||||
|
<div class="float-end">
|
||||||
|
<a href="#"
|
||||||
|
hx-get="{% querystring table.prefixed_order_by_field='' %}"
|
||||||
|
hx-target="#object_list"
|
||||||
|
hx-push-url="true"
|
||||||
|
class="text-danger"
|
||||||
|
><i class="mdi mdi-close"></i></a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
<a href="#"
|
<a href="#"
|
||||||
hx-get="{% querystring table.prefixed_order_by_field=column.order_by_alias.next %}"
|
hx-get="{% querystring table.prefixed_order_by_field=column.order_by_alias.next %}"
|
||||||
hx-target="#object_list"
|
hx-target="#object_list"
|
||||||
|
@ -21,10 +21,22 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% render_field form.name %}
|
{% render_field form.name %}
|
||||||
{% render_field form.description %}
|
{% render_field form.description %}
|
||||||
{% render_field form.mac_address %}
|
|
||||||
{% render_field form.vrf %}
|
|
||||||
{% render_field form.mtu %}
|
|
||||||
{% render_field form.tags %}
|
{% render_field form.tags %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field-group my-5">
|
||||||
|
<div class="row mb-2">
|
||||||
|
<h5 class="offset-sm-3">Addressing</h5>
|
||||||
|
</div>
|
||||||
|
{% render_field form.vrf %}
|
||||||
|
{% render_field form.mac_address %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field-group my-5">
|
||||||
|
<div class="row mb-2">
|
||||||
|
<h5 class="offset-sm-3">Operation</h5>
|
||||||
|
</div>
|
||||||
|
{% render_field form.mtu %}
|
||||||
{% render_field form.enabled %}
|
{% render_field form.enabled %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -107,7 +107,7 @@ class ContactAssignmentSerializer(NetBoxModelSerializer):
|
|||||||
'last_updated',
|
'last_updated',
|
||||||
]
|
]
|
||||||
|
|
||||||
@swagger_serializer_method(serializer_or_field=serializers.DictField)
|
@swagger_serializer_method(serializer_or_field=serializers.JSONField)
|
||||||
def get_object(self, instance):
|
def get_object(self, instance):
|
||||||
serializer = get_serializer_for_model(instance.content_type.model_class(), prefix=NESTED_SERIALIZER_PREFIX)
|
serializer = get_serializer_for_model(instance.content_type.model_class(), prefix=NESTED_SERIALIZER_PREFIX)
|
||||||
context = {'request': self.context['request']}
|
context = {'request': self.context['request']}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
from django.contrib.auth.models import Group, User
|
from django.contrib.auth.models import Group, User
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from drf_yasg.utils import swagger_serializer_method
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from netbox.api.fields import ContentTypeField
|
from netbox.api.fields import ContentTypeField
|
||||||
@ -56,8 +57,10 @@ class NestedObjectPermissionSerializer(WritableNestedSerializer):
|
|||||||
model = ObjectPermission
|
model = ObjectPermission
|
||||||
fields = ['id', 'url', 'display', 'name', 'enabled', 'object_types', 'groups', 'users', 'actions']
|
fields = ['id', 'url', 'display', 'name', 'enabled', 'object_types', 'groups', 'users', 'actions']
|
||||||
|
|
||||||
|
@swagger_serializer_method(serializer_or_field=serializers.ListField)
|
||||||
def get_groups(self, obj):
|
def get_groups(self, obj):
|
||||||
return [g.name for g in obj.groups.all()]
|
return [g.name for g in obj.groups.all()]
|
||||||
|
|
||||||
|
@swagger_serializer_method(serializer_or_field=serializers.ListField)
|
||||||
def get_users(self, obj):
|
def get_users(self, obj):
|
||||||
return [u.username for u in obj.users.all()]
|
return [u.username for u in obj.users.all()]
|
||||||
|
@ -39,12 +39,12 @@ class LoginView(View):
|
|||||||
def dispatch(self, *args, **kwargs):
|
def dispatch(self, *args, **kwargs):
|
||||||
return super().dispatch(*args, **kwargs)
|
return super().dispatch(*args, **kwargs)
|
||||||
|
|
||||||
def gen_auth_data(self, name, url):
|
def gen_auth_data(self, name, url, params):
|
||||||
display_name, icon_name = get_auth_backend_display(name)
|
display_name, icon_name = get_auth_backend_display(name)
|
||||||
return {
|
return {
|
||||||
'display_name': display_name,
|
'display_name': display_name,
|
||||||
'icon_name': icon_name,
|
'icon_name': icon_name,
|
||||||
'url': url,
|
'url': f'{url}?{urlencode(params)}',
|
||||||
}
|
}
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
@ -58,15 +58,18 @@ class LoginView(View):
|
|||||||
saml_idps = get_saml_idps()
|
saml_idps = get_saml_idps()
|
||||||
for name in load_backends(settings.AUTHENTICATION_BACKENDS).keys():
|
for name in load_backends(settings.AUTHENTICATION_BACKENDS).keys():
|
||||||
url = reverse('social:begin', args=[name, ])
|
url = reverse('social:begin', args=[name, ])
|
||||||
|
params = {}
|
||||||
|
next = request.GET.get('next')
|
||||||
|
if next:
|
||||||
|
params['next'] = next
|
||||||
if name.lower() == 'saml' and saml_idps:
|
if name.lower() == 'saml' and saml_idps:
|
||||||
for idp in saml_idps:
|
for idp in saml_idps:
|
||||||
params = {'idp': idp}
|
params['idp'] = idp
|
||||||
idp_url = f'{url}?{urlencode(params)}'
|
data = self.gen_auth_data(name, url, params)
|
||||||
data = self.gen_auth_data(name, idp_url)
|
|
||||||
data['display_name'] = f'{data["display_name"]} ({idp})'
|
data['display_name'] = f'{data["display_name"]} ({idp})'
|
||||||
auth_backends.append(data)
|
auth_backends.append(data)
|
||||||
else:
|
else:
|
||||||
auth_backends.append(self.gen_auth_data(name, url))
|
auth_backends.append(self.gen_auth_data(name, url, params))
|
||||||
|
|
||||||
return render(request, self.template_name, {
|
return render(request, self.template_name, {
|
||||||
'form': form,
|
'form': form,
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
from django.contrib.postgres.fields import JSONField
|
|
||||||
from drf_yasg import openapi
|
from drf_yasg import openapi
|
||||||
from drf_yasg.inspectors import FieldInspector, NotHandled, PaginatorInspector, SwaggerAutoSchema
|
from drf_yasg.inspectors import FieldInspector, NotHandled, PaginatorInspector, SwaggerAutoSchema
|
||||||
from drf_yasg.utils import get_serializer_ref_name
|
from drf_yasg.utils import get_serializer_ref_name
|
||||||
@ -131,15 +130,6 @@ class CustomFieldsDataFieldInspector(FieldInspector):
|
|||||||
return NotHandled
|
return NotHandled
|
||||||
|
|
||||||
|
|
||||||
class JSONFieldInspector(FieldInspector):
|
|
||||||
"""Required because by default, Swagger sees a JSONField as a string and not dict
|
|
||||||
"""
|
|
||||||
def process_result(self, result, method_name, obj, **kwargs):
|
|
||||||
if isinstance(result, openapi.Schema) and isinstance(obj, JSONField):
|
|
||||||
result.type = 'dict'
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
class NullablePaginatorInspector(PaginatorInspector):
|
class NullablePaginatorInspector(PaginatorInspector):
|
||||||
def process_result(self, result, method_name, obj, **kwargs):
|
def process_result(self, result, method_name, obj, **kwargs):
|
||||||
if method_name == 'get_paginated_response' and isinstance(result, openapi.Schema):
|
if method_name == 'get_paginated_response' and isinstance(result, openapi.Schema):
|
||||||
|
@ -110,6 +110,12 @@ class SelectSpeedWidget(forms.NumberInput):
|
|||||||
|
|
||||||
class NumericArrayField(SimpleArrayField):
|
class NumericArrayField(SimpleArrayField):
|
||||||
|
|
||||||
|
def clean(self, value):
|
||||||
|
if value and not self.to_python(value):
|
||||||
|
raise forms.ValidationError(f'Invalid list ({value}). '
|
||||||
|
f'Must be numeric and ranges must be in ascending order')
|
||||||
|
return super().clean(value)
|
||||||
|
|
||||||
def to_python(self, value):
|
def to_python(self, value):
|
||||||
if not value:
|
if not value:
|
||||||
return []
|
return []
|
||||||
|
@ -7,6 +7,10 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% for name in fields %}
|
{% for name in fields %}
|
||||||
{% render_field form|getfield:name %}
|
{% if name in form.nullable_fields %}
|
||||||
|
{% render_field form|getfield:name bulk_nullable=True %}
|
||||||
|
{% else %}
|
||||||
|
{% render_field form|getfield:name %}
|
||||||
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@ -100,7 +100,7 @@ class VirtualMachineWithConfigContextSerializer(VirtualMachineSerializer):
|
|||||||
'tags', 'custom_fields', 'config_context', 'created', 'last_updated',
|
'tags', 'custom_fields', 'config_context', 'created', 'last_updated',
|
||||||
]
|
]
|
||||||
|
|
||||||
@swagger_serializer_method(serializer_or_field=serializers.DictField)
|
@swagger_serializer_method(serializer_or_field=serializers.JSONField)
|
||||||
def get_config_context(self, obj):
|
def get_config_context(self, obj):
|
||||||
return obj.get_config_context()
|
return obj.get_config_context()
|
||||||
|
|
||||||
|
@ -323,6 +323,14 @@ class VMInterfaceForm(InterfaceCommonForm, NetBoxModelForm):
|
|||||||
label='VRF'
|
label='VRF'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fieldsets = (
|
||||||
|
('Interface', ('virtual_machine', 'name', 'description', 'tags')),
|
||||||
|
('Addressing', ('vrf', 'mac_address')),
|
||||||
|
('Operation', ('mtu', 'enabled')),
|
||||||
|
('Related Interfaces', ('parent', 'bridge')),
|
||||||
|
('802.1Q Switching', ('mode', 'vlan_group', 'untagged_vlan', 'tagged_vlans')),
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = VMInterface
|
model = VMInterface
|
||||||
fields = [
|
fields = [
|
||||||
|
@ -368,9 +368,14 @@ class VirtualMachine(NetBoxModel, ConfigContextModel):
|
|||||||
|
|
||||||
# Validate primary IP addresses
|
# Validate primary IP addresses
|
||||||
interfaces = self.interfaces.all()
|
interfaces = self.interfaces.all()
|
||||||
for field in ['primary_ip4', 'primary_ip6']:
|
for family in (4, 6):
|
||||||
|
field = f'primary_ip{family}'
|
||||||
ip = getattr(self, field)
|
ip = getattr(self, field)
|
||||||
if ip is not None:
|
if ip is not None:
|
||||||
|
if ip.address.version != family:
|
||||||
|
raise ValidationError({
|
||||||
|
field: f"Must be an IPv{family} address. ({ip} is an IPv{ip.address.version} address.)",
|
||||||
|
})
|
||||||
if ip.assigned_object in interfaces:
|
if ip.assigned_object in interfaces:
|
||||||
pass
|
pass
|
||||||
elif ip.nat_inside is not None and ip.nat_inside.assigned_object in interfaces:
|
elif ip.nat_inside is not None and ip.nat_inside.assigned_object in interfaces:
|
||||||
|
@ -12,7 +12,7 @@ __all__ = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
VMINTERFACE_BUTTONS = """
|
VMINTERFACE_BUTTONS = """
|
||||||
{% if perms.virtualization.edit_vminterface %}
|
{% if perms.virtualization.change_vminterface %}
|
||||||
<span class="dropdown">
|
<span class="dropdown">
|
||||||
<button type="button" class="btn btn-primary btn-sm dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false" title="Add">
|
<button type="button" class="btn btn-primary btn-sm dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false" title="Add">
|
||||||
<span class="mdi mdi-plus-thick" aria-hidden="true"></span>
|
<span class="mdi mdi-plus-thick" aria-hidden="true"></span>
|
||||||
@ -108,7 +108,7 @@ class VMInterfaceTable(BaseInterfaceTable):
|
|||||||
model = VMInterface
|
model = VMInterface
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'name', 'virtual_machine', 'enabled', 'mac_address', 'mtu', 'mode', 'description', 'tags',
|
'pk', 'id', 'name', 'virtual_machine', 'enabled', 'mac_address', 'mtu', 'mode', 'description', 'tags',
|
||||||
'vrf', 'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'created', 'last_updated',
|
'vrf', 'l2vpn', 'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'created', 'last_updated',
|
||||||
)
|
)
|
||||||
default_columns = ('pk', 'name', 'virtual_machine', 'enabled', 'description')
|
default_columns = ('pk', 'name', 'virtual_machine', 'enabled', 'description')
|
||||||
|
|
||||||
@ -129,7 +129,7 @@ class VirtualMachineVMInterfaceTable(VMInterfaceTable):
|
|||||||
model = VMInterface
|
model = VMInterface
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'name', 'enabled', 'parent', 'bridge', 'mac_address', 'mtu', 'mode', 'description', 'tags',
|
'pk', 'id', 'name', 'enabled', 'parent', 'bridge', 'mac_address', 'mtu', 'mode', 'description', 'tags',
|
||||||
'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'actions',
|
'vrf', 'l2vpn', 'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'actions',
|
||||||
)
|
)
|
||||||
default_columns = ('pk', 'name', 'enabled', 'mac_address', 'mtu', 'mode', 'description', 'ip_addresses')
|
default_columns = ('pk', 'name', 'enabled', 'mac_address', 'mtu', 'mode', 'description', 'ip_addresses')
|
||||||
row_attrs = {
|
row_attrs = {
|
||||||
|
@ -19,13 +19,13 @@ graphene-django==2.15.0
|
|||||||
gunicorn==20.1.0
|
gunicorn==20.1.0
|
||||||
Jinja2==3.1.2
|
Jinja2==3.1.2
|
||||||
Markdown==3.4.1
|
Markdown==3.4.1
|
||||||
mkdocs-material==8.4.1
|
mkdocs-material==8.4.2
|
||||||
mkdocstrings[python-legacy]==0.19.0
|
mkdocstrings[python-legacy]==0.19.0
|
||||||
netaddr==0.8.0
|
netaddr==0.8.0
|
||||||
Pillow==9.2.0
|
Pillow==9.2.0
|
||||||
psycopg2-binary==2.9.3
|
psycopg2-binary==2.9.3
|
||||||
PyYAML==6.0
|
PyYAML==6.0
|
||||||
sentry-sdk==1.9.5
|
sentry-sdk==1.9.7
|
||||||
social-auth-app-django==5.0.0
|
social-auth-app-django==5.0.0
|
||||||
social-auth-core==4.3.0
|
social-auth-core==4.3.0
|
||||||
svgwrite==1.4.3
|
svgwrite==1.4.3
|
||||||
|
Loading…
Reference in New Issue
Block a user