mirror of
https://github.com/netbox-community/netbox.git
synced 2026-04-14 21:20:06 +02:00
Compare commits
319 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
539448683c | ||
|
|
e208a28137 | ||
|
|
75e1b86613 | ||
|
|
e12334c01b | ||
|
|
ea6552b239 | ||
|
|
36afe5541f | ||
|
|
d57346d9f0 | ||
|
|
5aeb045fb5 | ||
|
|
6c12d8b402 | ||
|
|
58275977bb | ||
|
|
5054566abb | ||
|
|
28a11f6aad | ||
|
|
9b734bac93 | ||
|
|
0f277894b2 | ||
|
|
cb5ade07f0 | ||
|
|
71d918636c | ||
|
|
82cf60091a | ||
|
|
133ed53849 | ||
|
|
ab94e3d40e | ||
|
|
315fcdffb6 | ||
|
|
4ca688de57 | ||
|
|
ed7ebd9d98 | ||
|
|
7462e45c8e | ||
|
|
48037f6fed | ||
|
|
0bc05f27f9 | ||
|
|
a93aae12fa | ||
|
|
cb7e97c7f7 | ||
|
|
e864dc3ae0 | ||
|
|
dbb871b75a | ||
|
|
d75583828b | ||
|
|
7ff7c6d17e | ||
|
|
cc03d509d1 | ||
|
|
296e708e09 | ||
|
|
87bc20cdd5 | ||
|
|
1bbecef77d | ||
|
|
1ebeb71ad8 | ||
|
|
48e790c9f0 | ||
|
|
25fb457331 | ||
|
|
06c90cb86a | ||
|
|
bcc410d99f | ||
|
|
d630afaf14 | ||
|
|
d6a1cc5558 | ||
|
|
09f7df0726 | ||
|
|
f242f17ce5 | ||
|
|
2b1f4ab51a | ||
|
|
84502e80d0 | ||
|
|
7d71503ea2 | ||
|
|
02f9ca8f01 | ||
|
|
d0651f6474 | ||
|
|
fecd4e2f97 | ||
|
|
e07a5966ae | ||
|
|
f058ee3d60 | ||
|
|
49ba0dd495 | ||
|
|
b4ee2cf447 | ||
|
|
34098bb20a | ||
|
|
a19daa5466 | ||
|
|
40eec679d9 | ||
|
|
57556e3fdb | ||
|
|
5ad4e95207 | ||
|
|
f2d8ae29c2 | ||
|
|
f6eb5dda0f | ||
|
|
a06a300913 | ||
|
|
c7bbfb24c5 | ||
|
|
6c08941542 | ||
|
|
be1a29d7ee | ||
|
|
f06f8f3f1d | ||
|
|
a45ec6620a | ||
|
|
bd35afe320 | ||
|
|
364868a207 | ||
|
|
d4569df305 | ||
|
|
b62c5e1ac4 | ||
|
|
1277bb6138 | ||
|
|
e98e5e11a7 | ||
|
|
3ce2bf75b4 | ||
|
|
b1af9a7218 | ||
|
|
b73f7f7d00 | ||
|
|
9492b55f4b | ||
|
|
2563122352 | ||
|
|
0455e14c29 | ||
|
|
76c02d5aa9 | ||
|
|
8bc691099c | ||
|
|
95011821bb | ||
|
|
b8b12f3f90 | ||
|
|
e5b9e5a279 | ||
|
|
05059f4a86 | ||
|
|
2389feea6b | ||
|
|
e4e4c1c56d | ||
|
|
c99d8481b2 | ||
|
|
0923a3dec8 | ||
|
|
80b9c25674 | ||
|
|
6d13bc8b96 | ||
|
|
ee17e83da6 | ||
|
|
5ab9608e38 | ||
|
|
c7504628bd | ||
|
|
e54ed87863 | ||
|
|
55daf4c52f | ||
|
|
a45e8571da | ||
|
|
0154a09856 | ||
|
|
757c4f69d2 | ||
|
|
d5f37d7a87 | ||
|
|
f30786d8fe | ||
|
|
74aa822b27 | ||
|
|
bb73601d80 | ||
|
|
9bc66ee0bf | ||
|
|
99e9d96787 | ||
|
|
296b89ae02 | ||
|
|
3ec0551680 | ||
|
|
8a58d760fa | ||
|
|
f5c97e367c | ||
|
|
84670af18b | ||
|
|
a3a204f2fd | ||
|
|
ea756b29e9 | ||
|
|
b929e1aa1b | ||
|
|
91d5382a61 | ||
|
|
e76203238d | ||
|
|
3f58648115 | ||
|
|
b904dc5c75 | ||
|
|
2c0b6c4d55 | ||
|
|
bf27ff9593 | ||
|
|
29239ca58a | ||
|
|
981f31304d | ||
|
|
2a39ab47d6 | ||
|
|
aa01c16db0 | ||
|
|
2a78c05984 | ||
|
|
e04986617c | ||
|
|
bc66d9f136 | ||
|
|
b8ce81c8fe | ||
|
|
41d05490fc | ||
|
|
83cf193cdc | ||
|
|
d497198f49 | ||
|
|
82df20a8a9 | ||
|
|
f303ae2cd7 | ||
|
|
4e479c547f | ||
|
|
e44c0a2119 | ||
|
|
3ab0613708 | ||
|
|
9f16734266 | ||
|
|
1f336eee2e | ||
|
|
6030fc383a | ||
|
|
c3c7cf15b2 | ||
|
|
2b7049c39c | ||
|
|
3ededeb0e7 | ||
|
|
1fb6507cc1 | ||
|
|
753fedf5e7 | ||
|
|
ca021e808b | ||
|
|
38afed60ef | ||
|
|
66f6b2b6f9 | ||
|
|
45b53ee036 | ||
|
|
992630d670 | ||
|
|
61cef9400d | ||
|
|
d57f230f37 | ||
|
|
472dc3882e | ||
|
|
c8cd5fd6cd | ||
|
|
268ef4f59f | ||
|
|
671b1cd470 | ||
|
|
21f78049bc | ||
|
|
e28ed7446c | ||
|
|
2f5543933e | ||
|
|
9b57512b12 | ||
|
|
1fc43026d0 | ||
|
|
5804b53bb1 | ||
|
|
775d6aa936 | ||
|
|
639a739b5b | ||
|
|
b01d92c98b | ||
|
|
da79cc775d | ||
|
|
6f5fd26183 | ||
|
|
10157394ae | ||
|
|
ae0907fb37 | ||
|
|
fea6ad61fd | ||
|
|
675e68f276 | ||
|
|
20b907a8c9 | ||
|
|
8ccb0f7b63 | ||
|
|
068fce4d7c | ||
|
|
2e4bce2dad | ||
|
|
dad96c525f | ||
|
|
625c4eb5bb | ||
|
|
cac3c1221c | ||
|
|
02165a28a0 | ||
|
|
80cc7e0d91 | ||
|
|
3a9d00a537 | ||
|
|
4040e4f266 | ||
|
|
f938309ed9 | ||
|
|
86f6de40d2 | ||
|
|
83c6149e49 | ||
|
|
98d898aba9 | ||
|
|
e2665ef211 | ||
|
|
c384cec453 | ||
|
|
07bb6aa365 | ||
|
|
e3d9fe622d | ||
|
|
f3c34b30ec | ||
|
|
2281889e9d | ||
|
|
b19d0d61f4 | ||
|
|
d64c4d75f8 | ||
|
|
719effb548 | ||
|
|
b5bd8905ca | ||
|
|
cb5521f818 | ||
|
|
3cb854b7d5 | ||
|
|
d980837da0 | ||
|
|
5c19afc07c | ||
|
|
6659bb3abe | ||
|
|
67defb3228 | ||
|
|
cca4cc61b6 | ||
|
|
9b0c6110bb | ||
|
|
758b230403 | ||
|
|
8ea33df148 | ||
|
|
c86210f024 | ||
|
|
685c1afdcf | ||
|
|
d62a0d7d8d | ||
|
|
0a5f40338d | ||
|
|
1c527366c9 | ||
|
|
e1684fb645 | ||
|
|
969ae81574 | ||
|
|
baec71fcaf | ||
|
|
44abeeff5a | ||
|
|
fd6e0e9784 | ||
|
|
93e01d5b07 | ||
|
|
2a176df28a | ||
|
|
cd5d88ff8a | ||
|
|
6e3fd9d4b2 | ||
|
|
53ae164c75 | ||
|
|
fa5f9430fc | ||
|
|
351066c73f | ||
|
|
e6db3f75ea | ||
|
|
04244e188f | ||
|
|
eaad5cc26f | ||
|
|
c40640af81 | ||
|
|
3c6596de8f | ||
|
|
b3de0b9bee | ||
|
|
ec0fe62df5 | ||
|
|
d3a0566ee3 | ||
|
|
a1d82e45a0 | ||
|
|
694e3765dd | ||
|
|
303199dc8f | ||
|
|
e4f7f080b3 | ||
|
|
6eafffb497 | ||
|
|
53ea48efa9 | ||
|
|
983ba4fda8 | ||
|
|
54462595a6 | ||
|
|
8ab752b9ad | ||
|
|
b11cc31f9d | ||
|
|
3f02309538 | ||
|
|
53345f194a | ||
|
|
139557b8dd | ||
|
|
fcf02bd8bb | ||
|
|
7d6989ff34 | ||
|
|
1be917fb90 | ||
|
|
3b0b95c265 | ||
|
|
cdc2fb2f06 | ||
|
|
7ec656bc7c | ||
|
|
06bbae0f84 | ||
|
|
8ff9fd26d1 | ||
|
|
a0e23ac3c9 | ||
|
|
071d4a63aa | ||
|
|
7db2739465 | ||
|
|
1a404f5c0f | ||
|
|
74326edc20 | ||
|
|
2ef21f7097 | ||
|
|
3adcdc34c3 | ||
|
|
f33109e485 | ||
|
|
d10453883f | ||
|
|
6dbd8f6170 | ||
|
|
715f9d150c | ||
|
|
f4567ba099 | ||
|
|
3320e07b70 | ||
|
|
d5e8f7dafa | ||
|
|
32e2a17c88 | ||
|
|
3beef34355 | ||
|
|
85d6242962 | ||
|
|
bb1a44d35b | ||
|
|
ae6f1f9ae3 | ||
|
|
915ac90119 | ||
|
|
cc47afc401 | ||
|
|
20fee95a9a | ||
|
|
d2002c64b4 | ||
|
|
1b295f1d69 | ||
|
|
2c200a4fd3 | ||
|
|
fb71cafb51 | ||
|
|
f373adb636 | ||
|
|
e84b062393 | ||
|
|
ef52ac4203 | ||
|
|
b22e490847 | ||
|
|
945e7ade0a | ||
|
|
7300104cea | ||
|
|
2900429769 | ||
|
|
278c82dd88 | ||
|
|
951d856c3c | ||
|
|
c029782cf5 | ||
|
|
bdd23f3d17 | ||
|
|
af6e18b7d4 | ||
|
|
816c5d4bea | ||
|
|
f4c3c90bab | ||
|
|
862593f2dd | ||
|
|
f4c27fd494 | ||
|
|
ae736ef407 | ||
|
|
d95b1186fb | ||
|
|
d6b9d30086 | ||
|
|
9be5aa188c | ||
|
|
f113557e81 | ||
|
|
de812a5a85 | ||
|
|
0b7375136d | ||
|
|
1190adde2b | ||
|
|
2330874a8c | ||
|
|
dc738c7102 | ||
|
|
76fd3e3c61 | ||
|
|
4ee64a7731 | ||
|
|
0bb22dee0c | ||
|
|
6c383f293c | ||
|
|
5bf516c63d | ||
|
|
7df062d590 | ||
|
|
4b22be03a0 | ||
|
|
24769ce127 | ||
|
|
164e9db98d | ||
|
|
23f1c86e9c | ||
|
|
02ffdd9d5d | ||
|
|
5013297326 | ||
|
|
584e0a9b8c | ||
|
|
3ac9d0b8bf | ||
|
|
b387ea5f58 | ||
|
|
ba9f6bf359 | ||
|
|
ee6cbdcefe |
@@ -15,7 +15,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: v4.5.2
|
placeholder: v4.5.8
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
|
|||||||
2
.github/ISSUE_TEMPLATE/02-bug_report.yaml
vendored
2
.github/ISSUE_TEMPLATE/02-bug_report.yaml
vendored
@@ -27,7 +27,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: v4.5.2
|
placeholder: v4.5.8
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
|
|||||||
2
.github/ISSUE_TEMPLATE/03-performance.yaml
vendored
2
.github/ISSUE_TEMPLATE/03-performance.yaml
vendored
@@ -8,7 +8,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: v4.5.2
|
placeholder: v4.5.8
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
|
|||||||
22
.github/workflows/ci.yml
vendored
22
.github/workflows/ci.yml
vendored
@@ -53,15 +53,22 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out repo
|
- name: Check out repo
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
|
- name: Check Python linting & PEP8 compliance
|
||||||
|
uses: astral-sh/ruff-action@0ce1b0bf8b818ef400413f810f8a11cdbda0034b # v4.0.0
|
||||||
|
with:
|
||||||
|
version: "0.15.10"
|
||||||
|
args: "check --output-format=github"
|
||||||
|
src: "netbox/"
|
||||||
|
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
|
|
||||||
- name: Use Node.js ${{ matrix.node-version }}
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
|
|
||||||
@@ -69,7 +76,7 @@ jobs:
|
|||||||
run: npm install -g yarn
|
run: npm install -g yarn
|
||||||
|
|
||||||
- name: Setup Node.js with Yarn Caching
|
- name: Setup Node.js with Yarn Caching
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
cache: yarn
|
cache: yarn
|
||||||
@@ -82,10 +89,10 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
python -m pip install --upgrade pip
|
python -m pip install --upgrade pip
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
pip install ruff coverage tblib
|
pip install coverage tblib
|
||||||
|
|
||||||
- name: Build documentation
|
- name: Build documentation
|
||||||
run: mkdocs build
|
run: zensical build
|
||||||
|
|
||||||
- name: Collect static files
|
- name: Collect static files
|
||||||
run: python netbox/manage.py collectstatic --no-input
|
run: python netbox/manage.py collectstatic --no-input
|
||||||
@@ -93,9 +100,6 @@ jobs:
|
|||||||
- name: Check for missing migrations
|
- name: Check for missing migrations
|
||||||
run: python netbox/manage.py makemigrations --check
|
run: python netbox/manage.py makemigrations --check
|
||||||
|
|
||||||
- name: Check PEP8 compliance
|
|
||||||
run: ruff check netbox/
|
|
||||||
|
|
||||||
- name: Check UI ESLint, TypeScript, and Prettier Compliance
|
- name: Check UI ESLint, TypeScript, and Prettier Compliance
|
||||||
run: yarn --cwd netbox/project-static validate
|
run: yarn --cwd netbox/project-static validate
|
||||||
|
|
||||||
|
|||||||
37
.github/workflows/claude-code-review.yml
vendored
Normal file
37
.github/workflows/claude-code-review.yml
vendored
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
name: Claude Code Review
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [opened, synchronize, ready_for_review, reopened]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
claude-review:
|
||||||
|
# Only run for PRs submitted by organization members or owners
|
||||||
|
if: |
|
||||||
|
github.repository == 'netbox-community/netbox' &&
|
||||||
|
(github.event.pull_request.author_association == 'MEMBER' ||
|
||||||
|
github.event.pull_request.author_association == 'OWNER')
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: read
|
||||||
|
issues: read
|
||||||
|
id-token: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
fetch-depth: 1
|
||||||
|
|
||||||
|
- name: Run Claude Code Review
|
||||||
|
id: claude-review
|
||||||
|
uses: anthropics/claude-code-action@e763fe78de2db7389e04818a00b5ff8ba13d1360 # v1
|
||||||
|
with:
|
||||||
|
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||||
|
plugin_marketplaces: 'https://github.com/anthropics/claude-code.git'
|
||||||
|
plugins: 'code-review@claude-code-plugins'
|
||||||
|
prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}'
|
||||||
|
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
|
||||||
|
# or https://code.claude.com/docs/en/cli-reference for available options
|
||||||
80
.github/workflows/claude.yml
vendored
Normal file
80
.github/workflows/claude.yml
vendored
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
name: Claude Code
|
||||||
|
|
||||||
|
on:
|
||||||
|
issue_comment:
|
||||||
|
types: [created]
|
||||||
|
pull_request_review_comment:
|
||||||
|
types: [created]
|
||||||
|
issues:
|
||||||
|
types: [opened, assigned]
|
||||||
|
pull_request_review:
|
||||||
|
types: [submitted]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
claude:
|
||||||
|
if: |
|
||||||
|
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
|
||||||
|
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
|
||||||
|
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
|
||||||
|
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: read
|
||||||
|
issues: read
|
||||||
|
id-token: write
|
||||||
|
actions: read # Required for Claude to read CI results on PRs
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
fetch-depth: 1
|
||||||
|
|
||||||
|
# Workaround for claude-code-action bug with fork PRs: The action fetches by branch name
|
||||||
|
# (git fetch origin --depth=N <branch>), but fork PR branches don't exist on origin.
|
||||||
|
# Fix: redirect origin to the fork's URL so the action can fetch the branch directly.
|
||||||
|
- name: Configure git remote for fork PRs
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
# Determine PR number based on event type
|
||||||
|
if [ "${{ github.event_name }}" = "issue_comment" ]; then
|
||||||
|
PR_NUMBER="${{ github.event.issue.number }}"
|
||||||
|
elif [ "${{ github.event_name }}" = "pull_request_review_comment" ] || [ "${{ github.event_name }}" = "pull_request_review" ]; then
|
||||||
|
PR_NUMBER="${{ github.event.pull_request.number }}"
|
||||||
|
else
|
||||||
|
exit 0 # issues event — no PR branch to worry about
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Fetch fork info in one API call; silently skip if this is not a PR
|
||||||
|
PR_INFO=$(gh pr view "${PR_NUMBER}" --json isCrossRepository,headRepositoryOwner,headRepository 2>/dev/null || echo "")
|
||||||
|
if [ -z "$PR_INFO" ]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
IS_FORK=$(echo "$PR_INFO" | jq -r '.isCrossRepository')
|
||||||
|
if [ "$IS_FORK" = "true" ]; then
|
||||||
|
FORK_OWNER=$(echo "$PR_INFO" | jq -r '.headRepositoryOwner.login')
|
||||||
|
FORK_REPO=$(echo "$PR_INFO" | jq -r '.headRepository.name')
|
||||||
|
echo "Fork PR detected from ${FORK_OWNER}/${FORK_REPO}: updating origin to fork URL"
|
||||||
|
git remote set-url origin "https://github.com/${FORK_OWNER}/${FORK_REPO}.git"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Run Claude Code
|
||||||
|
id: claude
|
||||||
|
uses: anthropics/claude-code-action@e763fe78de2db7389e04818a00b5ff8ba13d1360 # v1
|
||||||
|
with:
|
||||||
|
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||||
|
|
||||||
|
# This is an optional setting that allows Claude to read CI results on PRs
|
||||||
|
additional_permissions: |
|
||||||
|
actions: read
|
||||||
|
|
||||||
|
# Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it.
|
||||||
|
# prompt: 'Update the pull request description to include a summary of changes.'
|
||||||
|
|
||||||
|
# Optional: Add claude_args to customize behavior and configuration
|
||||||
|
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
|
||||||
|
# or https://code.claude.com/docs/en/cli-reference for available options
|
||||||
|
# claude_args: '--allowed-tools Bash(gh pr:*)'
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@ jobs:
|
|||||||
if: github.repository == 'netbox-community/netbox'
|
if: github.repository == 'netbox-community/netbox'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@v9
|
- uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
|
||||||
with:
|
with:
|
||||||
close-issue-message: >
|
close-issue-message: >
|
||||||
This issue is being closed as no further information has been provided. If
|
This issue is being closed as no further information has been provided. If
|
||||||
|
|||||||
2
.github/workflows/close-stale-issues.yml
vendored
2
.github/workflows/close-stale-issues.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
|||||||
if: github.repository == 'netbox-community/netbox'
|
if: github.repository == 'netbox-community/netbox'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@v9
|
- uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
|
||||||
with:
|
with:
|
||||||
# General parameters
|
# General parameters
|
||||||
operations-per-run: 200
|
operations-per-run: 200
|
||||||
|
|||||||
6
.github/workflows/codeql.yml
vendored
6
.github/workflows/codeql.yml
vendored
@@ -27,16 +27,16 @@ jobs:
|
|||||||
build-mode: none
|
build-mode: none
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v4
|
uses: github/codeql-action/init@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v4.33.0
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
build-mode: ${{ matrix.build-mode }}
|
build-mode: ${{ matrix.build-mode }}
|
||||||
config-file: .github/codeql/codeql-config.yml
|
config-file: .github/codeql/codeql-config.yml
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v4
|
uses: github/codeql-action/analyze@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v4.33.0
|
||||||
with:
|
with:
|
||||||
category: "/language:${{matrix.language}}"
|
category: "/language:${{matrix.language}}"
|
||||||
|
|||||||
8
.github/workflows/lock-threads.yml
vendored
8
.github/workflows/lock-threads.yml
vendored
@@ -11,14 +11,14 @@ permissions:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
discussions: write
|
discussions: write
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: lock-threads
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lock:
|
lock:
|
||||||
if: github.repository == 'netbox-community/netbox'
|
if: github.repository == 'netbox-community/netbox'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: dessant/lock-threads@1bf7ec25051fe7c00bdd17e6a7cf3d7bfb7dc771 # v5.0.1
|
- uses: dessant/lock-threads@7266a7ce5c1df01b1c6db85bf8cd86c737dadbe7 # v6.0.0
|
||||||
with:
|
with:
|
||||||
issue-inactive-days: 90
|
|
||||||
pr-inactive-days: 30
|
|
||||||
discussion-inactive-days: 180
|
discussion-inactive-days: 180
|
||||||
issue-lock-reason: 'resolved'
|
|
||||||
|
|||||||
@@ -20,19 +20,19 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Create app token
|
- name: Create app token
|
||||||
uses: actions/create-github-app-token@v1
|
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
|
||||||
id: app-token
|
id: app-token
|
||||||
with:
|
with:
|
||||||
app-id: 1076524
|
app-id: 1076524
|
||||||
private-key: ${{ secrets.HOUSEKEEPING_SECRET_KEY }}
|
private-key: ${{ secrets.HOUSEKEEPING_SECRET_KEY }}
|
||||||
|
|
||||||
- name: Check out repo
|
- name: Check out repo
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
token: ${{ steps.app-token.outputs.token }}
|
token: ${{ steps.app-token.outputs.token }}
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||||
with:
|
with:
|
||||||
python-version: 3.12
|
python-version: 3.12
|
||||||
|
|
||||||
@@ -48,7 +48,7 @@ jobs:
|
|||||||
run: python netbox/manage.py makemessages -l ${{ env.LOCALE }}
|
run: python netbox/manage.py makemessages -l ${{ env.LOCALE }}
|
||||||
|
|
||||||
- name: Commit changes
|
- name: Commit changes
|
||||||
uses: EndBug/add-and-commit@a94899bca583c204427a224a7af87c02f9b325d5 # v9.1.4
|
uses: EndBug/add-and-commit@290ea2c423ad77ca9c62ae0f5b224379612c0321 # v10.0.0
|
||||||
with:
|
with:
|
||||||
add: 'netbox/translations/'
|
add: 'netbox/translations/'
|
||||||
default_author: github_actions
|
default_author: github_actions
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
rev: v0.14.1
|
rev: v0.15.2
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff
|
- id: ruff
|
||||||
name: "Ruff linter"
|
name: "Ruff linter"
|
||||||
@@ -21,11 +21,11 @@ repos:
|
|||||||
language: system
|
language: system
|
||||||
pass_filenames: false
|
pass_filenames: false
|
||||||
types: [python]
|
types: [python]
|
||||||
- id: mkdocs-build
|
- id: zensical-build
|
||||||
name: "Build documentation"
|
name: "Build documentation"
|
||||||
description: "Build the documentation with mkdocs"
|
description: "Build the documentation with Zensical"
|
||||||
files: 'docs/'
|
files: 'docs/'
|
||||||
entry: mkdocs build
|
entry: zensical build
|
||||||
language: system
|
language: system
|
||||||
pass_filenames: false
|
pass_filenames: false
|
||||||
- id: yarn-validate
|
- id: yarn-validate
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
version: 2
|
version: 2
|
||||||
build:
|
build:
|
||||||
os: ubuntu-22.04
|
os: ubuntu-24.04
|
||||||
tools:
|
tools:
|
||||||
python: "3.12"
|
python: "3.12"
|
||||||
mkdocs:
|
commands:
|
||||||
configuration: mkdocs.yml
|
- pip install -r requirements.txt
|
||||||
python:
|
- python -m zensical build --config-file mkdocs.yml
|
||||||
install:
|
- mkdir -p $READTHEDOCS_OUTPUT/html/
|
||||||
- requirements: requirements.txt
|
- cp -r netbox/project-static/docs/* $READTHEDOCS_OUTPUT/html/
|
||||||
|
|||||||
87
CLAUDE.md
Normal file
87
CLAUDE.md
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
# NetBox
|
||||||
|
|
||||||
|
Network source-of-truth and infrastructure resource modeling (IRM) tool combining DCIM and IPAM. Built on Django + PostgreSQL + Redis.
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
- Python 3.12+ / Django / Django REST Framework
|
||||||
|
- PostgreSQL (required), Redis (required for caching/queuing)
|
||||||
|
- GraphQL via Strawberry, background jobs via RQ
|
||||||
|
- Docs: MkDocs (in `docs/`)
|
||||||
|
|
||||||
|
## Repository Layout
|
||||||
|
- `netbox/` — Django project root; run all `manage.py` commands from here
|
||||||
|
- `netbox/netbox/` — Core settings, URLs, WSGI entrypoint
|
||||||
|
- `netbox/<app>/` — Django apps: `circuits`, `core`, `dcim`, `ipam`, `extras`, `tenancy`, `virtualization`, `wireless`, `users`, `vpn`
|
||||||
|
- `docs/` — MkDocs documentation source
|
||||||
|
- `contrib/` — Example configs (systemd, nginx, etc.) and other resources
|
||||||
|
|
||||||
|
## Development Setup
|
||||||
|
```bash
|
||||||
|
python -m venv ~/.venv/netbox
|
||||||
|
source ~/.venv/netbox/bin/activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
# Copy and configure
|
||||||
|
cp netbox/netbox/configuration.example.py netbox/netbox/configuration.py
|
||||||
|
# Edit configuration.py: set DATABASE, REDIS, SECRET_KEY, ALLOWED_HOSTS
|
||||||
|
|
||||||
|
cd netbox/
|
||||||
|
python manage.py migrate
|
||||||
|
python manage.py runserver
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key Commands
|
||||||
|
All commands run from the `netbox/` subdirectory with venv active.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Development server
|
||||||
|
python manage.py runserver
|
||||||
|
|
||||||
|
# Run full test suite
|
||||||
|
export NETBOX_CONFIGURATION=netbox.configuration_testing
|
||||||
|
python manage.py test
|
||||||
|
|
||||||
|
# Faster test runs (no DB rebuild, parallel)
|
||||||
|
python manage.py test --keepdb --parallel 4
|
||||||
|
|
||||||
|
# Migrations
|
||||||
|
python manage.py makemigrations
|
||||||
|
python manage.py migrate
|
||||||
|
|
||||||
|
# Shell
|
||||||
|
python manage.py nbshell # NetBox-enhanced shell
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture Conventions
|
||||||
|
- **Apps**: Each Django app owns its models, views, API serializers, filtersets, forms, and tests.
|
||||||
|
- **Views**: Use `register_model_view()` to register model views by action (e.g. "add", "list", etc.). List views typically don't need to add `select_related()` or `prefetch_related()` on their querysets: Prefetching is handled dynamically by the table class so that only relevant fields are prefetched.
|
||||||
|
- **REST API**: DRF serializers live in `<app>/api/serializers.py`; viewsets in `<app>/api/views.py`; URLs auto-registered in `<app>/api/urls.py`. REST API views typically don't need to add `select_related()` or `prefetch_related()` on their querysets: Prefetching is handled dynamically by the serializer so that only relevant fields are prefetched.
|
||||||
|
- **GraphQL**: Strawberry types in `<app>/graphql/types.py`.
|
||||||
|
- **Filtersets**: `<app>/filtersets.py` — used for both UI filtering and API `?filter=` params.
|
||||||
|
- **Tables**: `django-tables2` used for all object list views (`<app>/tables.py`).
|
||||||
|
- **Templates**: Django templates in `netbox/templates/<app>/`.
|
||||||
|
- **Tests**: Mirror the app structure in `<app>/tests/`. Use `netbox.configuration_testing` for test config.
|
||||||
|
|
||||||
|
## Coding Standards
|
||||||
|
- Follow existing Django conventions; don't reinvent patterns already present in the codebase.
|
||||||
|
- New models must include `created`, `last_updated` fields (inherit from `NetBoxModel` where appropriate).
|
||||||
|
- Every model exposed in the UI needs: model, serializer, filterset, form, table, views, URL route, and tests.
|
||||||
|
- API serializers must include a `url` field (absolute URL of the object).
|
||||||
|
- Use `FeatureQuery` for generic relations (config contexts, custom fields, tags, etc.).
|
||||||
|
- Avoid adding new dependencies without strong justification.
|
||||||
|
- Avoid running `ruff format` on existing files, as this tends to introduce unnecessary style changes.
|
||||||
|
- Don't craft Django database migrations manually: Prompt the user to run `manage.py makemigrations` instead.
|
||||||
|
|
||||||
|
## Branch & PR Conventions
|
||||||
|
- Branch naming: `<issue-number>-short-description` (e.g., `1234-device-typerror`)
|
||||||
|
- Use the `main` branch for patch releases; `feature` tracks work for the upcoming minor/major release.
|
||||||
|
- Every PR must reference an approved GitHub issue.
|
||||||
|
- PRs must include tests for new functionality.
|
||||||
|
|
||||||
|
## Gotchas
|
||||||
|
- `configuration.py` is gitignored — never commit it.
|
||||||
|
- `manage.py` lives in `netbox/`, NOT the repo root. Running from the wrong directory is a common mistake.
|
||||||
|
- `NETBOX_CONFIGURATION` env var controls which settings module loads; set to `netbox.configuration_testing` for tests.
|
||||||
|
- The `extras` app is a catch-all for cross-cutting features (custom fields, tags, webhooks, scripts).
|
||||||
|
- Plugins API: only documented public APIs are stable. Internal NetBox code is subject to change without notice.
|
||||||
|
- See `docs/development/` for the full contributing guide and code style details.
|
||||||
@@ -84,6 +84,8 @@ intake policy](https://github.com/netbox-community/netbox/wiki/Issue-Intake-Poli
|
|||||||
|
|
||||||
* It's very important that you not submit a pull request until a relevant issue has been opened **and** assigned to you. Otherwise, you risk wasting time on work that may ultimately not be needed.
|
* It's very important that you not submit a pull request until a relevant issue has been opened **and** assigned to you. Otherwise, you risk wasting time on work that may ultimately not be needed.
|
||||||
|
|
||||||
|
* Community members are limited to a maximum of **three open PRs** at any time. This is to avoid the accumulation of too much parallel work and maintain focus on already PRs under review. If you already have three NetBox PRs open, please wait for at least one of them to be merged (or closed) before opening another.
|
||||||
|
|
||||||
* New pull requests should generally be based off of the `main` branch. This branch, in keeping with the [trunk-based development](https://trunkbaseddevelopment.com/) approach, is used for ongoing development and bug fixes and always represents the newest stable code, from which releases are periodically branched. (If you're developing for an upcoming minor release, use `feature` instead.)
|
* New pull requests should generally be based off of the `main` branch. This branch, in keeping with the [trunk-based development](https://trunkbaseddevelopment.com/) approach, is used for ongoing development and bug fixes and always represents the newest stable code, from which releases are periodically branched. (If you're developing for an upcoming minor release, use `feature` instead.)
|
||||||
|
|
||||||
* In most cases, it is not necessary to add a changelog entry: A maintainer will take care of this when the PR is merged. (This helps avoid merge conflicts resulting from multiple PRs being submitted simultaneously.)
|
* In most cases, it is not necessary to add a changelog entry: A maintainer will take care of this when the PR is merged. (This helps avoid merge conflicts resulting from multiple PRs being submitted simultaneously.)
|
||||||
@@ -96,10 +98,10 @@ intake policy](https://github.com/netbox-community/netbox/wiki/Issue-Intake-Poli
|
|||||||
greater than 80 characters in length
|
greater than 80 characters in length
|
||||||
|
|
||||||
> [!CAUTION]
|
> [!CAUTION]
|
||||||
> Any contributions which include AI-generated or reproduced content will be rejected.
|
> Any contributions which include solely AI-generated or reproduced content will be rejected. All PRs must be submitted by a human.
|
||||||
|
|
||||||
* Some other tips to keep in mind:
|
* Some other tips to keep in mind:
|
||||||
* If you'd like to volunteer for someone else's issue, please post a comment on that issue letting us know. (This will allow the maintainers to assign it to you.)
|
* If you'd like to volunteer for someone else's issue, please post a comment on that issue letting us know. (GitHub allows only people who have commented on an issue to be assigned as its owner.)
|
||||||
* Check out our [developer docs](https://docs.netbox.dev/en/stable/development/getting-started/) for tips on setting up your development environment.
|
* Check out our [developer docs](https://docs.netbox.dev/en/stable/development/getting-started/) for tips on setting up your development environment.
|
||||||
* All new functionality must include relevant tests where applicable.
|
* All new functionality must include relevant tests where applicable.
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ colorama
|
|||||||
|
|
||||||
# The Python web framework on which NetBox is built
|
# The Python web framework on which NetBox is built
|
||||||
# https://docs.djangoproject.com/en/stable/releases/
|
# https://docs.djangoproject.com/en/stable/releases/
|
||||||
Django==5.2.*
|
Django==6.0.*
|
||||||
|
|
||||||
# Django middleware which permits cross-domain API requests
|
# Django middleware which permits cross-domain API requests
|
||||||
# https://github.com/adamchainz/django-cors-headers/blob/main/CHANGELOG.rst
|
# https://github.com/adamchainz/django-cors-headers/blob/main/CHANGELOG.rst
|
||||||
@@ -27,9 +27,7 @@ django-graphiql-debug-toolbar
|
|||||||
django-htmx
|
django-htmx
|
||||||
|
|
||||||
# Modified Preorder Tree Traversal (recursive nesting of objects)
|
# Modified Preorder Tree Traversal (recursive nesting of objects)
|
||||||
# https://github.com/django-mptt/django-mptt/blob/main/CHANGELOG.rst
|
django-mptt
|
||||||
# v0.18.0 introduces errant migrations which need to be resolved
|
|
||||||
django-mptt==0.17.0
|
|
||||||
|
|
||||||
# Context managers for PostgreSQL advisory locks
|
# Context managers for PostgreSQL advisory locks
|
||||||
# https://github.com/Xof/django-pglocks/blob/master/CHANGES.txt
|
# https://github.com/Xof/django-pglocks/blob/master/CHANGES.txt
|
||||||
@@ -37,7 +35,9 @@ django-pglocks
|
|||||||
|
|
||||||
# Prometheus metrics library for Django
|
# Prometheus metrics library for Django
|
||||||
# https://github.com/korfuri/django-prometheus/blob/master/CHANGELOG.md
|
# https://github.com/korfuri/django-prometheus/blob/master/CHANGELOG.md
|
||||||
django-prometheus
|
# TODO: 2.4.1 is incompatible with Django>=6.0, but a fixed release is expected
|
||||||
|
# https://github.com/django-commons/django-prometheus/issues/494
|
||||||
|
django-prometheus>=2.4.0,<2.5.0,!=2.4.1
|
||||||
|
|
||||||
# Django caching backend using Redis
|
# Django caching backend using Redis
|
||||||
# https://github.com/jazzband/django-redis/blob/master/CHANGELOG.rst
|
# https://github.com/jazzband/django-redis/blob/master/CHANGELOG.rst
|
||||||
@@ -57,7 +57,8 @@ django-storages
|
|||||||
|
|
||||||
# Abstraction models for rendering and paginating HTML tables
|
# Abstraction models for rendering and paginating HTML tables
|
||||||
# https://github.com/jieter/django-tables2/blob/master/CHANGELOG.md
|
# https://github.com/jieter/django-tables2/blob/master/CHANGELOG.md
|
||||||
django-tables2
|
# See #21902 for upgrading to django-tables2 v2.9+
|
||||||
|
django-tables2<2.9
|
||||||
|
|
||||||
# User-defined tags for objects
|
# User-defined tags for objects
|
||||||
# https://github.com/jazzband/django-taggit/blob/master/CHANGELOG.rst
|
# https://github.com/jazzband/django-taggit/blob/master/CHANGELOG.rst
|
||||||
@@ -70,7 +71,7 @@ django-timezone-field
|
|||||||
# A REST API framework for Django projects
|
# A REST API framework for Django projects
|
||||||
# https://www.django-rest-framework.org/community/release-notes/
|
# https://www.django-rest-framework.org/community/release-notes/
|
||||||
# TODO: Re-evaluate the monkey-patch of get_unique_validators() before upgrading
|
# TODO: Re-evaluate the monkey-patch of get_unique_validators() before upgrading
|
||||||
djangorestframework==3.16.1
|
djangorestframework==3.17.1
|
||||||
|
|
||||||
# Sane and flexible OpenAPI 3 schema generation for Django REST framework.
|
# Sane and flexible OpenAPI 3 schema generation for Django REST framework.
|
||||||
# https://github.com/tfranzel/drf-spectacular/blob/master/CHANGELOG.rst
|
# https://github.com/tfranzel/drf-spectacular/blob/master/CHANGELOG.rst
|
||||||
@@ -173,3 +174,7 @@ tablib
|
|||||||
# Timezone data (required by django-timezone-field on Python 3.9+)
|
# Timezone data (required by django-timezone-field on Python 3.9+)
|
||||||
# https://github.com/python/tzdata/blob/master/NEWS.md
|
# https://github.com/python/tzdata/blob/master/NEWS.md
|
||||||
tzdata
|
tzdata
|
||||||
|
|
||||||
|
# Documentation builder (succeeds mkdocs)
|
||||||
|
# https://github.com/zensical/zensical
|
||||||
|
zensical
|
||||||
|
|||||||
@@ -349,6 +349,7 @@
|
|||||||
"5gbase-t",
|
"5gbase-t",
|
||||||
"10gbase-br-d",
|
"10gbase-br-d",
|
||||||
"10gbase-br-u",
|
"10gbase-br-u",
|
||||||
|
"10gbase-cu",
|
||||||
"10gbase-cx4",
|
"10gbase-cx4",
|
||||||
"10gbase-er",
|
"10gbase-er",
|
||||||
"10gbase-lr",
|
"10gbase-lr",
|
||||||
@@ -367,6 +368,7 @@
|
|||||||
"40gbase-fr4",
|
"40gbase-fr4",
|
||||||
"40gbase-lr4",
|
"40gbase-lr4",
|
||||||
"40gbase-sr4",
|
"40gbase-sr4",
|
||||||
|
"40gbase-sr4-bd",
|
||||||
"50gbase-cr",
|
"50gbase-cr",
|
||||||
"50gbase-er",
|
"50gbase-er",
|
||||||
"50gbase-fr",
|
"50gbase-fr",
|
||||||
@@ -414,9 +416,13 @@
|
|||||||
"800gbase-dr8",
|
"800gbase-dr8",
|
||||||
"800gbase-sr8",
|
"800gbase-sr8",
|
||||||
"800gbase-vr8",
|
"800gbase-vr8",
|
||||||
|
"1.6tbase-cr8",
|
||||||
|
"1.6tbase-dr8",
|
||||||
|
"1.6tbase-dr8-2",
|
||||||
"100base-x-sfp",
|
"100base-x-sfp",
|
||||||
"1000base-x-gbic",
|
"1000base-x-gbic",
|
||||||
"1000base-x-sfp",
|
"1000base-x-sfp",
|
||||||
|
"2.5gbase-x-sfp",
|
||||||
"10gbase-x-sfpp",
|
"10gbase-x-sfpp",
|
||||||
"10gbase-x-xenpak",
|
"10gbase-x-xenpak",
|
||||||
"10gbase-x-xfp",
|
"10gbase-x-xfp",
|
||||||
@@ -446,6 +452,9 @@
|
|||||||
"400gbase-x-osfp-rhs",
|
"400gbase-x-osfp-rhs",
|
||||||
"800gbase-x-osfp",
|
"800gbase-x-osfp",
|
||||||
"800gbase-x-qsfpdd",
|
"800gbase-x-qsfpdd",
|
||||||
|
"1.6tbase-x-osfp1600",
|
||||||
|
"1.6tbase-x-osfp1600-rhs",
|
||||||
|
"1.6tbase-x-qsfpdd1600",
|
||||||
"1000base-kx",
|
"1000base-kx",
|
||||||
"2.5gbase-kx",
|
"2.5gbase-kx",
|
||||||
"5gbase-kr",
|
"5gbase-kr",
|
||||||
@@ -457,6 +466,7 @@
|
|||||||
"100gbase-kp4",
|
"100gbase-kp4",
|
||||||
"100gbase-kr2",
|
"100gbase-kr2",
|
||||||
"100gbase-kr4",
|
"100gbase-kr4",
|
||||||
|
"1.6tbase-kr8",
|
||||||
"ieee802.11a",
|
"ieee802.11a",
|
||||||
"ieee802.11g",
|
"ieee802.11g",
|
||||||
"ieee802.11n",
|
"ieee802.11n",
|
||||||
|
|||||||
9684
contrib/openapi.json
9684
contrib/openapi.json
File diff suppressed because one or more lines are too long
18
docs/_theme/partials/copyright.html
vendored
18
docs/_theme/partials/copyright.html
vendored
@@ -1,18 +0,0 @@
|
|||||||
<div class="md-copyright">
|
|
||||||
{% if config.copyright %}
|
|
||||||
<div class="md-copyright__highlight">
|
|
||||||
{{ config.copyright }}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% if not config.extra.generator == false %}
|
|
||||||
Made with
|
|
||||||
<a href="https://squidfunk.github.io/mkdocs-material/" target="_blank" rel="noopener">
|
|
||||||
Material for MkDocs
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% if not config.extra.build_public %}
|
|
||||||
<div class="md-copyright">
|
|
||||||
ℹ️ Documentation is being served locally
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
@@ -20,7 +20,9 @@ There are four core actions that can be permitted for each type of object within
|
|||||||
* **Change** - Modify an existing object
|
* **Change** - Modify an existing object
|
||||||
* **Delete** - Delete an existing object
|
* **Delete** - Delete an existing object
|
||||||
|
|
||||||
In addition to these, permissions can also grant custom actions that may be required by a specific model or plugin. For example, the `run` permission for scripts allows a user to execute custom scripts. These can be specified when granting a permission in the "additional actions" field.
|
In addition to these, permissions can also grant custom actions that may be required by a specific model or plugin. For example, the `sync` action for data sources allows a user to synchronize data from a remote source, and the `render_config` action for devices and virtual machines allows rendering configuration templates.
|
||||||
|
|
||||||
|
Some models have registered actions that appear as checkboxes in the "Actions" section when creating or editing a permission. These are shown in a flat list alongside the built-in CRUD actions. Additional actions (such as those not yet registered by a plugin, or for backwards compatibility) can be entered manually in the "Additional actions" field.
|
||||||
|
|
||||||
!!! note
|
!!! note
|
||||||
Internally, all actions granted by a permission (both built-in and custom) are stored as strings in an array field named `actions`.
|
Internally, all actions granted by a permission (both built-in and custom) are stored as strings in an array field named `actions`.
|
||||||
|
|||||||
@@ -4,9 +4,9 @@
|
|||||||
|
|
||||||
Default: `False`
|
Default: `False`
|
||||||
|
|
||||||
This setting enables debugging. Debugging should be enabled only during development or troubleshooting. Note that only
|
This setting enables debugging and displays a debugging toolbar in the user interface. Debugging should be enabled only during development or troubleshooting.
|
||||||
clients which access NetBox from a recognized [internal IP address](./system.md#internal_ips) will see debugging tools in the user
|
|
||||||
interface.
|
Note that the debugging toolbar will be displayed only for requests originating from [internal IP addresses](./system.md#internal_ips), if defined. If no internal IPs are defined, the toolbar will be displayed for all requests.
|
||||||
|
|
||||||
!!! warning
|
!!! warning
|
||||||
Never enable debugging on a production system, as it can expose sensitive data to unauthenticated users and impose a
|
Never enable debugging on a production system, as it can expose sensitive data to unauthenticated users and impose a
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ Some configuration parameters are primarily controlled via NetBox's admin interf
|
|||||||
* [`BANNER_BOTTOM`](./miscellaneous.md#banner_bottom)
|
* [`BANNER_BOTTOM`](./miscellaneous.md#banner_bottom)
|
||||||
* [`BANNER_LOGIN`](./miscellaneous.md#banner_login)
|
* [`BANNER_LOGIN`](./miscellaneous.md#banner_login)
|
||||||
* [`BANNER_TOP`](./miscellaneous.md#banner_top)
|
* [`BANNER_TOP`](./miscellaneous.md#banner_top)
|
||||||
|
* [`CHANGELOG_RETAIN_CREATE_LAST_UPDATE`](./miscellaneous.md#changelog_retain_create_last_update)
|
||||||
* [`CHANGELOG_RETENTION`](./miscellaneous.md#changelog_retention)
|
* [`CHANGELOG_RETENTION`](./miscellaneous.md#changelog_retention)
|
||||||
* [`CUSTOM_VALIDATORS`](./data-validation.md#custom_validators)
|
* [`CUSTOM_VALIDATORS`](./data-validation.md#custom_validators)
|
||||||
* [`DEFAULT_USER_PREFERENCES`](./default-values.md#default_user_preferences)
|
* [`DEFAULT_USER_PREFERENCES`](./default-values.md#default_user_preferences)
|
||||||
|
|||||||
@@ -73,6 +73,23 @@ This data enables the project maintainers to estimate how many NetBox deployment
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## CHANGELOG_RETAIN_CREATE_LAST_UPDATE
|
||||||
|
|
||||||
|
!!! tip "Dynamic Configuration Parameter"
|
||||||
|
|
||||||
|
Default: `False`
|
||||||
|
|
||||||
|
When pruning expired changelog entries (per `CHANGELOG_RETENTION`), retain each non-deleted object's original `create`
|
||||||
|
change record and its most recent `update` change record. If an object has a `delete` change record, its changelog
|
||||||
|
entries are pruned normally according to `CHANGELOG_RETENTION`.
|
||||||
|
|
||||||
|
!!! note
|
||||||
|
For objects without a `delete` change record, the original `create` record and most recent `update` record are
|
||||||
|
exempt from pruning. All other changelog records (including intermediate `update` records and all `delete` records)
|
||||||
|
remain subject to pruning per `CHANGELOG_RETENTION`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## CHANGELOG_RETENTION
|
## CHANGELOG_RETENTION
|
||||||
|
|
||||||
!!! tip "Dynamic Configuration Parameter"
|
!!! tip "Dynamic Configuration Parameter"
|
||||||
@@ -220,6 +237,14 @@ This parameter defines the URL of the repository that will be checked for new Ne
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## RQ
|
||||||
|
|
||||||
|
Default: `{}` (Empty)
|
||||||
|
|
||||||
|
This is a wrapper for passing global configuration parameters to [Django RQ](https://github.com/rq/django-rq) to customize its behavior. It is employed within NetBox primarily to alter conditions during testing.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## RQ_DEFAULT_TIMEOUT
|
## RQ_DEFAULT_TIMEOUT
|
||||||
|
|
||||||
Default: `300`
|
Default: `300`
|
||||||
|
|||||||
@@ -200,6 +200,48 @@ REDIS = {
|
|||||||
!!! note
|
!!! note
|
||||||
It is permissible to use Sentinel for only one database and not the other.
|
It is permissible to use Sentinel for only one database and not the other.
|
||||||
|
|
||||||
|
### SSL Configuration
|
||||||
|
|
||||||
|
If you need to configure SSL/TLS for Redis beyond the basic `SSL`, `CA_CERT_PATH`, and `INSECURE_SKIP_TLS_VERIFY` options (for example, client certificates, a specific TLS version, or custom ciphers), you can pass additional parameters via the `KWARGS` key in either the `tasks` or `caching` subsection.
|
||||||
|
|
||||||
|
NetBox already maps `CA_CERT_PATH` to `ssl_ca_certs` and (for caching) `INSECURE_SKIP_TLS_VERIFY` to `ssl_cert_reqs`; only add `KWARGS` when you need to override or extend those settings (for example, to supply client certificates or restrict TLS version or ciphers).
|
||||||
|
|
||||||
|
* `KWARGS` - Optional dictionary of additional SSL/TLS (or other) parameters passed to the Redis client. These are passed directly to the underlying Redis client: for `tasks` to [redis-py](https://redis-py.readthedocs.io/en/stable/connections.html), and for `caching` to the [django-redis](https://github.com/jazzband/django-redis#configure-as-cache-backend) connection pool.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```python
|
||||||
|
REDIS = {
|
||||||
|
'tasks': {
|
||||||
|
'HOST': 'redis.example.com',
|
||||||
|
'PORT': 1234,
|
||||||
|
'SSL': True,
|
||||||
|
'CA_CERT_PATH': '/etc/ssl/certs/ca.crt',
|
||||||
|
'KWARGS': {
|
||||||
|
'ssl_certfile': '/path/to/client-cert.pem',
|
||||||
|
'ssl_keyfile': '/path/to/client-key.pem',
|
||||||
|
'ssl_min_version': ssl.TLSVersion.TLSv1_2,
|
||||||
|
'ssl_ciphers': 'HIGH:!aNULL',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'caching': {
|
||||||
|
'HOST': 'redis.example.com',
|
||||||
|
'PORT': 1234,
|
||||||
|
'SSL': True,
|
||||||
|
'CA_CERT_PATH': '/etc/ssl/certs/ca.crt',
|
||||||
|
'KWARGS': {
|
||||||
|
'ssl_certfile': '/path/to/client-cert.pem',
|
||||||
|
'ssl_keyfile': '/path/to/client-key.pem',
|
||||||
|
'ssl_min_version': ssl.TLSVersion.TLSv1_2,
|
||||||
|
'ssl_ciphers': 'HIGH:!aNULL',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! note
|
||||||
|
If you use `ssl.TLSVersion` in your configuration (e.g. `ssl_min_version`), add `import ssl` at the top of your configuration file.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## SECRET_KEY
|
## SECRET_KEY
|
||||||
|
|||||||
@@ -105,6 +105,13 @@ A list of IP addresses recognized as internal to the system, used to control the
|
|||||||
example, the debugging toolbar will be viewable only when a client is accessing NetBox from one of the listed IP
|
example, the debugging toolbar will be viewable only when a client is accessing NetBox from one of the listed IP
|
||||||
addresses (and [`DEBUG`](./development.md#debug) is `True`).
|
addresses (and [`DEBUG`](./development.md#debug) is `True`).
|
||||||
|
|
||||||
|
!!! info "New in NetBox v4.6"
|
||||||
|
Setting this parameter to an empty list will enable the toolbar for all requests provided debugging is enabled:
|
||||||
|
|
||||||
|
```python
|
||||||
|
INTERNAL_IPS = []
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## ISOLATED_DEPLOYMENT
|
## ISOLATED_DEPLOYMENT
|
||||||
@@ -241,21 +248,49 @@ STORAGES = {
|
|||||||
|
|
||||||
Within the `STORAGES` dictionary, `"default"` is used for image uploads, "staticfiles" is for static files and `"scripts"` is used for custom scripts.
|
Within the `STORAGES` dictionary, `"default"` is used for image uploads, "staticfiles" is for static files and `"scripts"` is used for custom scripts.
|
||||||
|
|
||||||
If using a remote storage like S3, define the config as `STORAGES[key]["OPTIONS"]` for each storage item as needed. For example:
|
If using a remote storage such as S3 or an S3-compatible service, define the configuration as `STORAGES[key]["OPTIONS"]` for each storage item as needed. For example:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
STORAGES = {
|
STORAGES = {
|
||||||
"scripts": {
|
'default': {
|
||||||
"BACKEND": "storages.backends.s3boto3.S3Boto3Storage",
|
'BACKEND': 'storages.backends.s3.S3Storage',
|
||||||
"OPTIONS": {
|
'OPTIONS': {
|
||||||
'access_key': 'access key',
|
'bucket_name': 'netbox',
|
||||||
|
'access_key': 'access key',
|
||||||
'secret_key': 'secret key',
|
'secret_key': 'secret key',
|
||||||
"allow_overwrite": True,
|
'region_name': 'us-east-1',
|
||||||
}
|
'endpoint_url': 'https://s3.example.com',
|
||||||
},
|
'location': 'media/',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'staticfiles': {
|
||||||
|
'BACKEND': 'storages.backends.s3.S3Storage',
|
||||||
|
'OPTIONS': {
|
||||||
|
'bucket_name': 'netbox',
|
||||||
|
'access_key': 'access key',
|
||||||
|
'secret_key': 'secret key',
|
||||||
|
'region_name': 'us-east-1',
|
||||||
|
'endpoint_url': 'https://s3.example.com',
|
||||||
|
'location': 'static/',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'scripts': {
|
||||||
|
'BACKEND': 'storages.backends.s3.S3Storage',
|
||||||
|
'OPTIONS': {
|
||||||
|
'bucket_name': 'netbox',
|
||||||
|
'access_key': 'access key',
|
||||||
|
'secret_key': 'secret key',
|
||||||
|
'region_name': 'us-east-1',
|
||||||
|
'endpoint_url': 'https://s3.example.com',
|
||||||
|
'location': 'scripts/',
|
||||||
|
'file_overwrite': True,
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`bucket_name` is required for `S3Storage`. When using an S3-compatible service, set `region_name` and `endpoint_url` according to your provider.
|
||||||
|
|
||||||
The specific configuration settings for each storage backend can be found in the [django-storages documentation](https://django-storages.readthedocs.io/en/latest/index.html).
|
The specific configuration settings for each storage backend can be found in the [django-storages documentation](https://django-storages.readthedocs.io/en/latest/index.html).
|
||||||
|
|
||||||
!!! note
|
!!! note
|
||||||
@@ -279,6 +314,7 @@ STORAGES = {
|
|||||||
'bucket_name': os.environ.get('AWS_STORAGE_BUCKET_NAME'),
|
'bucket_name': os.environ.get('AWS_STORAGE_BUCKET_NAME'),
|
||||||
'access_key': os.environ.get('AWS_S3_ACCESS_KEY_ID'),
|
'access_key': os.environ.get('AWS_S3_ACCESS_KEY_ID'),
|
||||||
'secret_key': os.environ.get('AWS_S3_SECRET_ACCESS_KEY'),
|
'secret_key': os.environ.get('AWS_S3_SECRET_ACCESS_KEY'),
|
||||||
|
'region_name': os.environ.get('AWS_S3_REGION_NAME'),
|
||||||
'endpoint_url': os.environ.get('AWS_S3_ENDPOINT_URL'),
|
'endpoint_url': os.environ.get('AWS_S3_ENDPOINT_URL'),
|
||||||
'location': 'media/',
|
'location': 'media/',
|
||||||
}
|
}
|
||||||
@@ -289,6 +325,7 @@ STORAGES = {
|
|||||||
'bucket_name': os.environ.get('AWS_STORAGE_BUCKET_NAME'),
|
'bucket_name': os.environ.get('AWS_STORAGE_BUCKET_NAME'),
|
||||||
'access_key': os.environ.get('AWS_S3_ACCESS_KEY_ID'),
|
'access_key': os.environ.get('AWS_S3_ACCESS_KEY_ID'),
|
||||||
'secret_key': os.environ.get('AWS_S3_SECRET_ACCESS_KEY'),
|
'secret_key': os.environ.get('AWS_S3_SECRET_ACCESS_KEY'),
|
||||||
|
'region_name': os.environ.get('AWS_S3_REGION_NAME'),
|
||||||
'endpoint_url': os.environ.get('AWS_S3_ENDPOINT_URL'),
|
'endpoint_url': os.environ.get('AWS_S3_ENDPOINT_URL'),
|
||||||
'location': 'static/',
|
'location': 'static/',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ NetBox supports limited custom validation for custom field values. Following are
|
|||||||
* Text: Regular expression (optional)
|
* Text: Regular expression (optional)
|
||||||
* Integer: Minimum and/or maximum value (optional)
|
* Integer: Minimum and/or maximum value (optional)
|
||||||
* Selection: Must exactly match one of the prescribed choices
|
* Selection: Must exactly match one of the prescribed choices
|
||||||
|
* JSON: Must adhere to the defined validation schema (if any)
|
||||||
|
|
||||||
### Custom Selection Fields
|
### Custom Selection Fields
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ They can also be used as a mechanism for validating the integrity of data within
|
|||||||
Custom scripts are Python code which exists outside the NetBox code base, so they can be updated and changed without interfering with the core NetBox installation. And because they're completely custom, there is no inherent limitation on what a script can accomplish.
|
Custom scripts are Python code which exists outside the NetBox code base, so they can be updated and changed without interfering with the core NetBox installation. And because they're completely custom, there is no inherent limitation on what a script can accomplish.
|
||||||
|
|
||||||
!!! danger "Only install trusted scripts"
|
!!! danger "Only install trusted scripts"
|
||||||
Custom scripts have unrestricted access to change anything in the databse and are inherently unsafe and should only be installed and run from trusted sources. You should also review and set permissions for who can run scripts if the script can modify any data.
|
Custom scripts have unrestricted access to change anything in the database and are inherently unsafe and should only be installed and run from trusted sources. You should also review and set permissions for who can run scripts if the script can modify any data.
|
||||||
|
|
||||||
|
|
||||||
## Writing Custom Scripts
|
## Writing Custom Scripts
|
||||||
|
|
||||||
@@ -214,6 +215,7 @@ if obj.pk and hasattr(obj, 'snapshot'):
|
|||||||
obj.snapshot()
|
obj.snapshot()
|
||||||
|
|
||||||
obj.property = "New Value"
|
obj.property = "New Value"
|
||||||
|
obj._changelog_message = 'Example Message Text' # Optional
|
||||||
obj.full_clean()
|
obj.full_clean()
|
||||||
obj.save()
|
obj.save()
|
||||||
```
|
```
|
||||||
@@ -382,6 +384,18 @@ A calendar date. Returns a `datetime.date` object.
|
|||||||
|
|
||||||
A complete date & time. Returns a `datetime.datetime` object.
|
A complete date & time. Returns a `datetime.datetime` object.
|
||||||
|
|
||||||
|
## Uploading Scripts via the API
|
||||||
|
|
||||||
|
Script modules can be uploaded to NetBox via the REST API by sending a `multipart/form-data` POST request to `/api/extras/scripts/upload/`. The caller must have the `extras.add_scriptmodule` and `core.add_managedfile` permissions.
|
||||||
|
|
||||||
|
```no-highlight
|
||||||
|
curl -X POST \
|
||||||
|
-H "Authorization: Token $TOKEN" \
|
||||||
|
-H "Accept: application/json; indent=4" \
|
||||||
|
-F "file=@/path/to/myscript.py" \
|
||||||
|
http://netbox/api/extras/scripts/upload/
|
||||||
|
```
|
||||||
|
|
||||||
## Running Custom Scripts
|
## Running Custom Scripts
|
||||||
|
|
||||||
!!! note
|
!!! note
|
||||||
|
|||||||
@@ -32,6 +32,9 @@ Core model features are listed in the [features matrix](./models.md#features-mat
|
|||||||
|
|
||||||
### `models`
|
### `models`
|
||||||
|
|
||||||
|
!!! warning "Deprecated"
|
||||||
|
Usage of this key has been deprecated and will be removed in NetBox v4.7. Use `ObjectType.objects.public()` to find registered models.
|
||||||
|
|
||||||
This key lists all models which have been registered in NetBox which are not designated for private use. (Setting `_netbox_private` to True on a model excludes it from this list.) As with individual features under `model_features`, models are organized by app label.
|
This key lists all models which have been registered in NetBox which are not designated for private use. (Setting `_netbox_private` to True on a model excludes it from this list.) As with individual features under `model_features`, models are organized by app label.
|
||||||
|
|
||||||
### `plugins`
|
### `plugins`
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ NetBox uses [`pre-commit`](https://pre-commit.com/) to automatically validate co
|
|||||||
* Run the `ruff` Python linter
|
* Run the `ruff` Python linter
|
||||||
* Run Django's internal system check
|
* Run Django's internal system check
|
||||||
* Check for missing database migrations
|
* Check for missing database migrations
|
||||||
* Validate any changes to the documentation with `mkdocs`
|
* Validate any changes to the documentation with `zensical`
|
||||||
* Validate Typescript & Sass styling with `yarn`
|
* Validate Typescript & Sass styling with `yarn`
|
||||||
* Ensure that any modified static front end assets have been recompiled
|
* Ensure that any modified static front end assets have been recompiled
|
||||||
|
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ These are considered the "core" application models which are used to model netwo
|
|||||||
* [core.DataSource](../models/core/datasource.md)
|
* [core.DataSource](../models/core/datasource.md)
|
||||||
* [core.Job](../models/core/job.md)
|
* [core.Job](../models/core/job.md)
|
||||||
* [dcim.Cable](../models/dcim/cable.md)
|
* [dcim.Cable](../models/dcim/cable.md)
|
||||||
|
* [dcim.CableBundle](../models/dcim/cablebundle.md)
|
||||||
* [dcim.Device](../models/dcim/device.md)
|
* [dcim.Device](../models/dcim/device.md)
|
||||||
* [dcim.DeviceType](../models/dcim/devicetype.md)
|
* [dcim.DeviceType](../models/dcim/devicetype.md)
|
||||||
* [dcim.Module](../models/dcim/module.md)
|
* [dcim.Module](../models/dcim/module.md)
|
||||||
@@ -73,6 +74,7 @@ These are considered the "core" application models which are used to model netwo
|
|||||||
* [tenancy.Tenant](../models/tenancy/tenant.md)
|
* [tenancy.Tenant](../models/tenancy/tenant.md)
|
||||||
* [virtualization.Cluster](../models/virtualization/cluster.md)
|
* [virtualization.Cluster](../models/virtualization/cluster.md)
|
||||||
* [virtualization.VirtualMachine](../models/virtualization/virtualmachine.md)
|
* [virtualization.VirtualMachine](../models/virtualization/virtualmachine.md)
|
||||||
|
* [virtualization.VirtualMachineType](../models/virtualization/virtualmachinetype.md)
|
||||||
* [vpn.IKEPolicy](../models/vpn/ikepolicy.md)
|
* [vpn.IKEPolicy](../models/vpn/ikepolicy.md)
|
||||||
* [vpn.IKEProposal](../models/vpn/ikeproposal.md)
|
* [vpn.IKEProposal](../models/vpn/ikeproposal.md)
|
||||||
* [vpn.IPSecPolicy](../models/vpn/ipsecpolicy.md)
|
* [vpn.IPSecPolicy](../models/vpn/ipsecpolicy.md)
|
||||||
@@ -92,6 +94,7 @@ Organization models are used to organize and classify primary models.
|
|||||||
* [dcim.DeviceRole](../models/dcim/devicerole.md)
|
* [dcim.DeviceRole](../models/dcim/devicerole.md)
|
||||||
* [dcim.Manufacturer](../models/dcim/manufacturer.md)
|
* [dcim.Manufacturer](../models/dcim/manufacturer.md)
|
||||||
* [dcim.Platform](../models/dcim/platform.md)
|
* [dcim.Platform](../models/dcim/platform.md)
|
||||||
|
* [dcim.RackGroup](../models/dcim/rackgroup.md)
|
||||||
* [dcim.RackRole](../models/dcim/rackrole.md)
|
* [dcim.RackRole](../models/dcim/rackrole.md)
|
||||||
* [ipam.ASNRange](../models/ipam/asnrange.md)
|
* [ipam.ASNRange](../models/ipam/asnrange.md)
|
||||||
* [ipam.RIR](../models/ipam/rir.md)
|
* [ipam.RIR](../models/ipam/rir.md)
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ If a new Django release is adopted or other major dependencies (Python, PostgreS
|
|||||||
Start the documentation server and navigate to the current version of the installation docs:
|
Start the documentation server and navigate to the current version of the installation docs:
|
||||||
|
|
||||||
```no-highlight
|
```no-highlight
|
||||||
mkdocs serve
|
zensical serve
|
||||||
```
|
```
|
||||||
|
|
||||||
Follow these instructions to perform a new installation of NetBox in a temporary environment. This process must not be automated: The goal of this step is to catch any errors or omissions in the documentation and ensure that it is kept up to date for each release. Make any necessary changes to the documentation before proceeding with the release.
|
Follow these instructions to perform a new installation of NetBox in a temporary environment. This process must not be automated: The goal of this step is to catch any errors or omissions in the documentation and ensure that it is kept up to date for each release. Make any necessary changes to the documentation before proceeding with the release.
|
||||||
@@ -168,6 +168,14 @@ Update the static OpenAPI schema definition at `contrib/openapi.json` with the m
|
|||||||
./manage.py spectacular --format openapi-json > ../contrib/openapi.json
|
./manage.py spectacular --format openapi-json > ../contrib/openapi.json
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Update Development Dependencies
|
||||||
|
|
||||||
|
Keep development tooling versions consistent across the project. If you upgrade a dev-only dependency, update all places where it’s pinned so local tooling and CI run the same versions.
|
||||||
|
|
||||||
|
* Ruff:
|
||||||
|
* `.pre-commit-config.yaml`
|
||||||
|
* `.github/workflows/ci.yml`
|
||||||
|
|
||||||
### Submit a Pull Request
|
### Submit a Pull Request
|
||||||
|
|
||||||
Commit the above changes and submit a pull request titled **"Release vX.Y.Z"** to merge the current release branch (e.g. `release-vX.Y.Z`) into `main`. Copy the documented release notes into the pull request's body.
|
Commit the above changes and submit a pull request titled **"Release vX.Y.Z"** to merge the current release branch (e.g. `release-vX.Y.Z`) into `main`. Copy the documented release notes into the pull request's body.
|
||||||
|
|||||||
@@ -34,7 +34,8 @@ The following rules are ignored when linting.
|
|||||||
|
|
||||||
##### [E501](https://docs.astral.sh/ruff/rules/line-too-long/): Line too long
|
##### [E501](https://docs.astral.sh/ruff/rules/line-too-long/): Line too long
|
||||||
|
|
||||||
NetBox does not enforce a hard restriction on line length, although a maximum length of 120 characters is strongly encouraged for Python code where possible. The maximum length does not apply to HTML templates or to automatically generated code (e.g. database migrations).
|
NetBox enforces a maximum line length of 120 characters for Python code using Ruff (E501).
|
||||||
|
The maximum length does not apply to HTML templates or to automatically generated code (e.g. database migrations).
|
||||||
|
|
||||||
##### [F403](https://docs.astral.sh/ruff/rules/undefined-local-with-import-star/): Undefined local with import star
|
##### [F403](https://docs.astral.sh/ruff/rules/undefined-local-with-import-star/): Undefined local with import star
|
||||||
|
|
||||||
@@ -47,6 +48,14 @@ Wildcard imports (for example, `from .constants import *`) are acceptable under
|
|||||||
|
|
||||||
The justification for ignoring this rule is the same as F403 above.
|
The justification for ignoring this rule is the same as F403 above.
|
||||||
|
|
||||||
|
##### [RET504](https://docs.astral.sh/ruff/rules/unnecessary-assign/): Unnecessary assign
|
||||||
|
|
||||||
|
There are multiple instances where it is more readable and clearer to first assign to a variable and then return it.
|
||||||
|
|
||||||
|
##### [UP032](https://docs.astral.sh/ruff/rules/f-string/): f-string
|
||||||
|
|
||||||
|
For localizable strings, it is necessary to not use the `f-string` syntax, as Django's translation functions (e.g. `gettext_lazy`) require plain string literals.
|
||||||
|
|
||||||
### Introducing New Dependencies
|
### Introducing New Dependencies
|
||||||
|
|
||||||
The introduction of a new dependency is best avoided unless it is absolutely necessary. For small features, it's generally preferable to replicate functionality within the NetBox code base rather than to introduce reliance on an external project. This reduces both the burden of tracking new releases and our exposure to outside bugs and supply chain attacks.
|
The introduction of a new dependency is best avoided unless it is absolutely necessary. For small features, it's generally preferable to replicate functionality within the NetBox code base rather than to introduce reliance on an external project. This reduces both the burden of tracking new releases and our exposure to outside bugs and supply chain attacks.
|
||||||
|
|||||||
@@ -5,10 +5,6 @@ img {
|
|||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.md-content img {
|
|
||||||
background-color: rgba(255, 255, 255, 0.64);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Tables */
|
/* Tables */
|
||||||
table {
|
table {
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
|
|||||||
@@ -1,26 +1,44 @@
|
|||||||
# Virtualization
|
# Virtualization
|
||||||
|
|
||||||
Virtual machines and clusters can be modeled in NetBox alongside physical infrastructure. IP addresses and other resources are assigned to these objects just like physical objects, providing a seamless integration between physical and virtual networks.
|
Virtual machines, clusters, and standalone hypervisors can be modeled in NetBox alongside physical infrastructure. IP addresses and other resources are assigned to these objects just like physical objects, providing a seamless integration between physical and virtual networks.
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
flowchart TD
|
flowchart TD
|
||||||
ClusterGroup & ClusterType --> Cluster
|
ClusterGroup & ClusterType --> Cluster
|
||||||
|
VirtualMachineType --> VirtualMachine
|
||||||
|
Device --> VirtualMachine
|
||||||
Cluster --> VirtualMachine
|
Cluster --> VirtualMachine
|
||||||
Platform --> VirtualMachine
|
Platform --> VirtualMachine
|
||||||
VirtualMachine --> VMInterface
|
VirtualMachine --> VMInterface
|
||||||
|
|
||||||
click Cluster "../../models/virtualization/cluster/"
|
click Cluster "../../models/virtualization/cluster/"
|
||||||
click ClusterGroup "../../models/virtualization/clustergroup/"
|
click ClusterGroup "../../models/virtualization/clustergroup/"
|
||||||
click ClusterType "../../models/virtualization/clustertype/"
|
click ClusterType "../../models/virtualization/clustertype/"
|
||||||
click Platform "../../models/dcim/platform/"
|
click VirtualMachineType "../../models/virtualization/virtualmachinetype/"
|
||||||
click VirtualMachine "../../models/virtualization/virtualmachine/"
|
click Device "../../models/dcim/device/"
|
||||||
click VMInterface "../../models/virtualization/vminterface/"
|
click Platform "../../models/dcim/platform/"
|
||||||
|
click VirtualMachine "../../models/virtualization/virtualmachine/"
|
||||||
|
click VMInterface "../../models/virtualization/vminterface/"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Clusters
|
## Clusters
|
||||||
|
|
||||||
A cluster is one or more physical host devices on which virtual machines can run. Each cluster must have a type and operational status, and may be assigned to a group. (Both types and groups are user-defined.) Each cluster may designate one or more devices as hosts, however this is optional.
|
A cluster is one or more physical host devices on which virtual machines can run.
|
||||||
|
|
||||||
|
Each cluster must have a type and operational status, and may be assigned to a group. (Both types and groups are user-defined.) Each cluster may designate one or more devices as hosts, however this is optional.
|
||||||
|
|
||||||
|
## Virtual Machine Types
|
||||||
|
|
||||||
|
A virtual machine type provides reusable classification for virtual machines and can define create-time defaults for platform, vCPUs, and memory. This is useful when multiple virtual machines share a common sizing or profile while still allowing per-instance overrides after creation.
|
||||||
|
|
||||||
## Virtual Machines
|
## Virtual Machines
|
||||||
|
|
||||||
A virtual machine is a virtualized compute instance. These behave in NetBox very similarly to device objects, but without any physical attributes. For example, a VM may have interfaces assigned to it with IP addresses and VLANs, however its interfaces cannot be connected via cables (because they are virtual). Each VM may also define its compute, memory, and storage resources as well.
|
A virtual machine is a virtualized compute instance. These behave in NetBox very similarly to device objects, but without any physical attributes.
|
||||||
|
|
||||||
|
For example, a VM may have interfaces assigned to it with IP addresses and VLANs, however its interfaces cannot be connected via cables (because they are virtual). Each VM may define its compute, memory, and storage resources as well. A VM can optionally be assigned a [virtual machine type](../models/virtualization/virtualmachinetype.md) to classify it and provide default values for selected attributes at creation time.
|
||||||
|
|
||||||
|
A VM can be placed in one of three ways:
|
||||||
|
|
||||||
|
- Assigned to a site alone for logical grouping.
|
||||||
|
- Assigned to a cluster and optionally pinned to a specific host device within that cluster.
|
||||||
|
- Assigned directly to a standalone device that does not belong to any cluster.
|
||||||
|
|||||||
@@ -341,7 +341,7 @@ When retrieving devices and virtual machines via the REST API, each will include
|
|||||||
|
|
||||||
## Pagination
|
## Pagination
|
||||||
|
|
||||||
API responses which contain a list of many objects will be paginated for efficiency. The root JSON object returned by a list endpoint contains the following attributes:
|
API responses which contain a list of many objects will be paginated for efficiency. NetBox employs offset-based pagination by default, which forms a page by skipping the number of objects indicated by the `offset` URL parameter. The root JSON object returned by a list endpoint contains the following attributes:
|
||||||
|
|
||||||
* `count`: The total number of all objects matching the query
|
* `count`: The total number of all objects matching the query
|
||||||
* `next`: A hyperlink to the next page of results (if applicable)
|
* `next`: A hyperlink to the next page of results (if applicable)
|
||||||
@@ -398,6 +398,49 @@ The maximum number of objects that can be returned is limited by the [`MAX_PAGE_
|
|||||||
!!! warning
|
!!! warning
|
||||||
Disabling the page size limit introduces a potential for very resource-intensive requests, since one API request can effectively retrieve an entire table from the database.
|
Disabling the page size limit introduces a potential for very resource-intensive requests, since one API request can effectively retrieve an entire table from the database.
|
||||||
|
|
||||||
|
### Cursor-Based Pagination
|
||||||
|
|
||||||
|
For large datasets, offset-based pagination can become inefficient because the database must scan all rows up to the offset. As an alternative, cursor-based pagination uses the `start` query parameter to filter results by primary key (PK), enabling efficient keyset pagination.
|
||||||
|
|
||||||
|
To use cursor-based pagination, pass `start` (the minimum PK value) and `limit` (the page size):
|
||||||
|
|
||||||
|
```
|
||||||
|
http://netbox/api/dcim/devices/?start=0&limit=100
|
||||||
|
```
|
||||||
|
|
||||||
|
This returns objects with an `id` greater than or equal to zero, ordered by PK, limited to 100 results. Below is an example showing an arbitrary `start` value.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"count": null,
|
||||||
|
"next": "http://netbox/api/dcim/devices/?start=356&limit=100",
|
||||||
|
"previous": null,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"id": 109,
|
||||||
|
"name": "dist-router07",
|
||||||
|
...
|
||||||
|
},
|
||||||
|
...
|
||||||
|
{
|
||||||
|
"id": 356,
|
||||||
|
"name": "acc-switch492",
|
||||||
|
...
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
To iterate through all results, use the `id` of the last object in each response plus one as the `start` value for the next request. Continue until `next` is null.
|
||||||
|
|
||||||
|
!!! info
|
||||||
|
Some important differences from offset-based pagination:
|
||||||
|
|
||||||
|
* `start` and `offset` are **mutually exclusive**; specifying both will result in a 400 error.
|
||||||
|
* Results are always ordered by primary key when using `start`. This is required to ensure deterministic behavior.
|
||||||
|
* `count` is always `null` in cursor mode, as counting all matching rows would partially negate its performance benefit.
|
||||||
|
* `previous` is always `null`: cursor-based pagination supports only forward navigation.
|
||||||
|
|
||||||
## Interacting with Objects
|
## Interacting with Objects
|
||||||
|
|
||||||
### Retrieving Multiple Objects
|
### Retrieving Multiple Objects
|
||||||
|
|||||||
@@ -23,13 +23,23 @@ For example, you might create a NetBox webhook to [trigger a Slack message](http
|
|||||||
|
|
||||||
The following data is available as context for Jinja2 templates:
|
The following data is available as context for Jinja2 templates:
|
||||||
|
|
||||||
* `event` - The type of event which triggered the webhook: created, updated, or deleted.
|
* `event` - The type of event which triggered the webhook: `created`, `updated`, or `deleted`.
|
||||||
* `model` - The NetBox model which triggered the change.
|
|
||||||
* `timestamp` - The time at which the event occurred (in [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) format).
|
* `timestamp` - The time at which the event occurred (in [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) format).
|
||||||
* `username` - The name of the user account associated with the change.
|
* `object_type` - The NetBox model which triggered the change in the form `app_label.model_name`.
|
||||||
* `request_id` - The unique request ID. This may be used to correlate multiple changes associated with a single request.
|
* `request` - Data about the triggering request (if available).
|
||||||
|
* `request.id` - The UUID associated with the request
|
||||||
|
* `request.method` - The HTTP method (e.g. `GET` or `POST`)
|
||||||
|
* `request.path` - The URL path (ex: `/dcim/sites/123/edit/`)
|
||||||
|
* `request.user` - The name of the authenticated user who made the request (if available)
|
||||||
* `data` - A detailed representation of the object in its current state. This is typically equivalent to the model's representation in NetBox's REST API.
|
* `data` - A detailed representation of the object in its current state. This is typically equivalent to the model's representation in NetBox's REST API.
|
||||||
* `snapshots` - Minimal "snapshots" of the object state both before and after the change was made; provided as a dictionary with keys named `prechange` and `postchange`. These are not as extensive as the fully serialized representation, but contain enough information to convey what has changed.
|
* `snapshots` - Minimal "snapshots" of the object state both before and after the change was made; provided as a dictionary with keys named `prechange` and `postchange`. These are not as extensive as the fully serialized representation, but contain enough information to convey what has changed.
|
||||||
|
* ⚠️ `request_id` - The unique request ID. This may be used to correlate multiple changes associated with a single request.
|
||||||
|
* ⚠️ `username` - The name of the user account associated with the change.
|
||||||
|
|
||||||
|
!!! warning "Deprecation of legacy keys"
|
||||||
|
The `request_id` and `username` keys in the webhook payload above are deprecated and should no longer be used. Support for them will be removed in NetBox v4.7.0.
|
||||||
|
|
||||||
|
Use `request.user` and `request.id` from the `request` object included in the callback context instead.
|
||||||
|
|
||||||
### Default Request Body
|
### Default Request Body
|
||||||
|
|
||||||
@@ -38,27 +48,37 @@ If no body template is specified, the request body will be populated with a JSON
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"event": "created",
|
"event": "created",
|
||||||
"timestamp": "2021-03-09 17:55:33.968016+00:00",
|
"timestamp": "2026-03-06T15:11:23.503186+00:00",
|
||||||
"model": "site",
|
"object_type": "dcim.site",
|
||||||
"username": "jstretch",
|
"username": "jstretch",
|
||||||
"request_id": "fdbca812-3142-4783-b364-2e2bd5c16c6a",
|
"request_id": "17af32f0-852a-46ca-a7d4-33ecd0c13de6",
|
||||||
"data": {
|
"data": {
|
||||||
"id": 19,
|
"id": 4,
|
||||||
|
"url": "/api/dcim/sites/4/",
|
||||||
|
"display_url": "/dcim/sites/4/",
|
||||||
|
"display": "Site 1",
|
||||||
"name": "Site 1",
|
"name": "Site 1",
|
||||||
"slug": "site-1",
|
"slug": "site-1",
|
||||||
"status":
|
"status": {
|
||||||
"value": "active",
|
"value": "active",
|
||||||
"label": "Active",
|
"label": "Active"
|
||||||
"id": 1
|
|
||||||
},
|
},
|
||||||
"region": null,
|
"region": null,
|
||||||
...
|
...
|
||||||
},
|
},
|
||||||
|
"request": {
|
||||||
|
"id": "17af32f0-852a-46ca-a7d4-33ecd0c13de6",
|
||||||
|
"method": "POST",
|
||||||
|
"path": "/dcim/sites/add/",
|
||||||
|
"user": "jstretch"
|
||||||
|
},
|
||||||
"snapshots": {
|
"snapshots": {
|
||||||
"prechange": null,
|
"prechange": null,
|
||||||
"postchange": {
|
"postchange": {
|
||||||
"created": "2021-03-09",
|
"created": "2026-03-06T15:11:23.484Z",
|
||||||
"last_updated": "2021-03-09T17:55:33.851Z",
|
"owner": null,
|
||||||
|
"description": "",
|
||||||
|
"comments": "",
|
||||||
"name": "Site 1",
|
"name": "Site 1",
|
||||||
"slug": "site-1",
|
"slug": "site-1",
|
||||||
"status": "active",
|
"status": "active",
|
||||||
|
|||||||
@@ -36,13 +36,16 @@ If false, synchronization will be disabled.
|
|||||||
|
|
||||||
### Ignore Rules
|
### Ignore Rules
|
||||||
|
|
||||||
A set of rules (one per line) identifying filenames to ignore during synchronization. Some examples are provided below. See Python's [`fnmatch()` documentation](https://docs.python.org/3/library/fnmatch.html) for a complete reference.
|
A set of rules (one per line) identifying files or paths to ignore during synchronization. Rules are matched against both the full relative path (e.g. `subdir/file.txt`) and the bare filename, so path-based patterns can be used to exclude entire directories. Some examples are provided below. See Python's [`fnmatch()` documentation](https://docs.python.org/3/library/fnmatch.html) for a complete reference.
|
||||||
|
|
||||||
| Rule | Description |
|
| Rule | Description |
|
||||||
|----------------|------------------------------------------|
|
|-----------------------|------------------------------------------------------|
|
||||||
| `README` | Ignore any files named `README` |
|
| `README` | Ignore any files named `README` |
|
||||||
| `*.txt` | Ignore any files with a `.txt` extension |
|
| `*.txt` | Ignore any files with a `.txt` extension |
|
||||||
| `data???.json` | Ignore e.g. `data123.json` |
|
| `data???.json` | Ignore e.g. `data123.json` |
|
||||||
|
| `subdir/*` | Ignore all files within `subdir/` |
|
||||||
|
| `subdir/*/*` | Ignore all files one level deep within `subdir/` |
|
||||||
|
| `*/dev/*` | Ignore files inside any directory named `dev/` |
|
||||||
|
|
||||||
### Sync Interval
|
### Sync Interval
|
||||||
|
|
||||||
|
|||||||
15
docs/models/dcim/cablebundle.md
Normal file
15
docs/models/dcim/cablebundle.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# Cable Bundles
|
||||||
|
|
||||||
|
A cable bundle is a logical grouping of individual [cables](./cable.md). Bundles can be used to organize cables that share a common purpose, route, or physical grouping (such as a conduit or harness).
|
||||||
|
|
||||||
|
Assigning cables to a bundle is optional and does not affect cable tracing or connectivity. Bundles persist independently of their member cables: deleting a cable clears its bundle assignment but does not delete the bundle itself.
|
||||||
|
|
||||||
|
## Fields
|
||||||
|
|
||||||
|
### Name
|
||||||
|
|
||||||
|
A unique name for the cable bundle.
|
||||||
|
|
||||||
|
### Description
|
||||||
|
|
||||||
|
A brief description of the bundle's purpose or contents.
|
||||||
@@ -23,3 +23,8 @@ The device bay's name. Must be unique to the parent device.
|
|||||||
### Label
|
### Label
|
||||||
|
|
||||||
An alternative physical label identifying the device bay.
|
An alternative physical label identifying the device bay.
|
||||||
|
|
||||||
|
### Enabled
|
||||||
|
|
||||||
|
Whether this device bay is enabled. Disabled device bays are not available for installation.
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,18 @@ Device types are instantiated as devices installed within sites and/or equipment
|
|||||||
!!! note
|
!!! note
|
||||||
This parent/child relationship is **not** suitable for modeling chassis-based devices, wherein child members share a common control plane. Instead, line cards and similarly non-autonomous hardware should be modeled as modules or inventory items within a device.
|
This parent/child relationship is **not** suitable for modeling chassis-based devices, wherein child members share a common control plane. Instead, line cards and similarly non-autonomous hardware should be modeled as modules or inventory items within a device.
|
||||||
|
|
||||||
|
## Automatic Component Renaming
|
||||||
|
|
||||||
|
When adding component templates to a device type, the string `{vc_position}` can be used in component template names to reference the
|
||||||
|
`vc_position` field of the device being provisioned, when that device is a member of a Virtual Chassis.
|
||||||
|
|
||||||
|
For example, an interface template named `Gi{vc_position}/0/0` installed on a Virtual Chassis
|
||||||
|
member with position `2` will be rendered as `Gi2/0/0`.
|
||||||
|
|
||||||
|
If the device is not a member of a Virtual Chassis, `{vc_position}` defaults to `0`. A custom
|
||||||
|
fallback value can be specified using the syntax `{vc_position:X}`, where `X` is the desired default.
|
||||||
|
For example, `{vc_position:1}` will render as `1` when no Virtual Chassis position is set.
|
||||||
|
|
||||||
## Fields
|
## Fields
|
||||||
|
|
||||||
### Manufacturer
|
### Manufacturer
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Module Bays
|
# Module Bays
|
||||||
|
|
||||||
Module bays represent a space or slot within a device in which a field-replaceable [module](./module.md) may be installed. A common example is that of a chassis-based switch such as the Cisco Nexus 9000 or Juniper EX9200. Modules in turn hold additional components that become available to the parent device.
|
Module bays represent a space or slot within a device in which a field-replaceable [module](./module.md) may be installed. A common example is that of a chassis-based switch such as the Cisco Nexus 9000 or Juniper EX9200. Modules, in turn, hold additional components that become available to the parent device.
|
||||||
|
|
||||||
!!! note
|
!!! note
|
||||||
If you need to model child devices rather than modules, use a [device bay](./devicebay.md) instead.
|
If you need to model child devices rather than modules, use a [device bay](./devicebay.md) instead.
|
||||||
@@ -29,3 +29,8 @@ An alternative physical label identifying the module bay.
|
|||||||
### Position
|
### Position
|
||||||
|
|
||||||
The numeric position in which this module bay is situated. For example, this would be the number assigned to a slot within a chassis-based switch.
|
The numeric position in which this module bay is situated. For example, this would be the number assigned to a slot within a chassis-based switch.
|
||||||
|
|
||||||
|
### Enabled
|
||||||
|
|
||||||
|
Whether this module bay is enabled. Disabled module bays are not available for installation.
|
||||||
|
|
||||||
|
|||||||
@@ -20,8 +20,38 @@ When adding component templates to a module type, the string `{module}` can be u
|
|||||||
|
|
||||||
For example, you can create a module type with interface templates named `Gi{module}/0/[1-48]`. When a new module of this type is "installed" to a module bay with a position of "3", NetBox will automatically name these interfaces `Gi3/0/[1-48]`.
|
For example, you can create a module type with interface templates named `Gi{module}/0/[1-48]`. When a new module of this type is "installed" to a module bay with a position of "3", NetBox will automatically name these interfaces `Gi3/0/[1-48]`.
|
||||||
|
|
||||||
|
Similarly, the string `{vc_position}` can be used in component template names to reference the
|
||||||
|
`vc_position` field of the device being provisioned, when that device is a member of a Virtual Chassis.
|
||||||
|
|
||||||
|
For example, an interface template named `Gi{vc_position}/{module}/0` installed on a Virtual Chassis
|
||||||
|
member with position `2` and module bay position `3` will be rendered as `Gi2/3/0`.
|
||||||
|
|
||||||
|
If the device is not a member of a Virtual Chassis, `{vc_position}` defaults to `0`. A custom
|
||||||
|
fallback value can be specified using the syntax `{vc_position:X}`, where `X` is the desired default.
|
||||||
|
For example, `{vc_position:1}` will render as `1` when no Virtual Chassis position is set.
|
||||||
|
|
||||||
Automatic renaming is supported for all modular component types (those listed above).
|
Automatic renaming is supported for all modular component types (those listed above).
|
||||||
|
|
||||||
|
### Position Inheritance for Nested Modules
|
||||||
|
|
||||||
|
When using nested module bays (modules installed inside other modules), the `{module}` placeholder
|
||||||
|
can also be used in the **position** field of module bay templates to inherit the parent bay's
|
||||||
|
position. This allows a single module type to produce correctly named components at any nesting
|
||||||
|
depth, with a user-controlled separator.
|
||||||
|
|
||||||
|
For example, a line card module type might define sub-bay positions as `{module}/1`, `{module}/2`,
|
||||||
|
etc. When the line card is installed in a device bay with position `3`, these sub-bay positions
|
||||||
|
resolve to `3/1`, `3/2`, etc. An SFP module type with interface template `SFP {module}` installed
|
||||||
|
in sub-bay `3/2` then produces interface `SFP 3/2`.
|
||||||
|
|
||||||
|
The separator between levels is defined by the user in the position field template itself. Using
|
||||||
|
`{module}-1` produces positions like `3-1`, while `{module}.1` produces `3.1`. This provides
|
||||||
|
full flexibility without requiring a global separator configuration.
|
||||||
|
|
||||||
|
!!! note
|
||||||
|
If the position field does not contain `{module}`, no inheritance occurs and behavior is
|
||||||
|
unchanged from previous versions.
|
||||||
|
|
||||||
## Fields
|
## Fields
|
||||||
|
|
||||||
### Manufacturer
|
### Manufacturer
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Racks
|
# Racks
|
||||||
|
|
||||||
The rack model represents a physical two- or four-post equipment rack in which [devices](./device.md) can be installed. Each rack must be assigned to a [site](./site.md), and may optionally be assigned to a [location](./location.md) within that site. Racks can also be organized by user-defined functional roles. The name and facility ID of each rack within a location must be unique.
|
The rack model represents a physical two- or four-post equipment rack in which [devices](./device.md) can be installed. Each rack must be assigned to a [site](./site.md), and may optionally be assigned to a [location](./location.md) within that site. Racks can also be organized by user-defined functional roles or by [rack groups](./rackgroup.md). The name and facility ID of each rack within a location must be unique.
|
||||||
|
|
||||||
Rack height is measured in *rack units* (U); racks are commonly between 42U and 48U tall, but NetBox allows you to define racks of arbitrary height. A toggle is provided to indicate whether rack units are in ascending (from the ground up) or descending order.
|
Rack height is measured in *rack units* (U); racks are commonly between 42U and 48U tall, but NetBox allows you to define racks of arbitrary height. A toggle is provided to indicate whether rack units are in ascending (from the ground up) or descending order.
|
||||||
|
|
||||||
@@ -16,6 +16,10 @@ The [site](./site.md) to which the rack is assigned.
|
|||||||
|
|
||||||
The [location](./location.md) within a site where the rack has been installed (optional).
|
The [location](./location.md) within a site where the rack has been installed (optional).
|
||||||
|
|
||||||
|
### Rack Group
|
||||||
|
|
||||||
|
The [group](./rackgroup.md) used to organize racks by physical placement (optional).
|
||||||
|
|
||||||
### Name
|
### Name
|
||||||
|
|
||||||
The rack's name or identifier. Must be unique to the rack's location, if assigned.
|
The rack's name or identifier. Must be unique to the rack's location, if assigned.
|
||||||
|
|||||||
15
docs/models/dcim/rackgroup.md
Normal file
15
docs/models/dcim/rackgroup.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# Rack Groups
|
||||||
|
|
||||||
|
Racks can optionally be assigned to rack groups to reflect their physical placement. Rack groups provide a secondary means of categorization alongside [locations](./location.md), which is particularly useful for datacenter operators who need to group racks by row, aisle, or similar physical arrangement while keeping them assigned to the same location, such as a cage or room. Rack groups are flat and do not form a hierarchy.
|
||||||
|
|
||||||
|
Rack groups can also be used to scope [VLAN groups](../ipam/vlangroup.md), which can help model L2 domains spanning rows or pairs of racks.
|
||||||
|
|
||||||
|
## Fields
|
||||||
|
|
||||||
|
### Name
|
||||||
|
|
||||||
|
A unique human-friendly name.
|
||||||
|
|
||||||
|
### Slug
|
||||||
|
|
||||||
|
A unique URL-friendly identifier. (This value can be used for filtering.)
|
||||||
@@ -12,6 +12,10 @@ The [rack](./rack.md) being reserved.
|
|||||||
|
|
||||||
The rack unit or units being reserved. Multiple units can be expressed using commas and/or hyphens. For example, `1,3,5-7` specifies units 1, 3, 5, 6, and 7.
|
The rack unit or units being reserved. Multiple units can be expressed using commas and/or hyphens. For example, `1,3,5-7` specifies units 1, 3, 5, 6, and 7.
|
||||||
|
|
||||||
|
### Total U's
|
||||||
|
|
||||||
|
A calculated (read-only) field that reflects the total number of units in the reservation. Can be filtered upon using `unit_count_min` and `unit_count_max` parameters in the UI or API.
|
||||||
|
|
||||||
### Status
|
### Status
|
||||||
|
|
||||||
The current status of the reservation. (This is for documentation only: The status of a reservation has no impact on the installation of devices within a reserved rack unit.)
|
The current status of the reservation. (This is for documentation only: The status of a reservation has no impact on the installation of devices within a reserved rack unit.)
|
||||||
|
|||||||
@@ -118,3 +118,7 @@ For numeric custom fields only. The maximum valid value (optional).
|
|||||||
### Validation Regex
|
### Validation Regex
|
||||||
|
|
||||||
For string-based custom fields only. A regular expression used to validate the field's value (optional).
|
For string-based custom fields only. A regular expression used to validate the field's value (optional).
|
||||||
|
|
||||||
|
### Validation Schema
|
||||||
|
|
||||||
|
For JSON custom fields, users have the option of defining a [validation schema](https://json-schema.org). Any value applied to this custom field on a model will be validated against the provided schema, if any.
|
||||||
|
|||||||
@@ -77,14 +77,17 @@ The file path to a particular certificate authority (CA) file to use when valida
|
|||||||
|
|
||||||
## Context Data
|
## Context Data
|
||||||
|
|
||||||
The following context variables are available in to the text and link templates.
|
The following context variables are available to the text and link templates.
|
||||||
|
|
||||||
| Variable | Description |
|
| Variable | Description |
|
||||||
|--------------|----------------------------------------------------|
|
|---------------|------------------------------------------------------|
|
||||||
| `event` | The event type (`create`, `update`, or `delete`) |
|
| `event` | The event type (`create`, `update`, or `delete`) |
|
||||||
| `timestamp` | The time at which the event occured |
|
| `timestamp` | The time at which the event occurred |
|
||||||
| `model` | The type of object impacted |
|
| `object_type` | The type of object impacted (`app_label.model_name`) |
|
||||||
| `username` | The name of the user associated with the change |
|
| `username` | The name of the user associated with the change |
|
||||||
| `request_id` | The unique request ID |
|
| `request_id` | The unique request ID |
|
||||||
| `data` | A complete serialized representation of the object |
|
| `data` | A complete serialized representation of the object |
|
||||||
| `snapshots` | Pre- and post-change snapshots of the object |
|
| `snapshots` | Pre- and post-change snapshots of the object |
|
||||||
|
|
||||||
|
!!! warning "Deprecation of legacy fields"
|
||||||
|
The `request_id` and `username` fields in the webhook payload above are deprecated and should no longer be used. Support for them will be removed in NetBox v4.7.0. Use `request.user` and `request.id` from the `request` object included in the callback context instead. (Note that `request` is populated in the context only when the webhook is associated with a triggering request.)
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ The 16- or 32-bit AS number.
|
|||||||
|
|
||||||
The [Regional Internet Registry](./rir.md) or similar authority responsible for the allocation of this particular ASN.
|
The [Regional Internet Registry](./rir.md) or similar authority responsible for the allocation of this particular ASN.
|
||||||
|
|
||||||
|
### Role
|
||||||
|
|
||||||
|
The user-defined functional [role](./role.md) assigned to this ASN.
|
||||||
|
|
||||||
### Sites
|
### Sites
|
||||||
|
|
||||||
The [site(s)](../dcim/site.md) to which this ASN is assigned.
|
The [site(s)](../dcim/site.md) to which this ASN is assigned.
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ A unique URL-friendly identifier. (This value can be used for filtering.)
|
|||||||
|
|
||||||
The set of VLAN IDs which are encompassed by the group. By default, this will be the entire range of valid IEEE 802.1Q VLAN IDs (1 to 4094, inclusive). VLANs created within a group must have a VID that falls within one of these ranges. Ranges may not overlap.
|
The set of VLAN IDs which are encompassed by the group. By default, this will be the entire range of valid IEEE 802.1Q VLAN IDs (1 to 4094, inclusive). VLANs created within a group must have a VID that falls within one of these ranges. Ranges may not overlap.
|
||||||
|
|
||||||
|
### Total VLAN IDs
|
||||||
|
|
||||||
|
A read-only integer indicating the total count of VLAN IDs available within the group, calculated from the configured VLAN ID Ranges. For example, a group with ranges `100-199` and `300-399` would have a total of 200 VLAN IDs. This value is automatically computed and updated whenever the VLAN ID ranges are modified.
|
||||||
|
|
||||||
### Scope
|
### Scope
|
||||||
|
|
||||||
The domain covered by a VLAN group, defined as one of the supported object types. This conveys the context in which a VLAN group applies.
|
The domain covered by a VLAN group, defined as one of the supported object types. This conveys the context in which a VLAN group applies.
|
||||||
|
|||||||
@@ -1,18 +1,27 @@
|
|||||||
# Virtual Machines
|
# Virtual Machines
|
||||||
|
|
||||||
A virtual machine (VM) represents a virtual compute instance hosted within a [cluster](./cluster.md). Each VM must be assigned to a [site](../dcim/site.md) and/or cluster, and may optionally be assigned to a particular host [device](../dcim/device.md) within a cluster.
|
A virtual machine (VM) represents a virtual compute instance hosted within a cluster or directly on a device. Each VM must be assigned to at least one of: a [site](../dcim/site.md), a [cluster](./cluster.md), or a [device](../dcim/device.md).
|
||||||
|
|
||||||
Virtual machines may have virtual [interfaces](./vminterface.md) assigned to them, but do not support any physical component. When a VM has one or more interfaces with IP addresses assigned, a primary IP for the device can be designated, for both IPv4 and IPv6.
|
Virtual machines may have virtual [interfaces](./vminterface.md) assigned to them, but do not support any physical component. When a VM has one or more interfaces with IP addresses assigned, a primary IP for the VM can be designated, for both IPv4 and IPv6.
|
||||||
|
|
||||||
## Fields
|
## Fields
|
||||||
|
|
||||||
### Name
|
### Name
|
||||||
|
|
||||||
The virtual machine's configured name. Must be unique to the assigned cluster and tenant.
|
The virtual machine's configured name. Must be unique within its scoping context:
|
||||||
|
|
||||||
|
- If assigned to a **cluster**: unique within the cluster and tenant.
|
||||||
|
- If assigned to a **device** (no cluster): unique within the device and tenant.
|
||||||
|
|
||||||
|
### Type
|
||||||
|
|
||||||
|
The [virtual machine type](./virtualmachinetype.md) assigned to the VM. A type classifies a virtual machine and can provide default values for platform, vCPUs, and memory when the VM is created.
|
||||||
|
|
||||||
|
Changes made to a virtual machine type do **not** apply retroactively to existing virtual machines.
|
||||||
|
|
||||||
### Role
|
### Role
|
||||||
|
|
||||||
The functional [role](../dcim/devicerole.md) assigned to the VM.
|
The functional role assigned to the VM.
|
||||||
|
|
||||||
### Status
|
### Status
|
||||||
|
|
||||||
@@ -21,24 +30,28 @@ The VM's operational status.
|
|||||||
!!! tip
|
!!! tip
|
||||||
Additional statuses may be defined by setting `VirtualMachine.status` under the [`FIELD_CHOICES`](../../configuration/data-validation.md#field_choices) configuration parameter.
|
Additional statuses may be defined by setting `VirtualMachine.status` under the [`FIELD_CHOICES`](../../configuration/data-validation.md#field_choices) configuration parameter.
|
||||||
|
|
||||||
### Start on boot
|
### Start on Boot
|
||||||
|
|
||||||
The start on boot setting from the hypervisor.
|
The start on boot setting from the hypervisor.
|
||||||
|
|
||||||
!!! tip
|
!!! tip
|
||||||
Additional statuses may be defined by setting `VirtualMachine.start_on_boot` under the [`FIELD_CHOICES`](../../configuration/data-validation.md#field_choices) configuration parameter.
|
Additional statuses may be defined by setting `VirtualMachine.start_on_boot` under the [`FIELD_CHOICES`](../../configuration/data-validation.md#field_choices) configuration parameter.
|
||||||
|
|
||||||
### Site & Cluster
|
### Site / Cluster / Device
|
||||||
|
|
||||||
The [site](../dcim/site.md) and/or [cluster](./cluster.md) to which the VM is assigned.
|
The location or host for this VM. At least one must be specified:
|
||||||
|
|
||||||
### Device
|
- **Site only**: The VM exists at a site but is not assigned to a specific cluster or device.
|
||||||
|
- **Cluster only**: The VM belongs to a virtualization cluster. The site is automatically inferred from the cluster's scope.
|
||||||
|
- **Device only**: The VM runs directly on a physical host device without a cluster (e.g. containers). The site is automatically inferred from the device's site.
|
||||||
|
- **Cluster + Device**: The VM belongs to a cluster and is pinned to a specific host device within that cluster. The device must be a registered host of the assigned cluster.
|
||||||
|
|
||||||
The physical host [device](../dcim/device.md) within the assigned site/cluster on which this VM resides.
|
!!! info "New in NetBox v4.6"
|
||||||
|
Virtual machines can now be assigned directly to a device without requiring a cluster. This is particularly useful for modeling VMs running on standalone hosts outside of a cluster.
|
||||||
|
|
||||||
### Platform
|
### Platform
|
||||||
|
|
||||||
A VM may be associated with a particular [platform](../dcim/platform.md) to indicate its operating system.
|
A VM may be associated with a particular [platform](../dcim/platform.md) to indicate its operating system. If a virtual machine type defines a default platform, it will be applied when the VM is created unless an explicit platform is specified.
|
||||||
|
|
||||||
### Primary IPv4 & IPv6 Addresses
|
### Primary IPv4 & IPv6 Addresses
|
||||||
|
|
||||||
@@ -49,11 +62,11 @@ Each VM may designate one primary IPv4 address and/or one primary IPv6 address f
|
|||||||
|
|
||||||
### vCPUs
|
### vCPUs
|
||||||
|
|
||||||
The number of virtual CPUs provisioned. A VM may be allocated a partial vCPU count (e.g. 1.5 vCPU).
|
The number of virtual CPUs provisioned. A VM may be allocated a partial vCPU count (e.g. 1.5 vCPU). If a virtual machine type defines a default vCPU allocation, it will be applied when the VM is created unless an explicit value is specified.
|
||||||
|
|
||||||
### Memory
|
### Memory
|
||||||
|
|
||||||
The amount of running memory provisioned, in megabytes.
|
The amount of running memory provisioned, in megabytes. If a virtual machine type defines a default memory allocation, it will be applied when the VM is created unless an explicit value is specified.
|
||||||
|
|
||||||
### Disk
|
### Disk
|
||||||
|
|
||||||
@@ -64,4 +77,7 @@ The amount of disk storage provisioned, in megabytes.
|
|||||||
|
|
||||||
### Serial Number
|
### Serial Number
|
||||||
|
|
||||||
Optional serial number assigned to this virtual machine. Unlike devices, uniqueness is not enforced for virtual machine serial numbers.
|
Optional serial number assigned to this virtual machine.
|
||||||
|
|
||||||
|
!!! info
|
||||||
|
Unlike devices, uniqueness is not enforced for virtual machine serial numbers.
|
||||||
|
|||||||
27
docs/models/virtualization/virtualmachinetype.md
Normal file
27
docs/models/virtualization/virtualmachinetype.md
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# Virtual Machine Types
|
||||||
|
|
||||||
|
A virtual machine type defines a reusable classification and default configuration for [virtual machines](./virtualmachine.md).
|
||||||
|
|
||||||
|
A type can optionally provide default values for a VM's [platform](../dcim/platform.md), vCPU allocation, and memory allocation. When a virtual machine is created with an assigned type, any unset values among these fields will inherit their defaults from the type. Changes made to a virtual machine type do **not** apply retroactively to existing virtual machines.
|
||||||
|
|
||||||
|
## Fields
|
||||||
|
|
||||||
|
### Name
|
||||||
|
|
||||||
|
A unique human-friendly name.
|
||||||
|
|
||||||
|
### Slug
|
||||||
|
|
||||||
|
A unique URL-friendly identifier. (This value can be used for filtering.)
|
||||||
|
|
||||||
|
### Default Platform
|
||||||
|
|
||||||
|
If defined, virtual machines instantiated with this type will automatically inherit the selected platform when no explicit platform is provided.
|
||||||
|
|
||||||
|
### Default vCPUs
|
||||||
|
|
||||||
|
The default number of vCPUs to assign when creating a virtual machine from this type.
|
||||||
|
|
||||||
|
### Default Memory
|
||||||
|
|
||||||
|
The default amount of memory, in megabytes, to assign when creating a virtual machine from this type.
|
||||||
24
docs/plugins/development/permissions.md
Normal file
24
docs/plugins/development/permissions.md
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Custom Model Actions
|
||||||
|
|
||||||
|
Plugins can register custom permission actions for their models. These actions appear as checkboxes in the ObjectPermission form, making it easy for administrators to grant or restrict access to plugin-specific functionality without manually entering action names.
|
||||||
|
|
||||||
|
For example, a plugin might define a "sync" action for a model that syncs data from an external source, or a "bypass" action that allows users to bypass certain restrictions.
|
||||||
|
|
||||||
|
## Registering Model Actions
|
||||||
|
|
||||||
|
The preferred way to register custom actions is via Django's `Meta.permissions` on the model class. NetBox will automatically register these as model actions when the app is loaded:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from netbox.models import NetBoxModel
|
||||||
|
|
||||||
|
class MyModel(NetBoxModel):
|
||||||
|
# ...
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
permissions = [
|
||||||
|
('sync', 'Synchronize data from external source'),
|
||||||
|
('export', 'Export data to external system'),
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
Once registered, these actions appear as checkboxes in a flat list when creating or editing an ObjectPermission.
|
||||||
@@ -1,12 +1,14 @@
|
|||||||
# Search
|
# Search
|
||||||
|
|
||||||
Plugins can define and register their own models to extend NetBox's core search functionality. Typically, a plugin will include a file named `search.py`, which holds all search indexes for its models (see the example below).
|
Plugins can define and register their own models to extend NetBox's core search functionality. Typically, a plugin will include a file named `search.py`, which holds all search indexes for its models.
|
||||||
|
|
||||||
```python
|
```python title="search.py"
|
||||||
# search.py
|
# search.py
|
||||||
from netbox.search import SearchIndex
|
from netbox.search import SearchIndex, register_search
|
||||||
|
|
||||||
from .models import MyModel
|
from .models import MyModel
|
||||||
|
|
||||||
|
@register_search
|
||||||
class MyModelIndex(SearchIndex):
|
class MyModelIndex(SearchIndex):
|
||||||
model = MyModel
|
model = MyModel
|
||||||
fields = (
|
fields = (
|
||||||
@@ -17,15 +19,11 @@ class MyModelIndex(SearchIndex):
|
|||||||
display_attrs = ('site', 'device', 'status', 'description')
|
display_attrs = ('site', 'device', 'status', 'description')
|
||||||
```
|
```
|
||||||
|
|
||||||
Fields listed in `display_attrs` will not be cached for search, but will be displayed alongside the object when it appears in global search results. This is helpful for conveying to the user additional information about an object.
|
Decorate each `SearchIndex` subclass with `@register_search` to register it with NetBox. When using the default `search.py` module, no additional `indexes = [...]` list is required.
|
||||||
|
|
||||||
To register one or more indexes with NetBox, define a list named `indexes` at the end of this file:
|
Fields listed in `display_attrs` are not cached for matching, but they are displayed alongside the object in global search results to provide additional context.
|
||||||
|
|
||||||
```python
|
|
||||||
indexes = [MyModelIndex]
|
|
||||||
```
|
|
||||||
|
|
||||||
!!! tip
|
!!! tip
|
||||||
The path to the list of search indexes can be modified by setting `search_indexes` in the PluginConfig instance.
|
The legacy `indexes = [...]` list remains supported via `PluginConfig.search_indexes` for backward compatibility and custom loading patterns.
|
||||||
|
|
||||||
::: netbox.search.SearchIndex
|
::: netbox.search.SearchIndex
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
# UI Components
|
# UI Components
|
||||||
|
|
||||||
!!! note "New in NetBox v4.5"
|
!!! note "New in NetBox v4.6"
|
||||||
All UI components described here were introduced in NetBox v4.5. Be sure to set the minimum NetBox version to 4.5.0 for your plugin before incorporating any of these resources.
|
All UI components described here were introduced in NetBox v4.6. Be sure to set the minimum NetBox version to 4.6.0 for your plugin before incorporating any of these resources.
|
||||||
|
|
||||||
!!! danger "Beta Feature"
|
To simplify the process of designing your plugin's user interface, and to encourage a consistent look and feel throughout the entire application, NetBox provides a set of components that enable programmatic UI design. These make it possible to declare complex page layouts with little or no custom HTML.
|
||||||
UI components are considered a beta feature, and are still under active development. Please be aware that the API for resources on this page is subject to change in future releases.
|
|
||||||
|
|
||||||
To simply the process of designing your plugin's user interface, and to encourage a consistent look and feel throughout the entire application, NetBox provides a set of components that enable programmatic UI design. These make it possible to declare complex page layouts with little or no custom HTML.
|
|
||||||
|
|
||||||
## Page Layout
|
## Page Layout
|
||||||
|
|
||||||
@@ -75,9 +72,12 @@ class RecentChangesPanel(Panel):
|
|||||||
**super().get_context(context),
|
**super().get_context(context),
|
||||||
'changes': get_changes()[:10],
|
'changes': get_changes()[:10],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def should_render(self, context):
|
||||||
|
return len(context['changes']) > 0
|
||||||
```
|
```
|
||||||
|
|
||||||
NetBox also includes a set of panels suite for specific uses, such as display object details or embedding a table of related objects. These are listed below.
|
NetBox also includes a set of panels suited for specific uses, such as displaying object details or embedding a table of related objects. These are listed below.
|
||||||
|
|
||||||
::: netbox.ui.panels.Panel
|
::: netbox.ui.panels.Panel
|
||||||
|
|
||||||
@@ -85,26 +85,6 @@ NetBox also includes a set of panels suite for specific uses, such as display ob
|
|||||||
|
|
||||||
::: netbox.ui.panels.ObjectAttributesPanel
|
::: netbox.ui.panels.ObjectAttributesPanel
|
||||||
|
|
||||||
#### Object Attributes
|
|
||||||
|
|
||||||
The following classes are available to represent object attributes within an ObjectAttributesPanel. Additionally, plugins can subclass `netbox.ui.attrs.ObjectAttribute` to create custom classes.
|
|
||||||
|
|
||||||
| Class | Description |
|
|
||||||
|--------------------------------------|--------------------------------------------------|
|
|
||||||
| `netbox.ui.attrs.AddressAttr` | A physical or mailing address. |
|
|
||||||
| `netbox.ui.attrs.BooleanAttr` | A boolean value |
|
|
||||||
| `netbox.ui.attrs.ColorAttr` | A color expressed in RGB |
|
|
||||||
| `netbox.ui.attrs.ChoiceAttr` | A selection from a set of choices |
|
|
||||||
| `netbox.ui.attrs.GPSCoordinatesAttr` | GPS coordinates (latitude and longitude) |
|
|
||||||
| `netbox.ui.attrs.ImageAttr` | An attached image (displays the image) |
|
|
||||||
| `netbox.ui.attrs.NestedObjectAttr` | A related nested object |
|
|
||||||
| `netbox.ui.attrs.NumericAttr` | An integer or float value |
|
|
||||||
| `netbox.ui.attrs.RelatedObjectAttr` | A related object |
|
|
||||||
| `netbox.ui.attrs.TemplatedAttr` | Renders an attribute using a custom template |
|
|
||||||
| `netbox.ui.attrs.TextAttr` | A string (text) value |
|
|
||||||
| `netbox.ui.attrs.TimezoneAttr` | A timezone with annotated offset |
|
|
||||||
| `netbox.ui.attrs.UtilizationAttr` | A numeric value expressed as a utilization graph |
|
|
||||||
|
|
||||||
::: netbox.ui.panels.OrganizationalObjectPanel
|
::: netbox.ui.panels.OrganizationalObjectPanel
|
||||||
|
|
||||||
::: netbox.ui.panels.NestedGroupObjectPanel
|
::: netbox.ui.panels.NestedGroupObjectPanel
|
||||||
@@ -119,9 +99,13 @@ The following classes are available to represent object attributes within an Obj
|
|||||||
|
|
||||||
::: netbox.ui.panels.TemplatePanel
|
::: netbox.ui.panels.TemplatePanel
|
||||||
|
|
||||||
|
::: netbox.ui.panels.TextCodePanel
|
||||||
|
|
||||||
|
::: netbox.ui.panels.ContextTablePanel
|
||||||
|
|
||||||
::: netbox.ui.panels.PluginContentPanel
|
::: netbox.ui.panels.PluginContentPanel
|
||||||
|
|
||||||
## Panel Actions
|
### Panel Actions
|
||||||
|
|
||||||
Each panel may have actions associated with it. These render as links or buttons within the panel header, opposite the panel's title. For example, a common use case is to include an "Add" action on a panel which displays a list of objects. Below is an example of this.
|
Each panel may have actions associated with it. These render as links or buttons within the panel header, opposite the panel's title. For example, a common use case is to include an "Add" action on a panel which displays a list of objects. Below is an example of this.
|
||||||
|
|
||||||
@@ -146,3 +130,60 @@ panels.ObjectsTablePanel(
|
|||||||
::: netbox.ui.actions.AddObject
|
::: netbox.ui.actions.AddObject
|
||||||
|
|
||||||
::: netbox.ui.actions.CopyContent
|
::: netbox.ui.actions.CopyContent
|
||||||
|
|
||||||
|
## Object Attributes
|
||||||
|
|
||||||
|
The following classes are available to represent object attributes within an ObjectAttributesPanel. Additionally, plugins can subclass `netbox.ui.attrs.ObjectAttribute` to create custom classes.
|
||||||
|
|
||||||
|
| Class | Description |
|
||||||
|
|------------------------------------------|--------------------------------------------------|
|
||||||
|
| `netbox.ui.attrs.AddressAttr` | A physical or mailing address. |
|
||||||
|
| `netbox.ui.attrs.BooleanAttr` | A boolean value |
|
||||||
|
| `netbox.ui.attrs.ChoiceAttr` | A selection from a set of choices |
|
||||||
|
| `netbox.ui.attrs.ColorAttr` | A color expressed in RGB |
|
||||||
|
| `netbox.ui.attrs.DateTimeAttr` | A date or datetime value |
|
||||||
|
| `netbox.ui.attrs.GenericForeignKeyAttr` | A related object via a generic foreign key |
|
||||||
|
| `netbox.ui.attrs.GPSCoordinatesAttr` | GPS coordinates (latitude and longitude) |
|
||||||
|
| `netbox.ui.attrs.ImageAttr` | An attached image (displays the image) |
|
||||||
|
| `netbox.ui.attrs.NestedObjectAttr` | A related nested object (includes ancestors) |
|
||||||
|
| `netbox.ui.attrs.NumericAttr` | An integer or float value |
|
||||||
|
| `netbox.ui.attrs.RelatedObjectAttr` | A related object |
|
||||||
|
| `netbox.ui.attrs.RelatedObjectListAttr` | A list of related objects |
|
||||||
|
| `netbox.ui.attrs.TemplatedAttr` | Renders an attribute using a custom template |
|
||||||
|
| `netbox.ui.attrs.TextAttr` | A string (text) value |
|
||||||
|
| `netbox.ui.attrs.TimezoneAttr` | A timezone with annotated offset |
|
||||||
|
| `netbox.ui.attrs.UtilizationAttr` | A numeric value expressed as a utilization graph |
|
||||||
|
|
||||||
|
::: netbox.ui.attrs.ObjectAttribute
|
||||||
|
|
||||||
|
::: netbox.ui.attrs.AddressAttr
|
||||||
|
|
||||||
|
::: netbox.ui.attrs.BooleanAttr
|
||||||
|
|
||||||
|
::: netbox.ui.attrs.ChoiceAttr
|
||||||
|
|
||||||
|
::: netbox.ui.attrs.ColorAttr
|
||||||
|
|
||||||
|
::: netbox.ui.attrs.DateTimeAttr
|
||||||
|
|
||||||
|
::: netbox.ui.attrs.GenericForeignKeyAttr
|
||||||
|
|
||||||
|
::: netbox.ui.attrs.GPSCoordinatesAttr
|
||||||
|
|
||||||
|
::: netbox.ui.attrs.ImageAttr
|
||||||
|
|
||||||
|
::: netbox.ui.attrs.NestedObjectAttr
|
||||||
|
|
||||||
|
::: netbox.ui.attrs.NumericAttr
|
||||||
|
|
||||||
|
::: netbox.ui.attrs.RelatedObjectAttr
|
||||||
|
|
||||||
|
::: netbox.ui.attrs.RelatedObjectListAttr
|
||||||
|
|
||||||
|
::: netbox.ui.attrs.TemplatedAttr
|
||||||
|
|
||||||
|
::: netbox.ui.attrs.TextAttr
|
||||||
|
|
||||||
|
::: netbox.ui.attrs.TimezoneAttr
|
||||||
|
|
||||||
|
::: netbox.ui.attrs.UtilizationAttr
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ The resulting webhook payload will look like the following:
|
|||||||
"url": "/api/dcim/sites/2/",
|
"url": "/api/dcim/sites/2/",
|
||||||
...
|
...
|
||||||
},
|
},
|
||||||
|
"request": {...},
|
||||||
"snapshots": {...},
|
"snapshots": {...},
|
||||||
"context": {
|
"context": {
|
||||||
"foo": 123
|
"foo": 123
|
||||||
@@ -43,6 +44,11 @@ The resulting webhook payload will look like the following:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
!!! warning "Deprecation of legacy keys"
|
||||||
|
The `request_id` and `username` keys in the webhook payload above are deprecated and should no longer be used. Support for them will be removed in NetBox v4.7.0.
|
||||||
|
|
||||||
|
Use `request.user` and `request.id` from the `request` object included in the callback context instead.
|
||||||
|
|
||||||
!!! note "Consider namespacing webhook data"
|
!!! note "Consider namespacing webhook data"
|
||||||
The data returned from all webhook callbacks will be compiled into a single `context` dictionary. Any existing keys within this dictionary will be overwritten by subsequent callbacks which include those keys. To avoid collisions with webhook data provided by other plugins, consider namespacing your plugin's data within a nested dictionary as such:
|
The data returned from all webhook callbacks will be compiled into a single `context` dictionary. Any existing keys within this dictionary will be overwritten by subsequent callbacks which include those keys. To avoid collisions with webhook data provided by other plugins, consider namespacing your plugin's data within a nested dictionary as such:
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,14 @@ Minor releases are published in April, August, and December of each calendar yea
|
|||||||
|
|
||||||
This page contains a history of all major and minor releases since NetBox v2.0. For more detail on a specific patch release, please see the release notes page for that specific minor release.
|
This page contains a history of all major and minor releases since NetBox v2.0. For more detail on a specific patch release, please see the release notes page for that specific minor release.
|
||||||
|
|
||||||
|
#### [Version 4.6](./version-4.6.md) (May 2026)
|
||||||
|
|
||||||
|
* Virtual Machine Types ([#5795](https://github.com/netbox-community/netbox/issues/5795))
|
||||||
|
* Cable Bundles ([#20151](https://github.com/netbox-community/netbox/issues/20151))
|
||||||
|
* Rack Groups ([#20961](https://github.com/netbox-community/netbox/issues/20961))
|
||||||
|
* ETag Support for REST API ([#21356](https://github.com/netbox-community/netbox/issues/21356))
|
||||||
|
* Cursor-based Pagination for REST API ([#21363](https://github.com/netbox-community/netbox/issues/21363))
|
||||||
|
|
||||||
#### [Version 4.5](./version-4.5.md) (January 2026)
|
#### [Version 4.5](./version-4.5.md) (January 2026)
|
||||||
|
|
||||||
* Lookup Modifiers in Filter Forms ([#7604](https://github.com/netbox-community/netbox/issues/7604))
|
* Lookup Modifiers in Filter Forms ([#7604](https://github.com/netbox-community/netbox/issues/7604))
|
||||||
|
|||||||
@@ -1,5 +1,178 @@
|
|||||||
# NetBox v4.5
|
# NetBox v4.5
|
||||||
|
|
||||||
|
## v4.5.8 (2026-04-14)
|
||||||
|
|
||||||
|
### Enhancements
|
||||||
|
|
||||||
|
* [#21430](https://github.com/netbox-community/netbox/issues/21430) - Display the device role's color in the device view
|
||||||
|
* [#21795](https://github.com/netbox-community/netbox/issues/21795) - Update `humanize_speed` template filter to support decimal Gbps/Tbps values
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* [#21529](https://github.com/netbox-community/netbox/issues/21529) - Exclude non-existent custom fields from object changelog data returned via the REST API
|
||||||
|
* [#21542](https://github.com/netbox-community/netbox/issues/21542) - Expand interface speed field to 64-bit integer to prevent overflow for LAG interfaces exceeding ~2.1 Tbps
|
||||||
|
* [#21704](https://github.com/netbox-community/netbox/issues/21704) - Fix missing port mappings in device type YAML export
|
||||||
|
* [#21783](https://github.com/netbox-community/netbox/issues/21783) - Fix support for bulk import of cables connected to power feeds
|
||||||
|
* [#21801](https://github.com/netbox-community/netbox/issues/21801) - Prevent duplicate filename collision when uploading files using S3 storage
|
||||||
|
* [#21814](https://github.com/netbox-community/netbox/issues/21814) - Fix custom script "last run" time to reflect job start time rather than creation time
|
||||||
|
* [#21835](https://github.com/netbox-community/netbox/issues/21835) - Correct help text for color selection form fields
|
||||||
|
* [#21841](https://github.com/netbox-community/netbox/issues/21841) - Restore visibility of the edit button for script modules to non-superusers
|
||||||
|
* [#21845](https://github.com/netbox-community/netbox/issues/21845) - Fix CSV export of connection columns rendering template whitespace instead of a formatted value
|
||||||
|
* [#21869](https://github.com/netbox-community/netbox/issues/21869) - Remove redundant `ScriptModule` class synchronization triggered on save
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v4.5.7 (2026-04-03)
|
||||||
|
|
||||||
|
### Enhancements
|
||||||
|
|
||||||
|
* [#21095](https://github.com/netbox-community/netbox/issues/21095) - Adopt IEC unit labels (e.g. GiB) for virtual machine resources
|
||||||
|
* [#21696](https://github.com/netbox-community/netbox/issues/21696) - Add support for django-rq 4.0 and introduce `RQ` configuration parameter
|
||||||
|
* [#21701](https://github.com/netbox-community/netbox/issues/21701) - Support uploading custom scripts via the REST API (`/api/extras/scripts/upload/`)
|
||||||
|
* [#21760](https://github.com/netbox-community/netbox/issues/21760) - Add a 1C2P:2C1P breakout cable profile
|
||||||
|
|
||||||
|
### Performance Improvements
|
||||||
|
|
||||||
|
* [#21655](https://github.com/netbox-community/netbox/issues/21655) - Optimize queries for object and multi-object type custom fields
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* [#20474](https://github.com/netbox-community/netbox/issues/20474) - Fix installation of modules with placeholder values in component names
|
||||||
|
* [#21498](https://github.com/netbox-community/netbox/issues/21498) - Fix server error triggered by event rules referencing deleted objects
|
||||||
|
* [#21533](https://github.com/netbox-community/netbox/issues/21533) - Ensure read-only fields are included in REST API responses upon object creation
|
||||||
|
* [#21535](https://github.com/netbox-community/netbox/issues/21535) - Fix filtering of object-type custom fields when "is empty" is selected
|
||||||
|
* [#21784](https://github.com/netbox-community/netbox/issues/21784) - Fix `AttributeError` exception when sorting a table as an anonymous user
|
||||||
|
* [#21808](https://github.com/netbox-community/netbox/issues/21808) - Fix `RelatedObjectDoesNotExist` exception when viewing an interface with a virtual circuit termination
|
||||||
|
* [#21810](https://github.com/netbox-community/netbox/issues/21810) - Fix `AttributeError` exception when viewing virtual chassis member
|
||||||
|
* [#21825](https://github.com/netbox-community/netbox/issues/21825) - Fix sorting by broken columns in several object lists
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v4.5.6 (2026-03-31)
|
||||||
|
|
||||||
|
### Enhancements
|
||||||
|
|
||||||
|
* [#21480](https://github.com/netbox-community/netbox/issues/21480) - Add OSFP224 (1.6T) interface type
|
||||||
|
* [#21727](https://github.com/netbox-community/netbox/issues/21727) - Add 2.5GBASE-X SFP modular interface type
|
||||||
|
* [#21743](https://github.com/netbox-community/netbox/issues/21743) - Improve object change diff styling and layout
|
||||||
|
* [#21793](https://github.com/netbox-community/netbox/issues/21793) - Add 50 Gbps, 800 Gbps, and 1.6 Tbps interface speed options
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* [#20467](https://github.com/netbox-community/netbox/issues/20467) - Fix resolution of the `{module}` variable for position fields in nested modules
|
||||||
|
* [#21698](https://github.com/netbox-community/netbox/issues/21698) - Adjust custom field URL filter to support non-standard port numbers
|
||||||
|
* [#21707](https://github.com/netbox-community/netbox/issues/21707) - Fix grouping of owner fields in provider account add/edit forms
|
||||||
|
* [#21749](https://github.com/netbox-community/netbox/issues/21749) - Fix `FieldError` exception when sorting the circuit group assignment table by the member column
|
||||||
|
* [#21763](https://github.com/netbox-community/netbox/issues/21763) - Use separate add/remove form fields when editing a site or provider with a large number of ASNs assigned
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v4.5.5 (2026-03-17)
|
||||||
|
|
||||||
|
### Enhancements
|
||||||
|
|
||||||
|
* [#21114](https://github.com/netbox-community/netbox/issues/21114) - Support path exclusions for data source synchronization
|
||||||
|
* [#21578](https://github.com/netbox-community/netbox/issues/21578) - Support identifying scope object by name or slug when bulk importing scoped objects
|
||||||
|
|
||||||
|
### Performance Improvements
|
||||||
|
|
||||||
|
* [#21330](https://github.com/netbox-community/netbox/issues/21330) - Optimize the assignment of tags when saving objects
|
||||||
|
* [#21402](https://github.com/netbox-community/netbox/issues/21402) - Avoid excessive database queries when rendering unnamed devices via the REST API
|
||||||
|
* [#21611](https://github.com/netbox-community/netbox/issues/21611) - Replace inefficient calls to `.count()` with `.exists()`
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* [#19867](https://github.com/netbox-community/netbox/issues/19867) - Preserve the "per page" pagination setting when returning from object edit forms
|
||||||
|
* [#20077](https://github.com/netbox-community/netbox/issues/20077) - Fix form field focus bug in Microsoft Edge
|
||||||
|
* [#20385](https://github.com/netbox-community/netbox/issues/20385) - Enforce `MAX_PAGE_SIZE` limit for GraphQL API requests
|
||||||
|
* [#20468](https://github.com/netbox-community/netbox/issues/20468) - Fix range-based filter lookups for integer fields in GraphQL API
|
||||||
|
* [#20915](https://github.com/netbox-community/netbox/issues/20915) - Restore user language preference after login via social authentication
|
||||||
|
* [#20934](https://github.com/netbox-community/netbox/issues/20934) - Fix dark mode flicker on page load
|
||||||
|
* [#21012](https://github.com/netbox-community/netbox/issues/21012) - Add pagination for VLAN table on interface view to prevent silent truncation at 100 entries
|
||||||
|
* [#21380](https://github.com/netbox-community/netbox/issues/21380) - Fix display of the background tasks table on mobile
|
||||||
|
* [#21440](https://github.com/netbox-community/netbox/issues/21440) - Avoid erroneously clearing primary/OOB IP assignments during bulk import/update
|
||||||
|
* [#21468](https://github.com/netbox-community/netbox/issues/21468) - Preserve safe custom HTTP headers when copying requests for background job processing
|
||||||
|
* [#21486](https://github.com/netbox-community/netbox/issues/21486) - Fix `AttributeError` exception caused by missing `COOKIES` attribute on `NetBoxFakeRequest`
|
||||||
|
* [#21512](https://github.com/netbox-community/netbox/issues/21512) - Fix GraphQL filter field name mismatch for device component types (e.g. `console_ports`)
|
||||||
|
* [#21531](https://github.com/netbox-community/netbox/issues/21531) - Fix search functionality for location when combined with other filters
|
||||||
|
* [#21556](https://github.com/netbox-community/netbox/issues/21556) - Avoid clearing the platform field when changing device type in the device edit form
|
||||||
|
* [#21579](https://github.com/netbox-community/netbox/issues/21579) - Hide the script "Add" button for users lacking the required permission
|
||||||
|
* [#21580](https://github.com/netbox-community/netbox/issues/21580) - Hide the virtual machine "Add components" dropdown for users lacking change permission
|
||||||
|
* [#21586](https://github.com/netbox-community/netbox/issues/21586) - Fix broken "Add child group" link in site group view (was pointing to the region endpoint)
|
||||||
|
* [#21618](https://github.com/netbox-community/netbox/issues/21618) - Fix cable termination points being lost when bulk-editing the cable profile
|
||||||
|
* [#21651](https://github.com/netbox-community/netbox/issues/21651) - Disable sorting by the `is_primary` column in the MAC address list view
|
||||||
|
* [#21653](https://github.com/netbox-community/netbox/issues/21653) - Fix profile-based cable tracing when a single origin carries multiple positions
|
||||||
|
* [#21673](https://github.com/netbox-community/netbox/issues/21673) - Fix display of primary IP address with associated NAT IP on virtual machine view
|
||||||
|
* [#21686](https://github.com/netbox-community/netbox/issues/21686) - Clean up cached circuit attributes when reassigning a circuit termination
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v4.5.4 (2026-03-03)
|
||||||
|
|
||||||
|
### Enhancements
|
||||||
|
|
||||||
|
* [#21369](https://github.com/netbox-community/netbox/issues/21369) - Support lazy-loading of image attachments
|
||||||
|
* [#21385](https://github.com/netbox-community/netbox/issues/21385) - Add contact assignment support for virtual circuits
|
||||||
|
* [#21394](https://github.com/netbox-community/netbox/issues/21394) - Add 10GBASE-CU and 40GBASE-SR4 BiDi interface types
|
||||||
|
* [#21477](https://github.com/netbox-community/netbox/issues/21477) - Extend GraphQL API filters for cables
|
||||||
|
|
||||||
|
### Performance Improvements
|
||||||
|
|
||||||
|
* [#21456](https://github.com/netbox-community/netbox/issues/21456) - Improve performance of config context resolution via GraphQL API
|
||||||
|
* [#21459](https://github.com/netbox-community/netbox/issues/21459) - Avoid prefetching data for hidden table columns
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* [#20490](https://github.com/netbox-community/netbox/issues/20490) - Restrict visibility of scripts in list view to users with view permission
|
||||||
|
* [#20911](https://github.com/netbox-community/netbox/issues/20911) - Sort module bay options alphabetically when installing a module
|
||||||
|
* [#21347](https://github.com/netbox-community/netbox/issues/21347) - The allocation of IPv6 addresses from a non-pool prefix should start at one, not zero
|
||||||
|
* [#21429](https://github.com/netbox-community/netbox/issues/21429) - Termination type should persist when employing "create & add another" workflow for cables
|
||||||
|
* [#21478](https://github.com/netbox-community/netbox/issues/21478) - Fix GraphQL union type resolution for connected console ports
|
||||||
|
* [#21481](https://github.com/netbox-community/netbox/issues/21481) - Fix display of facility ID on rack view
|
||||||
|
* [#21518](https://github.com/netbox-community/netbox/issues/21518) - Fix decimal custom field displaying as unset when value is zero
|
||||||
|
* [#21524](https://github.com/netbox-community/netbox/issues/21524) - Avoid `IndexError` exception when encountering stale cable paths
|
||||||
|
* [#21527](https://github.com/netbox-community/netbox/issues/21527) - Fix display of primary IP address with associated NAT IP on device view
|
||||||
|
* [#21550](https://github.com/netbox-community/netbox/issues/21550) - Ensure pre-change snapshots are recorded for related objects
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v4.5.3 (2026-02-17)
|
||||||
|
|
||||||
|
### Enhancements
|
||||||
|
|
||||||
|
* [#19129](https://github.com/netbox-community/netbox/issues/19129) - Improve display of multiple MAC addresses within interfaces table
|
||||||
|
* [#20981](https://github.com/netbox-community/netbox/issues/20981) - Enhance JSON rendering for custom validators and protection rules in config revision view
|
||||||
|
* [#21240](https://github.com/netbox-community/netbox/issues/21240) - Add support for configuring Redis `KWARGS` parameters
|
||||||
|
* [#21257](https://github.com/netbox-community/netbox/issues/21257) - `ContentTypeFilter` now accepts multiple values
|
||||||
|
* [#21266](https://github.com/netbox-community/netbox/issues/21266) - Add table columns representing installed devices to the device bays table
|
||||||
|
* [#21267](https://github.com/netbox-community/netbox/issues/21267) - Normalize device height formatting in rack units (display "0U")
|
||||||
|
* [#21268](https://github.com/netbox-community/netbox/issues/21268) - Add device type details panel to device view
|
||||||
|
* [#21337](https://github.com/netbox-community/netbox/issues/21337) - Show the assigned platform's parent on the virtual machine UI view
|
||||||
|
|
||||||
|
### Performance Improvements
|
||||||
|
|
||||||
|
* [#20211](https://github.com/netbox-community/netbox/issues/20211) - Use thumbnails for image attachment hover previews to improve page load performance
|
||||||
|
* [#21016](https://github.com/netbox-community/netbox/issues/21016) - Restore missing SQL indexes for MPTT fields
|
||||||
|
* [#21196](https://github.com/netbox-community/netbox/issues/21196) - `q` filter should match on primary IP only for IP address values when filtering devices/VMs
|
||||||
|
* [#21420](https://github.com/netbox-community/netbox/issues/21420) - Improve query performance of `ContentTypeFilter`
|
||||||
|
* [#21421](https://github.com/netbox-community/netbox/issues/21421) - Eliminate extraneous application of `DISTINCT` to queries for `MultipleChoiceFilter`
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* [#20435](https://github.com/netbox-community/netbox/issues/20435) - Fix navigation menu margin issue when scrollbar appears
|
||||||
|
* [#21127](https://github.com/netbox-community/netbox/issues/21127) - Ensure assigned cable paths are cleared when removing terminations from a cable
|
||||||
|
* [#21277](https://github.com/netbox-community/netbox/issues/21277) - Record pre-change snapshot when adding cluster members via UI
|
||||||
|
* [#21320](https://github.com/netbox-community/netbox/issues/21320) - Avoid validation failures when site or optional fields are missing during rack import
|
||||||
|
* [#21354](https://github.com/netbox-community/netbox/issues/21354) - Fix base URL in Swagger when `BASE_PATH` is set
|
||||||
|
* [#21358](https://github.com/netbox-community/netbox/issues/21358) - Token list in UI cannot be ordered by token value
|
||||||
|
* [#21371](https://github.com/netbox-community/netbox/issues/21371) - Fix `KeyError` exception when triggering a webhook from an event rule
|
||||||
|
* [#21375](https://github.com/netbox-community/netbox/issues/21375) - Address failure condition in `ipam.0070_vlangroup_vlan_id_ranges` migration
|
||||||
|
* [#21390](https://github.com/netbox-community/netbox/issues/21390) - Avoid creating "empty" changelog records for related objects when processing manyo-to-many relations
|
||||||
|
* [#21397](https://github.com/netbox-community/netbox/issues/21397) - Correct rendering of owner field in CircuitType edit form
|
||||||
|
* [#21412](https://github.com/netbox-community/netbox/issues/21412) - Avoid `AttributeError` exception on initialization when a plugin has local imports in `__init__.py`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## v4.5.2 (2026-02-03)
|
## v4.5.2 (2026-02-03)
|
||||||
|
|
||||||
### Enhancements
|
### Enhancements
|
||||||
|
|||||||
123
docs/release-notes/version-4.6.md
Normal file
123
docs/release-notes/version-4.6.md
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
# NetBox v4.6
|
||||||
|
|
||||||
|
## v4.6.0-beta1 (2026-04-14)
|
||||||
|
|
||||||
|
### New Features
|
||||||
|
|
||||||
|
#### Virtual Machine Types ([#5795](https://github.com/netbox-community/netbox/issues/5795))
|
||||||
|
|
||||||
|
A new VirtualMachineType model has been introduced to enable categorization of virtual machines by instance type, analogous to how DeviceType categorizes physical hardware. VM types can be defined once and reused across many virtual machines.
|
||||||
|
|
||||||
|
#### Cable Bundles ([#20151](https://github.com/netbox-community/netbox/issues/20151))
|
||||||
|
|
||||||
|
A new CableBundle model allows individual cables to be grouped together to represent physical cable runs that are managed as a unit; e.g. a bundle of 48 CAT6 cables between two patch panels. (Please note that this feature is _not_ suitable for modeling individual fiber strands within a single cable.)
|
||||||
|
|
||||||
|
#### Rack Groups ([#20961](https://github.com/netbox-community/netbox/issues/20961))
|
||||||
|
|
||||||
|
A flat RackGroup model has been reintroduced to provide a lightweight secondary axis of rack organization (e.g. by row or aisle) that is independent of the location hierarchy. Racks carry an optional foreign key to a RackGroup, and RackGroup can also serve as a scope for VLANGroup assignments.
|
||||||
|
|
||||||
|
#### ETag Support for REST API ([#21356](https://github.com/netbox-community/netbox/issues/21356))
|
||||||
|
|
||||||
|
The REST API now returns an `ETag` header on responses for individual objects, derived from the object's last-updated timestamp. Clients can supply an `If-Match` header on PUT/PATCH requests to guard against conflicting concurrent updates; if the object has been modified since the ETag was issued, the server returns a 412 (Precondition Failed) response.
|
||||||
|
|
||||||
|
#### Cursor-based Pagination for REST API ([#21363](https://github.com/netbox-community/netbox/issues/21363))
|
||||||
|
|
||||||
|
A new `start` query parameter has been introduced as an efficient alternative to the existing `offset` parameter for paginating large result sets. Rather than scanning the table up to a relative offset, the `start` parameter filters for objects with a primary key equal to or greater than the given value, enabling constant-time pagination regardless of result set size.
|
||||||
|
|
||||||
|
### Enhancements
|
||||||
|
|
||||||
|
* [#12024](https://github.com/netbox-community/netbox/issues/12024) - Permit virtual machines to be assigned to devices without a cluster
|
||||||
|
* [#14329](https://github.com/netbox-community/netbox/issues/14329) - Improve diff highlighting for custom field data in change logs
|
||||||
|
* [#15513](https://github.com/netbox-community/netbox/issues/15513) - Add bulk creation support for IP prefixes
|
||||||
|
* [#17654](https://github.com/netbox-community/netbox/issues/17654) - Support role assignment for ASNs
|
||||||
|
* [#19025](https://github.com/netbox-community/netbox/issues/19025) - Support optional schema validation for JSON custom fields
|
||||||
|
* [#19034](https://github.com/netbox-community/netbox/issues/19034) - Annotate total reserved unit count on rack reservations
|
||||||
|
* [#19138](https://github.com/netbox-community/netbox/issues/19138) - Include NAT addresses for primary & out-of-band IP addresses in REST API
|
||||||
|
* [#19796](https://github.com/netbox-community/netbox/issues/19796) - Support `{module}` position inheritance for nested module bays
|
||||||
|
* [#19953](https://github.com/netbox-community/netbox/issues/19953) - Enable debugging support for ConfigTemplate rendering
|
||||||
|
* [#20123](https://github.com/netbox-community/netbox/issues/20123) - Introduce options to control adoption/replication of device components via REST API (replicates UI behavior)
|
||||||
|
* [#20152](https://github.com/netbox-community/netbox/issues/20152) - Support for marking module and device bays as disabled
|
||||||
|
* [#20162](https://github.com/netbox-community/netbox/issues/20162) - Provide an option to execute as a background job when adding components to devices in bulk
|
||||||
|
* [#20163](https://github.com/netbox-community/netbox/issues/20163) - Add changelog message support for bulk device component creation
|
||||||
|
* [#20698](https://github.com/netbox-community/netbox/issues/20698) - Add read-only `total_vlan_ids` attribute on VLAN group representation in REST & GraphQL APIs
|
||||||
|
* [#20916](https://github.com/netbox-community/netbox/issues/20916) - Include stack trace for unhandled exceptions in job logs
|
||||||
|
* [#21157](https://github.com/netbox-community/netbox/issues/21157) - Include all public model classes in export template context
|
||||||
|
* [#21409](https://github.com/netbox-community/netbox/issues/21409) - Introduce `CHANGELOG_RETAIN_CREATE_LAST_UPDATE` configuration parameter to retain creation & most recent update record in change log for each object
|
||||||
|
* [#21575](https://github.com/netbox-community/netbox/issues/21575) - Introduce `{vc_position}` template variable for device component template name/label
|
||||||
|
* [#21662](https://github.com/netbox-community/netbox/issues/21662) - Increase `rf_channel_frequency` precision to 3 decimal places
|
||||||
|
* [#21702](https://github.com/netbox-community/netbox/issues/21702) - Include a serialized representation of the HTTP request in each webhook
|
||||||
|
* [#21720](https://github.com/netbox-community/netbox/issues/21720) - Align HTTP basic auth regex of `EnhancedURLValidator` with Django's `URLValidator`
|
||||||
|
* [#21770](https://github.com/netbox-community/netbox/issues/21770) - Enable specifying columns to include/exclude on embedded tables
|
||||||
|
* [#21771](https://github.com/netbox-community/netbox/issues/21771) - Add support for partial tag assignment (`add_tags`) and removal (`remove_tags`) via REST API
|
||||||
|
* [#21780](https://github.com/netbox-community/netbox/issues/21780) - Add changelog message support to bulk creation of IP addresses
|
||||||
|
* [#21865](https://github.com/netbox-community/netbox/issues/21865) - Allow setting empty `INTERNAL_IPS` to enable debug toolbar for all clients
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
|
||||||
|
* [#21455](https://github.com/netbox-community/netbox/issues/21455) - Ensure PostgreSQL indexes exist to support the default ordering of each model
|
||||||
|
|
||||||
|
### Plugins
|
||||||
|
|
||||||
|
* [#20924](https://github.com/netbox-community/netbox/issues/20924) - Introduce support for declarative layouts and reusable UI components
|
||||||
|
* [#21357](https://github.com/netbox-community/netbox/issues/21357) - Provide an API for plugins to register custom model actions (for permission assignment)
|
||||||
|
|
||||||
|
### Deprecations
|
||||||
|
|
||||||
|
* [#21284](https://github.com/netbox-community/netbox/issues/21284) - Deprecate the `username` and `request_id` fields in event data
|
||||||
|
* [#21304](https://github.com/netbox-community/netbox/issues/21304) - Deprecate the `housekeeping` management command
|
||||||
|
* [#21331](https://github.com/netbox-community/netbox/issues/21331) - Deprecate NetBox's custom `querystring` template tag
|
||||||
|
* [#21881](https://github.com/netbox-community/netbox/issues/21881) - Deprecate legacy Sentry configuration parameters
|
||||||
|
* [#21884](https://github.com/netbox-community/netbox/issues/21884) - Deprecate the obsolete `DEFAULT_ACTION_PERMISSIONS` mapping
|
||||||
|
* [#21887](https://github.com/netbox-community/netbox/issues/21887) - Deprecate support for legacy view actions
|
||||||
|
* [#21890](https://github.com/netbox-community/netbox/issues/21890) - Deprecate `models` key in application registry
|
||||||
|
|
||||||
|
### Other Changes
|
||||||
|
|
||||||
|
* [#20984](https://github.com/netbox-community/netbox/issues/20984) - Upgrade to Django 6.0
|
||||||
|
* [#21635](https://github.com/netbox-community/netbox/issues/21635) - Migrate documentation site from mkdocs to Zensical
|
||||||
|
|
||||||
|
### REST API Changes
|
||||||
|
|
||||||
|
* New features:
|
||||||
|
* `ETag` response header and `If-Match` request header support for all individual object endpoints
|
||||||
|
* `start` query parameter for cursor-based pagination on all list endpoints
|
||||||
|
* `add_tags` and `remove_tags` write-only fields on all taggable model serializers
|
||||||
|
* New endpoints:
|
||||||
|
* `GET/POST /api/dcim/cable-bundles/`
|
||||||
|
* `GET/PUT/PATCH/DELETE /api/dcim/cable-bundles/<id>/`
|
||||||
|
* `GET/POST /api/dcim/rack-groups/`
|
||||||
|
* `GET/PUT/PATCH/DELETE /api/dcim/rack-groups/<id>/`
|
||||||
|
* `GET/POST /api/virtualization/virtual-machine-types/`
|
||||||
|
* `GET/PUT/PATCH/DELETE /api/virtualization/virtual-machine-types/<id>/`
|
||||||
|
* `dcim.Cable`
|
||||||
|
* Add optional foreign key field `bundle`
|
||||||
|
* `dcim.Device`
|
||||||
|
* The `primary_ip`, `primary_ip4`, `primary_ip6`, and `oob_ip` nested representations now include `nat_inside` and `nat_outside`
|
||||||
|
* `dcim.DeviceBay`
|
||||||
|
* Add boolean field `enabled`
|
||||||
|
* Add read-only boolean field `_occupied`
|
||||||
|
* `dcim.DeviceBayTemplate`
|
||||||
|
* Add boolean field `enabled`
|
||||||
|
* `dcim.Module`
|
||||||
|
* Add write-only boolean fields `replicate_components` and `adopt_components`
|
||||||
|
* `dcim.ModuleBay`
|
||||||
|
* Add boolean field `enabled`
|
||||||
|
* Add read-only boolean field `_occupied`
|
||||||
|
* `dcim.ModuleBayTemplate`
|
||||||
|
* Add boolean field `enabled`
|
||||||
|
* `dcim.Rack`
|
||||||
|
* Add optional foreign key field `group`
|
||||||
|
* `dcim.RackReservation`
|
||||||
|
* Add read-only integer field `unit_count`
|
||||||
|
* `extras.CustomField`
|
||||||
|
* Add JSON field `validation_schema`
|
||||||
|
* `ipam.ASN`
|
||||||
|
* Add optional foreign key field `role`
|
||||||
|
* `ipam.Role`
|
||||||
|
* Annotate count of assigned ASNs (`asn_count`)
|
||||||
|
* `ipam.VLANGroup`
|
||||||
|
* Add read-only field `total_vlan_ids`
|
||||||
|
* `virtualization.VirtualMachine`
|
||||||
|
* Add optional foreign key field `virtual_machine_type`
|
||||||
|
* The `primary_ip`, `primary_ip4`, and `primary_ip6` nested representations now include `nat_inside` and `nat_outside`
|
||||||
|
* The `cluster` field is now optional (nullable)
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# Note: NetBox has migrated from MkDocs to Zensical
|
||||||
site_name: NetBox Documentation
|
site_name: NetBox Documentation
|
||||||
site_dir: netbox/project-static/docs
|
site_dir: netbox/project-static/docs
|
||||||
site_url: https://docs.netbox.dev/
|
site_url: https://docs.netbox.dev/
|
||||||
@@ -151,6 +152,7 @@ nav:
|
|||||||
- Filters & Filter Sets: 'plugins/development/filtersets.md'
|
- Filters & Filter Sets: 'plugins/development/filtersets.md'
|
||||||
- Search: 'plugins/development/search.md'
|
- Search: 'plugins/development/search.md'
|
||||||
- Event Types: 'plugins/development/event-types.md'
|
- Event Types: 'plugins/development/event-types.md'
|
||||||
|
- Permissions: 'plugins/development/permissions.md'
|
||||||
- Data Backends: 'plugins/development/data-backends.md'
|
- Data Backends: 'plugins/development/data-backends.md'
|
||||||
- Webhooks: 'plugins/development/webhooks.md'
|
- Webhooks: 'plugins/development/webhooks.md'
|
||||||
- User Interface: 'plugins/development/user-interface.md'
|
- User Interface: 'plugins/development/user-interface.md'
|
||||||
@@ -189,6 +191,7 @@ nav:
|
|||||||
- Job: 'models/core/job.md'
|
- Job: 'models/core/job.md'
|
||||||
- DCIM:
|
- DCIM:
|
||||||
- Cable: 'models/dcim/cable.md'
|
- Cable: 'models/dcim/cable.md'
|
||||||
|
- CableBundle: 'models/dcim/cablebundle.md'
|
||||||
- ConsolePort: 'models/dcim/consoleport.md'
|
- ConsolePort: 'models/dcim/consoleport.md'
|
||||||
- ConsolePortTemplate: 'models/dcim/consoleporttemplate.md'
|
- ConsolePortTemplate: 'models/dcim/consoleporttemplate.md'
|
||||||
- ConsoleServerPort: 'models/dcim/consoleserverport.md'
|
- ConsoleServerPort: 'models/dcim/consoleserverport.md'
|
||||||
@@ -221,6 +224,7 @@ nav:
|
|||||||
- PowerPort: 'models/dcim/powerport.md'
|
- PowerPort: 'models/dcim/powerport.md'
|
||||||
- PowerPortTemplate: 'models/dcim/powerporttemplate.md'
|
- PowerPortTemplate: 'models/dcim/powerporttemplate.md'
|
||||||
- Rack: 'models/dcim/rack.md'
|
- Rack: 'models/dcim/rack.md'
|
||||||
|
- RackGroup: 'models/dcim/rackgroup.md'
|
||||||
- RackReservation: 'models/dcim/rackreservation.md'
|
- RackReservation: 'models/dcim/rackreservation.md'
|
||||||
- RackRole: 'models/dcim/rackrole.md'
|
- RackRole: 'models/dcim/rackrole.md'
|
||||||
- RackType: 'models/dcim/racktype.md'
|
- RackType: 'models/dcim/racktype.md'
|
||||||
@@ -285,6 +289,7 @@ nav:
|
|||||||
- VMInterface: 'models/virtualization/vminterface.md'
|
- VMInterface: 'models/virtualization/vminterface.md'
|
||||||
- VirtualDisk: 'models/virtualization/virtualdisk.md'
|
- VirtualDisk: 'models/virtualization/virtualdisk.md'
|
||||||
- VirtualMachine: 'models/virtualization/virtualmachine.md'
|
- VirtualMachine: 'models/virtualization/virtualmachine.md'
|
||||||
|
- VirtualMachineType: 'models/virtualization/virtualmachinetype.md'
|
||||||
- VPN:
|
- VPN:
|
||||||
- IKEPolicy: 'models/vpn/ikepolicy.md'
|
- IKEPolicy: 'models/vpn/ikepolicy.md'
|
||||||
- IKEProposal: 'models/vpn/ikeproposal.md'
|
- IKEProposal: 'models/vpn/ikeproposal.md'
|
||||||
@@ -322,6 +327,7 @@ nav:
|
|||||||
- git Cheat Sheet: 'development/git-cheat-sheet.md'
|
- git Cheat Sheet: 'development/git-cheat-sheet.md'
|
||||||
- Release Notes:
|
- Release Notes:
|
||||||
- Summary: 'release-notes/index.md'
|
- Summary: 'release-notes/index.md'
|
||||||
|
- Version 4.6: 'release-notes/version-4.6.md'
|
||||||
- Version 4.5: 'release-notes/version-4.5.md'
|
- Version 4.5: 'release-notes/version-4.5.md'
|
||||||
- Version 4.4: 'release-notes/version-4.4.md'
|
- Version 4.4: 'release-notes/version-4.4.md'
|
||||||
- Version 4.3: 'release-notes/version-4.3.md'
|
- Version 4.3: 'release-notes/version-4.3.md'
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from django.urls import include, path
|
from django.urls import include, path
|
||||||
|
|
||||||
from utilities.urls import get_model_urls
|
from utilities.urls import get_model_urls
|
||||||
|
|
||||||
from . import views
|
from . import views
|
||||||
|
|
||||||
app_name = 'account'
|
app_name = 'account'
|
||||||
|
|||||||
@@ -2,14 +2,15 @@ import logging
|
|||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.auth import login as auth_login, logout as auth_logout, update_session_auth_hash
|
from django.contrib.auth import login as auth_login
|
||||||
|
from django.contrib.auth import logout as auth_logout
|
||||||
|
from django.contrib.auth import update_session_auth_hash
|
||||||
from django.contrib.auth.forms import AuthenticationForm, PasswordChangeForm
|
from django.contrib.auth.forms import AuthenticationForm, PasswordChangeForm
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
from django.contrib.auth.models import update_last_login
|
from django.contrib.auth.models import update_last_login
|
||||||
from django.contrib.auth.signals import user_logged_in
|
from django.contrib.auth.signals import user_logged_in
|
||||||
from django.http import HttpResponseRedirect
|
from django.http import HttpResponseRedirect
|
||||||
from django.shortcuts import get_object_or_404, redirect
|
from django.shortcuts import get_object_or_404, redirect, render, resolve_url
|
||||||
from django.shortcuts import render, resolve_url
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
from django.utils.http import urlencode
|
from django.utils.http import urlencode
|
||||||
@@ -35,11 +36,11 @@ from utilities.request import safe_for_redirect
|
|||||||
from utilities.string import remove_linebreaks
|
from utilities.string import remove_linebreaks
|
||||||
from utilities.views import register_model_view
|
from utilities.views import register_model_view
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Login/logout
|
# Login/logout
|
||||||
#
|
#
|
||||||
|
|
||||||
|
|
||||||
class LoginView(View):
|
class LoginView(View):
|
||||||
"""
|
"""
|
||||||
Perform user authentication via the web UI.
|
Perform user authentication via the web UI.
|
||||||
@@ -139,9 +140,8 @@ class LoginView(View):
|
|||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
else:
|
username = form['username'].value()
|
||||||
username = form['username'].value()
|
logger.debug(f"Login form validation failed for username: {remove_linebreaks(username)}")
|
||||||
logger.debug(f"Login form validation failed for username: {remove_linebreaks(username)}")
|
|
||||||
|
|
||||||
return render(request, self.template_name, {
|
return render(request, self.template_name, {
|
||||||
'form': form,
|
'form': form,
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
from .serializers_.providers import *
|
|
||||||
from .serializers_.circuits import *
|
from .serializers_.circuits import *
|
||||||
|
from .serializers_.providers import *
|
||||||
|
|||||||
@@ -4,24 +4,34 @@ from rest_framework import serializers
|
|||||||
from circuits.choices import CircuitPriorityChoices, CircuitStatusChoices, VirtualCircuitTerminationRoleChoices
|
from circuits.choices import CircuitPriorityChoices, CircuitStatusChoices, VirtualCircuitTerminationRoleChoices
|
||||||
from circuits.constants import CIRCUIT_GROUP_ASSIGNMENT_MEMBER_MODELS, CIRCUIT_TERMINATION_TERMINATION_TYPES
|
from circuits.constants import CIRCUIT_GROUP_ASSIGNMENT_MEMBER_MODELS, CIRCUIT_TERMINATION_TERMINATION_TYPES
|
||||||
from circuits.models import (
|
from circuits.models import (
|
||||||
Circuit, CircuitGroup, CircuitGroupAssignment, CircuitTermination, CircuitType, VirtualCircuit,
|
Circuit,
|
||||||
VirtualCircuitTermination, VirtualCircuitType,
|
CircuitGroup,
|
||||||
|
CircuitGroupAssignment,
|
||||||
|
CircuitTermination,
|
||||||
|
CircuitType,
|
||||||
|
VirtualCircuit,
|
||||||
|
VirtualCircuitTermination,
|
||||||
|
VirtualCircuitType,
|
||||||
)
|
)
|
||||||
from dcim.api.serializers_.device_components import InterfaceSerializer
|
|
||||||
from dcim.api.serializers_.cables import CabledObjectSerializer
|
from dcim.api.serializers_.cables import CabledObjectSerializer
|
||||||
|
from dcim.api.serializers_.device_components import InterfaceSerializer
|
||||||
from netbox.api.fields import ChoiceField, ContentTypeField, RelatedObjectCountField
|
from netbox.api.fields import ChoiceField, ContentTypeField, RelatedObjectCountField
|
||||||
from netbox.api.gfk_fields import GFKSerializerField
|
from netbox.api.gfk_fields import GFKSerializerField
|
||||||
from netbox.api.serializers import (
|
from netbox.api.serializers import (
|
||||||
NetBoxModelSerializer, OrganizationalModelSerializer, PrimaryModelSerializer, WritableNestedSerializer,
|
NetBoxModelSerializer,
|
||||||
|
OrganizationalModelSerializer,
|
||||||
|
PrimaryModelSerializer,
|
||||||
|
WritableNestedSerializer,
|
||||||
)
|
)
|
||||||
from netbox.choices import DistanceUnitChoices
|
from netbox.choices import DistanceUnitChoices
|
||||||
from tenancy.api.serializers_.tenants import TenantSerializer
|
from tenancy.api.serializers_.tenants import TenantSerializer
|
||||||
|
|
||||||
from .providers import ProviderAccountSerializer, ProviderNetworkSerializer, ProviderSerializer
|
from .providers import ProviderAccountSerializer, ProviderNetworkSerializer, ProviderSerializer
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'CircuitSerializer',
|
|
||||||
'CircuitGroupAssignmentSerializer',
|
'CircuitGroupAssignmentSerializer',
|
||||||
'CircuitGroupSerializer',
|
'CircuitGroupSerializer',
|
||||||
|
'CircuitSerializer',
|
||||||
'CircuitTerminationSerializer',
|
'CircuitTerminationSerializer',
|
||||||
'CircuitTypeSerializer',
|
'CircuitTypeSerializer',
|
||||||
'VirtualCircuitSerializer',
|
'VirtualCircuitSerializer',
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from ipam.api.serializers_.asns import ASNSerializer
|
|||||||
from ipam.models import ASN
|
from ipam.models import ASN
|
||||||
from netbox.api.fields import RelatedObjectCountField, SerializedPKRelatedField
|
from netbox.api.fields import RelatedObjectCountField, SerializedPKRelatedField
|
||||||
from netbox.api.serializers import PrimaryModelSerializer
|
from netbox.api.serializers import PrimaryModelSerializer
|
||||||
|
|
||||||
from .nested import NestedProviderAccountSerializer
|
from .nested import NestedProviderAccountSerializer
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from netbox.api.routers import NetBoxRouter
|
from netbox.api.routers import NetBoxRouter
|
||||||
from . import views
|
|
||||||
|
|
||||||
|
from . import views
|
||||||
|
|
||||||
router = NetBoxRouter()
|
router = NetBoxRouter()
|
||||||
router.APIRootView = views.CircuitsRootView
|
router.APIRootView = views.CircuitsRootView
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from circuits import filtersets
|
|||||||
from circuits.models import *
|
from circuits.models import *
|
||||||
from dcim.api.views import PassThroughPortMixin
|
from dcim.api.views import PassThroughPortMixin
|
||||||
from netbox.api.viewsets import NetBoxModelViewSet
|
from netbox.api.viewsets import NetBoxModelViewSet
|
||||||
|
|
||||||
from . import serializers
|
from . import serializers
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ class CircuitsConfig(AppConfig):
|
|||||||
|
|
||||||
def ready(self):
|
def ready(self):
|
||||||
from netbox.models.features import register_models
|
from netbox.models.features import register_models
|
||||||
from . import signals, search # noqa: F401
|
|
||||||
|
from . import search, signals # noqa: F401
|
||||||
from .models import CircuitTermination
|
from .models import CircuitTermination
|
||||||
|
|
||||||
# Register models
|
# Register models
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
|
|
||||||
from utilities.choices import ChoiceSet
|
from utilities.choices import ChoiceSet
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Circuits
|
# Circuits
|
||||||
#
|
#
|
||||||
|
|
||||||
|
|
||||||
class CircuitStatusChoices(ChoiceSet):
|
class CircuitStatusChoices(ChoiceSet):
|
||||||
key = 'Circuit.status'
|
key = 'Circuit.status'
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
|
||||||
|
|
||||||
# models values for ContentTypes which may be CircuitTermination termination types
|
# models values for ContentTypes which may be CircuitTermination termination types
|
||||||
CIRCUIT_TERMINATION_TERMINATION_TYPES = (
|
CIRCUIT_TERMINATION_TERMINATION_TYPES = (
|
||||||
'region', 'sitegroup', 'site', 'location', 'providernetwork',
|
'region', 'sitegroup', 'site', 'location', 'providernetwork',
|
||||||
|
|||||||
@@ -9,9 +9,13 @@ from ipam.models import ASN
|
|||||||
from netbox.filtersets import NetBoxModelFilterSet, OrganizationalModelFilterSet, PrimaryModelFilterSet
|
from netbox.filtersets import NetBoxModelFilterSet, OrganizationalModelFilterSet, PrimaryModelFilterSet
|
||||||
from tenancy.filtersets import ContactModelFilterSet, TenancyFilterSet
|
from tenancy.filtersets import ContactModelFilterSet, TenancyFilterSet
|
||||||
from utilities.filters import (
|
from utilities.filters import (
|
||||||
ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter, TreeNodeMultipleChoiceFilter,
|
MultiValueCharFilter,
|
||||||
|
MultiValueContentTypeFilter,
|
||||||
|
MultiValueNumberFilter,
|
||||||
|
TreeNodeMultipleChoiceFilter,
|
||||||
)
|
)
|
||||||
from utilities.filtersets import register_filterset
|
from utilities.filtersets import register_filterset
|
||||||
|
|
||||||
from .choices import *
|
from .choices import *
|
||||||
from .models import *
|
from .models import *
|
||||||
|
|
||||||
@@ -21,9 +25,9 @@ __all__ = (
|
|||||||
'CircuitGroupFilterSet',
|
'CircuitGroupFilterSet',
|
||||||
'CircuitTerminationFilterSet',
|
'CircuitTerminationFilterSet',
|
||||||
'CircuitTypeFilterSet',
|
'CircuitTypeFilterSet',
|
||||||
'ProviderNetworkFilterSet',
|
|
||||||
'ProviderAccountFilterSet',
|
'ProviderAccountFilterSet',
|
||||||
'ProviderFilterSet',
|
'ProviderFilterSet',
|
||||||
|
'ProviderNetworkFilterSet',
|
||||||
'VirtualCircuitFilterSet',
|
'VirtualCircuitFilterSet',
|
||||||
'VirtualCircuitTerminationFilterSet',
|
'VirtualCircuitTerminationFilterSet',
|
||||||
'VirtualCircuitTypeFilterSet',
|
'VirtualCircuitTypeFilterSet',
|
||||||
@@ -99,11 +103,13 @@ class ProviderFilterSet(PrimaryModelFilterSet, ContactModelFilterSet):
|
|||||||
class ProviderAccountFilterSet(PrimaryModelFilterSet, ContactModelFilterSet):
|
class ProviderAccountFilterSet(PrimaryModelFilterSet, ContactModelFilterSet):
|
||||||
provider_id = django_filters.ModelMultipleChoiceFilter(
|
provider_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=Provider.objects.all(),
|
queryset=Provider.objects.all(),
|
||||||
|
distinct=False,
|
||||||
label=_('Provider (ID)'),
|
label=_('Provider (ID)'),
|
||||||
)
|
)
|
||||||
provider = django_filters.ModelMultipleChoiceFilter(
|
provider = django_filters.ModelMultipleChoiceFilter(
|
||||||
field_name='provider__slug',
|
field_name='provider__slug',
|
||||||
queryset=Provider.objects.all(),
|
queryset=Provider.objects.all(),
|
||||||
|
distinct=False,
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label=_('Provider (slug)'),
|
label=_('Provider (slug)'),
|
||||||
)
|
)
|
||||||
@@ -127,11 +133,13 @@ class ProviderAccountFilterSet(PrimaryModelFilterSet, ContactModelFilterSet):
|
|||||||
class ProviderNetworkFilterSet(PrimaryModelFilterSet):
|
class ProviderNetworkFilterSet(PrimaryModelFilterSet):
|
||||||
provider_id = django_filters.ModelMultipleChoiceFilter(
|
provider_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=Provider.objects.all(),
|
queryset=Provider.objects.all(),
|
||||||
|
distinct=False,
|
||||||
label=_('Provider (ID)'),
|
label=_('Provider (ID)'),
|
||||||
)
|
)
|
||||||
provider = django_filters.ModelMultipleChoiceFilter(
|
provider = django_filters.ModelMultipleChoiceFilter(
|
||||||
field_name='provider__slug',
|
field_name='provider__slug',
|
||||||
queryset=Provider.objects.all(),
|
queryset=Provider.objects.all(),
|
||||||
|
distinct=False,
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label=_('Provider (slug)'),
|
label=_('Provider (slug)'),
|
||||||
)
|
)
|
||||||
@@ -163,22 +171,26 @@ class CircuitTypeFilterSet(OrganizationalModelFilterSet):
|
|||||||
class CircuitFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
|
class CircuitFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
|
||||||
provider_id = django_filters.ModelMultipleChoiceFilter(
|
provider_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=Provider.objects.all(),
|
queryset=Provider.objects.all(),
|
||||||
|
distinct=False,
|
||||||
label=_('Provider (ID)'),
|
label=_('Provider (ID)'),
|
||||||
)
|
)
|
||||||
provider = django_filters.ModelMultipleChoiceFilter(
|
provider = django_filters.ModelMultipleChoiceFilter(
|
||||||
field_name='provider__slug',
|
field_name='provider__slug',
|
||||||
queryset=Provider.objects.all(),
|
queryset=Provider.objects.all(),
|
||||||
|
distinct=False,
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label=_('Provider (slug)'),
|
label=_('Provider (slug)'),
|
||||||
)
|
)
|
||||||
provider_account_id = django_filters.ModelMultipleChoiceFilter(
|
provider_account_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
field_name='provider_account',
|
field_name='provider_account',
|
||||||
queryset=ProviderAccount.objects.all(),
|
queryset=ProviderAccount.objects.all(),
|
||||||
|
distinct=False,
|
||||||
label=_('Provider account (ID)'),
|
label=_('Provider account (ID)'),
|
||||||
)
|
)
|
||||||
provider_account = django_filters.ModelMultipleChoiceFilter(
|
provider_account = django_filters.ModelMultipleChoiceFilter(
|
||||||
field_name='provider_account__account',
|
field_name='provider_account__account',
|
||||||
queryset=Provider.objects.all(),
|
queryset=Provider.objects.all(),
|
||||||
|
distinct=False,
|
||||||
to_field_name='account',
|
to_field_name='account',
|
||||||
label=_('Provider account (account)'),
|
label=_('Provider account (account)'),
|
||||||
)
|
)
|
||||||
@@ -189,16 +201,19 @@ class CircuitFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilt
|
|||||||
)
|
)
|
||||||
type_id = django_filters.ModelMultipleChoiceFilter(
|
type_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=CircuitType.objects.all(),
|
queryset=CircuitType.objects.all(),
|
||||||
|
distinct=False,
|
||||||
label=_('Circuit type (ID)'),
|
label=_('Circuit type (ID)'),
|
||||||
)
|
)
|
||||||
type = django_filters.ModelMultipleChoiceFilter(
|
type = django_filters.ModelMultipleChoiceFilter(
|
||||||
field_name='type__slug',
|
field_name='type__slug',
|
||||||
queryset=CircuitType.objects.all(),
|
queryset=CircuitType.objects.all(),
|
||||||
|
distinct=False,
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label=_('Circuit type (slug)'),
|
label=_('Circuit type (slug)'),
|
||||||
)
|
)
|
||||||
status = django_filters.MultipleChoiceFilter(
|
status = django_filters.MultipleChoiceFilter(
|
||||||
choices=CircuitStatusChoices,
|
choices=CircuitStatusChoices,
|
||||||
|
distinct=False,
|
||||||
null_value=None
|
null_value=None
|
||||||
)
|
)
|
||||||
region_id = TreeNodeMultipleChoiceFilter(
|
region_id = TreeNodeMultipleChoiceFilter(
|
||||||
@@ -245,10 +260,12 @@ class CircuitFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilt
|
|||||||
)
|
)
|
||||||
termination_a_id = django_filters.ModelMultipleChoiceFilter(
|
termination_a_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=CircuitTermination.objects.all(),
|
queryset=CircuitTermination.objects.all(),
|
||||||
|
distinct=False,
|
||||||
label=_('Termination A (ID)'),
|
label=_('Termination A (ID)'),
|
||||||
)
|
)
|
||||||
termination_z_id = django_filters.ModelMultipleChoiceFilter(
|
termination_z_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=CircuitTermination.objects.all(),
|
queryset=CircuitTermination.objects.all(),
|
||||||
|
distinct=False,
|
||||||
label=_('Termination A (ID)'),
|
label=_('Termination A (ID)'),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -279,9 +296,10 @@ class CircuitTerminationFilterSet(NetBoxModelFilterSet, CabledObjectFilterSet):
|
|||||||
)
|
)
|
||||||
circuit_id = django_filters.ModelMultipleChoiceFilter(
|
circuit_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=Circuit.objects.all(),
|
queryset=Circuit.objects.all(),
|
||||||
|
distinct=False,
|
||||||
label=_('Circuit'),
|
label=_('Circuit'),
|
||||||
)
|
)
|
||||||
termination_type = ContentTypeFilter()
|
termination_type = MultiValueContentTypeFilter()
|
||||||
region_id = TreeNodeMultipleChoiceFilter(
|
region_id = TreeNodeMultipleChoiceFilter(
|
||||||
queryset=Region.objects.all(),
|
queryset=Region.objects.all(),
|
||||||
field_name='_region',
|
field_name='_region',
|
||||||
@@ -310,12 +328,14 @@ class CircuitTerminationFilterSet(NetBoxModelFilterSet, CabledObjectFilterSet):
|
|||||||
)
|
)
|
||||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
|
distinct=False,
|
||||||
field_name='_site',
|
field_name='_site',
|
||||||
label=_('Site (ID)'),
|
label=_('Site (ID)'),
|
||||||
)
|
)
|
||||||
site = django_filters.ModelMultipleChoiceFilter(
|
site = django_filters.ModelMultipleChoiceFilter(
|
||||||
field_name='_site__slug',
|
field_name='_site__slug',
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
|
distinct=False,
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label=_('Site (slug)'),
|
label=_('Site (slug)'),
|
||||||
)
|
)
|
||||||
@@ -334,17 +354,20 @@ class CircuitTerminationFilterSet(NetBoxModelFilterSet, CabledObjectFilterSet):
|
|||||||
)
|
)
|
||||||
provider_network_id = django_filters.ModelMultipleChoiceFilter(
|
provider_network_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=ProviderNetwork.objects.all(),
|
queryset=ProviderNetwork.objects.all(),
|
||||||
|
distinct=False,
|
||||||
field_name='_provider_network',
|
field_name='_provider_network',
|
||||||
label=_('ProviderNetwork (ID)'),
|
label=_('ProviderNetwork (ID)'),
|
||||||
)
|
)
|
||||||
provider_id = django_filters.ModelMultipleChoiceFilter(
|
provider_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
field_name='circuit__provider_id',
|
field_name='circuit__provider_id',
|
||||||
queryset=Provider.objects.all(),
|
queryset=Provider.objects.all(),
|
||||||
|
distinct=False,
|
||||||
label=_('Provider (ID)'),
|
label=_('Provider (ID)'),
|
||||||
)
|
)
|
||||||
provider = django_filters.ModelMultipleChoiceFilter(
|
provider = django_filters.ModelMultipleChoiceFilter(
|
||||||
field_name='circuit__provider__slug',
|
field_name='circuit__provider__slug',
|
||||||
queryset=Provider.objects.all(),
|
queryset=Provider.objects.all(),
|
||||||
|
distinct=False,
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label=_('Provider (slug)'),
|
label=_('Provider (slug)'),
|
||||||
)
|
)
|
||||||
@@ -381,7 +404,7 @@ class CircuitGroupAssignmentFilterSet(NetBoxModelFilterSet):
|
|||||||
method='search',
|
method='search',
|
||||||
label=_('Search'),
|
label=_('Search'),
|
||||||
)
|
)
|
||||||
member_type = ContentTypeFilter()
|
member_type = MultiValueContentTypeFilter()
|
||||||
circuit = MultiValueCharFilter(
|
circuit = MultiValueCharFilter(
|
||||||
method='filter_circuit',
|
method='filter_circuit',
|
||||||
field_name='cid',
|
field_name='cid',
|
||||||
@@ -414,11 +437,13 @@ class CircuitGroupAssignmentFilterSet(NetBoxModelFilterSet):
|
|||||||
)
|
)
|
||||||
group_id = django_filters.ModelMultipleChoiceFilter(
|
group_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=CircuitGroup.objects.all(),
|
queryset=CircuitGroup.objects.all(),
|
||||||
|
distinct=False,
|
||||||
label=_('Circuit group (ID)'),
|
label=_('Circuit group (ID)'),
|
||||||
)
|
)
|
||||||
group = django_filters.ModelMultipleChoiceFilter(
|
group = django_filters.ModelMultipleChoiceFilter(
|
||||||
field_name='group__slug',
|
field_name='group__slug',
|
||||||
queryset=CircuitGroup.objects.all(),
|
queryset=CircuitGroup.objects.all(),
|
||||||
|
distinct=False,
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label=_('Circuit group (slug)'),
|
label=_('Circuit group (slug)'),
|
||||||
)
|
)
|
||||||
@@ -488,41 +513,49 @@ class VirtualCircuitFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
|||||||
provider_id = django_filters.ModelMultipleChoiceFilter(
|
provider_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
field_name='provider_network__provider',
|
field_name='provider_network__provider',
|
||||||
queryset=Provider.objects.all(),
|
queryset=Provider.objects.all(),
|
||||||
|
distinct=False,
|
||||||
label=_('Provider (ID)'),
|
label=_('Provider (ID)'),
|
||||||
)
|
)
|
||||||
provider = django_filters.ModelMultipleChoiceFilter(
|
provider = django_filters.ModelMultipleChoiceFilter(
|
||||||
field_name='provider_network__provider__slug',
|
field_name='provider_network__provider__slug',
|
||||||
queryset=Provider.objects.all(),
|
queryset=Provider.objects.all(),
|
||||||
|
distinct=False,
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label=_('Provider (slug)'),
|
label=_('Provider (slug)'),
|
||||||
)
|
)
|
||||||
provider_account_id = django_filters.ModelMultipleChoiceFilter(
|
provider_account_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
field_name='provider_account',
|
field_name='provider_account',
|
||||||
queryset=ProviderAccount.objects.all(),
|
queryset=ProviderAccount.objects.all(),
|
||||||
|
distinct=False,
|
||||||
label=_('Provider account (ID)'),
|
label=_('Provider account (ID)'),
|
||||||
)
|
)
|
||||||
provider_account = django_filters.ModelMultipleChoiceFilter(
|
provider_account = django_filters.ModelMultipleChoiceFilter(
|
||||||
field_name='provider_account__account',
|
field_name='provider_account__account',
|
||||||
queryset=Provider.objects.all(),
|
queryset=Provider.objects.all(),
|
||||||
|
distinct=False,
|
||||||
to_field_name='account',
|
to_field_name='account',
|
||||||
label=_('Provider account (account)'),
|
label=_('Provider account (account)'),
|
||||||
)
|
)
|
||||||
provider_network_id = django_filters.ModelMultipleChoiceFilter(
|
provider_network_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=ProviderNetwork.objects.all(),
|
queryset=ProviderNetwork.objects.all(),
|
||||||
|
distinct=False,
|
||||||
label=_('Provider network (ID)'),
|
label=_('Provider network (ID)'),
|
||||||
)
|
)
|
||||||
type_id = django_filters.ModelMultipleChoiceFilter(
|
type_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=VirtualCircuitType.objects.all(),
|
queryset=VirtualCircuitType.objects.all(),
|
||||||
|
distinct=False,
|
||||||
label=_('Virtual circuit type (ID)'),
|
label=_('Virtual circuit type (ID)'),
|
||||||
)
|
)
|
||||||
type = django_filters.ModelMultipleChoiceFilter(
|
type = django_filters.ModelMultipleChoiceFilter(
|
||||||
field_name='type__slug',
|
field_name='type__slug',
|
||||||
queryset=VirtualCircuitType.objects.all(),
|
queryset=VirtualCircuitType.objects.all(),
|
||||||
|
distinct=False,
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label=_('Virtual circuit type (slug)'),
|
label=_('Virtual circuit type (slug)'),
|
||||||
)
|
)
|
||||||
status = django_filters.MultipleChoiceFilter(
|
status = django_filters.MultipleChoiceFilter(
|
||||||
choices=CircuitStatusChoices,
|
choices=CircuitStatusChoices,
|
||||||
|
distinct=False,
|
||||||
null_value=None
|
null_value=None
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -548,41 +581,49 @@ class VirtualCircuitTerminationFilterSet(NetBoxModelFilterSet):
|
|||||||
)
|
)
|
||||||
virtual_circuit_id = django_filters.ModelMultipleChoiceFilter(
|
virtual_circuit_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=VirtualCircuit.objects.all(),
|
queryset=VirtualCircuit.objects.all(),
|
||||||
|
distinct=False,
|
||||||
label=_('Virtual circuit'),
|
label=_('Virtual circuit'),
|
||||||
)
|
)
|
||||||
role = django_filters.MultipleChoiceFilter(
|
role = django_filters.MultipleChoiceFilter(
|
||||||
choices=VirtualCircuitTerminationRoleChoices,
|
choices=VirtualCircuitTerminationRoleChoices,
|
||||||
|
distinct=False,
|
||||||
null_value=None
|
null_value=None
|
||||||
)
|
)
|
||||||
provider_id = django_filters.ModelMultipleChoiceFilter(
|
provider_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
field_name='virtual_circuit__provider_network__provider',
|
field_name='virtual_circuit__provider_network__provider',
|
||||||
queryset=Provider.objects.all(),
|
queryset=Provider.objects.all(),
|
||||||
|
distinct=False,
|
||||||
label=_('Provider (ID)'),
|
label=_('Provider (ID)'),
|
||||||
)
|
)
|
||||||
provider = django_filters.ModelMultipleChoiceFilter(
|
provider = django_filters.ModelMultipleChoiceFilter(
|
||||||
field_name='virtual_circuit__provider_network__provider__slug',
|
field_name='virtual_circuit__provider_network__provider__slug',
|
||||||
queryset=Provider.objects.all(),
|
queryset=Provider.objects.all(),
|
||||||
|
distinct=False,
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label=_('Provider (slug)'),
|
label=_('Provider (slug)'),
|
||||||
)
|
)
|
||||||
provider_account_id = django_filters.ModelMultipleChoiceFilter(
|
provider_account_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
field_name='virtual_circuit__provider_account',
|
field_name='virtual_circuit__provider_account',
|
||||||
queryset=ProviderAccount.objects.all(),
|
queryset=ProviderAccount.objects.all(),
|
||||||
|
distinct=False,
|
||||||
label=_('Provider account (ID)'),
|
label=_('Provider account (ID)'),
|
||||||
)
|
)
|
||||||
provider_account = django_filters.ModelMultipleChoiceFilter(
|
provider_account = django_filters.ModelMultipleChoiceFilter(
|
||||||
field_name='virtual_circuit__provider_account__account',
|
field_name='virtual_circuit__provider_account__account',
|
||||||
queryset=ProviderAccount.objects.all(),
|
queryset=ProviderAccount.objects.all(),
|
||||||
|
distinct=False,
|
||||||
to_field_name='account',
|
to_field_name='account',
|
||||||
label=_('Provider account (account)'),
|
label=_('Provider account (account)'),
|
||||||
)
|
)
|
||||||
provider_network_id = django_filters.ModelMultipleChoiceFilter(
|
provider_network_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=ProviderNetwork.objects.all(),
|
queryset=ProviderNetwork.objects.all(),
|
||||||
|
distinct=False,
|
||||||
field_name='virtual_circuit__provider_network',
|
field_name='virtual_circuit__provider_network',
|
||||||
label=_('Provider network (ID)'),
|
label=_('Provider network (ID)'),
|
||||||
)
|
)
|
||||||
interface_id = django_filters.ModelMultipleChoiceFilter(
|
interface_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=Interface.objects.all(),
|
queryset=Interface.objects.all(),
|
||||||
|
distinct=False,
|
||||||
field_name='interface',
|
field_name='interface',
|
||||||
label=_('Interface (ID)'),
|
label=_('Interface (ID)'),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,7 +4,10 @@ from django.core.exceptions import ObjectDoesNotExist
|
|||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from circuits.choices import (
|
from circuits.choices import (
|
||||||
CircuitCommitRateChoices, CircuitPriorityChoices, CircuitStatusChoices, VirtualCircuitTerminationRoleChoices,
|
CircuitCommitRateChoices,
|
||||||
|
CircuitPriorityChoices,
|
||||||
|
CircuitStatusChoices,
|
||||||
|
VirtualCircuitTerminationRoleChoices,
|
||||||
)
|
)
|
||||||
from circuits.constants import CIRCUIT_TERMINATION_TERMINATION_TYPES
|
from circuits.constants import CIRCUIT_TERMINATION_TERMINATION_TYPES
|
||||||
from circuits.models import *
|
from circuits.models import *
|
||||||
@@ -15,7 +18,10 @@ from netbox.forms import NetBoxModelBulkEditForm, OrganizationalModelBulkEditFor
|
|||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
from utilities.forms import add_blank_choice, get_field_value
|
from utilities.forms import add_blank_choice, get_field_value
|
||||||
from utilities.forms.fields import (
|
from utilities.forms.fields import (
|
||||||
ColorField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField,
|
ColorField,
|
||||||
|
ContentTypeChoiceField,
|
||||||
|
DynamicModelChoiceField,
|
||||||
|
DynamicModelMultipleChoiceField,
|
||||||
)
|
)
|
||||||
from utilities.forms.rendering import FieldSet
|
from utilities.forms.rendering import FieldSet
|
||||||
from utilities.forms.widgets import BulkEditNullBooleanSelect, DatePicker, HTMXSelect, NumberWithOptions
|
from utilities.forms.widgets import BulkEditNullBooleanSelect, DatePicker, HTMXSelect, NumberWithOptions
|
||||||
@@ -27,8 +33,8 @@ __all__ = (
|
|||||||
'CircuitGroupBulkEditForm',
|
'CircuitGroupBulkEditForm',
|
||||||
'CircuitTerminationBulkEditForm',
|
'CircuitTerminationBulkEditForm',
|
||||||
'CircuitTypeBulkEditForm',
|
'CircuitTypeBulkEditForm',
|
||||||
'ProviderBulkEditForm',
|
|
||||||
'ProviderAccountBulkEditForm',
|
'ProviderAccountBulkEditForm',
|
||||||
|
'ProviderBulkEditForm',
|
||||||
'ProviderNetworkBulkEditForm',
|
'ProviderNetworkBulkEditForm',
|
||||||
'VirtualCircuitBulkEditForm',
|
'VirtualCircuitBulkEditForm',
|
||||||
'VirtualCircuitTerminationBulkEditForm',
|
'VirtualCircuitTerminationBulkEditForm',
|
||||||
|
|||||||
@@ -12,14 +12,14 @@ from tenancy.models import Tenant
|
|||||||
from utilities.forms.fields import CSVChoiceField, CSVContentTypeField, CSVModelChoiceField, SlugField
|
from utilities.forms.fields import CSVChoiceField, CSVContentTypeField, CSVModelChoiceField, SlugField
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'CircuitImportForm',
|
|
||||||
'CircuitGroupAssignmentImportForm',
|
'CircuitGroupAssignmentImportForm',
|
||||||
'CircuitGroupImportForm',
|
'CircuitGroupImportForm',
|
||||||
|
'CircuitImportForm',
|
||||||
'CircuitTerminationImportForm',
|
'CircuitTerminationImportForm',
|
||||||
'CircuitTerminationImportRelatedForm',
|
'CircuitTerminationImportRelatedForm',
|
||||||
'CircuitTypeImportForm',
|
'CircuitTypeImportForm',
|
||||||
'ProviderImportForm',
|
|
||||||
'ProviderAccountImportForm',
|
'ProviderAccountImportForm',
|
||||||
|
'ProviderImportForm',
|
||||||
'ProviderNetworkImportForm',
|
'ProviderNetworkImportForm',
|
||||||
'VirtualCircuitImportForm',
|
'VirtualCircuitImportForm',
|
||||||
'VirtualCircuitTerminationImportForm',
|
'VirtualCircuitTerminationImportForm',
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ from django import forms
|
|||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
from circuits.choices import (
|
from circuits.choices import (
|
||||||
CircuitCommitRateChoices, CircuitPriorityChoices, CircuitStatusChoices, CircuitTerminationSideChoices,
|
CircuitCommitRateChoices,
|
||||||
|
CircuitPriorityChoices,
|
||||||
|
CircuitStatusChoices,
|
||||||
|
CircuitTerminationSideChoices,
|
||||||
VirtualCircuitTerminationRoleChoices,
|
VirtualCircuitTerminationRoleChoices,
|
||||||
)
|
)
|
||||||
from circuits.models import *
|
from circuits.models import *
|
||||||
@@ -10,7 +13,7 @@ from dcim.models import Location, Region, Site, SiteGroup
|
|||||||
from ipam.models import ASN
|
from ipam.models import ASN
|
||||||
from netbox.choices import DistanceUnitChoices
|
from netbox.choices import DistanceUnitChoices
|
||||||
from netbox.forms import NetBoxModelFilterSetForm, OrganizationalModelFilterSetForm, PrimaryModelFilterSetForm
|
from netbox.forms import NetBoxModelFilterSetForm, OrganizationalModelFilterSetForm, PrimaryModelFilterSetForm
|
||||||
from tenancy.forms import TenancyFilterForm, ContactModelFilterForm
|
from tenancy.forms import ContactModelFilterForm, TenancyFilterForm
|
||||||
from utilities.forms import add_blank_choice
|
from utilities.forms import add_blank_choice
|
||||||
from utilities.forms.fields import ColorField, DynamicModelMultipleChoiceField, TagFilterField
|
from utilities.forms.fields import ColorField, DynamicModelMultipleChoiceField, TagFilterField
|
||||||
from utilities.forms.rendering import FieldSet
|
from utilities.forms.rendering import FieldSet
|
||||||
@@ -22,8 +25,8 @@ __all__ = (
|
|||||||
'CircuitGroupFilterForm',
|
'CircuitGroupFilterForm',
|
||||||
'CircuitTerminationFilterForm',
|
'CircuitTerminationFilterForm',
|
||||||
'CircuitTypeFilterForm',
|
'CircuitTypeFilterForm',
|
||||||
'ProviderFilterForm',
|
|
||||||
'ProviderAccountFilterForm',
|
'ProviderAccountFilterForm',
|
||||||
|
'ProviderFilterForm',
|
||||||
'ProviderNetworkFilterForm',
|
'ProviderNetworkFilterForm',
|
||||||
'VirtualCircuitFilterForm',
|
'VirtualCircuitFilterForm',
|
||||||
'VirtualCircuitTerminationFilterForm',
|
'VirtualCircuitTerminationFilterForm',
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ from django.core.exceptions import ObjectDoesNotExist
|
|||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from circuits.choices import (
|
from circuits.choices import (
|
||||||
CircuitCommitRateChoices, CircuitTerminationPortSpeedChoices, VirtualCircuitTerminationRoleChoices,
|
CircuitCommitRateChoices,
|
||||||
|
CircuitTerminationPortSpeedChoices,
|
||||||
|
VirtualCircuitTerminationRoleChoices,
|
||||||
)
|
)
|
||||||
from circuits.constants import *
|
from circuits.constants import *
|
||||||
from circuits.models import *
|
from circuits.models import *
|
||||||
@@ -14,10 +16,13 @@ from netbox.forms import NetBoxModelForm, OrganizationalModelForm, PrimaryModelF
|
|||||||
from tenancy.forms import TenancyForm
|
from tenancy.forms import TenancyForm
|
||||||
from utilities.forms import get_field_value
|
from utilities.forms import get_field_value
|
||||||
from utilities.forms.fields import (
|
from utilities.forms.fields import (
|
||||||
ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField,
|
ContentTypeChoiceField,
|
||||||
|
DynamicModelChoiceField,
|
||||||
|
DynamicModelMultipleChoiceField,
|
||||||
|
SlugField,
|
||||||
)
|
)
|
||||||
from utilities.forms.mixins import DistanceValidationMixin
|
from utilities.forms.mixins import DistanceValidationMixin
|
||||||
from utilities.forms.rendering import FieldSet, InlineFields
|
from utilities.forms.rendering import FieldSet, InlineFields, M2MAddRemoveFields
|
||||||
from utilities.forms.widgets import DatePicker, HTMXSelect, NumberWithOptions
|
from utilities.forms.widgets import DatePicker, HTMXSelect, NumberWithOptions
|
||||||
from utilities.templatetags.builtins.filters import bettertitle
|
from utilities.templatetags.builtins.filters import bettertitle
|
||||||
|
|
||||||
@@ -27,8 +32,8 @@ __all__ = (
|
|||||||
'CircuitGroupForm',
|
'CircuitGroupForm',
|
||||||
'CircuitTerminationForm',
|
'CircuitTerminationForm',
|
||||||
'CircuitTypeForm',
|
'CircuitTypeForm',
|
||||||
'ProviderForm',
|
|
||||||
'ProviderAccountForm',
|
'ProviderAccountForm',
|
||||||
|
'ProviderForm',
|
||||||
'ProviderNetworkForm',
|
'ProviderNetworkForm',
|
||||||
'VirtualCircuitForm',
|
'VirtualCircuitForm',
|
||||||
'VirtualCircuitTerminationForm',
|
'VirtualCircuitTerminationForm',
|
||||||
@@ -43,17 +48,42 @@ class ProviderForm(PrimaryModelForm):
|
|||||||
label=_('ASNs'),
|
label=_('ASNs'),
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
|
add_asns = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=ASN.objects.all(),
|
||||||
|
label=_('Add ASNs'),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
remove_asns = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=ASN.objects.all(),
|
||||||
|
label=_('Remove ASNs'),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('name', 'slug', 'asns', 'description', 'tags'),
|
FieldSet('name', 'slug', M2MAddRemoveFields('asns'), 'description', 'tags'),
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Provider
|
model = Provider
|
||||||
fields = [
|
fields = [
|
||||||
'name', 'slug', 'asns', 'description', 'owner', 'comments', 'tags',
|
'name', 'slug', 'description', 'owner', 'comments', 'tags',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
if self.instance.pk and (count := self.instance.asns.count()) >= M2MAddRemoveFields.THRESHOLD:
|
||||||
|
# Add/remove mode for large M2M sets
|
||||||
|
self.fields.pop('asns')
|
||||||
|
self.fields['add_asns'].widget.add_query_param('provider_id__n', self.instance.pk)
|
||||||
|
self.fields['remove_asns'].widget.add_query_param('provider_id', self.instance.pk)
|
||||||
|
self.fields['remove_asns'].help_text = _("{count} ASNs currently assigned").format(count=count)
|
||||||
|
else:
|
||||||
|
# Simple mode for new objects or small M2M sets
|
||||||
|
self.fields.pop('add_asns')
|
||||||
|
self.fields.pop('remove_asns')
|
||||||
|
if self.instance.pk:
|
||||||
|
self.initial['asns'] = list(self.instance.asns.values_list('pk', flat=True))
|
||||||
|
|
||||||
|
|
||||||
class ProviderAccountForm(PrimaryModelForm):
|
class ProviderAccountForm(PrimaryModelForm):
|
||||||
provider = DynamicModelChoiceField(
|
provider = DynamicModelChoiceField(
|
||||||
@@ -63,10 +93,14 @@ class ProviderAccountForm(PrimaryModelForm):
|
|||||||
quick_add=True
|
quick_add=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fieldsets = (
|
||||||
|
FieldSet('provider', 'account', 'name', 'description', 'tags'),
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ProviderAccount
|
model = ProviderAccount
|
||||||
fields = [
|
fields = [
|
||||||
'provider', 'name', 'account', 'description', 'owner', 'comments', 'tags',
|
'provider', 'account', 'name', 'description', 'owner', 'comments', 'tags',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -91,13 +125,13 @@ class ProviderNetworkForm(PrimaryModelForm):
|
|||||||
|
|
||||||
class CircuitTypeForm(OrganizationalModelForm):
|
class CircuitTypeForm(OrganizationalModelForm):
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('name', 'slug', 'color', 'description', 'owner', 'tags'),
|
FieldSet('name', 'slug', 'color', 'description', 'tags'),
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CircuitType
|
model = CircuitType
|
||||||
fields = [
|
fields = [
|
||||||
'name', 'slug', 'color', 'description', 'comments', 'tags',
|
'name', 'slug', 'color', 'description', 'owner', 'comments', 'tags',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import strawberry
|
|||||||
from circuits.choices import *
|
from circuits.choices import *
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
|
'CircuitPriorityEnum',
|
||||||
'CircuitStatusEnum',
|
'CircuitStatusEnum',
|
||||||
'CircuitTerminationSideEnum',
|
'CircuitTerminationSideEnum',
|
||||||
'CircuitPriorityEnum',
|
|
||||||
'VirtualCircuitTerminationRoleEnum',
|
'VirtualCircuitTerminationRoleEnum',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Annotated, TYPE_CHECKING
|
from typing import TYPE_CHECKING, Annotated
|
||||||
|
|
||||||
import strawberry
|
import strawberry
|
||||||
import strawberry_django
|
import strawberry_django
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
from datetime import date
|
from datetime import date
|
||||||
from typing import Annotated, TYPE_CHECKING
|
from typing import TYPE_CHECKING, Annotated
|
||||||
|
|
||||||
import strawberry
|
import strawberry
|
||||||
import strawberry_django
|
import strawberry_django
|
||||||
from strawberry.scalars import ID
|
from strawberry.scalars import ID
|
||||||
from strawberry_django import BaseFilterLookup, FilterLookup, DateFilterLookup
|
from strawberry_django import BaseFilterLookup, DateFilterLookup, StrFilterLookup
|
||||||
|
|
||||||
from circuits import models
|
from circuits import models
|
||||||
from circuits.graphql.filter_mixins import CircuitTypeFilterMixin
|
from circuits.graphql.filter_mixins import CircuitTypeFilterMixin
|
||||||
@@ -19,6 +19,7 @@ if TYPE_CHECKING:
|
|||||||
from dcim.graphql.filters import InterfaceFilter, LocationFilter, RegionFilter, SiteFilter, SiteGroupFilter
|
from dcim.graphql.filters import InterfaceFilter, LocationFilter, RegionFilter, SiteFilter, SiteGroupFilter
|
||||||
from ipam.graphql.filters import ASNFilter
|
from ipam.graphql.filters import ASNFilter
|
||||||
from netbox.graphql.filter_lookups import IntegerLookup
|
from netbox.graphql.filter_lookups import IntegerLookup
|
||||||
|
|
||||||
from .enums import *
|
from .enums import *
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
@@ -27,8 +28,8 @@ __all__ = (
|
|||||||
'CircuitGroupFilter',
|
'CircuitGroupFilter',
|
||||||
'CircuitTerminationFilter',
|
'CircuitTerminationFilter',
|
||||||
'CircuitTypeFilter',
|
'CircuitTypeFilter',
|
||||||
'ProviderFilter',
|
|
||||||
'ProviderAccountFilter',
|
'ProviderAccountFilter',
|
||||||
|
'ProviderFilter',
|
||||||
'ProviderNetworkFilter',
|
'ProviderNetworkFilter',
|
||||||
'VirtualCircuitFilter',
|
'VirtualCircuitFilter',
|
||||||
'VirtualCircuitTerminationFilter',
|
'VirtualCircuitTerminationFilter',
|
||||||
@@ -61,9 +62,9 @@ class CircuitTerminationFilter(
|
|||||||
upstream_speed: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
upstream_speed: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||||
strawberry_django.filter_field()
|
strawberry_django.filter_field()
|
||||||
)
|
)
|
||||||
xconnect_id: FilterLookup[str] | None = strawberry_django.filter_field()
|
xconnect_id: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||||
pp_info: FilterLookup[str] | None = strawberry_django.filter_field()
|
pp_info: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
description: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||||
|
|
||||||
# Cached relations
|
# Cached relations
|
||||||
_provider_network: Annotated['ProviderNetworkFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
|
_provider_network: Annotated['ProviderNetworkFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
|
||||||
@@ -91,7 +92,7 @@ class CircuitFilter(
|
|||||||
TenancyFilterMixin,
|
TenancyFilterMixin,
|
||||||
PrimaryModelFilter
|
PrimaryModelFilter
|
||||||
):
|
):
|
||||||
cid: FilterLookup[str] | None = strawberry_django.filter_field()
|
cid: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||||
provider: Annotated['ProviderFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
|
provider: Annotated['ProviderFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
|
||||||
strawberry_django.filter_field()
|
strawberry_django.filter_field()
|
||||||
)
|
)
|
||||||
@@ -144,8 +145,8 @@ class CircuitGroupAssignmentFilter(CustomFieldsFilterMixin, TagsFilterMixin, Cha
|
|||||||
|
|
||||||
@strawberry_django.filter_type(models.Provider, lookups=True)
|
@strawberry_django.filter_type(models.Provider, lookups=True)
|
||||||
class ProviderFilter(ContactFilterMixin, PrimaryModelFilter):
|
class ProviderFilter(ContactFilterMixin, PrimaryModelFilter):
|
||||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
name: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||||
slug: FilterLookup[str] | None = strawberry_django.filter_field()
|
slug: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||||
asns: Annotated['ASNFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field()
|
asns: Annotated['ASNFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field()
|
||||||
circuits: Annotated['CircuitFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
|
circuits: Annotated['CircuitFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
|
||||||
strawberry_django.filter_field()
|
strawberry_django.filter_field()
|
||||||
@@ -158,18 +159,18 @@ class ProviderAccountFilter(ContactFilterMixin, PrimaryModelFilter):
|
|||||||
strawberry_django.filter_field()
|
strawberry_django.filter_field()
|
||||||
)
|
)
|
||||||
provider_id: ID | None = strawberry_django.filter_field()
|
provider_id: ID | None = strawberry_django.filter_field()
|
||||||
account: FilterLookup[str] | None = strawberry_django.filter_field()
|
account: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
name: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||||
|
|
||||||
|
|
||||||
@strawberry_django.filter_type(models.ProviderNetwork, lookups=True)
|
@strawberry_django.filter_type(models.ProviderNetwork, lookups=True)
|
||||||
class ProviderNetworkFilter(PrimaryModelFilter):
|
class ProviderNetworkFilter(PrimaryModelFilter):
|
||||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
name: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||||
provider: Annotated['ProviderFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
|
provider: Annotated['ProviderFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
|
||||||
strawberry_django.filter_field()
|
strawberry_django.filter_field()
|
||||||
)
|
)
|
||||||
provider_id: ID | None = strawberry_django.filter_field()
|
provider_id: ID | None = strawberry_django.filter_field()
|
||||||
service_id: FilterLookup[str] | None = strawberry_django.filter_field()
|
service_id: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||||
|
|
||||||
|
|
||||||
@strawberry_django.filter_type(models.VirtualCircuitType, lookups=True)
|
@strawberry_django.filter_type(models.VirtualCircuitType, lookups=True)
|
||||||
@@ -179,7 +180,7 @@ class VirtualCircuitTypeFilter(CircuitTypeFilterMixin, OrganizationalModelFilter
|
|||||||
|
|
||||||
@strawberry_django.filter_type(models.VirtualCircuit, lookups=True)
|
@strawberry_django.filter_type(models.VirtualCircuit, lookups=True)
|
||||||
class VirtualCircuitFilter(TenancyFilterMixin, PrimaryModelFilter):
|
class VirtualCircuitFilter(TenancyFilterMixin, PrimaryModelFilter):
|
||||||
cid: FilterLookup[str] | None = strawberry_django.filter_field()
|
cid: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||||
provider_network: Annotated['ProviderNetworkFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
|
provider_network: Annotated['ProviderNetworkFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
|
||||||
strawberry_django.filter_field()
|
strawberry_django.filter_field()
|
||||||
)
|
)
|
||||||
@@ -217,4 +218,4 @@ class VirtualCircuitTerminationFilter(CustomFieldsFilterMixin, TagsFilterMixin,
|
|||||||
strawberry_django.filter_field()
|
strawberry_django.filter_field()
|
||||||
)
|
)
|
||||||
interface_id: ID | None = strawberry_django.filter_field()
|
interface_id: ID | None = strawberry_django.filter_field()
|
||||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
description: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
from typing import List
|
|
||||||
|
|
||||||
import strawberry
|
import strawberry
|
||||||
import strawberry_django
|
import strawberry_django
|
||||||
|
|
||||||
@@ -9,34 +7,34 @@ from .types import *
|
|||||||
@strawberry.type(name="Query")
|
@strawberry.type(name="Query")
|
||||||
class CircuitsQuery:
|
class CircuitsQuery:
|
||||||
circuit: CircuitType = strawberry_django.field()
|
circuit: CircuitType = strawberry_django.field()
|
||||||
circuit_list: List[CircuitType] = strawberry_django.field()
|
circuit_list: list[CircuitType] = strawberry_django.field()
|
||||||
|
|
||||||
circuit_termination: CircuitTerminationType = strawberry_django.field()
|
circuit_termination: CircuitTerminationType = strawberry_django.field()
|
||||||
circuit_termination_list: List[CircuitTerminationType] = strawberry_django.field()
|
circuit_termination_list: list[CircuitTerminationType] = strawberry_django.field()
|
||||||
|
|
||||||
circuit_type: CircuitTypeType = strawberry_django.field()
|
circuit_type: CircuitTypeType = strawberry_django.field()
|
||||||
circuit_type_list: List[CircuitTypeType] = strawberry_django.field()
|
circuit_type_list: list[CircuitTypeType] = strawberry_django.field()
|
||||||
|
|
||||||
circuit_group: CircuitGroupType = strawberry_django.field()
|
circuit_group: CircuitGroupType = strawberry_django.field()
|
||||||
circuit_group_list: List[CircuitGroupType] = strawberry_django.field()
|
circuit_group_list: list[CircuitGroupType] = strawberry_django.field()
|
||||||
|
|
||||||
circuit_group_assignment: CircuitGroupAssignmentType = strawberry_django.field()
|
circuit_group_assignment: CircuitGroupAssignmentType = strawberry_django.field()
|
||||||
circuit_group_assignment_list: List[CircuitGroupAssignmentType] = strawberry_django.field()
|
circuit_group_assignment_list: list[CircuitGroupAssignmentType] = strawberry_django.field()
|
||||||
|
|
||||||
provider: ProviderType = strawberry_django.field()
|
provider: ProviderType = strawberry_django.field()
|
||||||
provider_list: List[ProviderType] = strawberry_django.field()
|
provider_list: list[ProviderType] = strawberry_django.field()
|
||||||
|
|
||||||
provider_account: ProviderAccountType = strawberry_django.field()
|
provider_account: ProviderAccountType = strawberry_django.field()
|
||||||
provider_account_list: List[ProviderAccountType] = strawberry_django.field()
|
provider_account_list: list[ProviderAccountType] = strawberry_django.field()
|
||||||
|
|
||||||
provider_network: ProviderNetworkType = strawberry_django.field()
|
provider_network: ProviderNetworkType = strawberry_django.field()
|
||||||
provider_network_list: List[ProviderNetworkType] = strawberry_django.field()
|
provider_network_list: list[ProviderNetworkType] = strawberry_django.field()
|
||||||
|
|
||||||
virtual_circuit: VirtualCircuitType = strawberry_django.field()
|
virtual_circuit: VirtualCircuitType = strawberry_django.field()
|
||||||
virtual_circuit_list: List[VirtualCircuitType] = strawberry_django.field()
|
virtual_circuit_list: list[VirtualCircuitType] = strawberry_django.field()
|
||||||
|
|
||||||
virtual_circuit_termination: VirtualCircuitTerminationType = strawberry_django.field()
|
virtual_circuit_termination: VirtualCircuitTerminationType = strawberry_django.field()
|
||||||
virtual_circuit_termination_list: List[VirtualCircuitTerminationType] = strawberry_django.field()
|
virtual_circuit_termination_list: list[VirtualCircuitTerminationType] = strawberry_django.field()
|
||||||
|
|
||||||
virtual_circuit_type: VirtualCircuitTypeType = strawberry_django.field()
|
virtual_circuit_type: VirtualCircuitTypeType = strawberry_django.field()
|
||||||
virtual_circuit_type_list: List[VirtualCircuitTypeType] = strawberry_django.field()
|
virtual_circuit_type_list: list[VirtualCircuitTypeType] = strawberry_django.field()
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from typing import Annotated, List, TYPE_CHECKING, Union
|
from typing import TYPE_CHECKING, Annotated
|
||||||
|
|
||||||
import strawberry
|
import strawberry
|
||||||
import strawberry_django
|
import strawberry_django
|
||||||
@@ -8,6 +8,7 @@ from dcim.graphql.mixins import CabledObjectMixin
|
|||||||
from extras.graphql.mixins import ContactsMixin, CustomFieldsMixin, TagsMixin
|
from extras.graphql.mixins import ContactsMixin, CustomFieldsMixin, TagsMixin
|
||||||
from netbox.graphql.types import BaseObjectType, ObjectType, OrganizationalObjectType, PrimaryObjectType
|
from netbox.graphql.types import BaseObjectType, ObjectType, OrganizationalObjectType, PrimaryObjectType
|
||||||
from tenancy.graphql.types import TenantType
|
from tenancy.graphql.types import TenantType
|
||||||
|
|
||||||
from .filters import *
|
from .filters import *
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@@ -20,9 +21,9 @@ __all__ = (
|
|||||||
'CircuitTerminationType',
|
'CircuitTerminationType',
|
||||||
'CircuitType',
|
'CircuitType',
|
||||||
'CircuitTypeType',
|
'CircuitTypeType',
|
||||||
'ProviderType',
|
|
||||||
'ProviderAccountType',
|
'ProviderAccountType',
|
||||||
'ProviderNetworkType',
|
'ProviderNetworkType',
|
||||||
|
'ProviderType',
|
||||||
'VirtualCircuitTerminationType',
|
'VirtualCircuitTerminationType',
|
||||||
'VirtualCircuitType',
|
'VirtualCircuitType',
|
||||||
'VirtualCircuitTypeType',
|
'VirtualCircuitTypeType',
|
||||||
@@ -36,10 +37,10 @@ __all__ = (
|
|||||||
pagination=True
|
pagination=True
|
||||||
)
|
)
|
||||||
class ProviderType(ContactsMixin, PrimaryObjectType):
|
class ProviderType(ContactsMixin, PrimaryObjectType):
|
||||||
networks: List[Annotated["ProviderNetworkType", strawberry.lazy('circuits.graphql.types')]]
|
networks: list[Annotated["ProviderNetworkType", strawberry.lazy('circuits.graphql.types')]]
|
||||||
circuits: List[Annotated["CircuitType", strawberry.lazy('circuits.graphql.types')]]
|
circuits: list[Annotated["CircuitType", strawberry.lazy('circuits.graphql.types')]]
|
||||||
asns: List[Annotated["ASNType", strawberry.lazy('ipam.graphql.types')]]
|
asns: list[Annotated["ASNType", strawberry.lazy('ipam.graphql.types')]]
|
||||||
accounts: List[Annotated["ProviderAccountType", strawberry.lazy('circuits.graphql.types')]]
|
accounts: list[Annotated["ProviderAccountType", strawberry.lazy('circuits.graphql.types')]]
|
||||||
|
|
||||||
|
|
||||||
@strawberry_django.type(
|
@strawberry_django.type(
|
||||||
@@ -50,7 +51,7 @@ class ProviderType(ContactsMixin, PrimaryObjectType):
|
|||||||
)
|
)
|
||||||
class ProviderAccountType(ContactsMixin, PrimaryObjectType):
|
class ProviderAccountType(ContactsMixin, PrimaryObjectType):
|
||||||
provider: Annotated["ProviderType", strawberry.lazy('circuits.graphql.types')]
|
provider: Annotated["ProviderType", strawberry.lazy('circuits.graphql.types')]
|
||||||
circuits: List[Annotated["CircuitType", strawberry.lazy('circuits.graphql.types')]]
|
circuits: list[Annotated["CircuitType", strawberry.lazy('circuits.graphql.types')]]
|
||||||
|
|
||||||
|
|
||||||
@strawberry_django.type(
|
@strawberry_django.type(
|
||||||
@@ -61,7 +62,7 @@ class ProviderAccountType(ContactsMixin, PrimaryObjectType):
|
|||||||
)
|
)
|
||||||
class ProviderNetworkType(PrimaryObjectType):
|
class ProviderNetworkType(PrimaryObjectType):
|
||||||
provider: Annotated["ProviderType", strawberry.lazy('circuits.graphql.types')]
|
provider: Annotated["ProviderType", strawberry.lazy('circuits.graphql.types')]
|
||||||
circuit_terminations: List[Annotated["CircuitTerminationType", strawberry.lazy('circuits.graphql.types')]]
|
circuit_terminations: list[Annotated["CircuitTerminationType", strawberry.lazy('circuits.graphql.types')]]
|
||||||
|
|
||||||
|
|
||||||
@strawberry_django.type(
|
@strawberry_django.type(
|
||||||
@@ -71,16 +72,17 @@ class ProviderNetworkType(PrimaryObjectType):
|
|||||||
pagination=True
|
pagination=True
|
||||||
)
|
)
|
||||||
class CircuitTerminationType(CustomFieldsMixin, TagsMixin, CabledObjectMixin, ObjectType):
|
class CircuitTerminationType(CustomFieldsMixin, TagsMixin, CabledObjectMixin, ObjectType):
|
||||||
circuit: Annotated["CircuitType", strawberry.lazy('circuits.graphql.types')]
|
circuit: Annotated['CircuitType', strawberry.lazy('circuits.graphql.types')]
|
||||||
|
|
||||||
@strawberry_django.field
|
@strawberry_django.field
|
||||||
def termination(self) -> Annotated[Union[
|
def termination(self) -> Annotated[
|
||||||
Annotated["LocationType", strawberry.lazy('dcim.graphql.types')],
|
Annotated['LocationType', strawberry.lazy('dcim.graphql.types')]
|
||||||
Annotated["RegionType", strawberry.lazy('dcim.graphql.types')],
|
| Annotated['RegionType', strawberry.lazy('dcim.graphql.types')]
|
||||||
Annotated["SiteGroupType", strawberry.lazy('dcim.graphql.types')],
|
| Annotated['SiteGroupType', strawberry.lazy('dcim.graphql.types')]
|
||||||
Annotated["SiteType", strawberry.lazy('dcim.graphql.types')],
|
| Annotated['SiteType', strawberry.lazy('dcim.graphql.types')]
|
||||||
Annotated["ProviderNetworkType", strawberry.lazy('circuits.graphql.types')],
|
| Annotated['ProviderNetworkType', strawberry.lazy('circuits.graphql.types')],
|
||||||
], strawberry.union("CircuitTerminationTerminationType")] | None:
|
strawberry.union('CircuitTerminationTerminationType'),
|
||||||
|
] | None:
|
||||||
return self.termination
|
return self.termination
|
||||||
|
|
||||||
|
|
||||||
@@ -93,7 +95,7 @@ class CircuitTerminationType(CustomFieldsMixin, TagsMixin, CabledObjectMixin, Ob
|
|||||||
class CircuitTypeType(OrganizationalObjectType):
|
class CircuitTypeType(OrganizationalObjectType):
|
||||||
color: str
|
color: str
|
||||||
|
|
||||||
circuits: List[Annotated["CircuitType", strawberry.lazy('circuits.graphql.types')]]
|
circuits: list[Annotated["CircuitType", strawberry.lazy('circuits.graphql.types')]]
|
||||||
|
|
||||||
|
|
||||||
@strawberry_django.type(
|
@strawberry_django.type(
|
||||||
@@ -109,7 +111,7 @@ class CircuitType(PrimaryObjectType, ContactsMixin):
|
|||||||
termination_z: CircuitTerminationType | None
|
termination_z: CircuitTerminationType | None
|
||||||
type: CircuitTypeType
|
type: CircuitTypeType
|
||||||
tenant: TenantType | None
|
tenant: TenantType | None
|
||||||
terminations: List[CircuitTerminationType]
|
terminations: list[CircuitTerminationType]
|
||||||
|
|
||||||
|
|
||||||
@strawberry_django.type(
|
@strawberry_django.type(
|
||||||
@@ -129,13 +131,14 @@ class CircuitGroupType(OrganizationalObjectType):
|
|||||||
pagination=True
|
pagination=True
|
||||||
)
|
)
|
||||||
class CircuitGroupAssignmentType(TagsMixin, BaseObjectType):
|
class CircuitGroupAssignmentType(TagsMixin, BaseObjectType):
|
||||||
group: Annotated["CircuitGroupType", strawberry.lazy('circuits.graphql.types')]
|
group: Annotated['CircuitGroupType', strawberry.lazy('circuits.graphql.types')]
|
||||||
|
|
||||||
@strawberry_django.field
|
@strawberry_django.field
|
||||||
def member(self) -> Annotated[Union[
|
def member(self) -> Annotated[
|
||||||
Annotated["CircuitType", strawberry.lazy('circuits.graphql.types')],
|
Annotated['CircuitType', strawberry.lazy('circuits.graphql.types')]
|
||||||
Annotated["VirtualCircuitType", strawberry.lazy('circuits.graphql.types')],
|
| Annotated['VirtualCircuitType', strawberry.lazy('circuits.graphql.types')],
|
||||||
], strawberry.union("CircuitGroupAssignmentMemberType")] | None:
|
strawberry.union('CircuitGroupAssignmentMemberType'),
|
||||||
|
] | None:
|
||||||
return self.member
|
return self.member
|
||||||
|
|
||||||
|
|
||||||
@@ -148,7 +151,7 @@ class CircuitGroupAssignmentType(TagsMixin, BaseObjectType):
|
|||||||
class VirtualCircuitTypeType(OrganizationalObjectType):
|
class VirtualCircuitTypeType(OrganizationalObjectType):
|
||||||
color: str
|
color: str
|
||||||
|
|
||||||
virtual_circuits: List[Annotated["VirtualCircuitType", strawberry.lazy('circuits.graphql.types')]]
|
virtual_circuits: list[Annotated["VirtualCircuitType", strawberry.lazy('circuits.graphql.types')]]
|
||||||
|
|
||||||
|
|
||||||
@strawberry_django.type(
|
@strawberry_django.type(
|
||||||
@@ -174,11 +177,11 @@ class VirtualCircuitTerminationType(CustomFieldsMixin, TagsMixin, ObjectType):
|
|||||||
filters=VirtualCircuitFilter,
|
filters=VirtualCircuitFilter,
|
||||||
pagination=True
|
pagination=True
|
||||||
)
|
)
|
||||||
class VirtualCircuitType(PrimaryObjectType):
|
class VirtualCircuitType(ContactsMixin, PrimaryObjectType):
|
||||||
provider_network: ProviderNetworkType = strawberry_django.field(select_related=["provider_network"])
|
provider_network: ProviderNetworkType = strawberry_django.field(select_related=["provider_network"])
|
||||||
provider_account: ProviderAccountType | None
|
provider_account: ProviderAccountType | None
|
||||||
type: Annotated["VirtualCircuitTypeType", strawberry.lazy('circuits.graphql.types')] = strawberry_django.field(
|
type: Annotated["VirtualCircuitTypeType", strawberry.lazy('circuits.graphql.types')] = strawberry_django.field(
|
||||||
select_related=["type"]
|
select_related=["type"]
|
||||||
)
|
)
|
||||||
tenant: TenantType | None
|
tenant: TenantType | None
|
||||||
terminations: List[VirtualCircuitTerminationType]
|
terminations: list[VirtualCircuitTerminationType]
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
import ipam.fields
|
import ipam.fields
|
||||||
from utilities.json import CustomFieldJSONEncoder
|
from utilities.json import CustomFieldJSONEncoder
|
||||||
from django.db import migrations, models
|
|
||||||
import django.db.models.deletion
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from django.db import migrations, models
|
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
import taggit.managers
|
import taggit.managers
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# Generated by Django 4.2.5 on 2023-10-20 21:25
|
# Generated by Django 4.2.5 on 2023-10-20 21:25
|
||||||
|
|
||||||
from django.db import migrations
|
from django.db import migrations
|
||||||
|
|
||||||
import utilities.fields
|
import utilities.fields
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
import taggit.managers
|
import taggit.managers
|
||||||
import utilities.json
|
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
import utilities.json
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
|||||||
35
netbox/circuits/migrations/0057_default_ordering_indexes.py
Normal file
35
netbox/circuits/migrations/0057_default_ordering_indexes.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
('circuits', '0056_gfk_indexes'),
|
||||||
|
('contenttypes', '0002_remove_content_type_name'),
|
||||||
|
('dcim', '0231_interface_rf_channel_frequency_precision'),
|
||||||
|
('extras', '0136_customfield_validation_schema'),
|
||||||
|
('tenancy', '0023_add_mptt_tree_indexes'),
|
||||||
|
('users', '0015_owner'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='circuit',
|
||||||
|
index=models.Index(fields=['provider', 'provider_account', 'cid'], name='circuits_ci_provide_a0c42c_idx'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='circuitgroupassignment',
|
||||||
|
index=models.Index(
|
||||||
|
fields=['group', 'member_type', 'member_id', 'priority', 'id'], name='circuits_ci_group_i_2f8327_idx'
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='virtualcircuit',
|
||||||
|
index=models.Index(
|
||||||
|
fields=['provider_network', 'provider_account', 'cid'], name='circuits_vi_provide_989efa_idx'
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='virtualcircuittermination',
|
||||||
|
index=models.Index(fields=['virtual_circuit', 'role', 'id'], name='circuits_vi_virtual_4b5c0c_idx'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -8,10 +8,16 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
from circuits.choices import *
|
from circuits.choices import *
|
||||||
from dcim.models import CabledObjectModel
|
from dcim.models import CabledObjectModel
|
||||||
from netbox.models import ChangeLoggedModel, OrganizationalModel, PrimaryModel
|
from netbox.models import ChangeLoggedModel, OrganizationalModel, PrimaryModel
|
||||||
from netbox.models.mixins import DistanceMixin
|
|
||||||
from netbox.models.features import (
|
from netbox.models.features import (
|
||||||
ContactsMixin, CustomFieldsMixin, CustomLinksMixin, ExportTemplatesMixin, ImageAttachmentsMixin, TagsMixin,
|
ContactsMixin,
|
||||||
|
CustomFieldsMixin,
|
||||||
|
CustomLinksMixin,
|
||||||
|
ExportTemplatesMixin,
|
||||||
|
ImageAttachmentsMixin,
|
||||||
|
TagsMixin,
|
||||||
)
|
)
|
||||||
|
from netbox.models.mixins import DistanceMixin
|
||||||
|
|
||||||
from .base import BaseCircuitType
|
from .base import BaseCircuitType
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
@@ -138,6 +144,9 @@ class Circuit(ContactsMixin, ImageAttachmentsMixin, DistanceMixin, PrimaryModel)
|
|||||||
name='%(app_label)s_%(class)s_unique_provideraccount_cid'
|
name='%(app_label)s_%(class)s_unique_provideraccount_cid'
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
indexes = (
|
||||||
|
models.Index(fields=('provider', 'provider_account', 'cid')), # Default ordering
|
||||||
|
)
|
||||||
verbose_name = _('circuit')
|
verbose_name = _('circuit')
|
||||||
verbose_name_plural = _('circuits')
|
verbose_name_plural = _('circuits')
|
||||||
|
|
||||||
@@ -215,6 +224,9 @@ class CircuitGroupAssignment(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin,
|
|||||||
name='%(app_label)s_%(class)s_unique_member_group'
|
name='%(app_label)s_%(class)s_unique_member_group'
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
indexes = (
|
||||||
|
models.Index(fields=('group', 'member_type', 'member_id', 'priority', 'id')), # Default ordering
|
||||||
|
)
|
||||||
verbose_name = _('Circuit group assignment')
|
verbose_name = _('Circuit group assignment')
|
||||||
verbose_name_plural = _('Circuit group assignments')
|
verbose_name_plural = _('Circuit group assignments')
|
||||||
|
|
||||||
@@ -341,6 +353,13 @@ class CircuitTermination(
|
|||||||
verbose_name = _('circuit termination')
|
verbose_name = _('circuit termination')
|
||||||
verbose_name_plural = _('circuit terminations')
|
verbose_name_plural = _('circuit terminations')
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
# Cache original values to detect changes
|
||||||
|
self._orig_circuit_id = self.__dict__.get('circuit_id')
|
||||||
|
self._orig_term_side = self.__dict__.get('term_side')
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f'{self.circuit}: Termination {self.term_side}'
|
return f'{self.circuit}: Termination {self.term_side}'
|
||||||
|
|
||||||
@@ -354,11 +373,39 @@ class CircuitTermination(
|
|||||||
raise ValidationError(_("A circuit termination must attach to a terminating object."))
|
raise ValidationError(_("A circuit termination must attach to a terminating object."))
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
|
is_new = self._state.adding
|
||||||
|
update_fields = kwargs.get('update_fields')
|
||||||
|
|
||||||
|
# Only consider circuit/term_side changes if those fields
|
||||||
|
# are actually being persisted
|
||||||
|
if update_fields is not None:
|
||||||
|
tracking_relevant = 'circuit' in update_fields or 'term_side' in update_fields
|
||||||
|
else:
|
||||||
|
tracking_relevant = True
|
||||||
|
|
||||||
|
circuit_changed = tracking_relevant and self._orig_circuit_id and self._orig_circuit_id != self.circuit_id
|
||||||
|
term_side_changed = tracking_relevant and self._orig_term_side and self._orig_term_side != self.term_side
|
||||||
|
|
||||||
# Cache objects associated with the terminating object (for filtering)
|
# Cache objects associated with the terminating object (for filtering)
|
||||||
self.cache_related_objects()
|
self.cache_related_objects()
|
||||||
|
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
# Clear the old termination reference if circuit or term_side changed
|
||||||
|
if circuit_changed or term_side_changed:
|
||||||
|
old_termination_name = f'termination_{self._orig_term_side.lower()}'
|
||||||
|
Circuit.objects.filter(pk=self._orig_circuit_id).update(**{old_termination_name: None})
|
||||||
|
|
||||||
|
# Update the cache if this is a new termination or circuit/term_side changed
|
||||||
|
if is_new or circuit_changed or term_side_changed:
|
||||||
|
# Update the new circuit's termination reference
|
||||||
|
termination_name = f'termination_{self.term_side.lower()}'
|
||||||
|
Circuit.objects.filter(pk=self.circuit_id).update(**{termination_name: self.pk})
|
||||||
|
|
||||||
|
# Update cached values for subsequent saves
|
||||||
|
self._orig_circuit_id = self.circuit_id
|
||||||
|
self._orig_term_side = self.term_side
|
||||||
|
|
||||||
def cache_related_objects(self):
|
def cache_related_objects(self):
|
||||||
self._provider_network = self._region = self._site_group = self._site = self._location = None
|
self._provider_network = self._region = self._site_group = self._site = self._location = None
|
||||||
if self.termination_type:
|
if self.termination_type:
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ from netbox.models import PrimaryModel
|
|||||||
from netbox.models.features import ContactsMixin
|
from netbox.models.features import ContactsMixin
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'ProviderNetwork',
|
|
||||||
'Provider',
|
'Provider',
|
||||||
'ProviderAccount',
|
'ProviderAccount',
|
||||||
|
'ProviderNetwork',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
|
|
||||||
from circuits.choices import *
|
from circuits.choices import *
|
||||||
from netbox.models import ChangeLoggedModel, PrimaryModel
|
from netbox.models import ChangeLoggedModel, PrimaryModel
|
||||||
from netbox.models.features import CustomFieldsMixin, CustomLinksMixin, ExportTemplatesMixin, TagsMixin
|
from netbox.models.features import ContactsMixin, CustomFieldsMixin, CustomLinksMixin, ExportTemplatesMixin, TagsMixin
|
||||||
|
|
||||||
from .base import BaseCircuitType
|
from .base import BaseCircuitType
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
@@ -29,7 +30,7 @@ class VirtualCircuitType(BaseCircuitType):
|
|||||||
verbose_name_plural = _('virtual circuit types')
|
verbose_name_plural = _('virtual circuit types')
|
||||||
|
|
||||||
|
|
||||||
class VirtualCircuit(PrimaryModel):
|
class VirtualCircuit(ContactsMixin, PrimaryModel):
|
||||||
"""
|
"""
|
||||||
A virtual connection between two or more endpoints, delivered across one or more physical circuits.
|
A virtual connection between two or more endpoints, delivered across one or more physical circuits.
|
||||||
"""
|
"""
|
||||||
@@ -96,6 +97,9 @@ class VirtualCircuit(PrimaryModel):
|
|||||||
name='%(app_label)s_%(class)s_unique_provideraccount_cid'
|
name='%(app_label)s_%(class)s_unique_provideraccount_cid'
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
indexes = (
|
||||||
|
models.Index(fields=('provider_network', 'provider_account', 'cid')), # Default ordering
|
||||||
|
)
|
||||||
verbose_name = _('virtual circuit')
|
verbose_name = _('virtual circuit')
|
||||||
verbose_name_plural = _('virtual circuits')
|
verbose_name_plural = _('virtual circuits')
|
||||||
|
|
||||||
@@ -149,6 +153,9 @@ class VirtualCircuitTermination(
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['virtual_circuit', 'role', 'pk']
|
ordering = ['virtual_circuit', 'role', 'pk']
|
||||||
|
indexes = (
|
||||||
|
models.Index(fields=('virtual_circuit', 'role', 'id')), # Default ordering
|
||||||
|
)
|
||||||
verbose_name = _('virtual circuit termination')
|
verbose_name = _('virtual circuit termination')
|
||||||
verbose_name_plural = _('virtual circuit terminations')
|
verbose_name_plural = _('virtual circuit terminations')
|
||||||
|
|
||||||
@@ -184,6 +191,8 @@ class VirtualCircuitTermination(
|
|||||||
return self.virtual_circuit.terminations.filter(
|
return self.virtual_circuit.terminations.filter(
|
||||||
role=VirtualCircuitTerminationRoleChoices.ROLE_HUB
|
role=VirtualCircuitTerminationRoleChoices.ROLE_HUB
|
||||||
)
|
)
|
||||||
|
# Fallback for unexpected roles
|
||||||
|
return self.virtual_circuit.terminations.none()
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
super().clean()
|
super().clean()
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from netbox.search import SearchIndex, register_search
|
from netbox.search import SearchIndex, register_search
|
||||||
|
|
||||||
from . import models
|
from . import models
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,20 +2,10 @@ from django.db.models.signals import post_delete, post_save
|
|||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
|
|
||||||
from dcim.signals import rebuild_paths
|
from dcim.signals import rebuild_paths
|
||||||
|
|
||||||
from .models import CircuitTermination
|
from .models import CircuitTermination
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_save, sender=CircuitTermination)
|
|
||||||
def update_circuit(instance, **kwargs):
|
|
||||||
"""
|
|
||||||
When a CircuitTermination has been modified, update its parent Circuit.
|
|
||||||
"""
|
|
||||||
termination_name = f'termination_{instance.term_side.lower()}'
|
|
||||||
instance.circuit.refresh_from_db()
|
|
||||||
setattr(instance.circuit, termination_name, instance)
|
|
||||||
instance.circuit.save()
|
|
||||||
|
|
||||||
|
|
||||||
@receiver((post_save, post_delete), sender=CircuitTermination)
|
@receiver((post_save, post_delete), sender=CircuitTermination)
|
||||||
def rebuild_cablepaths(instance, raw=False, **kwargs):
|
def rebuild_cablepaths(instance, raw=False, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
from circuits.models import *
|
from circuits.models import *
|
||||||
from netbox.tables import NetBoxTable, OrganizationalModelTable, PrimaryModelTable, columns
|
from netbox.tables import NetBoxTable, OrganizationalModelTable, PrimaryModelTable, columns
|
||||||
from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
|
from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
|
||||||
|
|
||||||
from .columns import CommitRateColumn
|
from .columns import CommitRateColumn
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
@@ -189,14 +190,16 @@ class CircuitGroupAssignmentTable(NetBoxTable):
|
|||||||
provider = tables.Column(
|
provider = tables.Column(
|
||||||
accessor='member__provider',
|
accessor='member__provider',
|
||||||
verbose_name=_('Provider'),
|
verbose_name=_('Provider'),
|
||||||
linkify=True
|
orderable=False,
|
||||||
|
linkify=True,
|
||||||
)
|
)
|
||||||
member_type = columns.ContentTypeColumn(
|
member_type = columns.ContentTypeColumn(
|
||||||
verbose_name=_('Type')
|
verbose_name=_('Type')
|
||||||
)
|
)
|
||||||
member = tables.Column(
|
member = tables.Column(
|
||||||
verbose_name=_('Circuit'),
|
verbose_name=_('Circuit'),
|
||||||
linkify=True
|
orderable=False,
|
||||||
|
linkify=True,
|
||||||
)
|
)
|
||||||
priority = tables.Column(
|
priority = tables.Column(
|
||||||
verbose_name=_('Priority'),
|
verbose_name=_('Priority'),
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ from netbox.tables import PrimaryModelTable, columns
|
|||||||
from tenancy.tables import ContactsColumnMixin
|
from tenancy.tables import ContactsColumnMixin
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'ProviderTable',
|
|
||||||
'ProviderAccountTable',
|
'ProviderAccountTable',
|
||||||
'ProviderNetworkTable',
|
'ProviderNetworkTable',
|
||||||
|
'ProviderTable',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ class VirtualCircuitTable(TenancyColumnsMixin, ContactsColumnMixin, PrimaryModel
|
|||||||
model = VirtualCircuit
|
model = VirtualCircuit
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'cid', 'provider', 'provider_account', 'provider_network', 'type', 'status', 'tenant',
|
'pk', 'id', 'cid', 'provider', 'provider_account', 'provider_network', 'type', 'status', 'tenant',
|
||||||
'tenant_group', 'description', 'comments', 'tags', 'created', 'last_updated',
|
'tenant_group', 'description', 'comments', 'contacts', 'tags', 'created', 'last_updated',
|
||||||
)
|
)
|
||||||
default_columns = (
|
default_columns = (
|
||||||
'pk', 'cid', 'provider', 'provider_account', 'provider_network', 'type', 'status', 'tenant',
|
'pk', 'cid', 'provider', 'provider_account', 'provider_network', 'type', 'status', 'tenant',
|
||||||
@@ -95,6 +95,7 @@ class VirtualCircuitTerminationTable(NetBoxTable):
|
|||||||
verbose_name=_('Provider network')
|
verbose_name=_('Provider network')
|
||||||
)
|
)
|
||||||
provider_account = tables.Column(
|
provider_account = tables.Column(
|
||||||
|
accessor=tables.A('virtual_circuit__provider_account'),
|
||||||
linkify=True,
|
linkify=True,
|
||||||
verbose_name=_('Account')
|
verbose_name=_('Account')
|
||||||
)
|
)
|
||||||
@@ -112,7 +113,7 @@ class VirtualCircuitTerminationTable(NetBoxTable):
|
|||||||
class Meta(NetBoxTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = VirtualCircuitTermination
|
model = VirtualCircuitTermination
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'virtual_circuit', 'provider', 'provider_network', 'provider_account', 'role', 'interfaces',
|
'pk', 'id', 'virtual_circuit', 'provider', 'provider_network', 'provider_account', 'role', 'interface',
|
||||||
'description', 'created', 'last_updated', 'actions',
|
'description', 'created', 'last_updated', 'actions',
|
||||||
)
|
)
|
||||||
default_columns = (
|
default_columns = (
|
||||||
|
|||||||
@@ -5,7 +5,16 @@ from circuits.filtersets import *
|
|||||||
from circuits.models import *
|
from circuits.models import *
|
||||||
from dcim.choices import InterfaceTypeChoices, LocationStatusChoices
|
from dcim.choices import InterfaceTypeChoices, LocationStatusChoices
|
||||||
from dcim.models import (
|
from dcim.models import (
|
||||||
Cable, Device, DeviceRole, DeviceType, Interface, Location, Manufacturer, Region, Site, SiteGroup
|
Cable,
|
||||||
|
Device,
|
||||||
|
DeviceRole,
|
||||||
|
DeviceType,
|
||||||
|
Interface,
|
||||||
|
Location,
|
||||||
|
Manufacturer,
|
||||||
|
Region,
|
||||||
|
Site,
|
||||||
|
SiteGroup,
|
||||||
)
|
)
|
||||||
from ipam.models import ASN, RIR
|
from ipam.models import ASN, RIR
|
||||||
from netbox.choices import DistanceUnitChoices
|
from netbox.choices import DistanceUnitChoices
|
||||||
|
|||||||
148
netbox/circuits/tests/test_models.py
Normal file
148
netbox/circuits/tests/test_models.py
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from circuits.models import Circuit, CircuitTermination, CircuitType, Provider, ProviderNetwork
|
||||||
|
from dcim.models import Site
|
||||||
|
|
||||||
|
|
||||||
|
class CircuitTerminationTestCase(TestCase):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
provider = Provider.objects.create(name='Provider 1', slug='provider-1')
|
||||||
|
circuit_type = CircuitType.objects.create(name='Circuit Type 1', slug='circuit-type-1')
|
||||||
|
|
||||||
|
cls.sites = (
|
||||||
|
Site.objects.create(name='Site 1', slug='site-1'),
|
||||||
|
Site.objects.create(name='Site 2', slug='site-2'),
|
||||||
|
)
|
||||||
|
|
||||||
|
cls.circuits = (
|
||||||
|
Circuit.objects.create(cid='Circuit 1', provider=provider, type=circuit_type),
|
||||||
|
Circuit.objects.create(cid='Circuit 2', provider=provider, type=circuit_type),
|
||||||
|
)
|
||||||
|
|
||||||
|
cls.provider_network = ProviderNetwork.objects.create(name='Provider Network 1', provider=provider)
|
||||||
|
|
||||||
|
def test_circuit_termination_creation_populates_circuit_cache(self):
|
||||||
|
"""
|
||||||
|
When a CircuitTermination is created, the parent Circuit's termination_a or termination_z
|
||||||
|
cache field should be populated.
|
||||||
|
"""
|
||||||
|
# Create A termination
|
||||||
|
termination_a = CircuitTermination.objects.create(
|
||||||
|
circuit=self.circuits[0],
|
||||||
|
term_side='A',
|
||||||
|
termination=self.sites[0],
|
||||||
|
)
|
||||||
|
self.circuits[0].refresh_from_db()
|
||||||
|
self.assertEqual(self.circuits[0].termination_a, termination_a)
|
||||||
|
self.assertIsNone(self.circuits[0].termination_z)
|
||||||
|
|
||||||
|
# Create Z termination
|
||||||
|
termination_z = CircuitTermination.objects.create(
|
||||||
|
circuit=self.circuits[0],
|
||||||
|
term_side='Z',
|
||||||
|
termination=self.sites[1],
|
||||||
|
)
|
||||||
|
self.circuits[0].refresh_from_db()
|
||||||
|
self.assertEqual(self.circuits[0].termination_a, termination_a)
|
||||||
|
self.assertEqual(self.circuits[0].termination_z, termination_z)
|
||||||
|
|
||||||
|
def test_circuit_termination_circuit_change_clears_old_cache(self):
|
||||||
|
"""
|
||||||
|
When a CircuitTermination's circuit is changed, the old Circuit's cache should be cleared
|
||||||
|
and the new Circuit's cache should be populated.
|
||||||
|
"""
|
||||||
|
# Create termination on self.circuits[0]
|
||||||
|
termination = CircuitTermination.objects.create(
|
||||||
|
circuit=self.circuits[0],
|
||||||
|
term_side='A',
|
||||||
|
termination=self.sites[0],
|
||||||
|
)
|
||||||
|
self.circuits[0].refresh_from_db()
|
||||||
|
self.assertEqual(self.circuits[0].termination_a, termination)
|
||||||
|
|
||||||
|
# Move termination to self.circuits[1]
|
||||||
|
termination.circuit = self.circuits[1]
|
||||||
|
termination.save()
|
||||||
|
|
||||||
|
self.circuits[0].refresh_from_db()
|
||||||
|
self.circuits[1].refresh_from_db()
|
||||||
|
|
||||||
|
# Old circuit's cache should be cleared
|
||||||
|
self.assertIsNone(self.circuits[0].termination_a)
|
||||||
|
# New circuit's cache should be populated
|
||||||
|
self.assertEqual(self.circuits[1].termination_a, termination)
|
||||||
|
|
||||||
|
def test_circuit_termination_term_side_change_clears_old_cache(self):
|
||||||
|
"""
|
||||||
|
When a CircuitTermination's term_side is changed, the old side's cache should be cleared
|
||||||
|
and the new side's cache should be populated.
|
||||||
|
"""
|
||||||
|
# Create A termination
|
||||||
|
termination = CircuitTermination.objects.create(
|
||||||
|
circuit=self.circuits[0],
|
||||||
|
term_side='A',
|
||||||
|
termination=self.sites[0],
|
||||||
|
)
|
||||||
|
self.circuits[0].refresh_from_db()
|
||||||
|
self.assertEqual(self.circuits[0].termination_a, termination)
|
||||||
|
self.assertIsNone(self.circuits[0].termination_z)
|
||||||
|
|
||||||
|
# Change from A to Z
|
||||||
|
termination.term_side = 'Z'
|
||||||
|
termination.save()
|
||||||
|
|
||||||
|
self.circuits[0].refresh_from_db()
|
||||||
|
|
||||||
|
# A side should be cleared, Z side should be populated
|
||||||
|
self.assertIsNone(self.circuits[0].termination_a)
|
||||||
|
self.assertEqual(self.circuits[0].termination_z, termination)
|
||||||
|
|
||||||
|
def test_circuit_termination_circuit_and_term_side_change(self):
|
||||||
|
"""
|
||||||
|
When both circuit and term_side are changed, the old Circuit's old side cache should be
|
||||||
|
cleared and the new Circuit's new side cache should be populated.
|
||||||
|
"""
|
||||||
|
# Create A termination on self.circuits[0]
|
||||||
|
termination = CircuitTermination.objects.create(
|
||||||
|
circuit=self.circuits[0],
|
||||||
|
term_side='A',
|
||||||
|
termination=self.sites[0],
|
||||||
|
)
|
||||||
|
self.circuits[0].refresh_from_db()
|
||||||
|
self.assertEqual(self.circuits[0].termination_a, termination)
|
||||||
|
|
||||||
|
# Change to self.circuits[1] Z side
|
||||||
|
termination.circuit = self.circuits[1]
|
||||||
|
termination.term_side = 'Z'
|
||||||
|
termination.save()
|
||||||
|
|
||||||
|
self.circuits[0].refresh_from_db()
|
||||||
|
self.circuits[1].refresh_from_db()
|
||||||
|
|
||||||
|
# Old circuit's A side should be cleared
|
||||||
|
self.assertIsNone(self.circuits[0].termination_a)
|
||||||
|
self.assertIsNone(self.circuits[0].termination_z)
|
||||||
|
# New circuit's Z side should be populated
|
||||||
|
self.assertIsNone(self.circuits[1].termination_a)
|
||||||
|
self.assertEqual(self.circuits[1].termination_z, termination)
|
||||||
|
|
||||||
|
def test_circuit_termination_deletion_clears_cache(self):
|
||||||
|
"""
|
||||||
|
When a CircuitTermination is deleted, the parent Circuit's cache should be cleared.
|
||||||
|
"""
|
||||||
|
termination = CircuitTermination.objects.create(
|
||||||
|
circuit=self.circuits[0],
|
||||||
|
term_side='A',
|
||||||
|
termination=self.sites[0],
|
||||||
|
)
|
||||||
|
self.circuits[0].refresh_from_db()
|
||||||
|
self.assertEqual(self.circuits[0].termination_a, termination)
|
||||||
|
|
||||||
|
# Delete the termination
|
||||||
|
termination.delete()
|
||||||
|
self.circuits[0].refresh_from_db()
|
||||||
|
|
||||||
|
# Cache should be cleared (SET_NULL behavior)
|
||||||
|
self.assertIsNone(self.circuits[0].termination_a)
|
||||||
@@ -1,23 +1,46 @@
|
|||||||
from django.test import RequestFactory, tag, TestCase
|
from circuits.tables import *
|
||||||
|
from utilities.testing import TableTestCases
|
||||||
from circuits.models import CircuitTermination
|
|
||||||
from circuits.tables import CircuitTerminationTable
|
|
||||||
|
|
||||||
|
|
||||||
@tag('regression')
|
class CircuitTypeTableTest(TableTestCases.StandardTableTestCase):
|
||||||
class CircuitTerminationTableTest(TestCase):
|
table = CircuitTypeTable
|
||||||
def test_every_orderable_field_does_not_throw_exception(self):
|
|
||||||
terminations = CircuitTermination.objects.all()
|
|
||||||
disallowed = {'actions', }
|
|
||||||
|
|
||||||
orderable_columns = [
|
|
||||||
column.name for column in CircuitTerminationTable(terminations).columns
|
|
||||||
if column.orderable and column.name not in disallowed
|
|
||||||
]
|
|
||||||
fake_request = RequestFactory().get("/")
|
|
||||||
|
|
||||||
for col in orderable_columns:
|
class CircuitTableTest(TableTestCases.StandardTableTestCase):
|
||||||
for dir in ('-', ''):
|
table = CircuitTable
|
||||||
table = CircuitTerminationTable(terminations)
|
|
||||||
table.order_by = f'{dir}{col}'
|
|
||||||
table.as_html(fake_request)
|
class CircuitTerminationTableTest(TableTestCases.StandardTableTestCase):
|
||||||
|
table = CircuitTerminationTable
|
||||||
|
|
||||||
|
|
||||||
|
class CircuitGroupTableTest(TableTestCases.StandardTableTestCase):
|
||||||
|
table = CircuitGroupTable
|
||||||
|
|
||||||
|
|
||||||
|
class CircuitGroupAssignmentTableTest(TableTestCases.StandardTableTestCase):
|
||||||
|
table = CircuitGroupAssignmentTable
|
||||||
|
|
||||||
|
|
||||||
|
class ProviderTableTest(TableTestCases.StandardTableTestCase):
|
||||||
|
table = ProviderTable
|
||||||
|
|
||||||
|
|
||||||
|
class ProviderAccountTableTest(TableTestCases.StandardTableTestCase):
|
||||||
|
table = ProviderAccountTable
|
||||||
|
|
||||||
|
|
||||||
|
class ProviderNetworkTableTest(TableTestCases.StandardTableTestCase):
|
||||||
|
table = ProviderNetworkTable
|
||||||
|
|
||||||
|
|
||||||
|
class VirtualCircuitTypeTableTest(TableTestCases.StandardTableTestCase):
|
||||||
|
table = VirtualCircuitTypeTable
|
||||||
|
|
||||||
|
|
||||||
|
class VirtualCircuitTableTest(TableTestCases.StandardTableTestCase):
|
||||||
|
table = VirtualCircuitTable
|
||||||
|
|
||||||
|
|
||||||
|
class VirtualCircuitTerminationTableTest(TableTestCases.StandardTableTestCase):
|
||||||
|
table = VirtualCircuitTerminationTable
|
||||||
|
|||||||
@@ -196,6 +196,20 @@ class CircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||||||
'comments': 'New comments',
|
'comments': 'New comments',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def test_circuit_type_display_colored(self):
|
||||||
|
circuit_type = CircuitType.objects.first()
|
||||||
|
circuit_type.color = '12ab34'
|
||||||
|
circuit_type.save()
|
||||||
|
|
||||||
|
circuit = Circuit.objects.first()
|
||||||
|
|
||||||
|
self.add_permissions('circuits.view_circuit')
|
||||||
|
response = self.client.get(circuit.get_absolute_url())
|
||||||
|
|
||||||
|
self.assertHttpStatus(response, 200)
|
||||||
|
self.assertContains(response, circuit_type.name)
|
||||||
|
self.assertContains(response, 'background-color: #12ab34')
|
||||||
|
|
||||||
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'], EXEMPT_EXCLUDE_MODELS=[])
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'], EXEMPT_EXCLUDE_MODELS=[])
|
||||||
def test_bulk_import_objects_with_terminations(self):
|
def test_bulk_import_objects_with_terminations(self):
|
||||||
site = Site.objects.first()
|
site = Site.objects.first()
|
||||||
|
|||||||
0
netbox/circuits/ui/__init__.py
Normal file
0
netbox/circuits/ui/__init__.py
Normal file
155
netbox/circuits/ui/panels.py
Normal file
155
netbox/circuits/ui/panels.py
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from netbox.ui import actions, attrs, panels
|
||||||
|
from utilities.data import resolve_attr_path
|
||||||
|
|
||||||
|
|
||||||
|
class CircuitCircuitTerminationPanel(panels.ObjectPanel):
|
||||||
|
"""
|
||||||
|
A panel showing the CircuitTermination assigned to the object.
|
||||||
|
"""
|
||||||
|
|
||||||
|
template_name = 'circuits/panels/circuit_circuit_termination.html'
|
||||||
|
title = _('Termination')
|
||||||
|
|
||||||
|
def __init__(self, side, accessor=None, **kwargs):
|
||||||
|
super().__init__(accessor=accessor, **kwargs)
|
||||||
|
self.side = side
|
||||||
|
|
||||||
|
def get_context(self, context):
|
||||||
|
return {
|
||||||
|
**super().get_context(context),
|
||||||
|
'side': self.side,
|
||||||
|
'termination': resolve_attr_path(context, f'{self.accessor}.termination_{self.side.lower()}'),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class CircuitGroupAssignmentsPanel(panels.ObjectsTablePanel):
|
||||||
|
"""
|
||||||
|
A panel showing all Circuit Groups attached to the object.
|
||||||
|
"""
|
||||||
|
|
||||||
|
title = _('Group Assignments')
|
||||||
|
actions = [
|
||||||
|
actions.AddObject(
|
||||||
|
'circuits.CircuitGroupAssignment',
|
||||||
|
url_params={
|
||||||
|
'member_type': lambda ctx: ContentType.objects.get_for_model(ctx['object']).pk,
|
||||||
|
'member': lambda ctx: ctx['object'].pk,
|
||||||
|
'return_url': lambda ctx: ctx['object'].get_absolute_url(),
|
||||||
|
},
|
||||||
|
label=_('Assign Group'),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
super().__init__(
|
||||||
|
'circuits.CircuitGroupAssignment',
|
||||||
|
filters={
|
||||||
|
'member_type_id': lambda ctx: ContentType.objects.get_for_model(ctx['object']).pk,
|
||||||
|
'member_id': lambda ctx: ctx['object'].pk,
|
||||||
|
},
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CircuitTerminationPanel(panels.ObjectAttributesPanel):
|
||||||
|
title = _('Circuit Termination')
|
||||||
|
circuit = attrs.RelatedObjectAttr('circuit', linkify=True)
|
||||||
|
provider = attrs.RelatedObjectAttr('circuit.provider', linkify=True)
|
||||||
|
termination = attrs.GenericForeignKeyAttr('termination', linkify=True, label=_('Termination point'))
|
||||||
|
connection = attrs.TemplatedAttr(
|
||||||
|
'pk',
|
||||||
|
template_name='circuits/circuit_termination/attrs/connection.html',
|
||||||
|
label=_('Connection'),
|
||||||
|
)
|
||||||
|
speed = attrs.TemplatedAttr(
|
||||||
|
'port_speed',
|
||||||
|
template_name='circuits/circuit_termination/attrs/speed.html',
|
||||||
|
label=_('Speed'),
|
||||||
|
)
|
||||||
|
xconnect_id = attrs.TextAttr('xconnect_id', label=_('Cross-Connect'), style='font-monospace')
|
||||||
|
pp_info = attrs.TextAttr('pp_info', label=_('Patch Panel/Port'))
|
||||||
|
description = attrs.TextAttr('description')
|
||||||
|
|
||||||
|
|
||||||
|
class CircuitGroupPanel(panels.OrganizationalObjectPanel):
|
||||||
|
tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group')
|
||||||
|
|
||||||
|
|
||||||
|
class CircuitGroupAssignmentPanel(panels.ObjectAttributesPanel):
|
||||||
|
group = attrs.RelatedObjectAttr('group', linkify=True)
|
||||||
|
provider = attrs.RelatedObjectAttr('member.provider', linkify=True)
|
||||||
|
member = attrs.GenericForeignKeyAttr('member', linkify=True)
|
||||||
|
priority = attrs.ChoiceAttr('priority')
|
||||||
|
|
||||||
|
|
||||||
|
class CircuitPanel(panels.ObjectAttributesPanel):
|
||||||
|
provider = attrs.RelatedObjectAttr('provider', linkify=True)
|
||||||
|
provider_account = attrs.RelatedObjectAttr('provider_account', linkify=True)
|
||||||
|
cid = attrs.TextAttr('cid', label=_('Circuit ID'), style='font-monospace', copy_button=True)
|
||||||
|
type = attrs.RelatedObjectAttr('type', linkify=True, colored=True)
|
||||||
|
status = attrs.ChoiceAttr('status')
|
||||||
|
distance = attrs.NumericAttr('distance', unit_accessor='get_distance_unit_display')
|
||||||
|
tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group')
|
||||||
|
install_date = attrs.DateTimeAttr('install_date', spec='date')
|
||||||
|
termination_date = attrs.DateTimeAttr('termination_date', spec='date')
|
||||||
|
commit_rate = attrs.TemplatedAttr('commit_rate', template_name='circuits/circuit/attrs/commit_rate.html')
|
||||||
|
description = attrs.TextAttr('description')
|
||||||
|
|
||||||
|
|
||||||
|
class CircuitTypePanel(panels.OrganizationalObjectPanel):
|
||||||
|
color = attrs.ColorAttr('color')
|
||||||
|
|
||||||
|
|
||||||
|
class ProviderPanel(panels.ObjectAttributesPanel):
|
||||||
|
name = attrs.TextAttr('name')
|
||||||
|
asns = attrs.RelatedObjectListAttr('asns', linkify=True, label=_('ASNs'))
|
||||||
|
description = attrs.TextAttr('description')
|
||||||
|
|
||||||
|
|
||||||
|
class ProviderAccountPanel(panels.ObjectAttributesPanel):
|
||||||
|
provider = attrs.RelatedObjectAttr('provider', linkify=True)
|
||||||
|
account = attrs.TextAttr('account', style='font-monospace', copy_button=True)
|
||||||
|
name = attrs.TextAttr('name')
|
||||||
|
description = attrs.TextAttr('description')
|
||||||
|
|
||||||
|
|
||||||
|
class ProviderNetworkPanel(panels.ObjectAttributesPanel):
|
||||||
|
provider = attrs.RelatedObjectAttr('provider', linkify=True)
|
||||||
|
name = attrs.TextAttr('name')
|
||||||
|
service_id = attrs.TextAttr('service_id', label=_('Service ID'), style='font-monospace', copy_button=True)
|
||||||
|
description = attrs.TextAttr('description')
|
||||||
|
|
||||||
|
|
||||||
|
class VirtualCircuitTypePanel(panels.OrganizationalObjectPanel):
|
||||||
|
color = attrs.ColorAttr('color')
|
||||||
|
|
||||||
|
|
||||||
|
class VirtualCircuitPanel(panels.ObjectAttributesPanel):
|
||||||
|
provider = attrs.RelatedObjectAttr('provider', linkify=True)
|
||||||
|
provider_network = attrs.RelatedObjectAttr('provider_network', linkify=True)
|
||||||
|
provider_account = attrs.RelatedObjectAttr('provider_account', linkify=True)
|
||||||
|
cid = attrs.TextAttr('cid', label=_('Circuit ID'), style='font-monospace', copy_button=True)
|
||||||
|
type = attrs.RelatedObjectAttr('type', linkify=True, colored=True)
|
||||||
|
status = attrs.ChoiceAttr('status')
|
||||||
|
tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group')
|
||||||
|
description = attrs.TextAttr('description')
|
||||||
|
|
||||||
|
|
||||||
|
class VirtualCircuitTerminationPanel(panels.ObjectAttributesPanel):
|
||||||
|
provider = attrs.RelatedObjectAttr('virtual_circuit.provider', linkify=True)
|
||||||
|
provider_network = attrs.RelatedObjectAttr('virtual_circuit.provider_network', linkify=True)
|
||||||
|
provider_account = attrs.RelatedObjectAttr('virtual_circuit.provider_account', linkify=True)
|
||||||
|
virtual_circuit = attrs.RelatedObjectAttr('virtual_circuit', linkify=True)
|
||||||
|
role = attrs.ChoiceAttr('role')
|
||||||
|
|
||||||
|
|
||||||
|
class VirtualCircuitTerminationInterfacePanel(panels.ObjectAttributesPanel):
|
||||||
|
title = _('Interface')
|
||||||
|
|
||||||
|
device = attrs.RelatedObjectAttr('interface.device', linkify=True)
|
||||||
|
interface = attrs.RelatedObjectAttr('interface', linkify=True)
|
||||||
|
type = attrs.ChoiceAttr('interface.type')
|
||||||
|
description = attrs.TextAttr('interface.description')
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
from django.urls import include, path
|
from django.urls import include, path
|
||||||
|
|
||||||
from utilities.urls import get_model_urls
|
from utilities.urls import get_model_urls
|
||||||
|
|
||||||
from . import views
|
from . import views
|
||||||
|
|
||||||
app_name = 'circuits'
|
app_name = 'circuits'
|
||||||
|
|||||||
@@ -1,18 +1,28 @@
|
|||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from dcim.views import PathTraceView
|
from dcim.views import PathTraceView
|
||||||
|
from extras.ui.panels import CustomFieldsPanel, ImageAttachmentsPanel, TagsPanel
|
||||||
from ipam.models import ASN
|
from ipam.models import ASN
|
||||||
from netbox.object_actions import AddObject, BulkDelete, BulkEdit, BulkExport, BulkImport
|
from netbox.object_actions import AddObject, BulkDelete, BulkEdit, BulkExport, BulkImport
|
||||||
|
from netbox.ui import actions, layout
|
||||||
|
from netbox.ui.panels import (
|
||||||
|
CommentsPanel,
|
||||||
|
ObjectsTablePanel,
|
||||||
|
RelatedObjectsPanel,
|
||||||
|
)
|
||||||
from netbox.views import generic
|
from netbox.views import generic
|
||||||
from utilities.query import count_related
|
from utilities.query import count_related
|
||||||
from utilities.views import GetRelatedModelsMixin, register_model_view
|
from utilities.views import GetRelatedModelsMixin, register_model_view
|
||||||
|
|
||||||
from . import filtersets, forms, tables
|
from . import filtersets, forms, tables
|
||||||
from .models import *
|
from .models import *
|
||||||
|
from .ui import panels
|
||||||
|
|
||||||
#
|
#
|
||||||
# Providers
|
# Providers
|
||||||
#
|
#
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(Provider, 'list', path='', detail=False)
|
@register_model_view(Provider, 'list', path='', detail=False)
|
||||||
class ProviderListView(generic.ObjectListView):
|
class ProviderListView(generic.ObjectListView):
|
||||||
queryset = Provider.objects.annotate(
|
queryset = Provider.objects.annotate(
|
||||||
@@ -28,6 +38,37 @@ class ProviderListView(generic.ObjectListView):
|
|||||||
@register_model_view(Provider)
|
@register_model_view(Provider)
|
||||||
class ProviderView(GetRelatedModelsMixin, generic.ObjectView):
|
class ProviderView(GetRelatedModelsMixin, generic.ObjectView):
|
||||||
queryset = Provider.objects.all()
|
queryset = Provider.objects.all()
|
||||||
|
layout = layout.SimpleLayout(
|
||||||
|
left_panels=[
|
||||||
|
panels.ProviderPanel(),
|
||||||
|
TagsPanel(),
|
||||||
|
CommentsPanel(),
|
||||||
|
],
|
||||||
|
right_panels=[
|
||||||
|
RelatedObjectsPanel(),
|
||||||
|
CustomFieldsPanel(),
|
||||||
|
],
|
||||||
|
bottom_panels=[
|
||||||
|
ObjectsTablePanel(
|
||||||
|
model='circuits.ProviderAccount',
|
||||||
|
filters={'provider_id': lambda ctx: ctx['object'].pk},
|
||||||
|
exclude_columns=['provider'],
|
||||||
|
actions=[
|
||||||
|
actions.AddObject(
|
||||||
|
'circuits.ProviderAccount', url_params={'provider': lambda ctx: ctx['object'].pk}
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
ObjectsTablePanel(
|
||||||
|
model='circuits.Circuit',
|
||||||
|
filters={'provider_id': lambda ctx: ctx['object'].pk},
|
||||||
|
exclude_columns=['provider'],
|
||||||
|
actions=[
|
||||||
|
actions.AddObject('circuits.Circuit', url_params={'provider': lambda ctx: ctx['object'].pk}),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
def get_extra_context(self, request, instance):
|
def get_extra_context(self, request, instance):
|
||||||
return {
|
return {
|
||||||
@@ -43,7 +84,7 @@ class ProviderView(GetRelatedModelsMixin, generic.ObjectView):
|
|||||||
'provider_id',
|
'provider_id',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -107,6 +148,33 @@ class ProviderAccountListView(generic.ObjectListView):
|
|||||||
@register_model_view(ProviderAccount)
|
@register_model_view(ProviderAccount)
|
||||||
class ProviderAccountView(GetRelatedModelsMixin, generic.ObjectView):
|
class ProviderAccountView(GetRelatedModelsMixin, generic.ObjectView):
|
||||||
queryset = ProviderAccount.objects.all()
|
queryset = ProviderAccount.objects.all()
|
||||||
|
layout = layout.SimpleLayout(
|
||||||
|
left_panels=[
|
||||||
|
panels.ProviderAccountPanel(),
|
||||||
|
TagsPanel(),
|
||||||
|
],
|
||||||
|
right_panels=[
|
||||||
|
RelatedObjectsPanel(),
|
||||||
|
CommentsPanel(),
|
||||||
|
CustomFieldsPanel(),
|
||||||
|
],
|
||||||
|
bottom_panels=[
|
||||||
|
ObjectsTablePanel(
|
||||||
|
model='circuits.Circuit',
|
||||||
|
filters={'provider_account_id': lambda ctx: ctx['object'].pk},
|
||||||
|
exclude_columns=['provider_account'],
|
||||||
|
actions=[
|
||||||
|
actions.AddObject(
|
||||||
|
'circuits.Circuit',
|
||||||
|
url_params={
|
||||||
|
'provider': lambda ctx: ctx['object'].provider.pk,
|
||||||
|
'provider_account': lambda ctx: ctx['object'].pk,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
def get_extra_context(self, request, instance):
|
def get_extra_context(self, request, instance):
|
||||||
return {
|
return {
|
||||||
@@ -173,6 +241,33 @@ class ProviderNetworkListView(generic.ObjectListView):
|
|||||||
@register_model_view(ProviderNetwork)
|
@register_model_view(ProviderNetwork)
|
||||||
class ProviderNetworkView(GetRelatedModelsMixin, generic.ObjectView):
|
class ProviderNetworkView(GetRelatedModelsMixin, generic.ObjectView):
|
||||||
queryset = ProviderNetwork.objects.all()
|
queryset = ProviderNetwork.objects.all()
|
||||||
|
layout = layout.SimpleLayout(
|
||||||
|
left_panels=[
|
||||||
|
panels.ProviderNetworkPanel(),
|
||||||
|
TagsPanel(),
|
||||||
|
],
|
||||||
|
right_panels=[
|
||||||
|
RelatedObjectsPanel(),
|
||||||
|
CommentsPanel(),
|
||||||
|
CustomFieldsPanel(),
|
||||||
|
],
|
||||||
|
bottom_panels=[
|
||||||
|
ObjectsTablePanel(
|
||||||
|
model='circuits.Circuit',
|
||||||
|
filters={'provider_network_id': lambda ctx: ctx['object'].pk},
|
||||||
|
),
|
||||||
|
ObjectsTablePanel(
|
||||||
|
model='circuits.VirtualCircuit',
|
||||||
|
filters={'provider_network_id': lambda ctx: ctx['object'].pk},
|
||||||
|
exclude_columns=['provider_network'],
|
||||||
|
actions=[
|
||||||
|
actions.AddObject(
|
||||||
|
'circuits.VirtualCircuit', url_params={'provider_network': lambda ctx: ctx['object'].pk}
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
def get_extra_context(self, request, instance):
|
def get_extra_context(self, request, instance):
|
||||||
return {
|
return {
|
||||||
@@ -250,6 +345,17 @@ class CircuitTypeListView(generic.ObjectListView):
|
|||||||
@register_model_view(CircuitType)
|
@register_model_view(CircuitType)
|
||||||
class CircuitTypeView(GetRelatedModelsMixin, generic.ObjectView):
|
class CircuitTypeView(GetRelatedModelsMixin, generic.ObjectView):
|
||||||
queryset = CircuitType.objects.all()
|
queryset = CircuitType.objects.all()
|
||||||
|
layout = layout.SimpleLayout(
|
||||||
|
left_panels=[
|
||||||
|
panels.CircuitTypePanel(),
|
||||||
|
TagsPanel(),
|
||||||
|
],
|
||||||
|
right_panels=[
|
||||||
|
RelatedObjectsPanel(),
|
||||||
|
CommentsPanel(),
|
||||||
|
CustomFieldsPanel(),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
def get_extra_context(self, request, instance):
|
def get_extra_context(self, request, instance):
|
||||||
return {
|
return {
|
||||||
@@ -317,6 +423,20 @@ class CircuitListView(generic.ObjectListView):
|
|||||||
@register_model_view(Circuit)
|
@register_model_view(Circuit)
|
||||||
class CircuitView(generic.ObjectView):
|
class CircuitView(generic.ObjectView):
|
||||||
queryset = Circuit.objects.all()
|
queryset = Circuit.objects.all()
|
||||||
|
layout = layout.SimpleLayout(
|
||||||
|
left_panels=[
|
||||||
|
panels.CircuitPanel(),
|
||||||
|
panels.CircuitGroupAssignmentsPanel(),
|
||||||
|
CustomFieldsPanel(),
|
||||||
|
TagsPanel(),
|
||||||
|
CommentsPanel(),
|
||||||
|
],
|
||||||
|
right_panels=[
|
||||||
|
panels.CircuitCircuitTerminationPanel(side='A'),
|
||||||
|
panels.CircuitCircuitTerminationPanel(side='Z'),
|
||||||
|
ImageAttachmentsPanel(),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(Circuit, 'add', detail=False)
|
@register_model_view(Circuit, 'add', detail=False)
|
||||||
@@ -389,6 +509,15 @@ class CircuitTerminationListView(generic.ObjectListView):
|
|||||||
@register_model_view(CircuitTermination)
|
@register_model_view(CircuitTermination)
|
||||||
class CircuitTerminationView(generic.ObjectView):
|
class CircuitTerminationView(generic.ObjectView):
|
||||||
queryset = CircuitTermination.objects.all()
|
queryset = CircuitTermination.objects.all()
|
||||||
|
layout = layout.SimpleLayout(
|
||||||
|
left_panels=[
|
||||||
|
panels.CircuitTerminationPanel(),
|
||||||
|
],
|
||||||
|
right_panels=[
|
||||||
|
CustomFieldsPanel(),
|
||||||
|
TagsPanel(),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(CircuitTermination, 'add', detail=False)
|
@register_model_view(CircuitTermination, 'add', detail=False)
|
||||||
@@ -445,6 +574,17 @@ class CircuitGroupListView(generic.ObjectListView):
|
|||||||
@register_model_view(CircuitGroup)
|
@register_model_view(CircuitGroup)
|
||||||
class CircuitGroupView(GetRelatedModelsMixin, generic.ObjectView):
|
class CircuitGroupView(GetRelatedModelsMixin, generic.ObjectView):
|
||||||
queryset = CircuitGroup.objects.all()
|
queryset = CircuitGroup.objects.all()
|
||||||
|
layout = layout.SimpleLayout(
|
||||||
|
left_panels=[
|
||||||
|
panels.CircuitGroupPanel(),
|
||||||
|
TagsPanel(),
|
||||||
|
],
|
||||||
|
right_panels=[
|
||||||
|
RelatedObjectsPanel(),
|
||||||
|
CommentsPanel(),
|
||||||
|
CustomFieldsPanel(),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
def get_extra_context(self, request, instance):
|
def get_extra_context(self, request, instance):
|
||||||
return {
|
return {
|
||||||
@@ -507,6 +647,15 @@ class CircuitGroupAssignmentListView(generic.ObjectListView):
|
|||||||
@register_model_view(CircuitGroupAssignment)
|
@register_model_view(CircuitGroupAssignment)
|
||||||
class CircuitGroupAssignmentView(generic.ObjectView):
|
class CircuitGroupAssignmentView(generic.ObjectView):
|
||||||
queryset = CircuitGroupAssignment.objects.all()
|
queryset = CircuitGroupAssignment.objects.all()
|
||||||
|
layout = layout.SimpleLayout(
|
||||||
|
left_panels=[
|
||||||
|
panels.CircuitGroupAssignmentPanel(),
|
||||||
|
TagsPanel(),
|
||||||
|
],
|
||||||
|
right_panels=[
|
||||||
|
CustomFieldsPanel(),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(CircuitGroupAssignment, 'add', detail=False)
|
@register_model_view(CircuitGroupAssignment, 'add', detail=False)
|
||||||
@@ -559,6 +708,17 @@ class VirtualCircuitTypeListView(generic.ObjectListView):
|
|||||||
@register_model_view(VirtualCircuitType)
|
@register_model_view(VirtualCircuitType)
|
||||||
class VirtualCircuitTypeView(GetRelatedModelsMixin, generic.ObjectView):
|
class VirtualCircuitTypeView(GetRelatedModelsMixin, generic.ObjectView):
|
||||||
queryset = VirtualCircuitType.objects.all()
|
queryset = VirtualCircuitType.objects.all()
|
||||||
|
layout = layout.SimpleLayout(
|
||||||
|
left_panels=[
|
||||||
|
panels.VirtualCircuitTypePanel(),
|
||||||
|
TagsPanel(),
|
||||||
|
],
|
||||||
|
right_panels=[
|
||||||
|
RelatedObjectsPanel(),
|
||||||
|
CommentsPanel(),
|
||||||
|
CustomFieldsPanel(),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
def get_extra_context(self, request, instance):
|
def get_extra_context(self, request, instance):
|
||||||
return {
|
return {
|
||||||
@@ -626,6 +786,31 @@ class VirtualCircuitListView(generic.ObjectListView):
|
|||||||
@register_model_view(VirtualCircuit)
|
@register_model_view(VirtualCircuit)
|
||||||
class VirtualCircuitView(generic.ObjectView):
|
class VirtualCircuitView(generic.ObjectView):
|
||||||
queryset = VirtualCircuit.objects.all()
|
queryset = VirtualCircuit.objects.all()
|
||||||
|
layout = layout.SimpleLayout(
|
||||||
|
left_panels=[
|
||||||
|
panels.VirtualCircuitPanel(),
|
||||||
|
TagsPanel(),
|
||||||
|
],
|
||||||
|
right_panels=[
|
||||||
|
CustomFieldsPanel(),
|
||||||
|
CommentsPanel(),
|
||||||
|
panels.CircuitGroupAssignmentsPanel(),
|
||||||
|
],
|
||||||
|
bottom_panels=[
|
||||||
|
ObjectsTablePanel(
|
||||||
|
model='circuits.VirtualCircuitTermination',
|
||||||
|
title=_('Terminations'),
|
||||||
|
filters={'virtual_circuit_id': lambda ctx: ctx['object'].pk},
|
||||||
|
exclude_columns=['virtual_circuit'],
|
||||||
|
actions=[
|
||||||
|
actions.AddObject(
|
||||||
|
'circuits.VirtualCircuitTermination',
|
||||||
|
url_params={'virtual_circuit': lambda ctx: ctx['object'].pk},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(VirtualCircuit, 'add', detail=False)
|
@register_model_view(VirtualCircuit, 'add', detail=False)
|
||||||
@@ -697,6 +882,16 @@ class VirtualCircuitTerminationListView(generic.ObjectListView):
|
|||||||
@register_model_view(VirtualCircuitTermination)
|
@register_model_view(VirtualCircuitTermination)
|
||||||
class VirtualCircuitTerminationView(generic.ObjectView):
|
class VirtualCircuitTerminationView(generic.ObjectView):
|
||||||
queryset = VirtualCircuitTermination.objects.all()
|
queryset = VirtualCircuitTermination.objects.all()
|
||||||
|
layout = layout.SimpleLayout(
|
||||||
|
left_panels=[
|
||||||
|
panels.VirtualCircuitTerminationPanel(),
|
||||||
|
TagsPanel(),
|
||||||
|
CustomFieldsPanel(),
|
||||||
|
],
|
||||||
|
right_panels=[
|
||||||
|
panels.VirtualCircuitTerminationInterfacePanel(),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(VirtualCircuitTermination, 'edit')
|
@register_model_view(VirtualCircuitTermination, 'edit')
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user