From e5cea213c55ef2d7d8a73033388f68cb148e3bd7 Mon Sep 17 00:00:00 2001 From: Jamie Murphy Date: Mon, 26 Jun 2023 22:12:48 +0100 Subject: [PATCH 1/9] initial oob_ip support for devices --- netbox/dcim/api/serializers.py | 5 ++ netbox/dcim/filtersets.py | 20 +++++++ netbox/dcim/forms/filtersets.py | 9 +++- netbox/dcim/forms/model_forms.py | 7 ++- .../0173_device_oob_ip4_device_oob_ip6.py | 25 +++++++++ netbox/dcim/models/devices.py | 54 +++++++++++++++++++ netbox/dcim/tables/devices.py | 15 +++++- netbox/dcim/views.py | 2 +- netbox/ipam/signals.py | 11 ++++ 9 files changed, 144 insertions(+), 4 deletions(-) create mode 100644 netbox/dcim/migrations/0173_device_oob_ip4_device_oob_ip6.py diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 2f854d3e4a3..2919ea39f7c 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -663,6 +663,9 @@ class DeviceSerializer(NetBoxModelSerializer): primary_ip = NestedIPAddressSerializer(read_only=True) primary_ip4 = NestedIPAddressSerializer(required=False, allow_null=True) primary_ip6 = NestedIPAddressSerializer(required=False, allow_null=True) + oob_ip = NestedIPAddressSerializer(read_only=True) + oob_ip4 = NestedIPAddressSerializer(required=False, allow_null=True) + oob_ip6 = NestedIPAddressSerializer(required=False, allow_null=True) parent_device = serializers.SerializerMethodField() cluster = NestedClusterSerializer(required=False, allow_null=True) virtual_chassis = NestedVirtualChassisSerializer(required=False, allow_null=True, default=None) @@ -676,6 +679,7 @@ class Meta: 'site', 'location', 'rack', 'position', 'face', 'parent_device', 'status', 'airflow', 'primary_ip', 'primary_ip4', 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'description', 'comments', 'config_template', 'local_context_data', 'tags', 'custom_fields', 'created', 'last_updated', + 'oob_ip', 'oob_ip4', 'oob_ip6', ] @extend_schema_field(NestedDeviceSerializer) @@ -699,6 +703,7 @@ class Meta(DeviceSerializer.Meta): 'site', 'location', 'rack', 'position', 'face', 'parent_device', 'status', 'airflow', 'primary_ip', 'primary_ip4', 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'description', 'comments', 'local_context_data', 'tags', 'custom_fields', 'config_context', 'created', 'last_updated', + 'oob_ip', 'oob_ip4', 'oob_ip6' ] @extend_schema_field(serializers.JSONField(allow_null=True)) diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py index e784be8e85b..8cce3b25228 100644 --- a/netbox/dcim/filtersets.py +++ b/netbox/dcim/filtersets.py @@ -941,6 +941,10 @@ class DeviceFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilter method='_has_primary_ip', label=_('Has a primary IP'), ) + has_oob_ip = django_filters.BooleanFilter( + method='_has_oob_ip', + label=_('Has a OOB IP'), + ) virtual_chassis_id = django_filters.ModelMultipleChoiceFilter( field_name='virtual_chassis', queryset=VirtualChassis.objects.all(), @@ -996,6 +1000,16 @@ class DeviceFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilter queryset=IPAddress.objects.all(), label=_('Primary IPv6 (ID)'), ) + oob_ip4_id = django_filters.ModelMultipleChoiceFilter( + field_name='oob_ip4', + queryset=IPAddress.objects.all(), + label=_('OOB IPv4 (ID)'), + ) + oob_ip6_id = django_filters.ModelMultipleChoiceFilter( + field_name='oob_ip6', + queryset=IPAddress.objects.all(), + label=_('OOB IPv6 (ID)'), + ) class Meta: model = Device @@ -1020,6 +1034,12 @@ def _has_primary_ip(self, queryset, name, value): return queryset.filter(params) return queryset.exclude(params) + def _has_oob_ip(self, queryset, name, value): + params = Q(oob_ip4__isnull=False) | Q(oob_ip6__isnull=False) + if value: + return queryset.filter(params) + return queryset.exclude(params) + def _virtual_chassis_member(self, queryset, name, value): return queryset.exclude(virtual_chassis__isnull=value) diff --git a/netbox/dcim/forms/filtersets.py b/netbox/dcim/forms/filtersets.py index 4edee6014e4..e7b63a52250 100644 --- a/netbox/dcim/forms/filtersets.py +++ b/netbox/dcim/forms/filtersets.py @@ -629,7 +629,7 @@ class DeviceFilterForm( ('Components', ( 'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces', 'pass_through_ports', )), - ('Miscellaneous', ('has_primary_ip', 'virtual_chassis_member', 'config_template_id', 'local_context_data')) + ('Miscellaneous', ('has_primary_ip', 'has_oob_ip', 'virtual_chassis_member', 'config_template_id', 'local_context_data')) ) region_id = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), @@ -723,6 +723,13 @@ class DeviceFilterForm( choices=BOOLEAN_WITH_BLANK_CHOICES ) ) + has_oob_ip = forms.NullBooleanField( + required=False, + label='Has a OOB IP', + widget=forms.Select( + choices=BOOLEAN_WITH_BLANK_CHOICES + ) + ) virtual_chassis_member = forms.NullBooleanField( required=False, label='Virtual chassis member', diff --git a/netbox/dcim/forms/model_forms.py b/netbox/dcim/forms/model_forms.py index 21921604502..d62eb272334 100644 --- a/netbox/dcim/forms/model_forms.py +++ b/netbox/dcim/forms/model_forms.py @@ -456,7 +456,7 @@ class Meta: 'name', 'device_role', 'device_type', 'serial', 'asset_tag', 'site', 'rack', 'location', 'position', 'face', 'status', 'airflow', 'platform', 'primary_ip4', 'primary_ip6', 'cluster', 'tenant_group', 'tenant', 'virtual_chassis', 'vc_position', 'vc_priority', 'description', 'config_template', 'comments', 'tags', - 'local_context_data' + 'local_context_data', 'oob_ip4', 'oob_ip6' ] def __init__(self, *args, **kwargs): @@ -490,6 +490,7 @@ def __init__(self, *args, **kwargs): ip_list = [(ip.id, f'{ip.address} (NAT)') for ip in nat_ips] ip_choices.append(('NAT IPs', ip_list)) self.fields['primary_ip{}'.format(family)].choices = ip_choices + self.fields['oob_ip{}'.format(family)].choices = ip_choices # If editing an existing device, exclude it from the list of occupied rack units. This ensures that a device # can be flipped from one face to another. @@ -509,6 +510,10 @@ def __init__(self, *args, **kwargs): self.fields['primary_ip4'].widget.attrs['readonly'] = True self.fields['primary_ip6'].choices = [] self.fields['primary_ip6'].widget.attrs['readonly'] = True + self.fields['oob_ip4'].choices = [] + self.fields['oob_ip4'].widget.attrs['readonly'] = True + self.fields['oob_ip6'].choices = [] + self.fields['oob_ip6'].widget.attrs['readonly'] = True # Rack position position = self.data.get('position') or self.initial.get('position') diff --git a/netbox/dcim/migrations/0173_device_oob_ip4_device_oob_ip6.py b/netbox/dcim/migrations/0173_device_oob_ip4_device_oob_ip6.py new file mode 100644 index 00000000000..15d1403abf4 --- /dev/null +++ b/netbox/dcim/migrations/0173_device_oob_ip4_device_oob_ip6.py @@ -0,0 +1,25 @@ +# Generated by Django 4.1.9 on 2023-06-26 21:06 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('ipam', '0066_iprange_mark_utilized'), + ('dcim', '0172_larger_power_draw_values'), + ] + + operations = [ + migrations.AddField( + model_name='device', + name='oob_ip4', + field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='ipam.ipaddress'), + ), + migrations.AddField( + model_name='device', + name='oob_ip6', + field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='ipam.ipaddress'), + ), + ] diff --git a/netbox/dcim/models/devices.py b/netbox/dcim/models/devices.py index 4cf330ffd98..4a3eab27fa9 100644 --- a/netbox/dcim/models/devices.py +++ b/netbox/dcim/models/devices.py @@ -604,6 +604,22 @@ class Device(PrimaryModel, ConfigContextModel): null=True, verbose_name='Primary IPv6' ) + oob_ip4 = models.OneToOneField( + to='ipam.IPAddress', + on_delete=models.SET_NULL, + related_name='+', + blank=True, + null=True, + verbose_name='OOB IPv4' + ) + oob_ip6 = models.OneToOneField( + to='ipam.IPAddress', + on_delete=models.SET_NULL, + related_name='+', + blank=True, + null=True, + verbose_name='OOB IPv6' + ) cluster = models.ForeignKey( to='virtualization.Cluster', on_delete=models.SET_NULL, @@ -802,6 +818,33 @@ def clean(self): 'primary_ip6': f"The specified IP address ({self.primary_ip6}) is not assigned to this device." }) + # OOB ip validation + if self.oob_ip4: + if self.oob_ip4.family != 4: + raise ValidationError({ + 'oob_ip4': f"{self.oob_ip4} is not an IPv4 address." + }) + if self.oob_ip4.assigned_object in vc_interfaces: + pass + elif self.oob_ip4.nat_inside is not None and self.oob_ip4.nat_inside.assigned_object in vc_interfaces: + pass + else: + raise ValidationError({ + 'oob_ip4': f"The specified IP address ({self.oob_ip4}) is not assigned to this device." + }) + if self.oob_ip6: + if self.oob_ip6.family != 6: + raise ValidationError({ + 'oob_ip6': f"{self.oob_ip6} is not an IPv6 address." + }) + if self.oob_ip6.assigned_object in vc_interfaces: + pass + elif self.oob_ip6.nat_inside is not None and self.oob_ip6.nat_inside.assigned_object in vc_interfaces: + pass + else: + raise ValidationError({ + 'oob_ip6': f"The specified IP address ({self.oob_ip6}) 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: @@ -912,6 +955,17 @@ def primary_ip(self): else: return None + @property + def oob_ip(self): + if ConfigItem('PREFER_IPV4')() and self.oob_ip4: + return self.oob_ip4 + elif self.oob_ip6: + return self.oob_ip6 + elif self.oob_ip4: + return self.oob_ip4 + else: + return None + @property def interfaces_count(self): return self.vc_interfaces().count() diff --git a/netbox/dcim/tables/devices.py b/netbox/dcim/tables/devices.py index db2655d2745..550b7889b9b 100644 --- a/netbox/dcim/tables/devices.py +++ b/netbox/dcim/tables/devices.py @@ -201,6 +201,19 @@ class DeviceTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable): linkify=True, verbose_name='IPv6 Address' ) + oob_ip = tables.Column( + linkify=True, + order_by=('oob_ip4', 'oob_ip6'), + verbose_name='OOB IP Address' + ) + oob_ip4 = tables.Column( + linkify=True, + verbose_name='OOB IPv4 Address' + ) + oob_ip6 = tables.Column( + linkify=True, + verbose_name='OOB IPv6 Address' + ) cluster = tables.Column( linkify=True ) @@ -238,7 +251,7 @@ class Meta(NetBoxTable.Meta): 'platform', 'serial', 'asset_tag', 'region', 'site_group', 'site', 'location', 'rack', 'parent_device', 'device_bay_position', 'position', 'face', 'airflow', 'primary_ip', 'primary_ip4', 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'description', 'config_template', 'comments', 'contacts', - 'tags', 'created', 'last_updated', + 'tags', 'created', 'last_updated', 'oob_ip', 'oob_ip4', 'oob_ip6', ) 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 b52e0afa516..641b3d5295b 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -2456,7 +2456,7 @@ def get_extra_context(self, request, instance): 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', ), + 'created', 'last_updated', 'actions', 'oob_ip', 'oob_ip4', 'oob_ip6' ), orderable=False ) diff --git a/netbox/ipam/signals.py b/netbox/ipam/signals.py index 8555f5e67ad..4a376376364 100644 --- a/netbox/ipam/signals.py +++ b/netbox/ipam/signals.py @@ -62,3 +62,14 @@ def clear_primary_ip(instance, **kwargs): virtualmachine = VirtualMachine.objects.filter(**{field_name: instance}).first() if virtualmachine: 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. + """ + field_name = f'oob_ip{instance.family}' + device = Device.objects.filter(**{field_name: instance}).first() + if device: + device.save() From b45606d6ba69eebd5fb0a5a83766002cd01b1251 Mon Sep 17 00:00:00 2001 From: Jamie Murphy Date: Mon, 26 Jun 2023 22:29:50 +0100 Subject: [PATCH 2/9] add primary ip and oob ip checkmark to ip address view --- netbox/ipam/models/ip.py | 26 ++++++++++++++++++++++++++ netbox/templates/ipam/ipaddress.html | 8 ++++++++ 2 files changed, 34 insertions(+) diff --git a/netbox/ipam/models/ip.py b/netbox/ipam/models/ip.py index 015f9220ccf..615aa69650e 100644 --- a/netbox/ipam/models/ip.py +++ b/netbox/ipam/models/ip.py @@ -849,6 +849,32 @@ def family(self): return self.address.version return None + @property + def is_oob_ip(self): + if self.assigned_object: + if self.family == 4: + if self.assigned_object.device.oob_ip4: + if self.assigned_object.device.oob_ip4.pk == self.pk: + return True + if self.family == 6: + if self.assigned_object.device.oob_ip6: + if self.assigned_object.device.oob_ip6.pk == self.pk: + return True + return False + + @property + def is_primary_ip(self): + if self.assigned_object: + if self.family == 4: + if self.assigned_object.device.primary_ip4: + if self.assigned_object.device.primary_ip4.pk == self.pk: + return True + if self.family == 6: + if self.assigned_object.device.primary_ip6: + if self.assigned_object.device.primary_ip6.pk == self.pk: + return True + return False + def _set_mask_length(self, value): """ Expose the IPNetwork object's prefixlen attribute on the parent model so that it can be manipulated directly, diff --git a/netbox/templates/ipam/ipaddress.html b/netbox/templates/ipam/ipaddress.html index e58ac736ffd..a3c55d76efb 100644 --- a/netbox/templates/ipam/ipaddress.html +++ b/netbox/templates/ipam/ipaddress.html @@ -96,6 +96,14 @@
{% endfor %} + + Primary IP + {% checkmark object.is_primary_ip %} + + + OOB IP + {% checkmark object.is_oob_ip %} + From 077e5c465e083f74a5ce16e2eaeb4bdfc14553c2 Mon Sep 17 00:00:00 2001 From: Jamie Murphy Date: Mon, 26 Jun 2023 22:30:49 +0100 Subject: [PATCH 3/9] add oob ip to device view and device edit view --- netbox/templates/dcim/device.html | 30 ++++++++++++++++++++++++++ netbox/templates/dcim/device_edit.html | 2 ++ 2 files changed, 32 insertions(+) diff --git a/netbox/templates/dcim/device.html b/netbox/templates/dcim/device.html index b0e67269cf5..bc1b6ae8f88 100644 --- a/netbox/templates/dcim/device.html +++ b/netbox/templates/dcim/device.html @@ -220,6 +220,36 @@
Management
{% endif %} + + OOB IPv4 + + {% if object.oob_ip4 %} + {{ object.oob_ip4.address.ip }} + {% if object.oob_ip4.nat_inside %} + (NAT for {{ object.oob_ip4.nat_inside.address.ip }}) + {% elif object.oob_ip4.nat_outside.exists %} + (NAT: {% for nat in object.oob_ip4.nat_outside.all %}{{ nat.address.ip }}{% if not forloop.last %}, {% endif %}{% endfor %}) + {% endif %} + {% else %} + {{ ''|placeholder }} + {% endif %} + + + + OOB IPv6 + + {% if object.oob_ip6 %} + {{ object.oob_ip6.address.ip }} + {% if object.oob_ip6.nat_inside %} + (NAT for {{ object.oob_ip6.nat_inside.address.ip }}) + {% elif object.oob_ip6.nat_outside.exists %} + (NAT: {% for nat in object.oob_ip6.nat_outside.all %}{{ nat.address.ip }}{% if not forloop.last %}, {% endif %}{% endfor %}) + {% endif %} + {% else %} + {{ ''|placeholder }} + {% endif %} + + {% if object.cluster %} Cluster diff --git a/netbox/templates/dcim/device_edit.html b/netbox/templates/dcim/device_edit.html index 17780b5133a..3963928ab96 100644 --- a/netbox/templates/dcim/device_edit.html +++ b/netbox/templates/dcim/device_edit.html @@ -66,6 +66,8 @@
Management
{% if object.pk %} {% render_field form.primary_ip4 %} {% render_field form.primary_ip6 %} + {% render_field form.oob_ip4 %} + {% render_field form.oob_ip6 %} {% endif %} From 9c681e63dc6e53f15b656e832ac774e196aa1c0f Mon Sep 17 00:00:00 2001 From: Jamie Murphy Date: Mon, 26 Jun 2023 22:55:59 +0100 Subject: [PATCH 4/9] pep8 --- netbox/dcim/views.py | 2 +- netbox/ipam/signals.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 641b3d5295b..0a9a776af57 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -2456,7 +2456,7 @@ def get_extra_context(self, request, instance): 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', 'oob_ip4', 'oob_ip6' ), + 'created', 'last_updated', 'actions', 'oob_ip', 'oob_ip4', 'oob_ip6'), orderable=False ) diff --git a/netbox/ipam/signals.py b/netbox/ipam/signals.py index 4a376376364..d29ba0dc40b 100644 --- a/netbox/ipam/signals.py +++ b/netbox/ipam/signals.py @@ -63,6 +63,7 @@ def clear_primary_ip(instance, **kwargs): if virtualmachine: virtualmachine.save() + @receiver(pre_delete, sender=IPAddress) def clear_oob_ip(instance, **kwargs): """ From a3275a9b6b54d1b5d043386ee5f282779a4c71f8 Mon Sep 17 00:00:00 2001 From: Jamie Murphy Date: Mon, 26 Jun 2023 23:07:20 +0100 Subject: [PATCH 5/9] make is_oob_ip and is_primary_ip generic for other models --- netbox/ipam/models/ip.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/netbox/ipam/models/ip.py b/netbox/ipam/models/ip.py index 615aa69650e..2316882de76 100644 --- a/netbox/ipam/models/ip.py +++ b/netbox/ipam/models/ip.py @@ -852,26 +852,28 @@ def family(self): @property def is_oob_ip(self): if self.assigned_object: + parent = getattr(self.assigned_object, 'parent_object', None) if self.family == 4: - if self.assigned_object.device.oob_ip4: - if self.assigned_object.device.oob_ip4.pk == self.pk: + if parent.oob_ip4: + if parent.oob_ip4.pk == self.pk: return True if self.family == 6: - if self.assigned_object.device.oob_ip6: - if self.assigned_object.device.oob_ip6.pk == self.pk: + if parent.oob_ip6: + if parent.oob_ip6.pk == 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 self.assigned_object.device.primary_ip4: - if self.assigned_object.device.primary_ip4.pk == self.pk: + if parent.primary_ip4: + if parent.primary_ip4.pk == self.pk: return True if self.family == 6: - if self.assigned_object.device.primary_ip6: - if self.assigned_object.device.primary_ip6.pk == self.pk: + if parent.primary_ip6: + if parent.primary_ip6.pk == self.pk: return True return False From d267e42674d2352c444c5c93010a46063436902e Mon Sep 17 00:00:00 2001 From: Jamie Murphy Date: Mon, 24 Jul 2023 21:51:29 +0100 Subject: [PATCH 6/9] refactor oob_ip --- netbox/dcim/api/serializers.py | 4 +- netbox/dcim/filtersets.py | 13 ++--- netbox/dcim/forms/model_forms.py | 13 ++--- .../0173_device_oob_ip4_device_oob_ip6.py | 25 ---------- netbox/dcim/migrations/0175_device_oob_ip.py | 25 ++++++++++ netbox/dcim/models/devices.py | 48 +++---------------- netbox/dcim/tables/devices.py | 11 +---- netbox/dcim/views.py | 2 +- netbox/ipam/models/ip.py | 11 ++--- netbox/templates/dcim/device.html | 30 ++++-------- netbox/templates/dcim/device_edit.html | 3 +- 11 files changed, 57 insertions(+), 128 deletions(-) delete mode 100644 netbox/dcim/migrations/0173_device_oob_ip4_device_oob_ip6.py create mode 100644 netbox/dcim/migrations/0175_device_oob_ip.py diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 48fd996cb03..9c32d4ccfbb 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -663,9 +663,7 @@ class DeviceSerializer(NetBoxModelSerializer): primary_ip = NestedIPAddressSerializer(read_only=True) primary_ip4 = NestedIPAddressSerializer(required=False, allow_null=True) primary_ip6 = NestedIPAddressSerializer(required=False, allow_null=True) - oob_ip = NestedIPAddressSerializer(read_only=True) - oob_ip4 = NestedIPAddressSerializer(required=False, allow_null=True) - oob_ip6 = NestedIPAddressSerializer(required=False, allow_null=True) + oob_ip = NestedIPAddressSerializer(required=False, allow_null=True) parent_device = serializers.SerializerMethodField() cluster = NestedClusterSerializer(required=False, allow_null=True) virtual_chassis = NestedVirtualChassisSerializer(required=False, allow_null=True, default=None) diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py index 1da8818adbb..b441d6673a9 100644 --- a/netbox/dcim/filtersets.py +++ b/netbox/dcim/filtersets.py @@ -1000,15 +1000,10 @@ class DeviceFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilter queryset=IPAddress.objects.all(), label=_('Primary IPv6 (ID)'), ) - oob_ip4_id = django_filters.ModelMultipleChoiceFilter( - field_name='oob_ip4', + oob_ip_id = django_filters.ModelMultipleChoiceFilter( + field_name='oob_ip', queryset=IPAddress.objects.all(), - label=_('OOB IPv4 (ID)'), - ) - oob_ip6_id = django_filters.ModelMultipleChoiceFilter( - field_name='oob_ip6', - queryset=IPAddress.objects.all(), - label=_('OOB IPv6 (ID)'), + label=_('OOB IP (ID)'), ) class Meta: @@ -1035,7 +1030,7 @@ def _has_primary_ip(self, queryset, name, value): return queryset.exclude(params) def _has_oob_ip(self, queryset, name, value): - params = Q(oob_ip4__isnull=False) | Q(oob_ip6__isnull=False) + params = Q(oob_ip__isnull=False) if value: return queryset.filter(params) return queryset.exclude(params) diff --git a/netbox/dcim/forms/model_forms.py b/netbox/dcim/forms/model_forms.py index 693411f0931..4a10fb22228 100644 --- a/netbox/dcim/forms/model_forms.py +++ b/netbox/dcim/forms/model_forms.py @@ -451,7 +451,7 @@ class Meta: 'name', 'device_role', 'device_type', 'serial', 'asset_tag', 'site', 'rack', 'location', 'position', 'face', 'latitude', 'longitude', 'status', 'airflow', 'platform', 'primary_ip4', 'primary_ip6', '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', 'oob_ip', ] def __init__(self, *args, **kwargs): @@ -460,6 +460,7 @@ def __init__(self, *args, **kwargs): if self.instance.pk: # Compile list of choices for primary IPv4 and IPv6 addresses + oob_ip_choices = [(None, '---------')] for family in [4, 6]: ip_choices = [(None, '---------')] @@ -475,6 +476,7 @@ def __init__(self, *args, **kwargs): 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)) # Collect NAT IPs nat_ips = IPAddress.objects.prefetch_related('nat_inside').filter( address__family=family, @@ -484,8 +486,9 @@ def __init__(self, *args, **kwargs): 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{}'.format(family)].choices = ip_choices + self.fields['oob_ip'].choices = oob_ip_choices # If editing an existing device, exclude it from the list of occupied rack units. This ensures that a device # can be flipped from one face to another. @@ -505,10 +508,8 @@ def __init__(self, *args, **kwargs): self.fields['primary_ip4'].widget.attrs['readonly'] = True self.fields['primary_ip6'].choices = [] self.fields['primary_ip6'].widget.attrs['readonly'] = True - self.fields['oob_ip4'].choices = [] - self.fields['oob_ip4'].widget.attrs['readonly'] = True - self.fields['oob_ip6'].choices = [] - self.fields['oob_ip6'].widget.attrs['readonly'] = True + self.fields['oob_ip'].choices = [] + self.fields['oob_ip'].widget.attrs['readonly'] = True # Rack position position = self.data.get('position') or self.initial.get('position') diff --git a/netbox/dcim/migrations/0173_device_oob_ip4_device_oob_ip6.py b/netbox/dcim/migrations/0173_device_oob_ip4_device_oob_ip6.py deleted file mode 100644 index 15d1403abf4..00000000000 --- a/netbox/dcim/migrations/0173_device_oob_ip4_device_oob_ip6.py +++ /dev/null @@ -1,25 +0,0 @@ -# Generated by Django 4.1.9 on 2023-06-26 21:06 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('ipam', '0066_iprange_mark_utilized'), - ('dcim', '0172_larger_power_draw_values'), - ] - - operations = [ - migrations.AddField( - model_name='device', - name='oob_ip4', - field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='ipam.ipaddress'), - ), - migrations.AddField( - model_name='device', - name='oob_ip6', - field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='ipam.ipaddress'), - ), - ] diff --git a/netbox/dcim/migrations/0175_device_oob_ip.py b/netbox/dcim/migrations/0175_device_oob_ip.py new file mode 100644 index 00000000000..bf6a88ba87a --- /dev/null +++ b/netbox/dcim/migrations/0175_device_oob_ip.py @@ -0,0 +1,25 @@ +# Generated by Django 4.1.9 on 2023-07-24 20:29 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + dependencies = [ + ('ipam', '0066_iprange_mark_utilized'), + ('dcim', '0174_rack_starting_unit'), + ] + + operations = [ + migrations.AddField( + model_name='device', + name='oob_ip', + field=models.OneToOneField( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name='+', + to='ipam.ipaddress', + ), + ), + ] diff --git a/netbox/dcim/models/devices.py b/netbox/dcim/models/devices.py index e2c98b9868b..73199638c1f 100644 --- a/netbox/dcim/models/devices.py +++ b/netbox/dcim/models/devices.py @@ -591,21 +591,13 @@ class Device(PrimaryModel, ConfigContextModel): null=True, verbose_name='Primary IPv6' ) - oob_ip4 = models.OneToOneField( + oob_ip = models.OneToOneField( to='ipam.IPAddress', on_delete=models.SET_NULL, related_name='+', blank=True, null=True, - verbose_name='OOB IPv4' - ) - oob_ip6 = models.OneToOneField( - to='ipam.IPAddress', - on_delete=models.SET_NULL, - related_name='+', - blank=True, - null=True, - verbose_name='OOB IPv6' + verbose_name='OOB IP' ) cluster = models.ForeignKey( to='virtualization.Cluster', @@ -820,31 +812,14 @@ def clean(self): }) # OOB ip validation - if self.oob_ip4: - if self.oob_ip4.family != 4: - raise ValidationError({ - 'oob_ip4': f"{self.oob_ip4} is not an IPv4 address." - }) - if self.oob_ip4.assigned_object in vc_interfaces: + if self.oob_ip: + if self.oob_ip.assigned_object in vc_interfaces: pass - elif self.oob_ip4.nat_inside is not None and self.oob_ip4.nat_inside.assigned_object in vc_interfaces: + elif self.oob_ip.nat_inside is not None and self.oob_ip.nat_inside.assigned_object in vc_interfaces: pass else: raise ValidationError({ - 'oob_ip4': f"The specified IP address ({self.oob_ip4}) is not assigned to this device." - }) - if self.oob_ip6: - if self.oob_ip6.family != 6: - raise ValidationError({ - 'oob_ip6': f"{self.oob_ip6} is not an IPv6 address." - }) - if self.oob_ip6.assigned_object in vc_interfaces: - pass - elif self.oob_ip6.nat_inside is not None and self.oob_ip6.nat_inside.assigned_object in vc_interfaces: - pass - else: - raise ValidationError({ - 'oob_ip6': f"The specified IP address ({self.oob_ip6}) is not assigned to this device." + '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: @@ -956,17 +931,6 @@ def primary_ip(self): else: return None - @property - def oob_ip(self): - if ConfigItem('PREFER_IPV4')() and self.oob_ip4: - return self.oob_ip4 - elif self.oob_ip6: - return self.oob_ip6 - elif self.oob_ip4: - return self.oob_ip4 - else: - return None - @property def interfaces_count(self): return self.vc_interfaces().count() diff --git a/netbox/dcim/tables/devices.py b/netbox/dcim/tables/devices.py index 883181ae8ba..7b38577ceb6 100644 --- a/netbox/dcim/tables/devices.py +++ b/netbox/dcim/tables/devices.py @@ -203,16 +203,7 @@ class DeviceTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable): ) oob_ip = tables.Column( linkify=True, - order_by=('oob_ip4', 'oob_ip6'), - verbose_name='OOB IP Address' - ) - oob_ip4 = tables.Column( - linkify=True, - verbose_name='OOB IPv4 Address' - ) - oob_ip6 = tables.Column( - linkify=True, - verbose_name='OOB IPv6 Address' + verbose_name='OOB IP' ) cluster = tables.Column( linkify=True diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index c751935d769..bec23d2f32b 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -2456,7 +2456,7 @@ def get_extra_context(self, request, instance): 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', 'oob_ip4', 'oob_ip6'), + 'created', 'last_updated', 'actions', 'oob_ip'), orderable=False ) diff --git a/netbox/ipam/models/ip.py b/netbox/ipam/models/ip.py index 8b3499c9c3d..ff5f796b4df 100644 --- a/netbox/ipam/models/ip.py +++ b/netbox/ipam/models/ip.py @@ -853,14 +853,9 @@ def family(self): def is_oob_ip(self): if self.assigned_object: parent = getattr(self.assigned_object, 'parent_object', None) - if self.family == 4: - if parent.oob_ip4: - if parent.oob_ip4.pk == self.pk: - return True - if self.family == 6: - if parent.oob_ip6: - if parent.oob_ip6.pk == self.pk: - return True + if parent.oob_ip: + if parent.oob_ip.pk == self.pk: + return True return False @property diff --git a/netbox/templates/dcim/device.html b/netbox/templates/dcim/device.html index bdfa1e617f0..a43424fe6d2 100644 --- a/netbox/templates/dcim/device.html +++ b/netbox/templates/dcim/device.html @@ -240,30 +240,16 @@
Management
- OOB IPv4 + OOB IP - {% if object.oob_ip4 %} - {{ object.oob_ip4.address.ip }} - {% if object.oob_ip4.nat_inside %} - (NAT for {{ object.oob_ip4.nat_inside.address.ip }}) - {% elif object.oob_ip4.nat_outside.exists %} - (NAT: {% for nat in object.oob_ip4.nat_outside.all %}{{ nat.address.ip }}{% if not forloop.last %}, {% endif %}{% endfor %}) - {% endif %} - {% else %} - {{ ''|placeholder }} - {% endif %} - - - - OOB IPv6 - - {% if object.oob_ip6 %} - {{ object.oob_ip6.address.ip }} - {% if object.oob_ip6.nat_inside %} - (NAT for {{ object.oob_ip6.nat_inside.address.ip }}) - {% elif object.oob_ip6.nat_outside.exists %} - (NAT: {% for nat in object.oob_ip6.nat_outside.all %}{{ nat.address.ip }}{% if not forloop.last %}, {% endif %}{% endfor %}) + {% if object.oob_ip %} + {{ object.oob_ip.address.ip }} + {% if object.oob_ip.nat_inside %} + (NAT for {{ object.oob_ip.nat_inside.address.ip }}) + {% elif object.oob_ip.nat_outside.exists %} + (NAT: {% for nat in object.oob_ip.nat_outside.all %}{{ nat.address.ip }}{% if not forloop.last %}, {% endif %}{% endfor %}) {% endif %} + {% copy_content "oob_ip" %} {% else %} {{ ''|placeholder }} {% endif %} diff --git a/netbox/templates/dcim/device_edit.html b/netbox/templates/dcim/device_edit.html index d54f57b9d1e..4029f502618 100644 --- a/netbox/templates/dcim/device_edit.html +++ b/netbox/templates/dcim/device_edit.html @@ -68,8 +68,7 @@
Management
{% if object.pk %} {% render_field form.primary_ip4 %} {% render_field form.primary_ip6 %} - {% render_field form.oob_ip4 %} - {% render_field form.oob_ip6 %} + {% render_field form.oob_ip %} {% endif %} From bdd7afb875a8596df6a5cb05d192a99bf68ba2e0 Mon Sep 17 00:00:00 2001 From: Jamie Murphy Date: Mon, 24 Jul 2023 22:07:31 +0100 Subject: [PATCH 7/9] fix oob ip signal --- netbox/ipam/signals.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/ipam/signals.py b/netbox/ipam/signals.py index d29ba0dc40b..ce4357178b0 100644 --- a/netbox/ipam/signals.py +++ b/netbox/ipam/signals.py @@ -70,7 +70,7 @@ def clear_oob_ip(instance, **kwargs): When an IPAddress is deleted, trigger save() on any Devices/VirtualMachines for which it was a OOB IP. """ - field_name = f'oob_ip{instance.family}' + field_name = f'oob_ip' device = Device.objects.filter(**{field_name: instance}).first() if device: device.save() From ea0f1bda4816ff4be4d7139afe23cf1a9e17fdb5 Mon Sep 17 00:00:00 2001 From: Jamie Murphy Date: Mon, 24 Jul 2023 22:26:16 +0100 Subject: [PATCH 8/9] string capitalisation --- netbox/dcim/forms/model_forms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/dcim/forms/model_forms.py b/netbox/dcim/forms/model_forms.py index 4a10fb22228..2518f0a05a2 100644 --- a/netbox/dcim/forms/model_forms.py +++ b/netbox/dcim/forms/model_forms.py @@ -486,7 +486,7 @@ def __init__(self, *args, **kwargs): 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)) + 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 From 9550f60a6903e0439df258f70316e8363a843a99 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 25 Jul 2023 14:17:26 -0400 Subject: [PATCH 9/9] Misc cleanup --- docs/models/dcim/device.md | 4 ++++ netbox/dcim/api/serializers.py | 20 +++++++++---------- netbox/dcim/filtersets.py | 2 +- netbox/dcim/forms/filtersets.py | 2 +- netbox/dcim/forms/model_forms.py | 7 +++---- ...s.py => 0176_device_component_counters.py} | 2 +- netbox/dcim/models/devices.py | 7 +++---- netbox/dcim/tables/devices.py | 4 ++-- netbox/dcim/views.py | 8 +++++--- netbox/ipam/models/ip.py | 17 ++++++---------- netbox/ipam/signals.py | 16 +++++---------- netbox/templates/dcim/device.html | 7 +------ 12 files changed, 42 insertions(+), 54 deletions(-) rename netbox/dcim/migrations/{0175_device_component_counters.py => 0176_device_component_counters.py} (98%) diff --git a/docs/models/dcim/device.md b/docs/models/dcim/device.md index 2216e351c75..c9f05cd93b1 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 7f7249e87d6..edcb6401919 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -687,11 +687,11 @@ class Meta: 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 Meta(DeviceSerializer.Meta): 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 b441d6673a9..e88fc120dc2 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 2c5dcd339fe..06d38627d64 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 2518f0a05a2..067cf2bda6f 100644 --- a/netbox/dcim/forms/model_forms.py +++ b/netbox/dcim/forms/model_forms.py @@ -449,9 +449,9 @@ class Meta: 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 @@ def __init__(self, *args, **kwargs): 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 @@ def __init__(self, *args, **kwargs): 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 9d033c103cf..fc22de81b62 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 b010250f17b..76100197b7b 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 @@ def clean(self): 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 @@ def clean(self): 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 @@ def clean(self): 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 7f74f2ba73e..c2651e4da10 100644 --- a/netbox/dcim/tables/devices.py +++ b/netbox/dcim/tables/devices.py @@ -271,8 +271,8 @@ class Meta(NetBoxTable.Meta): '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 cf753e259f5..1d46de23172 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 ff5f796b4df..a5d6eb08441 100644 --- a/netbox/ipam/models/ip.py +++ b/netbox/ipam/models/ip.py @@ -853,23 +853,18 @@ def family(self): 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 ce4357178b0..2a985c2940a 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 a43424fe6d2..4d1e3dc0817 100644 --- a/netbox/templates/dcim/device.html +++ b/netbox/templates/dcim/device.html @@ -240,15 +240,10 @@
Management
- OOB IP + Out-of-band IP {% if object.oob_ip %} {{ object.oob_ip.address.ip }} - {% if object.oob_ip.nat_inside %} - (NAT for {{ object.oob_ip.nat_inside.address.ip }}) - {% elif object.oob_ip.nat_outside.exists %} - (NAT: {% for nat in object.oob_ip.nat_outside.all %}{{ nat.address.ip }}{% if not forloop.last %}, {% endif %}{% endfor %}) - {% endif %} {% copy_content "oob_ip" %} {% else %} {{ ''|placeholder }}