Repository: netbox-community/netbox
Pull request: fix error for is_oob_ip for non-device parents
<!--
    Thank you for your interest in contributing to NetBox! Please note that
    our contribution policy requires that a feature request or bug report be
    approved and assigned prior to opening a pull request. This helps avoid
    waste time and effort on a proposed change that we might not be able to
    accept.

    IF YOUR PULL REQUEST DOES NOT REFERENCE AN ISSUE WHICH HAS BEEN ASSIGNED
    TO YOU, IT WILL BE CLOSED AUTOMATICALLY.

    Please specify your assigned issue number on the line below.
-->
### Fixes: #13619

IP addresses assigned to non devices (eg virtual machines) would error during rendering as oob_ip_id did not exist on them. Adjusted the code to check if attribute exists first


Problem statement:
Error when clicking on the "IP Address" field in the VM list
### NetBox version

v3.6.0

### Python version

3.11

### Steps to Reproduce

* Open the "Virtual Machines" list view
* Click on an arbitrary IP address of a VM

### Expected Behavior

The detail view for the IP address is opened.


### Observed Behavior

An exception is raised:

```
Traceback (most recent call last):
 File "/opt/netbox/lib64/python3.11/site-packages/django/template/base.py", line 880, in _resolve_lookup
   current = current[bit]
             ^^^^^^^^^^^^

During handling of the above exception ('IPAddress' object is not subscriptable), another exception occurred:
 File "/opt/netbox/lib64/python3.11/site-packages/django/core/handlers/exception.py", line 55, in inner
   response = get_response(request)
              ^^^^^^^^^^^^^^^^^^^^^
 File "/opt/netbox/lib64/python3.11/site-packages/django/core/handlers/base.py", line 197, in _get_response
   response = wrapped_callback(request, *callback_args, **callback_kwargs)
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 File "/opt/netbox/lib64/python3.11/site-packages/django/views/generic/base.py", line 104, in view
   return self.dispatch(request, *args, **kwargs)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 File "/opt/netbox/netbox/netbox/views/generic/base.py", line 26, in dispatch
   return super().dispatch(request, *args, **kwargs)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 File "/opt/netbox/netbox/utilities/views.py", line 99, in dispatch
   return super().dispatch(request, *args, **kwargs)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 File "/opt/netbox/lib64/python3.11/site-packages/django/views/generic/base.py", line 143, in dispatch
   return handler(request, *args, **kwargs)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 File "/opt/netbox/netbox/netbox/views/generic/object_views.py", line 70, in get
   return render(request, self.get_template_name(), {

 File "/opt/netbox/lib64/python3.11/site-packages/django/shortcuts.py", line 24, in render
   content = loader.render_to_string(template_name, context, request, using=using)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 File "/opt/netbox/lib64/python3.11/site-packages/django/template/loader.py", line 62, in render_to_string
   return template.render(context, request)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 File "/opt/netbox/lib64/python3.11/site-packages/django/template/backends/django.py", line 61, in render
   return self.template.render(context)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 File "/opt/netbox/lib64/python3.11/site-packages/django/template/base.py", line 175, in render
   return self._render(context)
          ^^^^^^^^^^^^^^^^^^^^^
 File "/opt/netbox/lib64/python3.11/site-packages/django/test/utils.py", line 112, in instrumented_test_render
   return self.nodelist.render(context)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 File "/opt/netbox/lib64/python3.11/site-packages/django/template/base.py", line 1005, in render
   return SafeString("".join([node.render_annotated(context) for node in self]))
                             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 File "/opt/netbox/lib64/python3.11/site-packages/django/template/base.py", line 1005, in <listcomp>
   return SafeString("".join([node.render_annotated(context) for node in self]))
                              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 File "/opt/netbox/lib64/python3.11/site-packages/django/template/base.py", line 966, in render_annotated
   return self.render(context)
          ^^^^^^^^^^^^^^^^^^^^
 File "/opt/netbox/lib64/python3.11/site-packages/django/template/loader_tags.py", line 157, in render
   return compiled_parent._render(context)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 File "/opt/netbox/lib64/python3.11/site-packages/django/test/utils.py", line 112, in instrumented_test_render
   return self.nodelist.render(context)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 File "/opt/netbox/lib64/python3.11/site-packages/django/template/base.py", line 1005, in render
   return SafeString("".join([node.render_annotated(context) for node in self]))
                             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 File "/opt/netbox/lib64/python3.11/site-packages/django/template/base.py", line 1005, in <listcomp>
   return SafeString("".join([node.render_annotated(context) for node in self]))
                              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 File "/opt/netbox/lib64/python3.11/site-packages/django/template/base.py", line 966, in render_annotated
   return self.render(context)
          ^^^^^^^^^^^^^^^^^^^^
 File "/opt/netbox/lib64/python3.11/site-packages/django/template/loader_tags.py", line 157, in render
   return compiled_parent._render(context)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 File "/opt/netbox/lib64/python3.11/site-packages/django/test/utils.py", line 112, in instrumented_test_render
   return self.nodelist.render(context)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 File "/opt/netbox/lib64/python3.11/site-packages/django/template/base.py", line 1005, in render
   return SafeString("".join([node.render_annotated(context) for node in self]))
                             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 File "/opt/netbox/lib64/python3.11/site-packages/django/template/base.py", line 1005, in <listcomp>
   return SafeString("".join([node.render_annotated(context) for node in self]))
                              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 File "/opt/netbox/lib64/python3.11/site-packages/django/template/base.py", line 966, in render_annotated
   return self.render(context)
          ^^^^^^^^^^^^^^^^^^^^
 File "/opt/netbox/lib64/python3.11/site-packages/django/template/loader_tags.py", line 157, in render
   return compiled_parent._render(context)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 File "/opt/netbox/lib64/python3.11/site-packages/django/test/utils.py", line 112, in instrumented_test_render
   return self.nodelist.render(context)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 File "/opt/netbox/lib64/python3.11/site-packages/django/template/base.py", line 1005, in render
   return SafeString("".join([node.render_annotated(context) for node in self]))
                             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 File "/opt/netbox/lib64/python3.11/site-packages/django/template/base.py", line 1005, in <listcomp>
   return SafeString("".join([node.render_annotated(context) for node in self]))
                              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 File "/opt/netbox/lib64/python3.11/site-packages/django/template/base.py", line 966, in render_annotated
   return self.render(context)
          ^^^^^^^^^^^^^^^^^^^^
 File "/opt/netbox/lib64/python3.11/site-packages/django/template/loader_tags.py", line 63, in render
   result = block.nodelist.render(context)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 File "/opt/netbox/lib64/python3.11/site-packages/django/template/base.py", line 1005, in render
   return SafeString("".join([node.render_annotated(context) for node in self]))
                             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 File "/opt/netbox/lib64/python3.11/site-packages/django/template/base.py", line 1005, in <listcomp>
   return SafeString("".join([node.render_annotated(context) for node in self]))
                              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 File "/opt/netbox/lib64/python3.11/site-packages/django/template/base.py", line 966, in render_annotated
   return self.render(context)
          ^^^^^^^^^^^^^^^^^^^^
 File "/opt/netbox/lib64/python3.11/site-packages/django/template/loader_tags.py", line 63, in render
   result = block.nodelist.render(context)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 File "/opt/netbox/lib64/python3.11/site-packages/django/template/base.py", line 1005, in render
   return SafeString("".join([node.render_annotated(context) for node in self]))
                             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 File "/opt/netbox/lib64/python3.11/site-packages/django/template/base.py", line 1005, in <listcomp>
   return SafeString("".join([node.render_annotated(context) for node in self]))
                              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 File "/opt/netbox/lib64/python3.11/site-packages/django/template/base.py", line 966, in render_annotated
   return self.render(context)
          ^^^^^^^^^^^^^^^^^^^^
 File "/opt/netbox/lib64/python3.11/site-packages/django/template/loader_tags.py", line 63, in render
   result = block.nodelist.render(context)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 File "/opt/netbox/lib64/python3.11/site-packages/django/template/base.py", line 1005, in render
   return SafeString("".join([node.render_annotated(context) for node in self]))
                             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 File "/opt/netbox/lib64/python3.11/site-packages/django/template/base.py", line 1005, in <listcomp>
   return SafeString("".join([node.render_annotated(context) for node in self]))
                              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 File "/opt/netbox/lib64/python3.11/site-packages/django/template/base.py", line 966, in render_annotated
   return self.render(context)
          ^^^^^^^^^^^^^^^^^^^^
 File "/opt/netbox/lib64/python3.11/site-packages/django/template/library.py", line 257, in render
   resolved_args, resolved_kwargs = self.get_resolved_arguments(context)
                                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 File "/opt/netbox/lib64/python3.11/site-packages/django/template/library.py", line 221, in get_resolved_arguments
   resolved_args = [var.resolve(context) for var in self.args]
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 File "/opt/netbox/lib64/python3.11/site-packages/django/template/library.py", line 221, in <listcomp>
   resolved_args = [var.resolve(context) for var in self.args]
                    ^^^^^^^^^^^^^^^^^^^^
 File "/opt/netbox/lib64/python3.11/site-packages/django/template/base.py", line 715, in resolve
   obj = self.var.resolve(context)
         ^^^^^^^^^^^^^^^^^^^^^^^^^
 File "/opt/netbox/lib64/python3.11/site-packages/django/template/base.py", line 847, in resolve
   value = self._resolve_lookup(context)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 File "/opt/netbox/lib64/python3.11/site-packages/django/template/base.py", line 890, in _resolve_lookup
   current = getattr(current, bit)
             ^^^^^^^^^^^^^^^^^^^^^
 File "/opt/netbox/netbox/ipam/models/ip.py", line 895, in is_oob_ip
   if parent.oob_ip_id == self.pk:
      ^^^^^^^^^^^^^^^^

Exception Type: AttributeError at /ipam/ip-addresses/10/
Exception Value: 'VirtualMachine' object has no attribute 'oob_ip_id'
Raised during: ipam.views.IPAddressView
```

