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 @@ NetBox logo +: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! + ![Master branch build status](https://github.com/netbox-community/netbox/workflows/CI/badge.svg?branch=master) 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 @@ ![NetBox](netbox_logo.svg "NetBox logo"){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 = """