diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml
index 08bccc551..9735da5b5 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.3.2
+ placeholder: v3.3.3
validations:
required: true
- type: dropdown
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml
index 4dbf51f2c..691e99cc6 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.3.2
+ placeholder: v3.3.3
validations:
required: true
- type: dropdown
diff --git a/README.md b/README.md
index 93e125079..654b290ee 100644
--- a/README.md
+++ b/README.md
@@ -2,8 +2,6 @@
-
-
NetBox is the leading solution for modeling and documenting modern networks. By
combining the traditional disciplines of IP address management (IPAM) and
datacenter infrastructure management (DCIM) with powerful APIs and extensions,
@@ -11,6 +9,16 @@ NetBox provides the ideal "source of truth" to power network automation.
Available as open source software under the Apache 2.0 license, NetBox is
employed by thousands of organizations around the world.
+
+
+[](https://github.com/netbox-community/netbox/commits)
+[](https://github.com/netbox-community/netbox/issues)
+[](https://github.com/netbox-community/netbox/pulls)
+[](https://github.com/netbox-community/netbox/graphs/contributors)
+ Stats via [Repography](https://repography.com)
+
+## About NetBox
+

Myriad infrastructure components can be modeled in NetBox, including:
@@ -57,7 +65,7 @@ complete list of requirements, see `requirements.txt`. The code is available
[on GitHub](https://github.com/netbox-community/netbox).
-
Thank you to our sponsors!
+
Thank you to our sponsors!
[](https://try.digitalocean.com/developer-cloud)
diff --git a/docs/installation/3-netbox.md b/docs/installation/3-netbox.md
index eeb5e6f20..f42e28deb 100644
--- a/docs/installation/3-netbox.md
+++ b/docs/installation/3-netbox.md
@@ -7,7 +7,7 @@ This section of the documentation discusses installing and configuring the NetBo
Begin by installing all system packages required by NetBox and its dependencies.
!!! warning "Python 3.8 or later required"
- NetBox v3.2 requires Python 3.8, 3.9, or 3.10.
+ NetBox requires Python 3.8, 3.9, or 3.10.
=== "Ubuntu"
diff --git a/docs/installation/index.md b/docs/installation/index.md
index 905add7ab..8b588fccd 100644
--- a/docs/installation/index.md
+++ b/docs/installation/index.md
@@ -2,6 +2,8 @@
The installation instructions provided here have been tested to work on Ubuntu 20.04 and CentOS 8.3. The particular commands needed to install dependencies on other distributions may vary significantly. Unfortunately, this is outside the control of the NetBox maintainers. Please consult your distribution's documentation for assistance with any errors.
+
+
The following sections detail how to set up a new instance of NetBox:
1. [PostgreSQL database](1-postgresql.md)
diff --git a/docs/installation/upgrading.md b/docs/installation/upgrading.md
index deeec883a..802c13e49 100644
--- a/docs/installation/upgrading.md
+++ b/docs/installation/upgrading.md
@@ -1,10 +1,19 @@
# Upgrading to a New NetBox Release
-## Review the Release Notes
+Upgrading NetBox to a new version is pretty simple, however users are cautioned to always review the release notes and save a backup of their current deployment prior to beginning an upgrade.
+
+NetBox can generally be upgraded directly to any newer release with no interim steps, with the one exception being incrementing major versions. This can be done only from the most recent _minor_ release of the major version. For example, NetBox v2.11.8 can be upgraded to version 3.3.2 following the steps below. However, a deployment of NetBox v2.10.10 or earlier must first be upgraded to any v2.11 release, and then to any v3.x release. (This is to accommodate the consolidation of database schema migrations effected by a major version change).
+
+[](../media/installation/upgrade_paths.png)
+
+!!! warning "Perform a Backup"
+ Always be sure to save a backup of your current NetBox deployment prior to starting the upgrade process.
+
+## 1. Review the Release Notes
Prior to upgrading your NetBox instance, be sure to carefully review all [release notes](../release-notes/index.md) that have been published since your current version was released. Although the upgrade process typically does not involve additional work, certain releases may introduce breaking or backward-incompatible changes. These are called out in the release notes under the release in which the change went into effect.
-## Update Dependencies to Required Versions
+## 2. Update Dependencies to Required Versions
NetBox v3.0 and later require the following:
@@ -14,7 +23,7 @@ NetBox v3.0 and later require the following:
| PostgreSQL | 10 |
| Redis | 4.0 |
-## Install the Latest Release
+## 3. Install the Latest Release
As with the initial installation, you can upgrade NetBox by either downloading the latest release package or by cloning the `master` branch of the git repository.
@@ -87,7 +96,7 @@ sudo git pull origin master
sudo git checkout v2.11.11
-## Run the Upgrade Script
+## 4. Run the Upgrade Script
Once the new code is in place, verify that any optional Python packages required by your deployment (e.g. `napalm` or `django-auth-ldap`) are listed in `local_requirements.txt`. Then, run the upgrade script:
@@ -118,7 +127,7 @@ This script performs the following actions:
been made to your local codebase and should be investigated. Never attempt to create new migrations unless you are
intentionally modifying the database schema.
-## Restart the NetBox Services
+## 5. Restart the NetBox Services
!!! warning
If you are upgrading from an installation that does not use a Python virtual environment (any release prior to v2.7.9), you'll need to update the systemd service files to reference the new Python and gunicorn executables before restarting the services. These are located in `/opt/netbox/venv/bin/`. See the example service files in `/opt/netbox/contrib/` for reference.
@@ -129,7 +138,7 @@ Finally, restart the gunicorn and RQ services:
sudo systemctl restart netbox netbox-rq
```
-## Verify Housekeeping Scheduling
+## 6. Verify Housekeeping Scheduling
If upgrading from a release prior to NetBox v3.0, check that a cron task (or similar scheduled process) has been configured to run NetBox's nightly housekeeping command. A shell script which invokes this command is included at `contrib/netbox-housekeeping.sh`. It can be linked from your system's daily cron task directory, or included within the crontab directly. (If NetBox has been installed in a nonstandard path, be sure to update the system paths within this script first.)
diff --git a/docs/media/installation/upgrade_paths.png b/docs/media/installation/upgrade_paths.png
new file mode 100644
index 000000000..494744b58
Binary files /dev/null and b/docs/media/installation/upgrade_paths.png differ
diff --git a/docs/release-notes/version-3.3.md b/docs/release-notes/version-3.3.md
index 3482c9061..da9dbd856 100644
--- a/docs/release-notes/version-3.3.md
+++ b/docs/release-notes/version-3.3.md
@@ -1,5 +1,34 @@
# NetBox v3.3
+## v3.3.3 (2022-09-15)
+
+### Enhancements
+
+* [#8580](https://github.com/netbox-community/netbox/issues/8580) - Add `occupied` filter for cabled objects to filter by cable or `mark_connected`
+* [#9577](https://github.com/netbox-community/netbox/issues/9577) - Add `has_front_image` and `has_rear_image` filters for device types
+* [#10268](https://github.com/netbox-community/netbox/issues/10268) - Omit trailing ".0" in device positions within UI
+* [#10359](https://github.com/netbox-community/netbox/issues/10359) - Add region and site group columns to the devices table
+
+### Bug Fixes
+
+* [#9231](https://github.com/netbox-community/netbox/issues/9231) - Fix `empty` lookup expression for string filters
+* [#10247](https://github.com/netbox-community/netbox/issues/10247) - Allow changing the pre-populated device/VM when creating new components
+* [#10250](https://github.com/netbox-community/netbox/issues/10250) - Fix exception when CableTermination validation fails during bulk import of cables
+* [#10258](https://github.com/netbox-community/netbox/issues/10258) - Enable the use of reports & scripts packaged in submodules
+* [#10259](https://github.com/netbox-community/netbox/issues/10259) - Fix `NoReverseMatch` exception when listing available prefixes with "flat" column displayed
+* [#10270](https://github.com/netbox-community/netbox/issues/10270) - Fix custom field validation when creating new services
+* [#10278](https://github.com/netbox-community/netbox/issues/10278) - Fix "create & add another" for image attachments
+* [#10294](https://github.com/netbox-community/netbox/issues/10294) - Fix spurious changelog diff for interface WWN field
+* [#10304](https://github.com/netbox-community/netbox/issues/10304) - Enable cloning for custom fields & custom links
+* [#10305](https://github.com/netbox-community/netbox/issues/10305) - Fix Virtual Chassis master field cannot be null according to the API
+* [#10307](https://github.com/netbox-community/netbox/issues/10307) - Correct value for "Passive 48V (4-pair)" PoE type selection
+* [#10333](https://github.com/netbox-community/netbox/issues/10333) - Show available values for `ui_visibility` field of CustomField for CSV import
+* [#10337](https://github.com/netbox-community/netbox/issues/10337) - Display SSO links when local authentication fails
+* [#10353](https://github.com/netbox-community/netbox/issues/10353) - Table action buttons should reserve return URL parameters
+* [#10362](https://github.com/netbox-community/netbox/issues/10362) - Correct display of custom fields when editing an L2VPN termination
+
+---
+
## v3.3.2 (2022-09-02)
### Enhancements
diff --git a/netbox/circuits/tests/test_filtersets.py b/netbox/circuits/tests/test_filtersets.py
index abcfa8a00..2646de3c2 100644
--- a/netbox/circuits/tests/test_filtersets.py
+++ b/netbox/circuits/tests/test_filtersets.py
@@ -344,6 +344,7 @@ class CircuitTerminationTestCase(TestCase, ChangeLoggedFilterSetTests):
Circuit(provider=providers[0], type=circuit_types[0], cid='Circuit 4'),
Circuit(provider=providers[0], type=circuit_types[0], cid='Circuit 5'),
Circuit(provider=providers[0], type=circuit_types[0], cid='Circuit 6'),
+ Circuit(provider=providers[0], type=circuit_types[0], cid='Circuit 7'),
)
Circuit.objects.bulk_create(circuits)
@@ -357,6 +358,7 @@ class CircuitTerminationTestCase(TestCase, ChangeLoggedFilterSetTests):
CircuitTermination(circuit=circuits[3], provider_network=provider_networks[0], term_side='A'),
CircuitTermination(circuit=circuits[4], provider_network=provider_networks[1], term_side='A'),
CircuitTermination(circuit=circuits[5], provider_network=provider_networks[2], term_side='A'),
+ CircuitTermination(circuit=circuits[6], provider_network=provider_networks[0], term_side='A', mark_connected=True),
))
CircuitTermination.objects.bulk_create(circuit_terminations)
@@ -364,7 +366,7 @@ class CircuitTerminationTestCase(TestCase, ChangeLoggedFilterSetTests):
def test_term_side(self):
params = {'term_side': 'A'}
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 7)
def test_port_speed(self):
params = {'port_speed': ['1000', '2000']}
@@ -397,11 +399,19 @@ class CircuitTerminationTestCase(TestCase, ChangeLoggedFilterSetTests):
def test_provider_network(self):
provider_networks = ProviderNetwork.objects.all()[:2]
params = {'provider_network_id': [provider_networks[0].pk, provider_networks[1].pk]}
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
def test_cabled(self):
params = {'cabled': True}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+ params = {'cabled': False}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 8)
+
+ def test_occupied(self):
+ params = {'occupied': True}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
+ params = {'occupied': False}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 7)
class ProviderNetworkTestCase(TestCase, ChangeLoggedFilterSetTests):
diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py
index 79f5339ad..897ee4ca3 100644
--- a/netbox/dcim/api/serializers.py
+++ b/netbox/dcim/api/serializers.py
@@ -1076,7 +1076,7 @@ class CablePathSerializer(serializers.ModelSerializer):
class VirtualChassisSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualchassis-detail')
- master = NestedDeviceSerializer(required=False)
+ master = NestedDeviceSerializer(required=False, allow_null=True, default=None)
member_count = serializers.IntegerField(read_only=True)
class Meta:
diff --git a/netbox/dcim/choices.py b/netbox/dcim/choices.py
index 019ae09a4..7d35a40f9 100644
--- a/netbox/dcim/choices.py
+++ b/netbox/dcim/choices.py
@@ -1096,7 +1096,7 @@ class InterfacePoETypeChoices(ChoiceSet):
(PASSIVE_24V_2PAIR, 'Passive 24V (2-pair)'),
(PASSIVE_24V_4PAIR, 'Passive 24V (4-pair)'),
(PASSIVE_48V_2PAIR, 'Passive 48V (2-pair)'),
- (PASSIVE_48V_2PAIR, 'Passive 48V (4-pair)'),
+ (PASSIVE_48V_4PAIR, 'Passive 48V (4-pair)'),
)
),
)
diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py
index 5d92af878..0a4439173 100644
--- a/netbox/dcim/filtersets.py
+++ b/netbox/dcim/filtersets.py
@@ -434,6 +434,14 @@ class DeviceTypeFilterSet(NetBoxModelFilterSet):
to_field_name='slug',
label='Manufacturer (slug)',
)
+ has_front_image = django_filters.BooleanFilter(
+ label='Has a front image',
+ method='_has_front_image'
+ )
+ has_rear_image = django_filters.BooleanFilter(
+ label='Has a rear image',
+ method='_has_rear_image'
+ )
console_ports = django_filters.BooleanFilter(
method='_console_ports',
label='Has console ports',
@@ -487,6 +495,18 @@ class DeviceTypeFilterSet(NetBoxModelFilterSet):
Q(comments__icontains=value)
)
+ def _has_front_image(self, queryset, name, value):
+ if value:
+ return queryset.exclude(front_image='')
+ else:
+ return queryset.filter(front_image='')
+
+ def _has_rear_image(self, queryset, name, value):
+ if value:
+ return queryset.exclude(rear_image='')
+ else:
+ return queryset.filter(rear_image='')
+
def _console_ports(self, queryset, name, value):
return queryset.exclude(consoleporttemplates__isnull=value)
@@ -1144,6 +1164,15 @@ class CabledObjectFilterSet(django_filters.FilterSet):
lookup_expr='isnull',
exclude=True
)
+ occupied = django_filters.BooleanFilter(
+ method='filter_occupied'
+ )
+
+ def filter_occupied(self, queryset, name, value):
+ if value:
+ return queryset.filter(Q(cable__isnull=False) | Q(mark_connected=True))
+ else:
+ return queryset.filter(cable__isnull=True, mark_connected=False)
class PathEndpointFilterSet(django_filters.FilterSet):
diff --git a/netbox/dcim/forms/bulk_create.py b/netbox/dcim/forms/bulk_create.py
index 43b852928..f6bc27079 100644
--- a/netbox/dcim/forms/bulk_create.py
+++ b/netbox/dcim/forms/bulk_create.py
@@ -3,7 +3,7 @@ from django import forms
from dcim.models import *
from extras.forms import CustomFieldsMixin
from extras.models import Tag
-from utilities.forms import DynamicModelMultipleChoiceField, ExpandableNameField, form_from_model
+from utilities.forms import BootstrapMixin, DynamicModelMultipleChoiceField, ExpandableNameField, form_from_model
from .object_create import ComponentCreateForm
__all__ = (
@@ -24,7 +24,7 @@ __all__ = (
# Device components
#
-class DeviceBulkAddComponentForm(CustomFieldsMixin, ComponentCreateForm):
+class DeviceBulkAddComponentForm(BootstrapMixin, CustomFieldsMixin, ComponentCreateForm):
pk = forms.ModelMultipleChoiceField(
queryset=Device.objects.all(),
widget=forms.MultipleHiddenInput()
@@ -37,6 +37,7 @@ class DeviceBulkAddComponentForm(CustomFieldsMixin, ComponentCreateForm):
queryset=Tag.objects.all(),
required=False
)
+ replication_fields = ('name', 'label')
class ConsolePortBulkCreateForm(
@@ -44,7 +45,7 @@ class ConsolePortBulkCreateForm(
DeviceBulkAddComponentForm
):
model = ConsolePort
- field_order = ('name_pattern', 'label_pattern', 'type', 'mark_connected', 'description', 'tags')
+ field_order = ('name', 'label', 'type', 'mark_connected', 'description', 'tags')
class ConsoleServerPortBulkCreateForm(
@@ -52,7 +53,7 @@ class ConsoleServerPortBulkCreateForm(
DeviceBulkAddComponentForm
):
model = ConsoleServerPort
- field_order = ('name_pattern', 'label_pattern', 'type', 'speed', 'description', 'tags')
+ field_order = ('name', 'label', 'type', 'speed', 'description', 'tags')
class PowerPortBulkCreateForm(
@@ -60,7 +61,7 @@ class PowerPortBulkCreateForm(
DeviceBulkAddComponentForm
):
model = PowerPort
- field_order = ('name_pattern', 'label_pattern', 'type', 'maximum_draw', 'allocated_draw', 'description', 'tags')
+ field_order = ('name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', 'tags')
class PowerOutletBulkCreateForm(
@@ -68,7 +69,7 @@ class PowerOutletBulkCreateForm(
DeviceBulkAddComponentForm
):
model = PowerOutlet
- field_order = ('name_pattern', 'label_pattern', 'type', 'feed_leg', 'description', 'tags')
+ field_order = ('name', 'label', 'type', 'feed_leg', 'description', 'tags')
class InterfaceBulkCreateForm(
@@ -79,7 +80,7 @@ class InterfaceBulkCreateForm(
):
model = Interface
field_order = (
- 'name_pattern', 'label_pattern', 'type', 'enabled', 'speed', 'duplex', 'mtu', 'mgmt_only', 'poe_mode',
+ 'name', 'label', 'type', 'enabled', 'speed', 'duplex', 'mtu', 'mgmt_only', 'poe_mode',
'poe_type', 'mark_connected', 'description', 'tags',
)
@@ -96,13 +97,13 @@ class RearPortBulkCreateForm(
DeviceBulkAddComponentForm
):
model = RearPort
- field_order = ('name_pattern', 'label_pattern', 'type', 'positions', 'mark_connected', 'description', 'tags')
+ field_order = ('name', 'label', 'type', 'positions', 'mark_connected', 'description', 'tags')
class ModuleBayBulkCreateForm(DeviceBulkAddComponentForm):
model = ModuleBay
- field_order = ('name_pattern', 'label_pattern', 'position_pattern', 'description', 'tags')
-
+ field_order = ('name', 'label', 'position_pattern', 'description', 'tags')
+ replication_fields = ('name', 'label', 'position')
position_pattern = ExpandableNameField(
label='Position',
required=False,
@@ -112,7 +113,7 @@ class ModuleBayBulkCreateForm(DeviceBulkAddComponentForm):
class DeviceBayBulkCreateForm(DeviceBulkAddComponentForm):
model = DeviceBay
- field_order = ('name_pattern', 'label_pattern', 'description', 'tags')
+ field_order = ('name', 'label', 'description', 'tags')
class InventoryItemBulkCreateForm(
@@ -121,6 +122,6 @@ class InventoryItemBulkCreateForm(
):
model = InventoryItem
field_order = (
- 'name_pattern', 'label_pattern', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered',
+ 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered',
'description', 'tags',
)
diff --git a/netbox/dcim/forms/filtersets.py b/netbox/dcim/forms/filtersets.py
index 173ea5d1e..96b0d1319 100644
--- a/netbox/dcim/forms/filtersets.py
+++ b/netbox/dcim/forms/filtersets.py
@@ -365,6 +365,7 @@ class DeviceTypeFilterForm(NetBoxModelFilterSetForm):
fieldsets = (
(None, ('q', 'tag')),
('Hardware', ('manufacturer_id', 'part_number', 'subdevice_role', 'airflow')),
+ ('Images', ('has_front_image', 'has_rear_image')),
('Components', (
'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces',
'pass_through_ports', 'device_bays', 'module_bays', 'inventory_items',
@@ -386,6 +387,20 @@ class DeviceTypeFilterForm(NetBoxModelFilterSetForm):
choices=add_blank_choice(DeviceAirflowChoices),
required=False
)
+ has_front_image = forms.NullBooleanField(
+ required=False,
+ label='Has a front image',
+ widget=StaticSelect(
+ choices=BOOLEAN_WITH_BLANK_CHOICES
+ )
+ )
+ has_rear_image = forms.NullBooleanField(
+ required=False,
+ label='Has a rear image',
+ widget=StaticSelect(
+ choices=BOOLEAN_WITH_BLANK_CHOICES
+ )
+ )
console_ports = forms.NullBooleanField(
required=False,
label='Has console ports',
@@ -936,12 +951,37 @@ class PowerFeedFilterForm(NetBoxModelFilterSetForm):
# Device components
#
-class ConsolePortFilterForm(DeviceComponentFilterForm):
+class CabledFilterForm(forms.Form):
+ cabled = forms.NullBooleanField(
+ required=False,
+ widget=StaticSelect(
+ choices=BOOLEAN_WITH_BLANK_CHOICES
+ )
+ )
+ occupied = forms.NullBooleanField(
+ required=False,
+ widget=StaticSelect(
+ choices=BOOLEAN_WITH_BLANK_CHOICES
+ )
+ )
+
+
+class PathEndpointFilterForm(CabledFilterForm):
+ connected = forms.NullBooleanField(
+ required=False,
+ widget=StaticSelect(
+ choices=BOOLEAN_WITH_BLANK_CHOICES
+ )
+ )
+
+
+class ConsolePortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
model = ConsolePort
fieldsets = (
(None, ('q', 'tag')),
('Attributes', ('name', 'label', 'type', 'speed')),
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
+ ('Connection', ('cabled', 'connected', 'occupied')),
)
type = MultipleChoiceField(
choices=ConsolePortTypeChoices,
@@ -954,12 +994,13 @@ class ConsolePortFilterForm(DeviceComponentFilterForm):
tag = TagFilterField(model)
-class ConsoleServerPortFilterForm(DeviceComponentFilterForm):
+class ConsoleServerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
model = ConsoleServerPort
fieldsets = (
(None, ('q', 'tag')),
('Attributes', ('name', 'label', 'type', 'speed')),
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
+ ('Connection', ('cabled', 'connected', 'occupied')),
)
type = MultipleChoiceField(
choices=ConsolePortTypeChoices,
@@ -972,12 +1013,13 @@ class ConsoleServerPortFilterForm(DeviceComponentFilterForm):
tag = TagFilterField(model)
-class PowerPortFilterForm(DeviceComponentFilterForm):
+class PowerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
model = PowerPort
fieldsets = (
(None, ('q', 'tag')),
('Attributes', ('name', 'label', 'type')),
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
+ ('Connection', ('cabled', 'connected', 'occupied')),
)
type = MultipleChoiceField(
choices=PowerPortTypeChoices,
@@ -986,12 +1028,13 @@ class PowerPortFilterForm(DeviceComponentFilterForm):
tag = TagFilterField(model)
-class PowerOutletFilterForm(DeviceComponentFilterForm):
+class PowerOutletFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
model = PowerOutlet
fieldsets = (
(None, ('q', 'tag')),
('Attributes', ('name', 'label', 'type')),
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
+ ('Connection', ('cabled', 'connected', 'occupied')),
)
type = MultipleChoiceField(
choices=PowerOutletTypeChoices,
@@ -1000,7 +1043,7 @@ class PowerOutletFilterForm(DeviceComponentFilterForm):
tag = TagFilterField(model)
-class InterfaceFilterForm(DeviceComponentFilterForm):
+class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
model = Interface
fieldsets = (
(None, ('q', 'tag')),
@@ -1009,6 +1052,7 @@ class InterfaceFilterForm(DeviceComponentFilterForm):
('PoE', ('poe_mode', 'poe_type')),
('Wireless', ('rf_role', 'rf_channel', 'rf_channel_width', 'tx_power')),
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
+ ('Connection', ('cabled', 'connected', 'occupied')),
)
kind = MultipleChoiceField(
choices=InterfaceKindChoices,
@@ -1089,11 +1133,12 @@ class InterfaceFilterForm(DeviceComponentFilterForm):
tag = TagFilterField(model)
-class FrontPortFilterForm(DeviceComponentFilterForm):
+class FrontPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
fieldsets = (
(None, ('q', 'tag')),
('Attributes', ('name', 'label', 'type', 'color')),
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
+ ('Cable', ('cabled', 'occupied')),
)
model = FrontPort
type = MultipleChoiceField(
@@ -1106,12 +1151,13 @@ class FrontPortFilterForm(DeviceComponentFilterForm):
tag = TagFilterField(model)
-class RearPortFilterForm(DeviceComponentFilterForm):
+class RearPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
model = RearPort
fieldsets = (
(None, ('q', 'tag')),
('Attributes', ('name', 'label', 'type', 'color')),
('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
+ ('Cable', ('cabled', 'occupied')),
)
type = MultipleChoiceField(
choices=PortTypeChoices,
diff --git a/netbox/dcim/forms/models.py b/netbox/dcim/forms/models.py
index d1d5b1683..4fa27ae69 100644
--- a/netbox/dcim/forms/models.py
+++ b/netbox/dcim/forms/models.py
@@ -986,47 +986,74 @@ class VCMemberSelectForm(BootstrapMixin, forms.Form):
# Device component templates
#
+class ComponentTemplateForm(BootstrapMixin, forms.ModelForm):
+ device_type = DynamicModelChoiceField(
+ queryset=DeviceType.objects.all()
+ )
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ # Disable reassignment of DeviceType when editing an existing instance
+ if self.instance.pk:
+ self.fields['device_type'].disabled = True
+
+
+class ModularComponentTemplateForm(ComponentTemplateForm):
+ module_type = DynamicModelChoiceField(
+ queryset=ModuleType.objects.all(),
+ required=False
+ )
+
+
+class ConsolePortTemplateForm(ModularComponentTemplateForm):
+ fieldsets = (
+ (None, ('device_type', 'module_type', 'name', 'label', 'type', 'description')),
+ )
-class ConsolePortTemplateForm(BootstrapMixin, forms.ModelForm):
class Meta:
model = ConsolePortTemplate
fields = [
'device_type', 'module_type', 'name', 'label', 'type', 'description',
]
widgets = {
- 'device_type': forms.HiddenInput(),
- 'module_type': forms.HiddenInput(),
'type': StaticSelect,
}
-class ConsoleServerPortTemplateForm(BootstrapMixin, forms.ModelForm):
+class ConsoleServerPortTemplateForm(ModularComponentTemplateForm):
+ fieldsets = (
+ (None, ('device_type', 'module_type', 'name', 'label', 'type', 'description')),
+ )
+
class Meta:
model = ConsoleServerPortTemplate
fields = [
'device_type', 'module_type', 'name', 'label', 'type', 'description',
]
widgets = {
- 'device_type': forms.HiddenInput(),
- 'module_type': forms.HiddenInput(),
'type': StaticSelect,
}
-class PowerPortTemplateForm(BootstrapMixin, forms.ModelForm):
+class PowerPortTemplateForm(ModularComponentTemplateForm):
+ fieldsets = (
+ (None, (
+ 'device_type', 'module_type', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description',
+ )),
+ )
+
class Meta:
model = PowerPortTemplate
fields = [
'device_type', 'module_type', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description',
]
widgets = {
- 'device_type': forms.HiddenInput(),
- 'module_type': forms.HiddenInput(),
'type': StaticSelect(),
}
-class PowerOutletTemplateForm(BootstrapMixin, forms.ModelForm):
+class PowerOutletTemplateForm(ModularComponentTemplateForm):
power_port = DynamicModelChoiceField(
queryset=PowerPortTemplate.objects.all(),
required=False,
@@ -1035,35 +1062,40 @@ class PowerOutletTemplateForm(BootstrapMixin, forms.ModelForm):
}
)
+ fieldsets = (
+ (None, ('device_type', 'module_type', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description')),
+ )
+
class Meta:
model = PowerOutletTemplate
fields = [
'device_type', 'module_type', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description',
]
widgets = {
- 'device_type': forms.HiddenInput(),
- 'module_type': forms.HiddenInput(),
'type': StaticSelect(),
'feed_leg': StaticSelect(),
}
-class InterfaceTemplateForm(BootstrapMixin, forms.ModelForm):
+class InterfaceTemplateForm(ModularComponentTemplateForm):
+ fieldsets = (
+ (None, ('device_type', 'module_type', 'name', 'label', 'type', 'mgmt_only', 'description')),
+ ('PoE', ('poe_mode', 'poe_type'))
+ )
+
class Meta:
model = InterfaceTemplate
fields = [
'device_type', 'module_type', 'name', 'label', 'type', 'mgmt_only', 'description', 'poe_mode', 'poe_type',
]
widgets = {
- 'device_type': forms.HiddenInput(),
- 'module_type': forms.HiddenInput(),
'type': StaticSelect(),
'poe_mode': StaticSelect(),
'poe_type': StaticSelect(),
}
-class FrontPortTemplateForm(BootstrapMixin, forms.ModelForm):
+class FrontPortTemplateForm(ModularComponentTemplateForm):
rear_port = DynamicModelChoiceField(
queryset=RearPortTemplate.objects.all(),
required=False,
@@ -1073,6 +1105,13 @@ class FrontPortTemplateForm(BootstrapMixin, forms.ModelForm):
}
)
+ fieldsets = (
+ (None, (
+ 'device_type', 'module_type', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position',
+ 'description',
+ )),
+ )
+
class Meta:
model = FrontPortTemplate
fields = [
@@ -1080,48 +1119,50 @@ class FrontPortTemplateForm(BootstrapMixin, forms.ModelForm):
'description',
]
widgets = {
- 'device_type': forms.HiddenInput(),
- 'module_type': forms.HiddenInput(),
'type': StaticSelect(),
}
-class RearPortTemplateForm(BootstrapMixin, forms.ModelForm):
+class RearPortTemplateForm(ModularComponentTemplateForm):
+ fieldsets = (
+ (None, ('device_type', 'module_type', 'name', 'label', 'type', 'color', 'positions', 'description')),
+ )
+
class Meta:
model = RearPortTemplate
fields = [
'device_type', 'module_type', 'name', 'label', 'type', 'color', 'positions', 'description',
]
widgets = {
- 'device_type': forms.HiddenInput(),
- 'module_type': forms.HiddenInput(),
'type': StaticSelect(),
}
-class ModuleBayTemplateForm(BootstrapMixin, forms.ModelForm):
+class ModuleBayTemplateForm(ComponentTemplateForm):
+ fieldsets = (
+ (None, ('device_type', 'name', 'label', 'position', 'description')),
+ )
+
class Meta:
model = ModuleBayTemplate
fields = [
'device_type', 'name', 'label', 'position', 'description',
]
- widgets = {
- 'device_type': forms.HiddenInput(),
- }
-class DeviceBayTemplateForm(BootstrapMixin, forms.ModelForm):
+class DeviceBayTemplateForm(ComponentTemplateForm):
+ fieldsets = (
+ (None, ('device_type', 'name', 'label', 'description')),
+ )
+
class Meta:
model = DeviceBayTemplate
fields = [
'device_type', 'name', 'label', 'description',
]
- widgets = {
- 'device_type': forms.HiddenInput(),
- }
-class InventoryItemTemplateForm(BootstrapMixin, forms.ModelForm):
+class InventoryItemTemplateForm(ComponentTemplateForm):
parent = DynamicModelChoiceField(
queryset=InventoryItemTemplate.objects.all(),
required=False,
@@ -1148,22 +1189,39 @@ class InventoryItemTemplateForm(BootstrapMixin, forms.ModelForm):
widget=forms.HiddenInput
)
+ fieldsets = (
+ (None, (
+ 'device_type', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'description',
+ 'component_type', 'component_id',
+ )),
+ )
+
class Meta:
model = InventoryItemTemplate
fields = [
'device_type', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'description',
'component_type', 'component_id',
]
- widgets = {
- 'device_type': forms.HiddenInput(),
- }
#
# Device components
#
-class ConsolePortForm(NetBoxModelForm):
+class DeviceComponentForm(NetBoxModelForm):
+ device = DynamicModelChoiceField(
+ queryset=Device.objects.all()
+ )
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ # Disable reassignment of Device when editing an existing instance
+ if self.instance.pk:
+ self.fields['device'].disabled = True
+
+
+class ModularDeviceComponentForm(DeviceComponentForm):
module = DynamicModelChoiceField(
queryset=Module.objects.all(),
required=False,
@@ -1172,25 +1230,31 @@ class ConsolePortForm(NetBoxModelForm):
}
)
+
+class ConsolePortForm(ModularDeviceComponentForm):
+ fieldsets = (
+ (None, (
+ 'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags',
+ )),
+ )
+
class Meta:
model = ConsolePort
fields = [
'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags',
]
widgets = {
- 'device': forms.HiddenInput(),
'type': StaticSelect(),
'speed': StaticSelect(),
}
-class ConsoleServerPortForm(NetBoxModelForm):
- module = DynamicModelChoiceField(
- queryset=Module.objects.all(),
- required=False,
- query_params={
- 'device_id': '$device',
- }
+class ConsoleServerPortForm(ModularDeviceComponentForm):
+
+ fieldsets = (
+ (None, (
+ 'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags',
+ )),
)
class Meta:
@@ -1199,42 +1263,32 @@ class ConsoleServerPortForm(NetBoxModelForm):
'device', 'module', 'name', 'label', 'type', 'speed', 'mark_connected', 'description', 'tags',
]
widgets = {
- 'device': forms.HiddenInput(),
'type': StaticSelect(),
'speed': StaticSelect(),
}
-class PowerPortForm(NetBoxModelForm):
- module = DynamicModelChoiceField(
- queryset=Module.objects.all(),
- required=False,
- query_params={
- 'device_id': '$device',
- }
+class PowerPortForm(ModularDeviceComponentForm):
+
+ fieldsets = (
+ (None, (
+ 'device', 'module', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'mark_connected',
+ 'description', 'tags',
+ )),
)
class Meta:
model = PowerPort
fields = [
'device', 'module', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'mark_connected',
- 'description',
- 'tags',
+ 'description', 'tags',
]
widgets = {
- 'device': forms.HiddenInput(),
'type': StaticSelect(),
}
-class PowerOutletForm(NetBoxModelForm):
- module = DynamicModelChoiceField(
- queryset=Module.objects.all(),
- required=False,
- query_params={
- 'device_id': '$device',
- }
- )
+class PowerOutletForm(ModularDeviceComponentForm):
power_port = DynamicModelChoiceField(
queryset=PowerPort.objects.all(),
required=False,
@@ -1243,6 +1297,13 @@ class PowerOutletForm(NetBoxModelForm):
}
)
+ fieldsets = (
+ (None, (
+ 'device', 'module', 'name', 'label', 'type', 'power_port', 'feed_leg', 'mark_connected', 'description',
+ 'tags',
+ )),
+ )
+
class Meta:
model = PowerOutlet
fields = [
@@ -1250,20 +1311,12 @@ class PowerOutletForm(NetBoxModelForm):
'tags',
]
widgets = {
- 'device': forms.HiddenInput(),
'type': StaticSelect(),
'feed_leg': StaticSelect(),
}
-class InterfaceForm(InterfaceCommonForm, NetBoxModelForm):
- module = DynamicModelChoiceField(
- queryset=Module.objects.all(),
- required=False,
- query_params={
- 'device_id': '$device',
- }
- )
+class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
parent = DynamicModelChoiceField(
queryset=Interface.objects.all(),
required=False,
@@ -1331,8 +1384,14 @@ class InterfaceForm(InterfaceCommonForm, NetBoxModelForm):
label='VRF'
)
+ wwn = forms.CharField(
+ empty_value=None,
+ required=False,
+ label='WWN'
+ )
+
fieldsets = (
- ('Interface', ('device', 'module', 'name', 'type', 'speed', 'duplex', 'label', 'description', 'tags')),
+ ('Interface', ('device', 'module', 'name', 'label', 'type', 'speed', 'duplex', 'description', 'tags')),
('Addressing', ('vrf', 'mac_address', 'wwn')),
('Operation', ('mtu', 'tx_power', 'enabled', 'mgmt_only', 'mark_connected')),
('Related Interfaces', ('parent', 'bridge', 'lag')),
@@ -1352,7 +1411,6 @@ class InterfaceForm(InterfaceCommonForm, NetBoxModelForm):
'untagged_vlan', 'tagged_vlans', 'vrf', 'tags',
]
widgets = {
- 'device': forms.HiddenInput(),
'type': StaticSelect(),
'speed': SelectSpeedWidget(),
'poe_mode': StaticSelect(),
@@ -1382,14 +1440,7 @@ class InterfaceForm(InterfaceCommonForm, NetBoxModelForm):
self.fields['bridge'].widget.add_query_param('device_id', device.virtual_chassis.master.pk)
-class FrontPortForm(NetBoxModelForm):
- module = DynamicModelChoiceField(
- queryset=Module.objects.all(),
- required=False,
- query_params={
- 'device_id': '$device',
- }
- )
+class FrontPortForm(ModularDeviceComponentForm):
rear_port = DynamicModelChoiceField(
queryset=RearPort.objects.all(),
query_params={
@@ -1397,6 +1448,13 @@ class FrontPortForm(NetBoxModelForm):
}
)
+ fieldsets = (
+ (None, (
+ 'device', 'module', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'mark_connected',
+ 'description', 'tags',
+ )),
+ )
+
class Meta:
model = FrontPort
fields = [
@@ -1404,18 +1462,15 @@ class FrontPortForm(NetBoxModelForm):
'description', 'tags',
]
widgets = {
- 'device': forms.HiddenInput(),
'type': StaticSelect(),
}
-class RearPortForm(NetBoxModelForm):
- module = DynamicModelChoiceField(
- queryset=Module.objects.all(),
- required=False,
- query_params={
- 'device_id': '$device',
- }
+class RearPortForm(ModularDeviceComponentForm):
+ fieldsets = (
+ (None, (
+ 'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'mark_connected', 'description', 'tags',
+ )),
)
class Meta:
@@ -1424,33 +1479,32 @@ class RearPortForm(NetBoxModelForm):
'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'mark_connected', 'description', 'tags',
]
widgets = {
- 'device': forms.HiddenInput(),
'type': StaticSelect(),
}
-class ModuleBayForm(NetBoxModelForm):
+class ModuleBayForm(DeviceComponentForm):
+ fieldsets = (
+ (None, ('device', 'name', 'label', 'position', 'description', 'tags',)),
+ )
class Meta:
model = ModuleBay
fields = [
'device', 'name', 'label', 'position', 'description', 'tags',
]
- widgets = {
- 'device': forms.HiddenInput(),
- }
-class DeviceBayForm(NetBoxModelForm):
+class DeviceBayForm(DeviceComponentForm):
+ fieldsets = (
+ (None, ('device', 'name', 'label', 'description', 'tags',)),
+ )
class Meta:
model = DeviceBay
fields = [
'device', 'name', 'label', 'description', 'tags',
]
- widgets = {
- 'device': forms.HiddenInput(),
- }
class PopulateDeviceBayForm(BootstrapMixin, forms.Form):
@@ -1473,10 +1527,7 @@ class PopulateDeviceBayForm(BootstrapMixin, forms.Form):
).exclude(pk=device_bay.device.pk)
-class InventoryItemForm(NetBoxModelForm):
- device = DynamicModelChoiceField(
- queryset=Device.objects.all()
- )
+class InventoryItemForm(DeviceComponentForm):
parent = DynamicModelChoiceField(
queryset=InventoryItem.objects.all(),
required=False,
diff --git a/netbox/dcim/forms/object_create.py b/netbox/dcim/forms/object_create.py
index d2c941b34..a03597db1 100644
--- a/netbox/dcim/forms/object_create.py
+++ b/netbox/dcim/forms/object_create.py
@@ -2,46 +2,56 @@ from django import forms
from dcim.models import *
from netbox.forms import NetBoxModelForm
-from utilities.forms import (
- BootstrapMixin, DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableNameField,
-)
+from utilities.forms import DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableNameField
+from . import models as model_forms
__all__ = (
- 'ComponentTemplateCreateForm',
- 'DeviceComponentCreateForm',
+ 'ComponentCreateForm',
+ 'ConsolePortCreateForm',
+ 'ConsolePortTemplateCreateForm',
+ 'ConsoleServerPortCreateForm',
+ 'ConsoleServerPortTemplateCreateForm',
+ 'DeviceBayCreateForm',
+ 'DeviceBayTemplateCreateForm',
'FrontPortCreateForm',
'FrontPortTemplateCreateForm',
+ 'InterfaceCreateForm',
+ 'InterfaceTemplateCreateForm',
'InventoryItemCreateForm',
- 'ModularComponentTemplateCreateForm',
+ 'InventoryItemTemplateCreateForm',
'ModuleBayCreateForm',
'ModuleBayTemplateCreateForm',
+ 'PowerOutletCreateForm',
+ 'PowerOutletTemplateCreateForm',
+ 'PowerPortCreateForm',
+ 'PowerPortTemplateCreateForm',
+ 'RearPortCreateForm',
+ 'RearPortTemplateCreateForm',
'VirtualChassisCreateForm',
)
-class ComponentCreateForm(BootstrapMixin, forms.Form):
+class ComponentCreateForm(forms.Form):
"""
- Subclass this form when facilitating the creation of one or more device component or component templates based on
+ Subclass this form when facilitating the creation of one or more component or component template objects based on
a name pattern.
"""
- name_pattern = ExpandableNameField(
- label='Name'
- )
- label_pattern = ExpandableNameField(
- label='Label',
+ name = ExpandableNameField()
+ label = ExpandableNameField(
required=False,
- help_text='Alphanumeric ranges are supported. (Must match the number of names being created.)'
+ help_text='Alphanumeric ranges are supported. (Must match the number of objects being created.)'
)
+ # Identify the fields which support replication (i.e. ExpandableNameFields). This is referenced by
+ # ComponentCreateView when creating objects.
+ replication_fields = ('name', 'label')
+
def clean(self):
super().clean()
- # Validate that all patterned fields generate an equal number of values
- patterned_fields = [
- field_name for field_name in self.fields if field_name.endswith('_pattern')
- ]
- pattern_count = len(self.cleaned_data['name_pattern'])
- for field_name in patterned_fields:
+ # Validate that all replication fields generate an equal number of values
+ pattern_count = len(self.cleaned_data[self.replication_fields[0]])
+ for field_name in self.replication_fields:
value_count = len(self.cleaned_data[field_name])
if self.cleaned_data[field_name] and value_count != pattern_count:
raise forms.ValidationError({
@@ -50,56 +60,55 @@ class ComponentCreateForm(BootstrapMixin, forms.Form):
}, code='label_pattern_mismatch')
-class ComponentTemplateCreateForm(ComponentCreateForm):
- """
- Creation form for component templates that can be assigned only to a DeviceType.
- """
- device_type = DynamicModelChoiceField(
- queryset=DeviceType.objects.all(),
- )
- field_order = ('device_type', 'name_pattern', 'label_pattern')
+#
+# Device component templates
+#
+
+class ConsolePortTemplateCreateForm(ComponentCreateForm, model_forms.ConsolePortTemplateForm):
+
+ class Meta(model_forms.ConsolePortTemplateForm.Meta):
+ exclude = ('name', 'label')
-class ModularComponentTemplateCreateForm(ComponentCreateForm):
- """
- Creation form for component templates that can be assigned to either a DeviceType *or* a ModuleType.
- """
- name_pattern = ExpandableNameField(
- label='Name',
- help_text="""
- Alphanumeric ranges are supported for bulk creation. Mixed cases and types within a single range
- are not supported. Example: [ge,xe]-0/0/[0-9]. {module} is accepted as a substitution for
- the module bay position.
- """
- )
- device_type = DynamicModelChoiceField(
- queryset=DeviceType.objects.all(),
- required=False
- )
- module_type = DynamicModelChoiceField(
- queryset=ModuleType.objects.all(),
- required=False
- )
- field_order = ('device_type', 'module_type', 'name_pattern', 'label_pattern')
+class ConsoleServerPortTemplateCreateForm(ComponentCreateForm, model_forms.ConsoleServerPortTemplateForm):
+
+ class Meta(model_forms.ConsoleServerPortTemplateForm.Meta):
+ exclude = ('name', 'label')
-class DeviceComponentCreateForm(ComponentCreateForm):
- device = DynamicModelChoiceField(
- queryset=Device.objects.all()
- )
- field_order = ('device', 'name_pattern', 'label_pattern')
+class PowerPortTemplateCreateForm(ComponentCreateForm, model_forms.PowerPortTemplateForm):
+
+ class Meta(model_forms.PowerPortTemplateForm.Meta):
+ exclude = ('name', 'label')
-class FrontPortTemplateCreateForm(ModularComponentTemplateCreateForm):
- rear_port_set = forms.MultipleChoiceField(
+class PowerOutletTemplateCreateForm(ComponentCreateForm, model_forms.PowerOutletTemplateForm):
+
+ class Meta(model_forms.PowerOutletTemplateForm.Meta):
+ exclude = ('name', 'label')
+
+
+class InterfaceTemplateCreateForm(ComponentCreateForm, model_forms.InterfaceTemplateForm):
+
+ class Meta(model_forms.InterfaceTemplateForm.Meta):
+ exclude = ('name', 'label')
+
+
+class FrontPortTemplateCreateForm(ComponentCreateForm, model_forms.FrontPortTemplateForm):
+ rear_port = forms.MultipleChoiceField(
choices=[],
label='Rear ports',
help_text='Select one rear port assignment for each front port being created.',
)
- field_order = (
- 'device_type', 'name_pattern', 'label_pattern', 'rear_port_set',
+
+ # Override fieldsets from FrontPortTemplateForm to omit rear_port_position
+ fieldsets = (
+ (None, ('device_type', 'module_type', 'name', 'label', 'type', 'color', 'rear_port', 'description')),
)
+ class Meta(model_forms.FrontPortTemplateForm.Meta):
+ exclude = ('name', 'label', 'rear_port', 'rear_port_position')
+
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -130,12 +139,12 @@ class FrontPortTemplateCreateForm(ModularComponentTemplateCreateForm):
choices.append(
('{}:{}'.format(rear_port.pk, i), '{}:{}'.format(rear_port.name, i))
)
- self.fields['rear_port_set'].choices = choices
+ self.fields['rear_port'].choices = choices
def get_iterative_data(self, iteration):
# Assign rear port and position from selected set
- rear_port, position = self.cleaned_data['rear_port_set'][iteration].split(':')
+ rear_port, position = self.cleaned_data['rear_port'][iteration].split(':')
return {
'rear_port': int(rear_port),
@@ -143,16 +152,94 @@ class FrontPortTemplateCreateForm(ModularComponentTemplateCreateForm):
}
-class FrontPortCreateForm(DeviceComponentCreateForm):
- rear_port_set = forms.MultipleChoiceField(
+class RearPortTemplateCreateForm(ComponentCreateForm, model_forms.RearPortTemplateForm):
+
+ class Meta(model_forms.RearPortTemplateForm.Meta):
+ exclude = ('name', 'label')
+
+
+class DeviceBayTemplateCreateForm(ComponentCreateForm, model_forms.DeviceBayTemplateForm):
+
+ class Meta(model_forms.DeviceBayTemplateForm.Meta):
+ exclude = ('name', 'label')
+
+
+class ModuleBayTemplateCreateForm(ComponentCreateForm, model_forms.ModuleBayTemplateForm):
+ position = ExpandableNameField(
+ label='Position',
+ required=False,
+ help_text='Alphanumeric ranges are supported. (Must match the number of objects being created.)'
+ )
+ replication_fields = ('name', 'label', 'position')
+
+ class Meta(model_forms.ModuleBayTemplateForm.Meta):
+ exclude = ('name', 'label', 'position')
+
+
+class InventoryItemTemplateCreateForm(ComponentCreateForm, model_forms.InventoryItemTemplateForm):
+
+ class Meta(model_forms.InventoryItemTemplateForm.Meta):
+ exclude = ('name', 'label')
+
+
+#
+# Device components
+#
+
+class ConsolePortCreateForm(ComponentCreateForm, model_forms.ConsolePortForm):
+
+ class Meta(model_forms.ConsolePortForm.Meta):
+ exclude = ('name', 'label')
+
+
+class ConsoleServerPortCreateForm(ComponentCreateForm, model_forms.ConsoleServerPortForm):
+
+ class Meta(model_forms.ConsoleServerPortForm.Meta):
+ exclude = ('name', 'label')
+
+
+class PowerPortCreateForm(ComponentCreateForm, model_forms.PowerPortForm):
+
+ class Meta(model_forms.PowerPortForm.Meta):
+ exclude = ('name', 'label')
+
+
+class PowerOutletCreateForm(ComponentCreateForm, model_forms.PowerOutletForm):
+
+ class Meta(model_forms.PowerOutletForm.Meta):
+ exclude = ('name', 'label')
+
+
+class InterfaceCreateForm(ComponentCreateForm, model_forms.InterfaceForm):
+
+ class Meta(model_forms.InterfaceForm.Meta):
+ exclude = ('name', 'label')
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ if 'module' in self.fields:
+ self.fields['name'].help_text += ' The string {module} will be replaced with the position ' \
+ 'of the assigned module, if any'
+
+
+class FrontPortCreateForm(ComponentCreateForm, model_forms.FrontPortForm):
+ rear_port = forms.MultipleChoiceField(
choices=[],
label='Rear ports',
help_text='Select one rear port assignment for each front port being created.',
)
- field_order = (
- 'device', 'name_pattern', 'label_pattern', 'rear_port_set',
+
+ # Override fieldsets from FrontPortForm to omit rear_port_position
+ fieldsets = (
+ (None, (
+ 'device', 'module', 'name', 'label', 'type', 'color', 'rear_port', 'mark_connected', 'description', 'tags',
+ )),
)
+ class Meta(model_forms.FrontPortForm.Meta):
+ exclude = ('name', 'label', 'rear_port', 'rear_port_position')
+
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -176,12 +263,12 @@ class FrontPortCreateForm(DeviceComponentCreateForm):
choices.append(
('{}:{}'.format(rear_port.pk, i), '{}:{}'.format(rear_port.name, i))
)
- self.fields['rear_port_set'].choices = choices
+ self.fields['rear_port'].choices = choices
def get_iterative_data(self, iteration):
# Assign rear port and position from selected set
- rear_port, position = self.cleaned_data['rear_port_set'][iteration].split(':')
+ rear_port, position = self.cleaned_data['rear_port'][iteration].split(':')
return {
'rear_port': int(rear_port),
@@ -189,28 +276,39 @@ class FrontPortCreateForm(DeviceComponentCreateForm):
}
-class ModuleBayTemplateCreateForm(ComponentTemplateCreateForm):
- position_pattern = ExpandableNameField(
+class RearPortCreateForm(ComponentCreateForm, model_forms.RearPortForm):
+
+ class Meta(model_forms.RearPortForm.Meta):
+ exclude = ('name', 'label')
+
+
+class DeviceBayCreateForm(ComponentCreateForm, model_forms.DeviceBayForm):
+
+ class Meta(model_forms.DeviceBayForm.Meta):
+ exclude = ('name', 'label')
+
+
+class ModuleBayCreateForm(ComponentCreateForm, model_forms.ModuleBayForm):
+ position = ExpandableNameField(
label='Position',
required=False,
- help_text='Alphanumeric ranges are supported. (Must match the number of names being created.)'
+ help_text='Alphanumeric ranges are supported. (Must match the number of objects being created.)'
)
- field_order = ('device_type', 'name_pattern', 'label_pattern', 'position_pattern')
+ replication_fields = ('name', 'label', 'position')
+
+ class Meta(model_forms.ModuleBayForm.Meta):
+ exclude = ('name', 'label', 'position')
-class ModuleBayCreateForm(DeviceComponentCreateForm):
- position_pattern = ExpandableNameField(
- label='Position',
- required=False,
- help_text='Alphanumeric ranges are supported. (Must match the number of names being created.)'
- )
- field_order = ('device', 'name_pattern', 'label_pattern', 'position_pattern')
+class InventoryItemCreateForm(ComponentCreateForm, model_forms.InventoryItemForm):
+
+ class Meta(model_forms.InventoryItemForm.Meta):
+ exclude = ('name', 'label')
-class InventoryItemCreateForm(ComponentCreateForm):
- # Device is assigned by the model form
- field_order = ('name_pattern', 'label_pattern')
-
+#
+# Virtual chassis
+#
class VirtualChassisCreateForm(NetBoxModelForm):
region = DynamicModelChoiceField(
diff --git a/netbox/dcim/models/cables.py b/netbox/dcim/models/cables.py
index ab1fe88e4..e05eb6d51 100644
--- a/netbox/dcim/models/cables.py
+++ b/netbox/dcim/models/cables.py
@@ -281,15 +281,11 @@ class CableTermination(models.Model):
# Validate interface type (if applicable)
if self.termination_type.model == 'interface' and self.termination.type in NONCONNECTABLE_IFACE_TYPES:
- raise ValidationError({
- 'termination': f'Cables cannot be terminated to {self.termination.get_type_display()} interfaces'
- })
+ raise ValidationError(f"Cables cannot be terminated to {self.termination.get_type_display()} interfaces")
# A CircuitTermination attached to a ProviderNetwork cannot have a Cable
if self.termination_type.model == 'circuittermination' and self.termination.provider_network is not None:
- raise ValidationError({
- 'termination': "Circuit terminations attached to a provider network may not be cabled."
- })
+ raise ValidationError("Circuit terminations attached to a provider network may not be cabled.")
def save(self, *args, **kwargs):
diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py
index 838336e21..8f1285901 100644
--- a/netbox/dcim/models/device_components.py
+++ b/netbox/dcim/models/device_components.py
@@ -908,18 +908,20 @@ class FrontPort(ModularComponentModel, CabledObjectModel):
def clean(self):
super().clean()
- # Validate rear port assignment
- if self.rear_port.device != self.device:
- raise ValidationError({
- "rear_port": f"Rear port ({self.rear_port}) must belong to the same device"
- })
+ if hasattr(self, 'rear_port'):
- # Validate rear port position assignment
- if self.rear_port_position > self.rear_port.positions:
- raise ValidationError({
- "rear_port_position": f"Invalid rear port position ({self.rear_port_position}): Rear port "
- f"{self.rear_port.name} has only {self.rear_port.positions} positions"
- })
+ # Validate rear port assignment
+ if self.rear_port.device != self.device:
+ raise ValidationError({
+ "rear_port": f"Rear port ({self.rear_port}) must belong to the same device"
+ })
+
+ # Validate rear port position assignment
+ if self.rear_port_position > self.rear_port.positions:
+ raise ValidationError({
+ "rear_port_position": f"Invalid rear port position ({self.rear_port_position}): Rear port "
+ f"{self.rear_port.name} has only {self.rear_port.positions} positions"
+ })
class RearPort(ModularComponentModel, CabledObjectModel):
diff --git a/netbox/dcim/tables/devices.py b/netbox/dcim/tables/devices.py
index 036f83306..142c7ef67 100644
--- a/netbox/dcim/tables/devices.py
+++ b/netbox/dcim/tables/devices.py
@@ -143,6 +143,15 @@ class DeviceTable(TenancyColumnsMixin, NetBoxTable):
template_code=DEVICE_LINK
)
status = columns.ChoiceFieldColumn()
+ region = tables.Column(
+ accessor=Accessor('site__region'),
+ linkify=True
+ )
+ site_group = tables.Column(
+ accessor=Accessor('site__group'),
+ linkify=True,
+ verbose_name='Site Group'
+ )
site = tables.Column(
linkify=True
)
@@ -152,6 +161,9 @@ class DeviceTable(TenancyColumnsMixin, NetBoxTable):
rack = tables.Column(
linkify=True
)
+ position = columns.TemplateColumn(
+ template_code='{{ value|floatformat }}'
+ )
device_role = columns.ColoredLabelColumn(
verbose_name='Role'
)
@@ -199,10 +211,10 @@ class DeviceTable(TenancyColumnsMixin, NetBoxTable):
class Meta(NetBoxTable.Meta):
model = Device
fields = (
- 'pk', 'id', 'name', 'status', 'tenant', 'tenant_group', 'device_role', 'manufacturer', 'device_type', 'platform', 'serial',
- 'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'primary_ip', 'airflow', 'primary_ip4',
- 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'contacts', 'tags',
- 'created', 'last_updated',
+ 'pk', 'id', 'name', 'status', 'tenant', 'tenant_group', 'device_role', 'manufacturer', 'device_type',
+ 'platform', 'serial', 'asset_tag', 'region', 'site_group', 'site', 'location', 'rack', 'position', 'face',
+ 'airflow', 'primary_ip', 'primary_ip4', 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position',
+ 'vc_priority', 'comments', 'contacts', 'tags', 'created', 'last_updated',
)
default_columns = (
'pk', 'name', 'status', 'tenant', 'site', 'location', 'rack', 'device_role', 'manufacturer', 'device_type',
diff --git a/netbox/dcim/tables/template_code.py b/netbox/dcim/tables/template_code.py
index d34003ee5..dfc77b854 100644
--- a/netbox/dcim/tables/template_code.py
+++ b/netbox/dcim/tables/template_code.py
@@ -239,7 +239,7 @@ INTERFACE_BUTTONS = """
{% include 'extras/inc/job_label.html' with result=script.result %}
diff --git a/netbox/templates/generic/object_edit.html b/netbox/templates/generic/object_edit.html
index 4ce270b30..56e4f5a32 100644
--- a/netbox/templates/generic/object_edit.html
+++ b/netbox/templates/generic/object_edit.html
@@ -59,9 +59,11 @@ Context:
{# Render grouped fields according to Form #}
{% for group, fields in form.fieldsets %}
-
-
{{ group }}
-
+ {% if group %}
+
+
{{ group }}
+
+ {% endif %}
{% for name in fields %}
{% with field=form|getfield:name %}
{% if not field.field.widget.is_hidden %}
diff --git a/netbox/templates/ipam/l2vpntermination_edit.html b/netbox/templates/ipam/l2vpntermination_edit.html
index c66b8a3d1..4379a0899 100644
--- a/netbox/templates/ipam/l2vpntermination_edit.html
+++ b/netbox/templates/ipam/l2vpntermination_edit.html
@@ -46,4 +46,12 @@