Hints from issue discussion:
```
    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
```

additional logic will be needed here to check if the parent is a device before calling parent.oob_ip_id

Commit message: fix error for is_oob_ip for non-device parents

Patch to review:
diff --git a/netbox/ipam/models/ip.py b/netbox/ipam/models/ip.py
index 553f5eb9212..a0531c02821 100644
--- a/netbox/ipam/models/ip.py
+++ b/netbox/ipam/models/ip.py
@@ -892,7 +892,7 @@ def family(self):
     def is_oob_ip(self):
         if self.assigned_object:
             parent = getattr(self.assigned_object, 'parent_object', None)
-            if parent.oob_ip_id == self.pk:
+            if parent.__class__.__name__ == "Device" and parent.oob_ip_id == self.pk:
                 return True
         return False
 


Code review comments:
diff_hunk: @@ -892,7 +892,7 @@ def family(self):
     def is_oob_ip(self):
         if self.assigned_object:
             parent = getattr(self.assigned_object, 'parent_object', None)
-            if parent.oob_ip_id == self.pk:
+            if parent.__class__.__name__ == "Device" and parent.oob_ip_id == self.pk:
path: netbox/ipam/models/ip.py
text: @user1:
I think it'd be much cleaner to use hasattr(parent, oob_ip_id)

@user2:
Also, when checking the type of an object, a string comparison is never the right option. Use isinstance.

@author:
adjusted to hasattr. it worked as expected locally