diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml
index 1c330e8a8..3af825d30 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yaml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yaml
@@ -14,7 +14,7 @@ body:
attributes:
label: NetBox version
description: What version of NetBox are you currently running?
- placeholder: v3.1.7
+ placeholder: v3.1.9
validations:
required: true
- type: dropdown
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml
index eea258c09..f5bf198b8 100644
--- a/.github/ISSUE_TEMPLATE/feature_request.yaml
+++ b/.github/ISSUE_TEMPLATE/feature_request.yaml
@@ -14,7 +14,7 @@ body:
attributes:
label: NetBox version
description: What version of NetBox are you currently running?
- placeholder: v3.1.7
+ placeholder: v3.1.9
validations:
required: true
- type: dropdown
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index c8e3f47ab..9ba75118b 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -38,6 +38,19 @@ jobs:
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
+
+ - name: Install Yarn Package Manager
+ run: npm install -g yarn
+
+ - name: Setup Node.js with Yarn Caching
+ uses: actions/setup-node@v2
+ with:
+ node-version: ${{ matrix.node-version }}
+ cache: yarn
+ cache-dependency-path: netbox/project-static/yarn.lock
+
+ - name: Install Frontend Dependencies
+ run: yarn --cwd netbox/project-static
- name: Install dependencies & set up configuration
run: |
@@ -45,7 +58,6 @@ jobs:
pip install -r requirements.txt
pip install pycodestyle coverage
ln -s configuration.testing.py netbox/netbox/configuration.py
- yarn --cwd netbox/project-static
- name: Build documentation
run: mkdocs build
@@ -63,7 +75,7 @@ jobs:
run: scripts/verify-bundles.sh
- name: Run tests
- run: coverage run --source="netbox/" netbox/manage.py test netbox/
+ run: coverage run --source="netbox/" netbox/manage.py test netbox/ --parallel
- name: Show coverage report
run: coverage report --skip-covered --omit *migrations*
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index a3627a2b1..ee69605c7 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -16,13 +16,6 @@ categories for discussions:
feature request
* **Q&A** - Request help with installing or using NetBox
-### Mailing List
-
-We also have a Google Groups [mailing list](https://groups.google.com/g/netbox-discuss)
-for general discussion, however we're encouraging people to use GitHub
-discussions where possible, as it's much easier for newcomers to review past
-discussions.
-
### Slack
For real-time chat, you can join the **#netbox** Slack channel on [NetDev Community](https://netdev.chat/).
diff --git a/README.md b/README.md
index 888b881ab..42bf8b619 100644
--- a/README.md
+++ b/README.md
@@ -2,6 +2,8 @@
+:loudspeaker: The **[2022 NetBox community survey](https://forms.gle/KR8YbR8GiJ9EYXM28)** is now open! We collect this feedback and demographic data from NetBox users around the world to help shape the project's long-term development goals. Please take a few minutes to share your responses!
+

NetBox is an infrastructure resource modeling (IRM) tool designed to empower
@@ -68,7 +70,6 @@ The complete documentation for NetBox can be found at [Read the Docs](https://ne
* [GitHub Discussions](https://github.com/netbox-community/netbox/discussions) - Discussion forum hosted by GitHub; ideal for Q&A and other structured discussions
* [Slack](https://netdev.chat/) - Real-time chat hosted by the NetDev Community; best for unstructured discussion or just hanging out
-* [Google Group](https://groups.google.com/g/netbox-discuss) - Legacy mailing list; slowly being replaced by GitHub discussions
### Installation
diff --git a/docs/configuration/remote-authentication.md b/docs/configuration/remote-authentication.md
index c00da8b67..73d29415b 100644
--- a/docs/configuration/remote-authentication.md
+++ b/docs/configuration/remote-authentication.md
@@ -35,7 +35,7 @@ The list of groups to assign a new user account when created using remote authen
Default: `{}` (Empty dictionary)
-A mapping of permissions to assign a new user account when created using remote authentication. Each key in the dictionary should be set to a dictionary of the attributes to be applied to the permission, or `None` to allow all objects. (Requires `REMOTE_AUTH_ENABLED`.)
+A mapping of permissions to assign a new user account when created using remote authentication. Each key in the dictionary should be set to a dictionary of the attributes to be applied to the permission, or `None` to allow all objects. (Requires `REMOTE_AUTH_ENABLED` as True and `REMOTE_AUTH_GROUP_SYNC_ENABLED` as False.)
---
@@ -43,7 +43,7 @@ A mapping of permissions to assign a new user account when created using remote
Default: `False`
-NetBox can be configured to support remote user authentication by inferring user authentication from an HTTP header set by the HTTP reverse proxy (e.g. nginx or Apache). Set this to `True` to enable this functionality. (Local authentication will still take effect as a fallback.)
+NetBox can be configured to support remote user authentication by inferring user authentication from an HTTP header set by the HTTP reverse proxy (e.g. nginx or Apache). Set this to `True` to enable this functionality. (Local authentication will still take effect as a fallback.) (`REMOTE_AUTH_DEFAULT_GROUPS` will not function if `REMOTE_AUTH_ENABLED` is enabled)
---
diff --git a/docs/development/index.md b/docs/development/index.md
index 03e2cc0c3..85762d0fe 100644
--- a/docs/development/index.md
+++ b/docs/development/index.md
@@ -7,9 +7,8 @@ NetBox is maintained as a [GitHub project](https://github.com/netbox-community/n
There are several official forums for communication among the developers and community members:
* [GitHub issues](https://github.com/netbox-community/netbox/issues) - All feature requests, bug reports, and other substantial changes to the code base **must** be documented in a GitHub issue.
-* [GitHub Discussions](https://github.com/netbox-community/netbox/discussions) - The preferred forum for general discussion and support issues. Ideal for shaping a feature request prior to submitting an issue.
+* [GitHub discussions](https://github.com/netbox-community/netbox/discussions) - The preferred forum for general discussion and support issues. Ideal for shaping a feature request prior to submitting an issue.
* [#netbox on NetDev Community Slack](https://netdev.chat/) - Good for quick chats. Avoid any discussion that might need to be referenced later on, as the chat history is not retained long.
-* [Google Group](https://groups.google.com/g/netbox-discuss) - Legacy mailing list; slowly being phased out in favor of GitHub discussions.
## Governance
diff --git a/docs/index.md b/docs/index.md
index 943f1d7ab..5742c0ab6 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -1,5 +1,7 @@
{style="height: 100px; margin-bottom: 3em"}
+:loudspeaker: The **[2022 NetBox community survey](https://forms.gle/KR8YbR8GiJ9EYXM28)** is now open! We collect this feedback and demographic data from NetBox users around the world to help shape the project's long-term development goals. Please take a few minutes to share your responses!
+
# What is NetBox?
NetBox is an infrastructure resource modeling (IRM) application designed to empower network automation. Initially conceived by the network engineering team at [DigitalOcean](https://www.digitalocean.com/), NetBox was developed specifically to address the needs of network and infrastructure engineers. NetBox is made available as open source under the Apache 2 license. It encompasses the following aspects of network management:
diff --git a/docs/installation/index.md b/docs/installation/index.md
index 74b51da7f..a06a60bf6 100644
--- a/docs/installation/index.md
+++ b/docs/installation/index.md
@@ -11,10 +11,6 @@ The following sections detail how to set up a new instance of NetBox:
5. [HTTP server](5-http-server.md)
6. [LDAP authentication](6-ldap.md) (optional)
-The video below demonstrates the installation of NetBox v3.0 on Ubuntu 20.04 for your reference.
-
-
-
## Requirements
| Dependency | Minimum Version |
diff --git a/docs/release-notes/version-3.1.md b/docs/release-notes/version-3.1.md
index 27aaa4b4c..5decff1b3 100644
--- a/docs/release-notes/version-3.1.md
+++ b/docs/release-notes/version-3.1.md
@@ -1,5 +1,56 @@
# NetBox v3.1
+## v3.1.9 (2022-03-07)
+
+### Enhancements
+
+* [#8594](https://github.com/netbox-community/netbox/issues/8594) - Enable filtering by exact description match for all applicable models
+* [#8629](https://github.com/netbox-community/netbox/issues/8629) - Add description to tag table search function
+* [#8664](https://github.com/netbox-community/netbox/issues/8664) - Show assigned ASNs/sites under list views
+* [#8736](https://github.com/netbox-community/netbox/issues/8736) - Add PC and UPC fiber end faces for LC/SC/LSH port types
+* [#8758](https://github.com/netbox-community/netbox/issues/8758) - Allow empty string substitution when renaming objects in bulk
+* [#8762](https://github.com/netbox-community/netbox/issues/8762) - Link to rack elevations list from site view
+* [#8766](https://github.com/netbox-community/netbox/issues/8766) - Add SCTP to service protocols list
+
+### Bug Fixes
+
+* [#8546](https://github.com/netbox-community/netbox/issues/8546) - Fix bulk import to restrict bridge, parent, and LAG to device interfaces
+* [#8633](https://github.com/netbox-community/netbox/issues/8633) - Prevent navigation sidebar pin from disappearing at certain breakpoints
+* [#8674](https://github.com/netbox-community/netbox/issues/8674) - Fix rendering of tabbed content in documentation
+* [#8710](https://github.com/netbox-community/netbox/issues/8710) - Fix dynamic scope selection form fields when creating a VLAN group
+* [#8713](https://github.com/netbox-community/netbox/issues/8713) - Restore missing "add" button on services list view
+* [#8715](https://github.com/netbox-community/netbox/issues/8715) - Avoid returning multiple objects when restricting querysets using multiple tags in permissions
+* [#8717](https://github.com/netbox-community/netbox/issues/8717) - Fix redirection after bulk edit/delete of prefixes from aggregate view
+* [#8724](https://github.com/netbox-community/netbox/issues/8724) - Fix exception during device import with invalid device type
+* [#8807](https://github.com/netbox-community/netbox/issues/8807) - Correct REST API URL for FHRP group assignments
+* [#8808](https://github.com/netbox-community/netbox/issues/8808) - Fix members count under FHRP group list
+
+---
+
+## v3.1.8 (2022-02-15)
+
+### Enhancements
+
+* [#7150](https://github.com/netbox-community/netbox/issues/7150) - Linkify devices on the far side of a rack elevation
+* [#8398](https://github.com/netbox-community/netbox/issues/8398) - Embiggen configuration form fields for banner message content
+* [#8556](https://github.com/netbox-community/netbox/issues/8556) - Add full username column to changelog table
+* [#8620](https://github.com/netbox-community/netbox/issues/8620) - Enable tab completion for `nbshell`
+
+### Bug Fixes
+
+* [#8331](https://github.com/netbox-community/netbox/issues/8331) - Implement `replaceAll` string utility function to improve browser compatibility
+* [#8391](https://github.com/netbox-community/netbox/issues/8391) - Null date columns should return empty strings during CSV export
+* [#8548](https://github.com/netbox-community/netbox/issues/8548) - Fix display of VC members when position is zero
+* [#8561](https://github.com/netbox-community/netbox/issues/8561) - Include option to connect a rear port to a console port
+* [#8564](https://github.com/netbox-community/netbox/issues/8564) - Fix errant table configuration key `available_columns`
+* [#8577](https://github.com/netbox-community/netbox/issues/8577) - Show contact assignment counts in global search results
+* [#8578](https://github.com/netbox-community/netbox/issues/8578) - Object change log tables should honor user's configured preferences
+* [#8604](https://github.com/netbox-community/netbox/issues/8604) - Fix tag filter on config context list filter form
+* [#8609](https://github.com/netbox-community/netbox/issues/8609) - Display validation error when attempting to assign VLANs to interface with no mode during bulk edit
+* [#8611](https://github.com/netbox-community/netbox/issues/8611) - Fix bulk editing for certain custom link, webhook, and journal entry fields
+
+---
+
## v3.1.7 (2022-02-03)
### Enhancements
diff --git a/mkdocs.yml b/mkdocs.yml
index 3fb838ffd..6245f3bb2 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -8,11 +8,13 @@ theme:
icon:
repo: fontawesome/brands/github
palette:
- - scheme: default
+ - media: "(prefers-color-scheme: light)"
+ scheme: default
toggle:
icon: material/lightbulb-outline
name: Switch to Dark Mode
- - scheme: slate
+ - media: "(prefers-color-scheme: dark)"
+ scheme: slate
toggle:
icon: material/lightbulb
name: Switch to Light Mode
@@ -34,7 +36,8 @@ markdown_extensions:
emoji_index: !!python/name:materialx.emoji.twemoji
emoji_generator: !!python/name:materialx.emoji.to_svg
- pymdownx.superfences
- - pymdownx.tabbed
+ - pymdownx.tabbed:
+ alternate_style: true
nav:
- Introduction: 'index.md'
- Installation:
diff --git a/netbox/circuits/filtersets.py b/netbox/circuits/filtersets.py
index fd582dd99..5a6a95785 100644
--- a/netbox/circuits/filtersets.py
+++ b/netbox/circuits/filtersets.py
@@ -98,7 +98,7 @@ class ProviderNetworkFilterSet(PrimaryModelFilterSet):
class Meta:
model = ProviderNetwork
- fields = ['id', 'name']
+ fields = ['id', 'name', 'description']
def search(self, queryset, name, value):
if not value.strip():
@@ -115,7 +115,7 @@ class CircuitTypeFilterSet(OrganizationalModelFilterSet):
class Meta:
model = CircuitType
- fields = ['id', 'name', 'slug']
+ fields = ['id', 'name', 'slug', 'description']
class CircuitFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
@@ -193,7 +193,7 @@ class CircuitFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
class Meta:
model = Circuit
- fields = ['id', 'cid', 'install_date', 'commit_rate']
+ fields = ['id', 'cid', 'description', 'install_date', 'commit_rate']
def search(self, queryset, name, value):
if not value.strip():
@@ -234,7 +234,7 @@ class CircuitTerminationFilterSet(ChangeLoggedModelFilterSet, CableTerminationFi
class Meta:
model = CircuitTermination
- fields = ['id', 'term_side', 'port_speed', 'upstream_speed', 'xconnect_id']
+ fields = ['id', 'term_side', 'port_speed', 'upstream_speed', 'xconnect_id', 'description']
def search(self, queryset, name, value):
if not value.strip():
diff --git a/netbox/circuits/tests/test_filtersets.py b/netbox/circuits/tests/test_filtersets.py
index 4880a8388..20416c4e6 100644
--- a/netbox/circuits/tests/test_filtersets.py
+++ b/netbox/circuits/tests/test_filtersets.py
@@ -108,8 +108,8 @@ class CircuitTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
def setUpTestData(cls):
CircuitType.objects.bulk_create((
- CircuitType(name='Circuit Type 1', slug='circuit-type-1'),
- CircuitType(name='Circuit Type 2', slug='circuit-type-2'),
+ CircuitType(name='Circuit Type 1', slug='circuit-type-1', description='foobar1'),
+ CircuitType(name='Circuit Type 2', slug='circuit-type-2', description='foobar2'),
CircuitType(name='Circuit Type 3', slug='circuit-type-3'),
))
@@ -121,6 +121,10 @@ class CircuitTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'slug': ['circuit-type-1']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+ def test_description(self):
+ params = {'description': ['foobar1', 'foobar2']}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
class CircuitTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = Circuit.objects.all()
@@ -187,8 +191,8 @@ class CircuitTestCase(TestCase, ChangeLoggedFilterSetTests):
ProviderNetwork.objects.bulk_create(provider_networks)
circuits = (
- Circuit(provider=providers[0], tenant=tenants[0], type=circuit_types[0], cid='Test Circuit 1', install_date='2020-01-01', commit_rate=1000, status=CircuitStatusChoices.STATUS_ACTIVE),
- Circuit(provider=providers[0], tenant=tenants[0], type=circuit_types[0], cid='Test Circuit 2', install_date='2020-01-02', commit_rate=2000, status=CircuitStatusChoices.STATUS_ACTIVE),
+ Circuit(provider=providers[0], tenant=tenants[0], type=circuit_types[0], cid='Test Circuit 1', install_date='2020-01-01', commit_rate=1000, status=CircuitStatusChoices.STATUS_ACTIVE, description='foobar1'),
+ Circuit(provider=providers[0], tenant=tenants[0], type=circuit_types[0], cid='Test Circuit 2', install_date='2020-01-02', commit_rate=2000, status=CircuitStatusChoices.STATUS_ACTIVE, description='foobar2'),
Circuit(provider=providers[0], tenant=tenants[1], type=circuit_types[0], cid='Test Circuit 3', install_date='2020-01-03', commit_rate=3000, status=CircuitStatusChoices.STATUS_PLANNED),
Circuit(provider=providers[1], tenant=tenants[1], type=circuit_types[1], cid='Test Circuit 4', install_date='2020-01-04', commit_rate=4000, status=CircuitStatusChoices.STATUS_PLANNED),
Circuit(provider=providers[1], tenant=tenants[2], type=circuit_types[1], cid='Test Circuit 5', install_date='2020-01-05', commit_rate=5000, status=CircuitStatusChoices.STATUS_OFFLINE),
@@ -241,6 +245,10 @@ class CircuitTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'status': [CircuitStatusChoices.STATUS_ACTIVE, CircuitStatusChoices.STATUS_PLANNED]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
+ def test_description(self):
+ params = {'description': ['foobar1', 'foobar2']}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
def test_region(self):
regions = Region.objects.all()[:2]
params = {'region_id': [regions[0].pk, regions[1].pk]}
@@ -319,8 +327,8 @@ class CircuitTerminationTestCase(TestCase, ChangeLoggedFilterSetTests):
Circuit.objects.bulk_create(circuits)
circuit_terminations = ((
- CircuitTermination(circuit=circuits[0], site=sites[0], term_side='A', port_speed=1000, upstream_speed=1000, xconnect_id='ABC'),
- CircuitTermination(circuit=circuits[0], site=sites[1], term_side='Z', port_speed=1000, upstream_speed=1000, xconnect_id='DEF'),
+ CircuitTermination(circuit=circuits[0], site=sites[0], term_side='A', port_speed=1000, upstream_speed=1000, xconnect_id='ABC', description='foobar1'),
+ CircuitTermination(circuit=circuits[0], site=sites[1], term_side='Z', port_speed=1000, upstream_speed=1000, xconnect_id='DEF', description='foobar2'),
CircuitTermination(circuit=circuits[1], site=sites[1], term_side='A', port_speed=2000, upstream_speed=2000, xconnect_id='GHI'),
CircuitTermination(circuit=circuits[1], site=sites[2], term_side='Z', port_speed=2000, upstream_speed=2000, xconnect_id='JKL'),
CircuitTermination(circuit=circuits[2], site=sites[2], term_side='A', port_speed=3000, upstream_speed=3000, xconnect_id='MNO'),
@@ -349,6 +357,10 @@ class CircuitTerminationTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'xconnect_id': ['ABC', 'DEF']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+ def test_description(self):
+ params = {'description': ['foobar1', 'foobar2']}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
def test_circuit_id(self):
circuits = Circuit.objects.all()[:2]
params = {'circuit_id': [circuits[0].pk, circuits[1].pk]}
@@ -386,8 +398,8 @@ class ProviderNetworkTestCase(TestCase, ChangeLoggedFilterSetTests):
Provider.objects.bulk_create(providers)
provider_networks = (
- ProviderNetwork(name='Provider Network 1', provider=providers[0]),
- ProviderNetwork(name='Provider Network 2', provider=providers[1]),
+ ProviderNetwork(name='Provider Network 1', provider=providers[0], description='foobar1'),
+ ProviderNetwork(name='Provider Network 2', provider=providers[1], description='foobar2'),
ProviderNetwork(name='Provider Network 3', provider=providers[2]),
)
ProviderNetwork.objects.bulk_create(provider_networks)
@@ -396,6 +408,10 @@ class ProviderNetworkTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'name': ['Provider Network 1', 'Provider Network 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+ def test_description(self):
+ params = {'description': ['foobar1', 'foobar2']}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
def test_provider(self):
providers = Provider.objects.all()[:2]
params = {'provider_id': [providers[0].pk, providers[1].pk]}
diff --git a/netbox/dcim/choices.py b/netbox/dcim/choices.py
index 1d3b59497..8900b2d89 100644
--- a/netbox/dcim/choices.py
+++ b/netbox/dcim/choices.py
@@ -1013,13 +1013,19 @@ class PortTypeChoices(ChoiceSet):
TYPE_MRJ21 = 'mrj21'
TYPE_ST = 'st'
TYPE_SC = 'sc'
+ TYPE_SC_PC = 'sc-pc'
+ TYPE_SC_UPC = 'sc-upc'
TYPE_SC_APC = 'sc-apc'
TYPE_FC = 'fc'
TYPE_LC = 'lc'
+ TYPE_LC_PC = 'lc-pc'
+ TYPE_LC_UPC = 'lc-upc'
TYPE_LC_APC = 'lc-apc'
TYPE_MTRJ = 'mtrj'
TYPE_MPO = 'mpo'
TYPE_LSH = 'lsh'
+ TYPE_LSH_PC = 'lsh-pc'
+ TYPE_LSH_UPC = 'lsh-upc'
TYPE_LSH_APC = 'lsh-apc'
TYPE_SPLICE = 'splice'
TYPE_CS = 'cs'
@@ -1059,12 +1065,18 @@ class PortTypeChoices(ChoiceSet):
(
(TYPE_FC, 'FC'),
(TYPE_LC, 'LC'),
+ (TYPE_LC_PC, 'LC/PC'),
+ (TYPE_LC_UPC, 'LC/UPC'),
(TYPE_LC_APC, 'LC/APC'),
(TYPE_LSH, 'LSH'),
+ (TYPE_LSH_PC, 'LSH/PC'),
+ (TYPE_LSH_UPC, 'LSH/UPC'),
(TYPE_LSH_APC, 'LSH/APC'),
(TYPE_MPO, 'MPO'),
(TYPE_MTRJ, 'MTRJ'),
(TYPE_SC, 'SC'),
+ (TYPE_SC_PC, 'SC/PC'),
+ (TYPE_SC_UPC, 'SC/UPC'),
(TYPE_SC_APC, 'SC/APC'),
(TYPE_ST, 'ST'),
(TYPE_CS, 'CS'),
diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py
index d9c75d3fa..62326b289 100644
--- a/netbox/dcim/filtersets.py
+++ b/netbox/dcim/filtersets.py
@@ -142,7 +142,7 @@ class SiteFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
model = Site
fields = [
'id', 'name', 'slug', 'facility', 'asn', 'latitude', 'longitude', 'contact_name', 'contact_phone',
- 'contact_email',
+ 'contact_email', 'description'
]
def search(self, queryset, name, value):
@@ -237,7 +237,7 @@ class RackRoleFilterSet(OrganizationalModelFilterSet):
class Meta:
model = RackRole
- fields = ['id', 'name', 'slug', 'color']
+ fields = ['id', 'name', 'slug', 'color', 'description']
class RackFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
@@ -385,7 +385,7 @@ class RackReservationFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
class Meta:
model = RackReservation
- fields = ['id', 'created']
+ fields = ['id', 'created', 'description']
def search(self, queryset, name, value):
if not value.strip():
@@ -586,7 +586,7 @@ class DeviceRoleFilterSet(OrganizationalModelFilterSet):
class Meta:
model = DeviceRole
- fields = ['id', 'name', 'slug', 'color', 'vm_role']
+ fields = ['id', 'name', 'slug', 'color', 'vm_role', 'description']
class PlatformFilterSet(OrganizationalModelFilterSet):
diff --git a/netbox/dcim/forms/bulk_edit.py b/netbox/dcim/forms/bulk_edit.py
index 9127b072f..3b604d79d 100644
--- a/netbox/dcim/forms/bulk_edit.py
+++ b/netbox/dcim/forms/bulk_edit.py
@@ -1043,8 +1043,14 @@ class InterfaceBulkEditForm(
def clean(self):
super().clean()
+ if not self.cleaned_data['mode']:
+ if self.cleaned_data['untagged_vlan']:
+ raise forms.ValidationError({'untagged_vlan': "Interface mode must be specified to assign VLANs"})
+ elif self.cleaned_data['tagged_vlans']:
+ raise forms.ValidationError({'tagged_vlans': "Interface mode must be specified to assign VLANs"})
+
# Untagged interfaces cannot be assigned tagged VLANs
- if self.cleaned_data['mode'] == InterfaceModeChoices.MODE_ACCESS and self.cleaned_data['tagged_vlans']:
+ elif self.cleaned_data['mode'] == InterfaceModeChoices.MODE_ACCESS and self.cleaned_data['tagged_vlans']:
raise forms.ValidationError({
'mode': "An access interface cannot have tagged VLANs assigned."
})
diff --git a/netbox/dcim/forms/bulk_import.py b/netbox/dcim/forms/bulk_import.py
index df8c4ec01..59b59bb00 100644
--- a/netbox/dcim/forms/bulk_import.py
+++ b/netbox/dcim/forms/bulk_import.py
@@ -605,6 +605,19 @@ class InterfaceCSVForm(CustomFieldModelCSVForm):
'rf_channel_width', 'tx_power',
)
+ def __init__(self, data=None, *args, **kwargs):
+ super().__init__(data, *args, **kwargs)
+
+ if data:
+ # Limit interface choices for parent, bridge and lag to device only
+ params = {}
+ if data.get('device'):
+ params[f"device__{self.fields['device'].to_field_name}"] = data.get('device')
+ if params:
+ self.fields['parent'].queryset = self.fields['parent'].queryset.filter(**params)
+ self.fields['bridge'].queryset = self.fields['bridge'].queryset.filter(**params)
+ self.fields['lag'].queryset = self.fields['lag'].queryset.filter(**params)
+
def clean_enabled(self):
# Make sure enabled is True when it's not included in the uploaded data
if 'enabled' not in self.data:
diff --git a/netbox/dcim/models/devices.py b/netbox/dcim/models/devices.py
index a2ae20319..6b8ff043d 100644
--- a/netbox/dcim/models/devices.py
+++ b/netbox/dcim/models/devices.py
@@ -670,10 +670,11 @@ class Device(PrimaryModel, ConfigContextModel):
})
# Prevent 0U devices from being assigned to a specific position
- if self.position and self.device_type.u_height == 0:
- raise ValidationError({
- 'position': f"A U0 device type ({self.device_type}) cannot be assigned to a rack position."
- })
+ if hasattr(self, 'device_type'):
+ if self.position and self.device_type.u_height == 0:
+ raise ValidationError({
+ 'position': f"A U0 device type ({self.device_type}) cannot be assigned to a rack position."
+ })
if self.rack:
diff --git a/netbox/dcim/svg.py b/netbox/dcim/svg.py
index 1058d8385..e333320b6 100644
--- a/netbox/dcim/svg.py
+++ b/netbox/dcim/svg.py
@@ -126,10 +126,16 @@ class RackElevationSVG:
link.add(drawing.text(str(name), insert=text, fill='white', class_='device-image-label'))
def _draw_device_rear(self, drawing, device, start, end, text):
- rect = drawing.rect(start, end, class_="slot blocked")
- rect.set_desc(self._get_device_description(device))
- drawing.add(rect)
- drawing.add(drawing.text(get_device_name(device), insert=text))
+ link = drawing.add(
+ drawing.a(
+ href='{}{}'.format(self.base_url, reverse('dcim:device', kwargs={'pk': device.pk})),
+ target='_top',
+ fill='black'
+ )
+ )
+ link.set_desc(self._get_device_description(device))
+ link.add(drawing.rect(start, end, class_="slot blocked"))
+ link.add(drawing.text(get_device_name(device), insert=text))
# Embed rear device type image if one exists
if self.include_images and device.device_type.rear_image:
diff --git a/netbox/dcim/tables/sites.py b/netbox/dcim/tables/sites.py
index e658f1caa..bf4812cfa 100644
--- a/netbox/dcim/tables/sites.py
+++ b/netbox/dcim/tables/sites.py
@@ -85,6 +85,10 @@ class SiteTable(BaseTable):
accessor=tables.A('asns__count'),
viewname='ipam:asn_list',
url_params={'site_id': 'pk'},
+ verbose_name='ASN Count'
+ )
+ asns = tables.ManyToManyColumn(
+ linkify_item=True,
verbose_name='ASNs'
)
tenant = TenantColumn()
@@ -96,8 +100,8 @@ class SiteTable(BaseTable):
class Meta(BaseTable.Meta):
model = Site
fields = (
- 'pk', 'id', 'name', 'slug', 'status', 'facility', 'region', 'group', 'tenant', 'asn_count', 'time_zone',
- 'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'contact_name',
+ 'pk', 'id', 'name', 'slug', 'status', 'facility', 'region', 'group', 'tenant', 'asns', 'asn_count',
+ 'time_zone', 'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'contact_name',
'contact_phone', 'contact_email', 'comments', 'tags', 'created', 'last_updated',
)
default_columns = ('pk', 'name', 'status', 'facility', 'region', 'group', 'tenant', 'description')
diff --git a/netbox/dcim/tables/template_code.py b/netbox/dcim/tables/template_code.py
index 233694a7a..1d68c466a 100644
--- a/netbox/dcim/tables/template_code.py
+++ b/netbox/dcim/tables/template_code.py
@@ -298,6 +298,8 @@ REARPORT_BUTTONS = """