Explorar o código

Oob ip (devices) (#13013)

* initial oob_ip support for devices

* add primary ip and oob ip checkmark to ip address view

* add oob ip to device view and device edit view

* pep8

* make is_oob_ip and is_primary_ip generic for other models

* refactor oob_ip

* fix oob ip signal

* string capitalisation

* Misc cleanup

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
Jamie (Bear) Murphy %!s(int64=2) %!d(string=hai) anos
pai
achega
154b8236a2

+ 4 - 0
docs/models/dcim/device.md

@@ -87,6 +87,10 @@ Each device may designate one primary IPv4 address and/or one primary IPv6 addre
 !!! tip
 !!! tip
     NetBox will prefer IPv6 addresses over IPv4 addresses by default. This can be changed by setting the `PREFER_IPV4` configuration parameter.
     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
 ### 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.)
 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.)

+ 11 - 10
netbox/dcim/api/serializers.py

@@ -663,6 +663,7 @@ class DeviceSerializer(NetBoxModelSerializer):
     primary_ip = NestedIPAddressSerializer(read_only=True)
     primary_ip = NestedIPAddressSerializer(read_only=True)
     primary_ip4 = NestedIPAddressSerializer(required=False, allow_null=True)
     primary_ip4 = NestedIPAddressSerializer(required=False, allow_null=True)
     primary_ip6 = NestedIPAddressSerializer(required=False, allow_null=True)
     primary_ip6 = NestedIPAddressSerializer(required=False, allow_null=True)
+    oob_ip = NestedIPAddressSerializer(required=False, allow_null=True)
     parent_device = serializers.SerializerMethodField()
     parent_device = serializers.SerializerMethodField()
     cluster = NestedClusterSerializer(required=False, allow_null=True)
     cluster = NestedClusterSerializer(required=False, allow_null=True)
     virtual_chassis = NestedVirtualChassisSerializer(required=False, allow_null=True, default=None)
     virtual_chassis = NestedVirtualChassisSerializer(required=False, allow_null=True, default=None)
