diff --git a/docs/models/dcim/device.md b/docs/models/dcim/device.md index 2216e351c..c9f05cd93 100644 --- a/docs/models/dcim/device.md +++ b/docs/models/dcim/device.md @@ -87,6 +87,10 @@ Each device may designate one primary IPv4 address and/or one primary IPv6 addre !!! tip NetBox will prefer IPv6 addresses over IPv4 addresses by default. This can be changed by setting the `PREFER_IPV4` configuration parameter. +### Out-of-band (OOB) IP Address + +Each device may designate its out-of-band IP address. Out-of-band IPs are typically used to access network infrastructure via a physically separate management network. + ### Cluster If this device will serve as a host for a virtualization [cluster](../virtualization/cluster.md), it can be assigned here. (Host devices can also be assigned by editing the cluster.) diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 7f7249e87..edcb64019 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -687,11 +687,11 @@ class DeviceSerializer(NetBoxModelSerializer): fields = [ 'id', 'url', 'display', 'name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'latitude', 'longitude', 'parent_device', 'status', - 'airflow', 'primary_ip', 'primary_ip4', 'primary_ip6', 'oob_ip', 'cluster', 'virtual_chassis', 'vc_position', - 'vc_priority', 'description', 'comments', 'config_template', 'local_context_data', 'tags', 'custom_fields', - 'created', 'last_updated', 'console_port_count', 'console_server_port_count', 'power_port_count', - 'power_outlet_count', 'interface_count', 'front_port_count', 'rear_port_count', 'device_bay_count', - 'module_bay_count', 'inventory_item_count', + 'airflow', 'primary_ip', 'primary_ip4', 'primary_ip6', 'oob_ip', 'cluster', 'virtual_chassis', + 'vc_position', 'vc_priority', 'description', 'comments', 'config_template', 'local_context_data', 'tags', + 'custom_fields', 'created', 'last_updated', 'console_port_count', 'console_server_port_count', + 'power_port_count', 'power_outlet_count', 'interface_count', 'front_port_count', 'rear_port_count', + 'device_bay_count', 'module_bay_count', 'inventory_item_count', ] @extend_schema_field(NestedDeviceSerializer) @@ -713,11 +713,11 @@ class DeviceWithConfigContextSerializer(DeviceSerializer): fields = [ 'id', 'url', 'display', 'name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'parent_device', 'status', 'airflow', 'primary_ip', - 'primary_ip4', 'primary_ip6', 'oob_ip', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'description', - 'comments', 'local_context_data', 'tags', 'custom_fields', 'config_context', 'config_template', - 'created', 'last_updated', 'console_port_count', 'console_server_port_count', 'power_port_count', - 'power_outlet_count', 'interface_count', 'front_port_count', 'rear_port_count', 'device_bay_count', - 'module_bay_count', 'inventory_item_count', + 'primary_ip4', 'primary_ip6', 'oob_ip', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', + 'description', 'comments', 'local_context_data', 'tags', 'custom_fields', 'config_context', + 'config_template', 'created', 'last_updated', 'console_port_count', 'console_server_port_count', + 'power_port_count', 'power_outlet_count', 'interface_count', 'front_port_count', 'rear_port_count', + 'device_bay_count', 'module_bay_count', 'inventory_item_count', ] @extend_schema_field(serializers.JSONField(allow_null=True)) diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py index b441d6673..e88fc120d 100644 --- a/netbox/dcim/filtersets.py +++ b/netbox/dcim/filtersets.py @@ -943,7 +943,7 @@ class DeviceFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilter ) has_oob_ip = django_filters.BooleanFilter( method='_has_oob_ip', - label=_('Has a OOB IP'), + label=_('Has an out-of-band IP'), ) virtual_chassis_id = django_filters.ModelMultipleChoiceFilter( field_name='virtual_chassis', diff --git a/netbox/dcim/forms/filtersets.py b/netbox/dcim/forms/filtersets.py index 2c5dcd339..06d38627d 100644 --- a/netbox/dcim/forms/filtersets.py +++ b/netbox/dcim/forms/filtersets.py @@ -725,7 +725,7 @@ class DeviceFilterForm( ) has_oob_ip = forms.NullBooleanField( required=False, - label='Has a OOB IP', + label='Has an OOB IP', widget=forms.Select( choices=BOOLEAN_WITH_BLANK_CHOICES ) diff --git a/netbox/dcim/forms/model_forms.py b/netbox/dcim/forms/model_forms.py index 2518f0a05..067cf2bda 100644 --- a/netbox/dcim/forms/model_forms.py +++ b/netbox/dcim/forms/model_forms.py @@ -449,9 +449,9 @@ class DeviceForm(TenancyForm, NetBoxModelForm): model = Device fields = [ 'name', 'device_role', 'device_type', 'serial', 'asset_tag', 'site', 'rack', 'location', 'position', 'face', - 'latitude', 'longitude', 'status', 'airflow', 'platform', 'primary_ip4', 'primary_ip6', 'cluster', + 'latitude', 'longitude', 'status', 'airflow', 'platform', 'primary_ip4', 'primary_ip6', 'oob_ip', 'cluster', 'tenant_group', 'tenant', 'virtual_chassis', 'vc_position', 'vc_priority', 'description', 'config_template', - 'comments', 'tags', 'local_context_data', 'oob_ip', + 'comments', 'tags', 'local_context_data', ] def __init__(self, *args, **kwargs): @@ -476,7 +476,7 @@ class DeviceForm(TenancyForm, NetBoxModelForm): if interface_ips: ip_list = [(ip.id, f'{ip.address} ({ip.assigned_object})') for ip in interface_ips] ip_choices.append(('Interface IPs', ip_list)) - oob_ip_choices.append(('Interface IPv{}s'.format(family), ip_list)) + oob_ip_choices.extend(ip_list) # Collect NAT IPs nat_ips = IPAddress.objects.prefetch_related('nat_inside').filter( address__family=family, @@ -486,7 +486,6 @@ class DeviceForm(TenancyForm, NetBoxModelForm): if nat_ips: ip_list = [(ip.id, f'{ip.address} (NAT)') for ip in nat_ips] ip_choices.append(('NAT IPs', ip_list)) - oob_ip_choices.append(('NAT IPv{}s'.format(family), ip_list)) self.fields['primary_ip{}'.format(family)].choices = ip_choices self.fields['oob_ip'].choices = oob_ip_choices diff --git a/netbox/dcim/migrations/0175_device_component_counters.py b/netbox/dcim/migrations/0176_device_component_counters.py similarity index 98% rename from netbox/dcim/migrations/0175_device_component_counters.py rename to netbox/dcim/migrations/0176_device_component_counters.py index 9d033c103..fc22de81b 100644 --- a/netbox/dcim/migrations/0175_device_component_counters.py +++ b/netbox/dcim/migrations/0176_device_component_counters.py @@ -39,7 +39,7 @@ def recalculate_device_counts(apps, schema_editor): class Migration(migrations.Migration): dependencies = [ - ('dcim', '0174_rack_starting_unit'), + ('dcim', '0175_device_oob_ip'), ] operations = [ diff --git a/netbox/dcim/models/devices.py b/netbox/dcim/models/devices.py index b010250f1..76100197b 100644 --- a/netbox/dcim/models/devices.py +++ b/netbox/dcim/models/devices.py @@ -597,7 +597,7 @@ class Device(PrimaryModel, ConfigContextModel): related_name='+', blank=True, null=True, - verbose_name='OOB IP' + verbose_name='Out-of-band IP' ) cluster = models.ForeignKey( to='virtualization.Cluster', @@ -824,7 +824,7 @@ class Device(PrimaryModel, ConfigContextModel): except DeviceType.DoesNotExist: pass - # Validate primary IP addresses + # Validate primary & OOB IP addresses vc_interfaces = self.vc_interfaces(if_master=False) if self.primary_ip4: if self.primary_ip4.family != 4: @@ -852,8 +852,6 @@ class Device(PrimaryModel, ConfigContextModel): raise ValidationError({ 'primary_ip6': f"The specified IP address ({self.primary_ip6}) is not assigned to this device." }) - - # OOB ip validation if self.oob_ip: if self.oob_ip.assigned_object in vc_interfaces: pass @@ -863,6 +861,7 @@ class Device(PrimaryModel, ConfigContextModel): raise ValidationError({ 'oob_ip': f"The specified IP address ({self.oob_ip}) is not assigned to this device." }) + # Validate manufacturer/platform if hasattr(self, 'device_type') and self.platform: if self.platform.manufacturer and self.platform.manufacturer != self.device_type.manufacturer: diff --git a/netbox/dcim/tables/devices.py b/netbox/dcim/tables/devices.py index 7f74f2ba7..c2651e4da 100644 --- a/netbox/dcim/tables/devices.py +++ b/netbox/dcim/tables/devices.py @@ -271,8 +271,8 @@ class DeviceTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable): 'pk', 'id', 'name', 'status', 'tenant', 'tenant_group', 'device_role', 'manufacturer', 'device_type', 'platform', 'serial', 'asset_tag', 'region', 'site_group', 'site', 'location', 'rack', 'parent_device', 'device_bay_position', 'position', 'face', 'latitude', 'longitude', 'airflow', 'primary_ip', 'primary_ip4', - 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'description', 'config_template', - 'comments', 'contacts', 'oob_ip', 'tags', 'created', 'last_updated', + 'primary_ip6', 'oob_ip', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'description', + 'config_template', '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/views.py b/netbox/dcim/views.py index cf753e259..1d46de231 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -2452,11 +2452,13 @@ class InterfaceView(generic.ObjectView): queryset = Interface.objects.all() def get_extra_context(self, request, instance): - # Get assigned VDC's + # Get assigned VDCs vdc_table = tables.VirtualDeviceContextTable( data=instance.vdcs.restrict(request.user, 'view').prefetch_related('device'), - exclude=('tenant', 'tenant_group', 'primary_ip', 'primary_ip4', 'primary_ip6', 'comments', 'tags', - 'created', 'last_updated', 'actions', 'oob_ip'), + exclude=( + 'tenant', 'tenant_group', 'primary_ip', 'primary_ip4', 'primary_ip6', 'oob_ip', 'comments', 'tags', + 'created', 'last_updated', 'actions', + ), orderable=False ) diff --git a/netbox/ipam/models/ip.py b/netbox/ipam/models/ip.py index ff5f796b4..a5d6eb084 100644 --- a/netbox/ipam/models/ip.py +++ b/netbox/ipam/models/ip.py @@ -853,23 +853,18 @@ class IPAddress(PrimaryModel): def is_oob_ip(self): if self.assigned_object: parent = getattr(self.assigned_object, 'parent_object', None) - if parent.oob_ip: - if parent.oob_ip.pk == self.pk: - return True + if parent.oob_ip_id == self.pk: + return True return False @property def is_primary_ip(self): if self.assigned_object: parent = getattr(self.assigned_object, 'parent_object', None) - if self.family == 4: - if parent.primary_ip4: - if parent.primary_ip4.pk == self.pk: - return True - if self.family == 6: - if parent.primary_ip6: - if parent.primary_ip6.pk == self.pk: - return True + if self.family == 4 and parent.primary_ip4_id == self.pk: + return True + if self.family == 6 and parent.primary_ip6_id == self.pk: + return True return False def _set_mask_length(self, value): diff --git a/netbox/ipam/signals.py b/netbox/ipam/signals.py index ce4357178..2a985c294 100644 --- a/netbox/ipam/signals.py +++ b/netbox/ipam/signals.py @@ -52,25 +52,19 @@ def handle_prefix_deleted(instance, **kwargs): @receiver(pre_delete, sender=IPAddress) def clear_primary_ip(instance, **kwargs): """ - When an IPAddress is deleted, trigger save() on any Devices/VirtualMachines for which it - was a primary IP. + When an IPAddress is deleted, trigger save() on any Devices/VirtualMachines for which it was a primary IP. """ field_name = f'primary_ip{instance.family}' - device = Device.objects.filter(**{field_name: instance}).first() - if device: + if device := Device.objects.filter(**{field_name: instance}).first(): device.save() - virtualmachine = VirtualMachine.objects.filter(**{field_name: instance}).first() - if virtualmachine: + if virtualmachine := VirtualMachine.objects.filter(**{field_name: instance}).first(): virtualmachine.save() @receiver(pre_delete, sender=IPAddress) def clear_oob_ip(instance, **kwargs): """ - When an IPAddress is deleted, trigger save() on any Devices/VirtualMachines for which it - was a OOB IP. + When an IPAddress is deleted, trigger save() on any Devices for which it was a OOB IP. """ - field_name = f'oob_ip' - device = Device.objects.filter(**{field_name: instance}).first() - if device: + if device := Device.objects.filter(oob_ip=instance).first(): device.save() diff --git a/netbox/templates/dcim/device.html b/netbox/templates/dcim/device.html index a43424fe6..4d1e3dc08 100644 --- a/netbox/templates/dcim/device.html +++ b/netbox/templates/dcim/device.html @@ -240,15 +240,10 @@