diff --git a/docs/release-notes/version-3.1.md b/docs/release-notes/version-3.1.md index 5e8138642..d565fda03 100644 --- a/docs/release-notes/version-3.1.md +++ b/docs/release-notes/version-3.1.md @@ -6,7 +6,7 @@ ### Breaking Changes * The `tenant` and `tenant_id` filters for the Cable model now filter on the tenant assigned directly to each cable, rather than on the parent object of either termination. -* The `cable_peer` and `cable_peer_type` attributes of the interface model has been renamed to `link_peer` and `link_peer_type`, respectively, to accommodate wireless links. +* The `cable_peer` and `cable_peer_type` attributes of cable termination models have been renamed to `link_peer` and `link_peer_type`, respectively, to accommodate wireless links between interfaces. ### New Features @@ -68,6 +68,7 @@ Multiple interfaces can be bridged to a single virtual interface to effect a bri * [#6874](https://github.com/netbox-community/netbox/issues/6874) - Add tenant assignment for locations * [#7354](https://github.com/netbox-community/netbox/issues/7354) - Relax uniqueness constraints on region, site group, and location names * [#7530](https://github.com/netbox-community/netbox/issues/7530) - Move device type component lists to separate views +* [#7606](https://github.com/netbox-community/netbox/issues/7606) - Model transmit power for interfaces ### Other Changes @@ -113,6 +114,7 @@ Multiple interfaces can be bridged to a single virtual interface to effect a bri * Added `rf_channel` field * Added `rf_channel_frequency` field * Added `rf_chanel_width` field + * Added `tx_power` field * Added `wwn` field * `cable_peer` has been renamed to `link_peer` * `cable_peer_type` has been renamed to `link_peer_type` diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index a5f4ac5fe..a1e8156dc 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -628,7 +628,7 @@ class InterfaceSerializer(PrimaryModelSerializer, LinkTerminationSerializer, Con fields = [ 'id', 'url', 'display', 'device', 'name', 'label', 'type', 'enabled', 'parent', 'bridge', 'lag', 'mtu', 'mac_address', 'wwn', 'mgmt_only', 'description', 'mode', 'rf_role', 'rf_channel', 'rf_channel_frequency', - 'rf_channel_width', 'untagged_vlan', 'tagged_vlans', 'mark_connected', 'cable', 'wireless_link', + 'rf_channel_width', 'tx_power', 'untagged_vlan', 'tagged_vlans', 'mark_connected', 'cable', 'wireless_link', 'link_peer', 'link_peer_type', 'connected_endpoint', 'connected_endpoint_type', 'connected_endpoint_reachable', 'tags', 'custom_fields', 'created', 'last_updated', 'count_ipaddresses', '_occupied', diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py index c049025b7..bced6d882 100644 --- a/netbox/dcim/filtersets.py +++ b/netbox/dcim/filtersets.py @@ -1011,7 +1011,7 @@ class InterfaceFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableT model = Interface fields = [ 'id', 'name', 'label', 'type', 'enabled', 'mtu', 'mgmt_only', 'mode', 'rf_role', 'rf_channel', - 'rf_channel_frequency', 'rf_channel_width', 'description', + 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'description', ] def filter_device(self, queryset, name, value): diff --git a/netbox/dcim/forms/bulk_edit.py b/netbox/dcim/forms/bulk_edit.py index b1dce2281..eac2ed314 100644 --- a/netbox/dcim/forms/bulk_edit.py +++ b/netbox/dcim/forms/bulk_edit.py @@ -940,7 +940,7 @@ class PowerOutletBulkEditForm( class InterfaceBulkEditForm( form_from_model(Interface, [ 'label', 'type', 'parent', 'bridge', 'lag', 'mac_address', 'wwn', 'mtu', 'mgmt_only', 'mark_connected', - 'description', 'mode', 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', + 'description', 'mode', 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', ]), BootstrapMixin, AddRemoveTagsForm, @@ -996,7 +996,7 @@ class InterfaceBulkEditForm( class Meta: nullable_fields = [ 'label', 'parent', 'bridge', 'lag', 'mac_address', 'wwn', 'mtu', 'description', 'mode', 'rf_channel', - 'rf_channel_frequency', 'rf_channel_width', 'untagged_vlan', 'tagged_vlans', + 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'untagged_vlan', 'tagged_vlans', ] def __init__(self, *args, **kwargs): diff --git a/netbox/dcim/forms/bulk_import.py b/netbox/dcim/forms/bulk_import.py index 18bdb3d3f..94c56b22a 100644 --- a/netbox/dcim/forms/bulk_import.py +++ b/netbox/dcim/forms/bulk_import.py @@ -602,7 +602,7 @@ class InterfaceCSVForm(CustomFieldModelCSVForm): fields = ( 'device', 'name', 'label', 'parent', 'bridge', 'lag', 'type', 'enabled', 'mark_connected', 'mac_address', 'wwn', 'mtu', 'mgmt_only', 'description', 'mode', 'rf_role', 'rf_channel', 'rf_channel_frequency', - 'rf_channel_width', + 'rf_channel_width', 'tx_power', ) def clean_enabled(self): diff --git a/netbox/dcim/forms/filtersets.py b/netbox/dcim/forms/filtersets.py index 6530b3b46..b57282120 100644 --- a/netbox/dcim/forms/filtersets.py +++ b/netbox/dcim/forms/filtersets.py @@ -957,7 +957,7 @@ class InterfaceFilterForm(DeviceComponentFilterForm): field_groups = [ ['q', 'tag'], ['name', 'label', 'kind', 'type', 'enabled', 'mgmt_only', 'mac_address', 'wwn'], - ['rf_role', 'rf_channel', 'rf_channel_width'], + ['rf_role', 'rf_channel', 'rf_channel_width', 'tx_power'], ['region_id', 'site_group_id', 'site_id', 'location_id', 'device_id'], ] kind = forms.MultipleChoiceField( @@ -1010,6 +1010,12 @@ class InterfaceFilterForm(DeviceComponentFilterForm): required=False, label='Channel width (MHz)' ) + tx_power = forms.IntegerField( + required=False, + label='Transmit power (dBm)', + min_value=0, + max_value=127 + ) tag = TagFilterField(model) diff --git a/netbox/dcim/forms/models.py b/netbox/dcim/forms/models.py index a2b5e7dba..3f87d1bd7 100644 --- a/netbox/dcim/forms/models.py +++ b/netbox/dcim/forms/models.py @@ -1150,7 +1150,7 @@ class InterfaceForm(BootstrapMixin, InterfaceCommonForm, CustomFieldModelForm): fields = [ 'device', 'name', 'label', 'type', 'enabled', 'parent', 'bridge', 'lag', 'mac_address', 'wwn', 'mtu', 'mgmt_only', 'mark_connected', 'description', 'mode', 'rf_role', 'rf_channel', 'rf_channel_frequency', - 'rf_channel_width', 'wireless_lans', 'untagged_vlan', 'tagged_vlans', 'tags', + 'rf_channel_width', 'tx_power', 'wireless_lans', 'untagged_vlan', 'tagged_vlans', 'tags', ] widgets = { 'device': forms.HiddenInput(), diff --git a/netbox/dcim/migrations/0140_wireless.py b/netbox/dcim/migrations/0140_wireless.py index 012b78dd4..430782cf0 100644 --- a/netbox/dcim/migrations/0140_wireless.py +++ b/netbox/dcim/migrations/0140_wireless.py @@ -1,4 +1,5 @@ from django.db import migrations, models +import django.core.validators import django.db.models.deletion @@ -30,6 +31,11 @@ class Migration(migrations.Migration): name='rf_channel_width', field=models.DecimalField(blank=True, decimal_places=3, max_digits=7, null=True), ), + migrations.AddField( + model_name='interface', + name='tx_power', + field=models.PositiveSmallIntegerField(blank=True, null=True, validators=[django.core.validators.MaxValueValidator(127)]), + ), migrations.AddField( model_name='interface', name='wireless_lans', diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index 2a6adfa0c..e166c44ab 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -560,6 +560,12 @@ class Interface(ComponentModel, BaseInterface, LinkTermination, PathEndpoint): null=True, verbose_name='Channel width (MHz)' ) + tx_power = models.PositiveSmallIntegerField( + blank=True, + null=True, + validators=(MaxValueValidator(127),), + verbose_name='Transmit power (dBm)' + ) wireless_link = models.ForeignKey( to='wireless.WirelessLink', on_delete=models.SET_NULL, diff --git a/netbox/dcim/tables/devices.py b/netbox/dcim/tables/devices.py index 167dba95d..e2e11dc6e 100644 --- a/netbox/dcim/tables/devices.py +++ b/netbox/dcim/tables/devices.py @@ -498,9 +498,9 @@ class InterfaceTable(DeviceComponentTable, BaseInterfaceTable, PathEndpointTable model = Interface fields = ( 'pk', 'name', 'device', 'label', 'enabled', 'type', 'mgmt_only', 'mtu', 'mode', 'mac_address', 'wwn', - 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'description', 'mark_connected', - 'cable', 'cable_color', 'wireless_link', 'wireless_lans', 'link_peer', 'connection', 'tags', 'ip_addresses', - 'untagged_vlan', 'tagged_vlans', + 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'description', + 'mark_connected', 'cable', 'cable_color', 'wireless_link', 'wireless_lans', 'link_peer', 'connection', + 'tags', 'ip_addresses', 'untagged_vlan', 'tagged_vlans', ) default_columns = ('pk', 'name', 'device', 'label', 'enabled', 'type', 'description') @@ -533,9 +533,9 @@ class DeviceInterfaceTable(InterfaceTable): model = Interface fields = ( 'pk', 'name', 'label', 'enabled', 'type', 'parent', 'bridge', 'lag', 'mgmt_only', 'mtu', 'mode', - 'mac_address', 'wwn', 'rf_role', 'rf_channel', 'rf_channel_width', 'description', 'mark_connected', 'cable', - 'cable_color', 'wireless_link', 'wireless_lans', 'link_peer', 'connection', 'tags', 'ip_addresses', - 'untagged_vlan', 'tagged_vlans', 'actions', + 'mac_address', 'wwn', 'rf_role', 'rf_channel', 'rf_channel_width', 'tx_power', 'description', + 'mark_connected', 'cable', 'cable_color', 'wireless_link', 'wireless_lans', 'link_peer', 'connection', + 'tags', 'ip_addresses', 'untagged_vlan', 'tagged_vlans', 'actions', ) order_by = ('name',) default_columns = ( diff --git a/netbox/dcim/tests/test_api.py b/netbox/dcim/tests/test_api.py index b3f182ce7..042b4ce28 100644 --- a/netbox/dcim/tests/test_api.py +++ b/netbox/dcim/tests/test_api.py @@ -1198,6 +1198,7 @@ class InterfaceTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase 'name': 'Interface 4', 'type': '1000base-t', 'mode': InterfaceModeChoices.MODE_TAGGED, + 'tx_power': 10, 'tagged_vlans': [vlans[0].pk, vlans[1].pk], 'untagged_vlan': vlans[2].pk, }, @@ -1207,6 +1208,7 @@ class InterfaceTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase 'type': '1000base-t', 'mode': InterfaceModeChoices.MODE_TAGGED, 'bridge': interfaces[0].pk, + 'tx_power': 10, 'tagged_vlans': [vlans[0].pk, vlans[1].pk], 'untagged_vlan': vlans[2].pk, }, @@ -1216,6 +1218,7 @@ class InterfaceTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase 'type': 'virtual', 'mode': InterfaceModeChoices.MODE_TAGGED, 'parent': interfaces[1].pk, + 'tx_power': 10, 'tagged_vlans': [vlans[0].pk, vlans[1].pk], 'untagged_vlan': vlans[2].pk, }, diff --git a/netbox/dcim/tests/test_filtersets.py b/netbox/dcim/tests/test_filtersets.py index 51cfafaf2..37d11be40 100644 --- a/netbox/dcim/tests/test_filtersets.py +++ b/netbox/dcim/tests/test_filtersets.py @@ -2061,9 +2061,9 @@ class InterfaceTestCase(TestCase, ChangeLoggedFilterSetTests): Interface(device=devices[0], name='Interface 1', label='A', type=InterfaceTypeChoices.TYPE_1GE_SFP, enabled=True, mgmt_only=True, mtu=100, mode=InterfaceModeChoices.MODE_ACCESS, mac_address='00-00-00-00-00-01', description='First'), Interface(device=devices[1], name='Interface 2', label='B', type=InterfaceTypeChoices.TYPE_1GE_GBIC, enabled=True, mgmt_only=True, mtu=200, mode=InterfaceModeChoices.MODE_TAGGED, mac_address='00-00-00-00-00-02', description='Second'), Interface(device=devices[2], name='Interface 3', label='C', type=InterfaceTypeChoices.TYPE_1GE_FIXED, enabled=False, mgmt_only=False, mtu=300, mode=InterfaceModeChoices.MODE_TAGGED_ALL, mac_address='00-00-00-00-00-03', description='Third'), - Interface(device=devices[3], name='Interface 4', label='D', type=InterfaceTypeChoices.TYPE_OTHER, enabled=True, mgmt_only=True), - Interface(device=devices[3], name='Interface 5', label='E', type=InterfaceTypeChoices.TYPE_OTHER, enabled=True, mgmt_only=True), - Interface(device=devices[3], name='Interface 6', label='F', type=InterfaceTypeChoices.TYPE_OTHER, enabled=False, mgmt_only=False), + Interface(device=devices[3], name='Interface 4', label='D', type=InterfaceTypeChoices.TYPE_OTHER, enabled=True, mgmt_only=True, tx_power=40), + Interface(device=devices[3], name='Interface 5', label='E', type=InterfaceTypeChoices.TYPE_OTHER, enabled=True, mgmt_only=True, tx_power=40), + Interface(device=devices[3], name='Interface 6', label='F', type=InterfaceTypeChoices.TYPE_OTHER, enabled=False, mgmt_only=False, tx_power=40), Interface(device=devices[3], name='Interface 7', type=InterfaceTypeChoices.TYPE_80211AC, rf_role=WirelessRoleChoices.ROLE_AP, rf_channel=WirelessChannelChoices.CHANNEL_24G_1, rf_channel_frequency=2412, rf_channel_width=22), Interface(device=devices[3], name='Interface 8', type=InterfaceTypeChoices.TYPE_80211AC, rf_role=WirelessRoleChoices.ROLE_STATION, rf_channel=WirelessChannelChoices.CHANNEL_5G_32, rf_channel_frequency=5160, rf_channel_width=20), ) @@ -2224,6 +2224,10 @@ class InterfaceTestCase(TestCase, ChangeLoggedFilterSetTests): params = {'rf_channel_width': [22, 20]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_tx_power(self): + params = {'tx_power': [40]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3) + class FrontPortTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = FrontPort.objects.all() diff --git a/netbox/dcim/tests/test_views.py b/netbox/dcim/tests/test_views.py index 92757f28d..cd354117e 100644 --- a/netbox/dcim/tests/test_views.py +++ b/netbox/dcim/tests/test_views.py @@ -1608,6 +1608,7 @@ class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase): 'mgmt_only': True, 'description': 'A front port', 'mode': InterfaceModeChoices.MODE_TAGGED, + 'tx_power': 10, 'untagged_vlan': vlans[0].pk, 'tagged_vlans': [v.pk for v in vlans[1:4]], 'tags': [t.pk for t in tags], @@ -1641,6 +1642,7 @@ class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase): 'mgmt_only': True, 'description': 'New description', 'mode': InterfaceModeChoices.MODE_TAGGED, + 'tx_power': 10, 'untagged_vlan': vlans[0].pk, 'tagged_vlans': [v.pk for v in vlans[1:4]], } diff --git a/netbox/templates/dcim/interface.html b/netbox/templates/dcim/interface.html index eb47f5655..5851b3aeb 100644 --- a/netbox/templates/dcim/interface.html +++ b/netbox/templates/dcim/interface.html @@ -105,6 +105,10 @@