@@ -686,11 +687,11 @@ class DeviceSerializer(NetBoxModelSerializer):
         fields = [
         fields = [
             'id', 'url', 'display', 'name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag',
             'id', 'url', 'display', 'name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag',
             'site', 'location', 'rack', 'position', 'face', 'latitude', 'longitude', 'parent_device', 'status',
             'site', 'location', 'rack', 'position', 'face', 'latitude', 'longitude', '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', '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)
     @extend_schema_field(NestedDeviceSerializer)
@@ -712,11 +713,11 @@ class DeviceWithConfigContextSerializer(DeviceSerializer):
         fields = [
         fields = [
             'id', 'url', 'display', 'name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag',
             'id', 'url', 'display', 'name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag',
             'site', 'location', 'rack', 'position', 'face', 'parent_device', 'status', 'airflow', 'primary_ip',
             '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', '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))
     @extend_schema_field(serializers.JSONField(allow_null=True))

+ 15 - 0
netbox/dcim/filtersets.py

@@ -941,6 +941,10 @@ class DeviceFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilter
         method='_has_primary_ip',
         method='_has_primary_ip',
         label=_('Has a primary IP'),
         label=_('Has a primary IP'),
     )
     )
+    has_oob_ip = django_filters.BooleanFilter(
+        method='_has_oob_ip',
+        label=_('Has an out-of-band IP'),
+    )
     virtual_chassis_id = django_filters.ModelMultipleChoiceFilter(
     virtual_chassis_id = django_filters.ModelMultipleChoiceFilter(
         field_name='virtual_chassis',
         field_name='virtual_chassis',
         queryset=VirtualChassis.objects.all(),
         queryset=VirtualChassis.objects.all(),
@@ -996,6 +1000,11 @@ class DeviceFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilter
         queryset=IPAddress.objects.all(),
         queryset=IPAddress.objects.all(),
         label=_('Primary IPv6 (ID)'),
         label=_('Primary IPv6 (ID)'),
     )
     )
+    oob_ip_id = django_filters.ModelMultipleChoiceFilter(
+        field_name='oob_ip',
+        queryset=IPAddress.objects.all(),
+        label=_('OOB IP (ID)'),
+    )
 
 
     class Meta:
     class Meta:
         model = Device
         model = Device
@@ -1020,6 +1029,12 @@ class DeviceFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilter
             return queryset.filter(params)
             return queryset.filter(params)
         return queryset.exclude(params)
         return queryset.exclude(params)
 
 
+    def _has_oob_ip(self, queryset, name, value):
+        params = Q(oob_ip__isnull=False)
+        if value:
+            return queryset.filter(params)
+        return queryset.exclude(params)
+
     def _virtual_chassis_member(self, queryset, name, value):
     def _virtual_chassis_member(self, queryset, name, value):
         return queryset.exclude(virtual_chassis__isnull=value)
         return queryset.exclude(virtual_chassis__isnull=value)
 
 

+ 8 - 1
netbox/dcim/forms/filtersets.py

@@ -629,7 +629,7 @@ class DeviceFilterForm(
         ('Components', (
         ('Components', (
             'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces', 'pass_through_ports',
             '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(
     region_id = DynamicModelMultipleChoiceField(
         queryset=Region.objects.all(),
         queryset=Region.objects.all(),
@@ -723,6 +723,13 @@ class DeviceFilterForm(
             choices=BOOLEAN_WITH_BLANK_CHOICES
             choices=BOOLEAN_WITH_BLANK_CHOICES
         )
         )
     )
     )
+    has_oob_ip = forms.NullBooleanField(
+        required=False,
+        label='Has an OOB IP',
+        widget=forms.Select(
+            choices=BOOLEAN_WITH_BLANK_CHOICES
+        )
+    )
     virtual_chassis_member = forms.NullBooleanField(
     virtual_chassis_member = forms.NullBooleanField(
         required=False,
         required=False,
         label='Virtual chassis member',
         label='Virtual chassis member',

+ 7 - 2
netbox/dcim/forms/model_forms.py

@@ -449,9 +449,9 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
         model = Device
         model = Device
         fields = [
         fields = [
             'name', 'device_role', 'device_type', 'serial', 'asset_tag', 'site', 'rack', 'location', 'position', 'face',
             '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',
             'tenant_group', 'tenant', 'virtual_chassis', 'vc_position', 'vc_priority', 'description', 'config_template',
-            'comments', 'tags', 'local_context_data'
+            'comments', 'tags', 'local_context_data',
         ]
         ]
 
 
     def __init__(self, *args, **kwargs):
     def __init__(self, *args, **kwargs):
@@ -460,6 +460,7 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
         if self.instance.pk:
         if self.instance.pk:
 
 
             # Compile list of choices for primary IPv4 and IPv6 addresses
             # Compile list of choices for primary IPv4 and IPv6 addresses
+            oob_ip_choices = [(None, '---------')]
             for family in [4, 6]:
             for family in [4, 6]:
                 ip_choices = [(None, '---------')]
                 ip_choices = [(None, '---------')]
 
 
@@ -475,6 +476,7 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
                 if interface_ips:
                 if interface_ips:
                     ip_list = [(ip.id, f'{ip.address} ({ip.assigned_object})') for ip in interface_ips]
                     ip_list = [(ip.id, f'{ip.address} ({ip.assigned_object})') for ip in interface_ips]
                     ip_choices.append(('Interface IPs', ip_list))
                     ip_choices.append(('Interface IPs', ip_list))
+                    oob_ip_choices.extend(ip_list)
                 # Collect NAT IPs
                 # Collect NAT IPs
                 nat_ips = IPAddress.objects.prefetch_related('nat_inside').filter(
                 nat_ips = IPAddress.objects.prefetch_related('nat_inside').filter(
                     address__family=family,
                     address__family=family,
@@ -485,6 +487,7 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
                     ip_list = [(ip.id, f'{ip.address} (NAT)') for ip in nat_ips]
                     ip_list = [(ip.id, f'{ip.address} (NAT)') for ip in nat_ips]
                     ip_choices.append(('NAT IPs', ip_list))
                     ip_choices.append(('NAT IPs', ip_list))
                 self.fields['primary_ip{}'.format(family)].choices = ip_choices
                 self.fields['primary_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
             # 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.
             # can be flipped from one face to another.
@@ -504,6 +507,8 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
             self.fields['primary_ip4'].widget.attrs['readonly'] = True
             self.fields['primary_ip4'].widget.attrs['readonly'] = True
             self.fields['primary_ip6'].choices = []
             self.fields['primary_ip6'].choices = []
             self.fields['primary_ip6'].widget.attrs['readonly'] = True
             self.fields['primary_ip6'].widget.attrs['readonly'] = True
+            self.fields['oob_ip'].choices = []
+            self.fields['oob_ip'].widget.attrs['readonly'] = True
 
 
         # Rack position
         # Rack position
         position = self.data.get('position') or self.initial.get('position')
         position = self.data.get('position') or self.initial.get('position')

+ 25 - 0
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',
+            ),
+        ),
+    ]

+ 1 - 1
netbox/dcim/migrations/0175_device_component_counters.py → netbox/dcim/migrations/0176_device_component_counters.py

@@ -39,7 +39,7 @@ def recalculate_device_counts(apps, schema_editor):
 
 
 class Migration(migrations.Migration):
 class Migration(migrations.Migration):
     dependencies = [
     dependencies = [
-        ('dcim', '0174_rack_starting_unit'),
+        ('dcim', '0175_device_oob_ip'),
     ]
     ]
 
 
     operations = [
     operations = [

+ 18 - 1
netbox/dcim/models/devices.py

@@ -591,6 +591,14 @@ class Device(PrimaryModel, ConfigContextModel):
         null=True,
         null=True,
         verbose_name='Primary IPv6'
         verbose_name='Primary IPv6'
     )
     )
+    oob_ip = models.OneToOneField(
+        to='ipam.IPAddress',
+        on_delete=models.SET_NULL,
+        related_name='+',
+        blank=True,
+        null=True,
+        verbose_name='Out-of-band IP'
+    )
     cluster = models.ForeignKey(
     cluster = models.ForeignKey(
         to='virtualization.Cluster',
         to='virtualization.Cluster',
         on_delete=models.SET_NULL,
         on_delete=models.SET_NULL,
@@ -816,7 +824,7 @@ class Device(PrimaryModel, ConfigContextModel):
             except DeviceType.DoesNotExist:
             except DeviceType.DoesNotExist:
                 pass
                 pass
 
 
-        # Validate primary IP addresses
+        # Validate primary & OOB IP addresses
         vc_interfaces = self.vc_interfaces(if_master=False)
         vc_interfaces = self.vc_interfaces(if_master=False)
         if self.primary_ip4:
         if self.primary_ip4:
             if self.primary_ip4.family != 4:
             if self.primary_ip4.family != 4:
@@ -844,6 +852,15 @@ class Device(PrimaryModel, ConfigContextModel):
                 raise ValidationError({
                 raise ValidationError({
                     'primary_ip6': f"The specified IP address ({self.primary_ip6}) is not assigned to this device."
                     'primary_ip6': f"The specified IP address ({self.primary_ip6}) is not assigned to this device."
                 })
                 })
+        if self.oob_ip:
+            if self.oob_ip.assigned_object in vc_interfaces:
+                pass
+            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_ip': f"The specified IP address ({self.oob_ip}) is not assigned to this device."
+                })
 
 
         # Validate manufacturer/platform
         # Validate manufacturer/platform
         if hasattr(self, 'device_type') and self.platform:
         if hasattr(self, 'device_type') and self.platform:

+ 6 - 2
netbox/dcim/tables/devices.py

@@ -201,6 +201,10 @@ class DeviceTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
         linkify=True,
         linkify=True,
         verbose_name='IPv6 Address'
         verbose_name='IPv6 Address'
     )
     )
+    oob_ip = tables.Column(
+        linkify=True,
+        verbose_name='OOB IP'
+    )
     cluster = tables.Column(
     cluster = tables.Column(
         linkify=True
         linkify=True
     )
     )
@@ -267,8 +271,8 @@ class DeviceTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
             'pk', 'id', 'name', 'status', 'tenant', 'tenant_group', 'device_role', 'manufacturer', 'device_type',
             'pk', 'id', 'name', 'status', 'tenant', 'tenant_group', 'device_role', 'manufacturer', 'device_type',
             'platform', 'serial', 'asset_tag', 'region', 'site_group', 'site', 'location', 'rack', 'parent_device',
             'platform', 'serial', 'asset_tag', 'region', 'site_group', 'site', 'location', 'rack', 'parent_device',
             'device_bay_position', 'position', 'face', 'latitude', 'longitude', 'airflow', 'primary_ip', 'primary_ip4',
             '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', '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 = (
         default_columns = (
             'pk', 'name', 'status', 'tenant', 'site', 'location', 'rack', 'device_role', 'manufacturer', 'device_type',
             'pk', 'name', 'status', 'tenant', 'site', 'location', 'rack', 'device_role', 'manufacturer', 'device_type',

+ 5 - 3
netbox/dcim/views.py

@@ -2452,11 +2452,13 @@ class InterfaceView(generic.ObjectView):
     queryset = Interface.objects.all()
     queryset = Interface.objects.all()
 
 
     def get_extra_context(self, request, instance):
     def get_extra_context(self, request, instance):
-        # Get assigned VDC's
+        # Get assigned VDCs
         vdc_table = tables.VirtualDeviceContextTable(
         vdc_table = tables.VirtualDeviceContextTable(
             data=instance.vdcs.restrict(request.user, 'view').prefetch_related('device'),
             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', ),
+            exclude=(
+                'tenant', 'tenant_group', 'primary_ip', 'primary_ip4', 'primary_ip6', 'oob_ip', 'comments', 'tags',
+                'created', 'last_updated', 'actions',
+            ),
             orderable=False
             orderable=False
         )
         )
 
 

+ 18 - 0
netbox/ipam/models/ip.py

@@ -849,6 +849,24 @@ class IPAddress(PrimaryModel):
             return self.address.version
             return self.address.version
         return None
         return None
 
 
+    @property
+    def is_oob_ip(self):
+        if self.assigned_object:
+            parent = getattr(self.assigned_object, 'parent_object', None)
+            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 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):
     def _set_mask_length(self, value):
         """
         """
         Expose the IPNetwork object's prefixlen attribute on the parent model so that it can be manipulated directly,
         Expose the IPNetwork object's prefixlen attribute on the parent model so that it can be manipulated directly,

+ 12 - 6
netbox/ipam/signals.py

@@ -52,13 +52,19 @@ def handle_prefix_deleted(instance, **kwargs):
 @receiver(pre_delete, sender=IPAddress)
 @receiver(pre_delete, sender=IPAddress)
 def clear_primary_ip(instance, **kwargs):
 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}'
     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()
         device.save()
-    virtualmachine = VirtualMachine.objects.filter(**{field_name: instance}).first()
-    if virtualmachine:
+    if virtualmachine := VirtualMachine.objects.filter(**{field_name: instance}).first():
         virtualmachine.save()
         virtualmachine.save()
+
+
+@receiver(pre_delete, sender=IPAddress)
+def clear_oob_ip(instance, **kwargs):
+    """
+    When an IPAddress is deleted, trigger save() on any Devices for which it was a OOB IP.
+    """
+    if device := Device.objects.filter(oob_ip=instance).first():
+        device.save()

+ 11 - 0
netbox/templates/dcim/device.html

@@ -239,6 +239,17 @@
                               {% endif %}
                               {% endif %}
                             </td>
                             </td>
                         </tr>
                         </tr>
+                        <tr>
+                            <th scope="row">Out-of-band IP</th>
+                            <td>
+                              {% if object.oob_ip %}
+                                <a href="{{ object.oob_ip.get_absolute_url }}" id="oob_ip">{{ object.oob_ip.address.ip }}</a>
+                                {% copy_content "oob_ip" %}
+                              {% else %}
+                                {{ ''|placeholder }}
+                              {% endif %}
+                            </td>
+                        </tr>
                         {% if object.cluster %}
                         {% if object.cluster %}
                             <tr>
                             <tr>
                                 <th>Cluster</th>
                                 <th>Cluster</th>

+ 1 - 0
netbox/templates/dcim/device_edit.html

@@ -68,6 +68,7 @@
       {% if object.pk %}
       {% if object.pk %}
         {% render_field form.primary_ip4 %}
         {% render_field form.primary_ip4 %}
         {% render_field form.primary_ip6 %}
         {% render_field form.primary_ip6 %}
+        {% render_field form.oob_ip %}
       {% endif %}
       {% endif %}
     </div>
     </div>
     
     

+ 8 - 0
netbox/templates/ipam/ipaddress.html

@@ -96,6 +96,14 @@
                         {% endfor %}
                         {% endfor %}
                       </td>
                       </td>
                   </tr>
                   </tr>
+                  <tr>
+                    <td>Primary IP</td>
+                    <td>{% checkmark object.is_primary_ip %}</td>
+                  </tr>
+                  <tr>
+                    <td>OOB IP</td>
+                    <td>{% checkmark object.is_oob_ip %}</td>
+                  </tr>
               </table>
               </table>
           </div>
           </div>
       </div>
       </div>