Primary IP attribute of a device persists in cache after deleting an IP address #2704

Closed
opened 2025-12-29 18:21:12 +01:00 by adam · 25 comments
Owner

Originally created by @InsaneSplash on GitHub (Jun 28, 2019).

Environment

  • Python version: 3.6.8
  • NetBox version: 2.6.1

Steps to Reproduce

  1. Delete an IP address attached to an interface from within a device displayed interface list.

Expected Behavior

Refresh showing the update device with the IP address removed.

Observed Behavior

An error occurs when trying to view the device at all times.

image

Originally created by @InsaneSplash on GitHub (Jun 28, 2019). ### Environment * Python version: 3.6.8 * NetBox version: 2.6.1 ### Steps to Reproduce 1. Delete an IP address attached to an interface from within a device displayed interface list. ### Expected Behavior Refresh showing the update device with the IP address removed. ### Observed Behavior An error occurs when trying to view the device at all times. ![image](https://user-images.githubusercontent.com/16985649/60335965-62ec1680-999f-11e9-9c75-1d20d84348bc.png)
adam added the type: bugstatus: accepted labels 2025-12-29 18:21:12 +01:00
adam closed this issue 2025-12-29 18:21:12 +01:00
Author
Owner

@InsaneSplash commented on GitHub (Jun 28, 2019):

An update to this, that after about 15min, the page now loads correctly....
maybe a caching issue?

@InsaneSplash commented on GitHub (Jun 28, 2019): An update to this, that after about 15min, the page now loads correctly.... maybe a caching issue?
Author
Owner

@DanSheps commented on GitHub (Jun 28, 2019):

I am unable to reproduce this, please provide reproducible steps.

You can use https://master.netbox.dansheps.com to build test-cases if you need.

@DanSheps commented on GitHub (Jun 28, 2019): I am unable to reproduce this, please provide reproducible steps. You can use https://master.netbox.dansheps.com to build test-cases if you need.
Author
Owner

@InsaneSplash commented on GitHub (Jul 4, 2019):

Sure, Ill do some more testing over the next couple of days and revert.

@InsaneSplash commented on GitHub (Jul 4, 2019): Sure, Ill do some more testing over the next couple of days and revert.
Author
Owner

@InsaneSplash commented on GitHub (Jul 17, 2019):

We had the same issue when deleting an IP Address from an interface on a device. It presets the option to confirm the removal, and then when the page refreshes, the above message is displayed.

@InsaneSplash commented on GitHub (Jul 17, 2019): We had the same issue when deleting an IP Address from an interface on a device. It presets the option to confirm the removal, and then when the page refreshes, the above message is displayed.
Author
Owner

@InsaneSplash commented on GitHub (Jul 17, 2019):

image

@InsaneSplash commented on GitHub (Jul 17, 2019): ![image](https://user-images.githubusercontent.com/16985649/61373755-ef04a600-a89a-11e9-896c-de7dc9831825.png)
Author
Owner

@InsaneSplash commented on GitHub (Jul 17, 2019):

Environment:


Request Method: GET
Request URL: http://10.18.0.130/dcim/devices/275/

Django Version: 2.2.3
Python Version: 3.6.8
Installed Applications:
['django.contrib.admin',
 'django.contrib.auth',
 'django.contrib.contenttypes',
 'django.contrib.sessions',
 'django.contrib.messages',
 'django.contrib.staticfiles',
 'django.contrib.humanize',
 'cacheops',
 'corsheaders',
 'debug_toolbar',
 'django_filters',
 'django_tables2',
 'django_prometheus',
 'mptt',
 'rest_framework',
 'taggit',
 'taggit_serializer',
 'timezone_field',
 'circuits',
 'dcim',
 'ipam',
 'extras',
 'secrets',
 'tenancy',
 'users',
 'utilities',
 'virtualization',
 'drf_yasg',
 'django_rq']
Installed Middleware:
('debug_toolbar.middleware.DebugToolbarMiddleware',
 'django_prometheus.middleware.PrometheusBeforeMiddleware',
 'corsheaders.middleware.CorsMiddleware',
 'django.contrib.sessions.middleware.SessionMiddleware',
 'django.middleware.common.CommonMiddleware',
 'django.middleware.csrf.CsrfViewMiddleware',
 'django.contrib.auth.middleware.AuthenticationMiddleware',
 'django.contrib.messages.middleware.MessageMiddleware',
 'django.middleware.clickjacking.XFrameOptionsMiddleware',
 'django.middleware.security.SecurityMiddleware',
 'utilities.middleware.ExceptionHandlingMiddleware',
 'utilities.middleware.LoginRequiredMiddleware',
 'utilities.middleware.APIVersionMiddleware',
 'extras.middleware.ObjectChangeMiddleware',
 'django_prometheus.middleware.PrometheusAfterMiddleware')


Template error:
In template /opt/netbox/netbox/templates/_base.html, error at line 0
   IPAddress matching query does not exist.
   1 : {% load static %}
   2 : {% load helpers %}
   3 : <!DOCTYPE html>
   4 : <html lang="en">
   5 : <head>
   6 :     <title>{% block title %}Home{% endblock %} - NetBox</title>
   7 :     <link rel="stylesheet" href="{% static 'bootstrap-3.3.7-dist/css/bootstrap.min.css' %}">
   8 :     <link rel="stylesheet" href="{% static 'font-awesome-4.7.0/css/font-awesome.min.css' %}">
   9 :     <link rel="stylesheet" href="{% static 'jquery-ui-1.12.1/jquery-ui.css' %}">
   10 :     <link rel="stylesheet" href="{% static 'select2-4.0.5/css/select2.min.css' %}">


Traceback:

File "/usr/lib/python3.6/site-packages/django/db/models/fields/related_descriptors.py" in __get__
  164.             rel_obj = self.field.get_cached_value(instance)

File "/usr/lib/python3.6/site-packages/django/db/models/fields/mixins.py" in get_cached_value
  13.             return instance._state.fields_cache[cache_name]

During handling of the above exception ('primary_ip4'), another exception occurred:

File "/usr/lib/python3.6/site-packages/django/core/handlers/exception.py" in inner
  34.             response = get_response(request)

File "/usr/lib/python3.6/site-packages/django/core/handlers/base.py" in _get_response
  115.                 response = self.process_exception_by_middleware(e, request)

File "/usr/lib/python3.6/site-packages/django/core/handlers/base.py" in _get_response
  113.                 response = wrapped_callback(request, *callback_args, **callback_kwargs)

File "/usr/lib/python3.6/site-packages/django/views/generic/base.py" in view
  71.             return self.dispatch(request, *args, **kwargs)

File "/usr/lib/python3.6/site-packages/django/contrib/auth/mixins.py" in dispatch
  85.         return super().dispatch(request, *args, **kwargs)

File "/usr/lib/python3.6/site-packages/django/views/generic/base.py" in dispatch
  97.         return handler(request, *args, **kwargs)

File "/opt/netbox/netbox/dcim/views.py" in get
  996.             'show_graphs': show_graphs,

File "/usr/lib/python3.6/site-packages/django/shortcuts.py" in render
  36.     content = loader.render_to_string(template_name, context, request, using=using)

File "/usr/lib/python3.6/site-packages/django/template/loader.py" in render_to_string
  62.     return template.render(context, request)

File "/usr/lib/python3.6/site-packages/django/template/backends/django.py" in render
  61.             return self.template.render(context)

File "/usr/lib/python3.6/site-packages/django/template/base.py" in render
  171.                     return self._render(context)

File "/usr/lib/python3.6/site-packages/django/test/utils.py" in instrumented_test_render
  96.     return self.nodelist.render(context)

File "/usr/lib/python3.6/site-packages/django/template/base.py" in render
  937.                 bit = node.render_annotated(context)

File "/usr/lib/python3.6/site-packages/django/template/base.py" in render_annotated
  904.             return self.render(context)

File "/usr/lib/python3.6/site-packages/django/template/loader_tags.py" in render
  150.             return compiled_parent._render(context)

File "/usr/lib/python3.6/site-packages/django/test/utils.py" in instrumented_test_render
  96.     return self.nodelist.render(context)

File "/usr/lib/python3.6/site-packages/django/template/base.py" in render
  937.                 bit = node.render_annotated(context)

File "/usr/lib/python3.6/site-packages/django/template/base.py" in render_annotated
  904.             return self.render(context)

File "/usr/lib/python3.6/site-packages/django/template/loader_tags.py" in render
  62.                 result = block.nodelist.render(context)

File "/usr/lib/python3.6/site-packages/django/template/base.py" in render
  937.                 bit = node.render_annotated(context)

File "/usr/lib/python3.6/site-packages/django/template/base.py" in render_annotated
  904.             return self.render(context)

File "/usr/lib/python3.6/site-packages/django/template/library.py" in render
  192.         output = self.func(*resolved_args, **resolved_kwargs)

File "/opt/netbox/netbox/extras/templatetags/custom_links.py" in custom_links
  75.     rendered = Environment().from_string(source=template_code).render(**context)

File "/usr/lib/python3.6/site-packages/jinja2/asyncsupport.py" in render
  76.             return original_render(self, *args, **kwargs)

File "/usr/lib/python3.6/site-packages/jinja2/environment.py" in render
  1008.         return self.environment.handle_exception(exc_info, True)

File "/usr/lib/python3.6/site-packages/jinja2/environment.py" in handle_exception
  780.         reraise(exc_type, exc_value, tb)

File "/usr/lib/python3.6/site-packages/jinja2/_compat.py" in reraise
  37.             raise value.with_traceback(tb)

File "<template>" in top-level template code
  1. <source code not available>

File "/usr/lib/python3.6/site-packages/jinja2/environment.py" in getattr
  430.             return getattr(obj, attribute)

File "/opt/netbox/netbox/dcim/models.py" in primary_ip
  1727.         if settings.PREFER_IPV4 and self.primary_ip4:

File "/usr/lib/python3.6/site-packages/django/db/models/fields/related_descriptors.py" in __get__
  178.                 rel_obj = self.get_object(instance)

File "/usr/lib/python3.6/site-packages/django/db/models/fields/related_descriptors.py" in get_object
  298.         return super().get_object(instance)

File "/usr/lib/python3.6/site-packages/django/db/models/fields/related_descriptors.py" in get_object
  145.         return qs.get(self.field.get_reverse_related_filter(instance))

File "/usr/lib/python3.6/site-packages/cacheops/query.py" in get
  356.         return qs._no_monkey.get(qs, *args, **kwargs)

File "/usr/lib/python3.6/site-packages/django/db/models/query.py" in get
  408.                 self.model._meta.object_name

Exception Type: DoesNotExist at /dcim/devices/275/
Exception Value: IPAddress matching query does not exist.

@InsaneSplash commented on GitHub (Jul 17, 2019): ``` Environment: Request Method: GET Request URL: http://10.18.0.130/dcim/devices/275/ Django Version: 2.2.3 Python Version: 3.6.8 Installed Applications: ['django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'django.contrib.humanize', 'cacheops', 'corsheaders', 'debug_toolbar', 'django_filters', 'django_tables2', 'django_prometheus', 'mptt', 'rest_framework', 'taggit', 'taggit_serializer', 'timezone_field', 'circuits', 'dcim', 'ipam', 'extras', 'secrets', 'tenancy', 'users', 'utilities', 'virtualization', 'drf_yasg', 'django_rq'] Installed Middleware: ('debug_toolbar.middleware.DebugToolbarMiddleware', 'django_prometheus.middleware.PrometheusBeforeMiddleware', 'corsheaders.middleware.CorsMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.security.SecurityMiddleware', 'utilities.middleware.ExceptionHandlingMiddleware', 'utilities.middleware.LoginRequiredMiddleware', 'utilities.middleware.APIVersionMiddleware', 'extras.middleware.ObjectChangeMiddleware', 'django_prometheus.middleware.PrometheusAfterMiddleware') Template error: In template /opt/netbox/netbox/templates/_base.html, error at line 0 IPAddress matching query does not exist. 1 : {% load static %} 2 : {% load helpers %} 3 : <!DOCTYPE html> 4 : <html lang="en"> 5 : <head> 6 : <title>{% block title %}Home{% endblock %} - NetBox</title> 7 : <link rel="stylesheet" href="{% static 'bootstrap-3.3.7-dist/css/bootstrap.min.css' %}"> 8 : <link rel="stylesheet" href="{% static 'font-awesome-4.7.0/css/font-awesome.min.css' %}"> 9 : <link rel="stylesheet" href="{% static 'jquery-ui-1.12.1/jquery-ui.css' %}"> 10 : <link rel="stylesheet" href="{% static 'select2-4.0.5/css/select2.min.css' %}"> Traceback: File "/usr/lib/python3.6/site-packages/django/db/models/fields/related_descriptors.py" in __get__ 164. rel_obj = self.field.get_cached_value(instance) File "/usr/lib/python3.6/site-packages/django/db/models/fields/mixins.py" in get_cached_value 13. return instance._state.fields_cache[cache_name] During handling of the above exception ('primary_ip4'), another exception occurred: File "/usr/lib/python3.6/site-packages/django/core/handlers/exception.py" in inner 34. response = get_response(request) File "/usr/lib/python3.6/site-packages/django/core/handlers/base.py" in _get_response 115. response = self.process_exception_by_middleware(e, request) File "/usr/lib/python3.6/site-packages/django/core/handlers/base.py" in _get_response 113. response = wrapped_callback(request, *callback_args, **callback_kwargs) File "/usr/lib/python3.6/site-packages/django/views/generic/base.py" in view 71. return self.dispatch(request, *args, **kwargs) File "/usr/lib/python3.6/site-packages/django/contrib/auth/mixins.py" in dispatch 85. return super().dispatch(request, *args, **kwargs) File "/usr/lib/python3.6/site-packages/django/views/generic/base.py" in dispatch 97. return handler(request, *args, **kwargs) File "/opt/netbox/netbox/dcim/views.py" in get 996. 'show_graphs': show_graphs, File "/usr/lib/python3.6/site-packages/django/shortcuts.py" in render 36. content = loader.render_to_string(template_name, context, request, using=using) File "/usr/lib/python3.6/site-packages/django/template/loader.py" in render_to_string 62. return template.render(context, request) File "/usr/lib/python3.6/site-packages/django/template/backends/django.py" in render 61. return self.template.render(context) File "/usr/lib/python3.6/site-packages/django/template/base.py" in render 171. return self._render(context) File "/usr/lib/python3.6/site-packages/django/test/utils.py" in instrumented_test_render 96. return self.nodelist.render(context) File "/usr/lib/python3.6/site-packages/django/template/base.py" in render 937. bit = node.render_annotated(context) File "/usr/lib/python3.6/site-packages/django/template/base.py" in render_annotated 904. return self.render(context) File "/usr/lib/python3.6/site-packages/django/template/loader_tags.py" in render 150. return compiled_parent._render(context) File "/usr/lib/python3.6/site-packages/django/test/utils.py" in instrumented_test_render 96. return self.nodelist.render(context) File "/usr/lib/python3.6/site-packages/django/template/base.py" in render 937. bit = node.render_annotated(context) File "/usr/lib/python3.6/site-packages/django/template/base.py" in render_annotated 904. return self.render(context) File "/usr/lib/python3.6/site-packages/django/template/loader_tags.py" in render 62. result = block.nodelist.render(context) File "/usr/lib/python3.6/site-packages/django/template/base.py" in render 937. bit = node.render_annotated(context) File "/usr/lib/python3.6/site-packages/django/template/base.py" in render_annotated 904. return self.render(context) File "/usr/lib/python3.6/site-packages/django/template/library.py" in render 192. output = self.func(*resolved_args, **resolved_kwargs) File "/opt/netbox/netbox/extras/templatetags/custom_links.py" in custom_links 75. rendered = Environment().from_string(source=template_code).render(**context) File "/usr/lib/python3.6/site-packages/jinja2/asyncsupport.py" in render 76. return original_render(self, *args, **kwargs) File "/usr/lib/python3.6/site-packages/jinja2/environment.py" in render 1008. return self.environment.handle_exception(exc_info, True) File "/usr/lib/python3.6/site-packages/jinja2/environment.py" in handle_exception 780. reraise(exc_type, exc_value, tb) File "/usr/lib/python3.6/site-packages/jinja2/_compat.py" in reraise 37. raise value.with_traceback(tb) File "<template>" in top-level template code 1. <source code not available> File "/usr/lib/python3.6/site-packages/jinja2/environment.py" in getattr 430. return getattr(obj, attribute) File "/opt/netbox/netbox/dcim/models.py" in primary_ip 1727. if settings.PREFER_IPV4 and self.primary_ip4: File "/usr/lib/python3.6/site-packages/django/db/models/fields/related_descriptors.py" in __get__ 178. rel_obj = self.get_object(instance) File "/usr/lib/python3.6/site-packages/django/db/models/fields/related_descriptors.py" in get_object 298. return super().get_object(instance) File "/usr/lib/python3.6/site-packages/django/db/models/fields/related_descriptors.py" in get_object 145. return qs.get(self.field.get_reverse_related_filter(instance)) File "/usr/lib/python3.6/site-packages/cacheops/query.py" in get 356. return qs._no_monkey.get(qs, *args, **kwargs) File "/usr/lib/python3.6/site-packages/django/db/models/query.py" in get 408. self.model._meta.object_name Exception Type: DoesNotExist at /dcim/devices/275/ Exception Value: IPAddress matching query does not exist. ```
Author
Owner

@DanSheps commented on GitHub (Jul 17, 2019):

Please provide reproducible steps, starting from creating the device, otherwise we cannot action this as it cannot be reproduced

@DanSheps commented on GitHub (Jul 17, 2019): Please provide reproducible steps, starting from creating the device, otherwise we cannot action this as it cannot be reproduced
Author
Owner

@InsaneSplash commented on GitHub (Jul 17, 2019):

  1. View a device's interfaces.
    image

  2. Add an IP Address on to an interface (Primary IP not selected in this screenshot, works, if I select the IP as primary it fails)
    image

  3. Delete IP Address
    image

  4. Error is then displayed.
    image

NOTE: This only happens when the IP is selected as the primary IP.

@InsaneSplash commented on GitHub (Jul 17, 2019): 1. View a device's interfaces. ![image](https://user-images.githubusercontent.com/16985649/61374235-26c01d80-a89c-11e9-9f50-453101a85db2.png) 2. Add an IP Address on to an interface (Primary IP not selected in this screenshot, works, if I select the IP as primary it fails) ![image](https://user-images.githubusercontent.com/16985649/61374569-fcbb2b00-a89c-11e9-8129-e66593fb809d.png) 3. Delete IP Address ![image](https://user-images.githubusercontent.com/16985649/61374630-207e7100-a89d-11e9-82e5-2667f662fb4b.png) 4. Error is then displayed. ![image](https://user-images.githubusercontent.com/16985649/61378521-d8178100-a8a5-11e9-9b55-28043a89be80.png) **NOTE: This only happens when the IP is selected as the primary IP.**
Author
Owner

@DanSheps commented on GitHub (Jul 17, 2019):

NOTE: This only happens when the IP is selected as the primary IP.

In the future, this is a very relevant piece of information. However, I am still not able to reproduce this.

Could you try reproducing this here:

https://master.netbox.dansheps.com

@DanSheps commented on GitHub (Jul 17, 2019): > NOTE: This only happens when the IP is selected as the primary IP. In the future, this is a very relevant piece of information. However, I am still not able to reproduce this. Could you try reproducing this here: https://master.netbox.dansheps.com
Author
Owner

@InsaneSplash commented on GitHub (Jul 22, 2019):

You are correct, I am unable to reproduce this on the demo website. Could it be related to a corrupt DB schema? If required, I could provide remote screen access.

@InsaneSplash commented on GitHub (Jul 22, 2019): You are correct, I am unable to reproduce this on the demo website. Could it be related to a corrupt DB schema? If required, I could provide remote screen access.
Author
Owner

@r3dsn0w commented on GitHub (Jul 29, 2019):

I just got the same error
steps to reproduce:

  • Have a device with an interface, a corresponding ip address which is assigned to this interface and set as the primary ip4 of the device
  • Delete the ip address on the interface
  • Add an ip to the interface -> Assign IP -> Search
  • Dont check the box "Make this the primary IP for the device/VM"
  • Click Update

The solution/workaround is to check the box to set the ip as the primary ip, then no error is returned

@r3dsn0w commented on GitHub (Jul 29, 2019): I just got the same error steps to reproduce: - Have a device with an interface, a corresponding ip address which is assigned to this interface and set as the primary ip4 of the device - Delete the ip address on the interface - Add an ip to the interface -> Assign IP -> Search - **Dont** check the box "Make this the primary IP for the device/VM" - Click Update The solution/workaround is to check the box to set the ip as the primary ip, then no error is returned
Author
Owner

@candlerb commented on GitHub (Aug 28, 2019):

Do you by any chance have any Custom Links defined? I notice from the backtrace:

File "/opt/netbox/netbox/extras/templatetags/custom_links.py" in custom_links
  75.     rendered = Environment().from_string(source=template_code).render(**context)

If so, it's likely that your custom link URL is dependent on the device having a primary IP address, and is raising an exception if it does not.

The workaround is to wrap a condition around your button text (not URL), so that it expands to empty string if the necessary field(s) are not present in the device:

{% if ... %}Button Text Here{% endif %}
@candlerb commented on GitHub (Aug 28, 2019): Do you by any chance have any Custom Links defined? I notice from the backtrace: ``` File "/opt/netbox/netbox/extras/templatetags/custom_links.py" in custom_links 75. rendered = Environment().from_string(source=template_code).render(**context) ``` If so, it's likely that your custom link URL is dependent on the device having a primary IP address, and is raising an exception if it does not. The workaround is to wrap a condition around your button text (not URL), so that it expands to empty string if the necessary field(s) are not present in the device: ``` {% if ... %}Button Text Here{% endif %} ```
Author
Owner

@jeremystretch commented on GitHub (Sep 25, 2019):

I'm not able to replicate this on v2.6.4.

@jeremystretch commented on GitHub (Sep 25, 2019): I'm not able to replicate this on v2.6.4.
Author
Owner

@rkandilarov commented on GitHub (Oct 7, 2019):

@DanSheps , @jeremystretch

I got into the similar error having custom links using {{obj.primary_ip}} even with existence check. I manage to reproduce it several times. Here you may see it at https://master.netbox.dansheps.com/dcim/devices/72/ .

Steps to reproduce:

  • Create custom link in dcim/devices
    • Text: {% if obj.primary_ip is not defined %}undefined{% elif obj.primary_ip is none%}none{% else %}defined with value{% endif %}
    • URL: http://aways-shown-no-jinja.com
  • Create device, and assign it an default IPv4 address on any interface.
  • Delete the ip from the device view, i.e. press the red trash button/icon at the right end of the ip line under the interface line.

I have tested various jinja styles of checking the existence of obj.primary_ip in the Text field but seems not to work. And please excuse me as I'm not python guy if there is dummy reason for this error.

Regards!

@rkandilarov commented on GitHub (Oct 7, 2019): @DanSheps , @jeremystretch I got into the similar error having custom links using {{obj.primary_ip}} even with existence check. I manage to reproduce it several times. Here you may see it at https://master.netbox.dansheps.com/dcim/devices/72/ . Steps to reproduce: - Create custom link in dcim/devices - Text: ```{% if obj.primary_ip is not defined %}undefined{% elif obj.primary_ip is none%}none{% else %}defined with value{% endif %}``` - URL: *http://aways-shown-no-jinja.com* - Create device, and assign it an default IPv4 address on any interface. - Delete the ip from the device view, i.e. press the *red trash button/icon* at the right end of the ip line under the interface line. I have tested various jinja styles of checking the existence of `obj.primary_ip` in the Text field but seems not to work. And please excuse me as I'm not python guy if there is dummy reason for this error. Regards!
Author
Owner

@candlerb commented on GitHub (Oct 7, 2019):

What do you actually see when you do this? At that dansheps link I see the button with text "none", which is what I would expect.

The "Text" must evaluate to exactly empty string to suppress the button and hence the expansion of the URL. If the text evaluates to any other string, then you will get a button with that text as that label, and the URL template will be expanded. And that seems to be what's happening here.

So in real life, you'd do something like this:

Text: {% if obj.primary_ip %}Click Me{% endif %}
URL:  http://{{ obj.primary_ip }}

so that the button is only displayed if obj.primary_ip is not None.

@candlerb commented on GitHub (Oct 7, 2019): What do you actually see when you do this? At that dansheps link I see the button with text "none", which is what I would expect. The "Text" must evaluate to exactly *empty string* to suppress the button and hence the expansion of the URL. If the text evaluates to any other string, then you will get a button with that text as that label, *and* the URL template will be expanded. And that seems to be what's happening here. So in real life, you'd do something like this: ``` Text: {% if obj.primary_ip %}Click Me{% endif %} URL: http://{{ obj.primary_ip }} ``` so that the button is only displayed if `obj.primary_ip` is not None.
Author
Owner

@rkandilarov commented on GitHub (Oct 7, 2019):

@candlerb,

I have prepared new device https://master.netbox.dansheps.com/dcim/devices/73/ you can delete the IP from that view to see the exact error. It is the same error as screenshoted from @InsaneSplash in the very first message in this issue.

Note that in my case the URL is not using jinja, it was just for the test to see the state of the obj.primary_ip variable. So in case all is ok, you'll see undefined link to dummy URL not an error page.

Anyway you'll see what I mean when press the delete trash button.

@rkandilarov commented on GitHub (Oct 7, 2019): @candlerb, I have prepared new device https://master.netbox.dansheps.com/dcim/devices/73/ you can delete the IP from that view to see the exact error. It is the same error as screenshoted from @InsaneSplash in the very first message in this issue. Note that in my case the URL is not using jinja, it was just for the test to see the state of the `obj.primary_ip` variable. So in case all is ok, you'll see *undefined* link to dummy URL not an error page. Anyway you'll see what I mean when press the delete trash button.
Author
Owner

@rkandilarov commented on GitHub (Oct 7, 2019):

Hm.. seems that 15 minute reset on netbox.dansheps.com is clearing the error on the device view, but you can still reproduce the bug and see immediately the error following my steps above. Can this be a caching error?

@rkandilarov commented on GitHub (Oct 7, 2019): Hm.. seems that 15 minute reset on netbox.dansheps.com is clearing the error on the device view, but you can still reproduce the bug and see immediately the error following my steps above. Can this be a caching error?
Author
Owner

@candlerb commented on GitHub (Oct 7, 2019):

I pressed the red Delete button for the device, and it was happy.

image

What should I have done - delete the IP address from the interface?

On device 72, I tried adding an IP address to interface "eth", and then deleting it again. All was fine. However I hadn't marked the address as primary.

So then I added address "1.2.3.4/24" and checked Primary, and then I deleted it. Ah, then I got the error:

image

Refreshing the page https://master.netbox.dansheps.com/dcim/devices/72/ I still see the error. This is about 12:25 UTC.

If it goes after 15 minutes, then clearly it's some caching artefact.

Thanks for making this reproducible!

@candlerb commented on GitHub (Oct 7, 2019): I pressed the red Delete button for the device, and it was happy. ![image](https://user-images.githubusercontent.com/44789/66311034-721b2280-e905-11e9-9ea8-cbba508e94cf.png) What should I have done - delete the IP address from the interface? On device 72, I tried adding an IP address to interface "eth", and then deleting it again. All was fine. However I hadn't marked the address as primary. So then I added address "1.2.3.4/24" and checked Primary, and then I deleted it. Ah, then I got the error: ![image](https://user-images.githubusercontent.com/44789/66311185-cc1be800-e905-11e9-8d2f-ab66e7ba3bcd.png) Refreshing the page https://master.netbox.dansheps.com/dcim/devices/72/ I still see the error. This is about 12:25 UTC. If it goes after 15 minutes, then clearly it's some caching artefact. Thanks for making this reproducible!
Author
Owner

@candlerb commented on GitHub (Oct 7, 2019):

Exception was still present at 12:33 UTC, gone at 12:39 UTC.

So to be clear, the steps to reproduce are:

  • [EDIT] In Admin, create a custom link.
    • Content type: dcim > device
    • Name: LinkUsingDevicesIP
    • Text: {% if obj.primary_ip is not defined %}undefined{% elif obj.primary_ip is none%}none{% else %}defined with value{% endif %}
    • URL: http://aways-shown-no-jinja.com
  • Start with a device than has an interface
  • Next to the interface, click "+", add any IP address (e.g. "1.2.3.4/24") and also click the button "Make this the primary IP". Save; returns to the device view with the IP address shown on the interface
  • Click the Trash icon next to the IP address (to remove the IP address from the interface) and confirm

But there must be a bit more to it than this. I just tried it on my home instance of Netbox, on a device with two interfaces (eth and wifi) where only wifi had an IP address, which was primary. Deleting that address didn't trigger this problem. So either it's a race condition, or it's sensitive to something else about the device type, or type/number of interfaces.

EDIT: I had not created the custom link in my Netbox instance. After creating it, the problem occurs.

@candlerb commented on GitHub (Oct 7, 2019): Exception was still present at 12:33 UTC, gone at 12:39 UTC. So to be clear, the steps to reproduce are: * [EDIT] In Admin, create a custom link. * Content type: `dcim > device` * Name: `LinkUsingDevicesIP` * Text: `{% if obj.primary_ip is not defined %}undefined{% elif obj.primary_ip is none%}none{% else %}defined with value{% endif %}` * URL: `http://aways-shown-no-jinja.com` * Start with a device than has an interface * Next to the interface, click "+", add any IP address (e.g. "1.2.3.4/24") and also click the button "Make this the primary IP". Save; returns to the device view with the IP address shown on the interface * Click the Trash icon next to the IP address (to remove the IP address from the interface) and confirm ~~But there must be a bit more to it than this. I just tried it on my home instance of Netbox, on a device with two interfaces (eth and wifi) where only wifi had an IP address, which was primary. Deleting that address didn't trigger this problem. So either it's a race condition, or it's sensitive to something else about the device type, or type/number of interfaces.~~ EDIT: I had not created the custom link in my Netbox instance. After creating it, the problem occurs.
Author
Owner

@candlerb commented on GitHub (Oct 7, 2019):

It's perfectly repeatable on dansheps.com though (just done it again).

@candlerb commented on GitHub (Oct 7, 2019): It's perfectly repeatable on dansheps.com though (just done it again).
Author
Owner

@candlerb commented on GitHub (Oct 7, 2019):

Note: I first created a device (with an eth but no primary IP), and then added the Custom Link. Then browsing to the device I got the exception below, which at a glance seems consistent with the original one.

So the problem appears to be: device.primary_ip4 can sometimes raise an exception instead of returning None.

Internal Server Error: /dcim/devices/37/

DoesNotExist at /dcim/devices/37/
IPAddress matching query does not exist.

Request Method: GET
Request URL: http://netbox.example.net/dcim/devices/37/
Django Version: 2.2.5
Python Executable: /usr/bin/python3
Python Version: 3.5.2
Python Path: ['/opt/netbox/netbox', '/usr/local/bin', '/usr/lib/python35.zip', '/usr/lib/python3.5', '/usr/lib/python3.5/plat-x86_64-linux-gnu', '/usr/lib/python3.5/lib-dynload', '/usr/local/lib/python3.5/dist-packages', '/usr/lib/python3/dist-packages']
Server time: Mon, 7 Oct 2019 13:17:08 +0000
Installed Applications:
['django.contrib.admin',
 'django.contrib.auth',
 'django.contrib.contenttypes',
 'django.contrib.sessions',
 'django.contrib.messages',
 'django.contrib.staticfiles',
 'django.contrib.humanize',
 'cacheops',
 'corsheaders',
 'debug_toolbar',
 'django_filters',
 'django_tables2',
 'django_prometheus',
 'mptt',
 'rest_framework',
 'taggit',
 'taggit_serializer',
 'timezone_field',
 'circuits',
 'dcim',
 'ipam',
 'extras',
 'secrets',
 'tenancy',
 'users',
 'utilities',
 'virtualization',
 'drf_yasg',
 'django_rq']
Installed Middleware:
('debug_toolbar.middleware.DebugToolbarMiddleware',
 'django_prometheus.middleware.PrometheusBeforeMiddleware',
 'corsheaders.middleware.CorsMiddleware',
 'django.contrib.sessions.middleware.SessionMiddleware',
 'django.middleware.common.CommonMiddleware',
 'django.middleware.csrf.CsrfViewMiddleware',
 'django.contrib.auth.middleware.AuthenticationMiddleware',
 'django.contrib.messages.middleware.MessageMiddleware',
 'django.middleware.clickjacking.XFrameOptionsMiddleware',
 'django.middleware.security.SecurityMiddleware',
 'utilities.middleware.ExceptionHandlingMiddleware',
 'utilities.middleware.LoginRequiredMiddleware',
 'utilities.middleware.APIVersionMiddleware',
 'extras.middleware.ObjectChangeMiddleware',
 'django_prometheus.middleware.PrometheusAfterMiddleware')


Traceback:

File "/usr/local/lib/python3.5/dist-packages/django/db/models/fields/related_descriptors.py" in __get__
  164.             rel_obj = self.field.get_cached_value(instance)

File "/usr/local/lib/python3.5/dist-packages/django/db/models/fields/mixins.py" in get_cached_value
  13.             return instance._state.fields_cache[cache_name]

During handling of the above exception ('primary_ip4'), another exception occurred:

File "/usr/local/lib/python3.5/dist-packages/django/core/handlers/exception.py" in inner
  34.             response = get_response(request)

File "/usr/local/lib/python3.5/dist-packages/django/core/handlers/base.py" in _get_response
  115.                 response = self.process_exception_by_middleware(e, request)

File "/usr/local/lib/python3.5/dist-packages/django/core/handlers/base.py" in _get_response
  113.                 response = wrapped_callback(request, *callback_args, **callback_kwargs)

File "/usr/local/lib/python3.5/dist-packages/django/views/generic/base.py" in view
  71.             return self.dispatch(request, *args, **kwargs)

File "/usr/local/lib/python3.5/dist-packages/django/contrib/auth/mixins.py" in dispatch
  85.         return super().dispatch(request, *args, **kwargs)

File "/usr/local/lib/python3.5/dist-packages/django/views/generic/base.py" in dispatch
  97.         return handler(request, *args, **kwargs)

File "/opt/netbox/netbox/dcim/views.py" in get
  991.             'show_interface_graphs': Graph.objects.filter(type=GRAPH_TYPE_INTERFACE).exists(),

File "/usr/local/lib/python3.5/dist-packages/django/shortcuts.py" in render
  36.     content = loader.render_to_string(template_name, context, request, using=using)

File "/usr/local/lib/python3.5/dist-packages/django/template/loader.py" in render_to_string
  62.     return template.render(context, request)

File "/usr/local/lib/python3.5/dist-packages/django/template/backends/django.py" in render
  61.             return self.template.render(context)

File "/usr/local/lib/python3.5/dist-packages/django/template/base.py" in render
  171.                     return self._render(context)

File "/usr/local/lib/python3.5/dist-packages/django/test/utils.py" in instrumented_test_render
  96.     return self.nodelist.render(context)

File "/usr/local/lib/python3.5/dist-packages/django/template/base.py" in render
  937.                 bit = node.render_annotated(context)

File "/usr/local/lib/python3.5/dist-packages/django/template/base.py" in render_annotated
  904.             return self.render(context)

File "/usr/local/lib/python3.5/dist-packages/django/template/loader_tags.py" in render
  150.             return compiled_parent._render(context)

File "/usr/local/lib/python3.5/dist-packages/django/test/utils.py" in instrumented_test_render
  96.     return self.nodelist.render(context)

File "/usr/local/lib/python3.5/dist-packages/django/template/base.py" in render
  937.                 bit = node.render_annotated(context)

File "/usr/local/lib/python3.5/dist-packages/django/template/base.py" in render_annotated
  904.             return self.render(context)

File "/usr/local/lib/python3.5/dist-packages/django/template/loader_tags.py" in render
  62.                 result = block.nodelist.render(context)

File "/usr/local/lib/python3.5/dist-packages/django/template/base.py" in render
  937.                 bit = node.render_annotated(context)

File "/usr/local/lib/python3.5/dist-packages/django/template/base.py" in render_annotated
  904.             return self.render(context)

File "/usr/local/lib/python3.5/dist-packages/django/template/library.py" in render
  192.         output = self.func(*resolved_args, **resolved_kwargs)

File "/opt/netbox/netbox/extras/templatetags/custom_links.py" in custom_links
  49.             text_rendered = Environment().from_string(source=cl.text).render(**context)

File "/usr/local/lib/python3.5/dist-packages/jinja2/environment.py" in render
  1008.         return self.environment.handle_exception(exc_info, True)

File "/usr/local/lib/python3.5/dist-packages/jinja2/environment.py" in handle_exception
  780.         reraise(exc_type, exc_value, tb)

File "/usr/local/lib/python3.5/dist-packages/jinja2/_compat.py" in reraise
  37.             raise value.with_traceback(tb)

File "<template>" in top-level template code
  1. <source code not available>

File "/usr/local/lib/python3.5/dist-packages/jinja2/environment.py" in getattr
  430.             return getattr(obj, attribute)

File "/opt/netbox/netbox/dcim/models.py" in primary_ip
  1800.         elif self.primary_ip4:

File "/usr/local/lib/python3.5/dist-packages/django/db/models/fields/related_descriptors.py" in __get__
  178.                 rel_obj = self.get_object(instance)

File "/usr/local/lib/python3.5/dist-packages/django/db/models/fields/related_descriptors.py" in get_object
  298.         return super().get_object(instance)

File "/usr/local/lib/python3.5/dist-packages/django/db/models/fields/related_descriptors.py" in get_object
  145.         return qs.get(self.field.get_reverse_related_filter(instance))

File "/usr/local/lib/python3.5/dist-packages/cacheops/query.py" in get
  356.         return qs._no_monkey.get(qs, *args, **kwargs)

File "/usr/local/lib/python3.5/dist-packages/django/db/models/query.py" in get
  408.                 self.model._meta.object_name

Exception Type: DoesNotExist at /dcim/devices/37/
Exception Value: IPAddress matching query does not exist.
Request information:
USER: admin

GET: No GET data

POST: No POST data

FILES: No FILES data
@candlerb commented on GitHub (Oct 7, 2019): Note: I first created a device (with an eth but no primary IP), and then added the Custom Link. Then browsing to the device I got the exception below, which at a glance seems consistent with the original one. So the problem appears to be: `device.primary_ip4` can sometimes raise an exception instead of returning None. ``` Internal Server Error: /dcim/devices/37/ DoesNotExist at /dcim/devices/37/ IPAddress matching query does not exist. Request Method: GET Request URL: http://netbox.example.net/dcim/devices/37/ Django Version: 2.2.5 Python Executable: /usr/bin/python3 Python Version: 3.5.2 Python Path: ['/opt/netbox/netbox', '/usr/local/bin', '/usr/lib/python35.zip', '/usr/lib/python3.5', '/usr/lib/python3.5/plat-x86_64-linux-gnu', '/usr/lib/python3.5/lib-dynload', '/usr/local/lib/python3.5/dist-packages', '/usr/lib/python3/dist-packages'] Server time: Mon, 7 Oct 2019 13:17:08 +0000 Installed Applications: ['django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'django.contrib.humanize', 'cacheops', 'corsheaders', 'debug_toolbar', 'django_filters', 'django_tables2', 'django_prometheus', 'mptt', 'rest_framework', 'taggit', 'taggit_serializer', 'timezone_field', 'circuits', 'dcim', 'ipam', 'extras', 'secrets', 'tenancy', 'users', 'utilities', 'virtualization', 'drf_yasg', 'django_rq'] Installed Middleware: ('debug_toolbar.middleware.DebugToolbarMiddleware', 'django_prometheus.middleware.PrometheusBeforeMiddleware', 'corsheaders.middleware.CorsMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.security.SecurityMiddleware', 'utilities.middleware.ExceptionHandlingMiddleware', 'utilities.middleware.LoginRequiredMiddleware', 'utilities.middleware.APIVersionMiddleware', 'extras.middleware.ObjectChangeMiddleware', 'django_prometheus.middleware.PrometheusAfterMiddleware') Traceback: File "/usr/local/lib/python3.5/dist-packages/django/db/models/fields/related_descriptors.py" in __get__ 164. rel_obj = self.field.get_cached_value(instance) File "/usr/local/lib/python3.5/dist-packages/django/db/models/fields/mixins.py" in get_cached_value 13. return instance._state.fields_cache[cache_name] During handling of the above exception ('primary_ip4'), another exception occurred: File "/usr/local/lib/python3.5/dist-packages/django/core/handlers/exception.py" in inner 34. response = get_response(request) File "/usr/local/lib/python3.5/dist-packages/django/core/handlers/base.py" in _get_response 115. response = self.process_exception_by_middleware(e, request) File "/usr/local/lib/python3.5/dist-packages/django/core/handlers/base.py" in _get_response 113. response = wrapped_callback(request, *callback_args, **callback_kwargs) File "/usr/local/lib/python3.5/dist-packages/django/views/generic/base.py" in view 71. return self.dispatch(request, *args, **kwargs) File "/usr/local/lib/python3.5/dist-packages/django/contrib/auth/mixins.py" in dispatch 85. return super().dispatch(request, *args, **kwargs) File "/usr/local/lib/python3.5/dist-packages/django/views/generic/base.py" in dispatch 97. return handler(request, *args, **kwargs) File "/opt/netbox/netbox/dcim/views.py" in get 991. 'show_interface_graphs': Graph.objects.filter(type=GRAPH_TYPE_INTERFACE).exists(), File "/usr/local/lib/python3.5/dist-packages/django/shortcuts.py" in render 36. content = loader.render_to_string(template_name, context, request, using=using) File "/usr/local/lib/python3.5/dist-packages/django/template/loader.py" in render_to_string 62. return template.render(context, request) File "/usr/local/lib/python3.5/dist-packages/django/template/backends/django.py" in render 61. return self.template.render(context) File "/usr/local/lib/python3.5/dist-packages/django/template/base.py" in render 171. return self._render(context) File "/usr/local/lib/python3.5/dist-packages/django/test/utils.py" in instrumented_test_render 96. return self.nodelist.render(context) File "/usr/local/lib/python3.5/dist-packages/django/template/base.py" in render 937. bit = node.render_annotated(context) File "/usr/local/lib/python3.5/dist-packages/django/template/base.py" in render_annotated 904. return self.render(context) File "/usr/local/lib/python3.5/dist-packages/django/template/loader_tags.py" in render 150. return compiled_parent._render(context) File "/usr/local/lib/python3.5/dist-packages/django/test/utils.py" in instrumented_test_render 96. return self.nodelist.render(context) File "/usr/local/lib/python3.5/dist-packages/django/template/base.py" in render 937. bit = node.render_annotated(context) File "/usr/local/lib/python3.5/dist-packages/django/template/base.py" in render_annotated 904. return self.render(context) File "/usr/local/lib/python3.5/dist-packages/django/template/loader_tags.py" in render 62. result = block.nodelist.render(context) File "/usr/local/lib/python3.5/dist-packages/django/template/base.py" in render 937. bit = node.render_annotated(context) File "/usr/local/lib/python3.5/dist-packages/django/template/base.py" in render_annotated 904. return self.render(context) File "/usr/local/lib/python3.5/dist-packages/django/template/library.py" in render 192. output = self.func(*resolved_args, **resolved_kwargs) File "/opt/netbox/netbox/extras/templatetags/custom_links.py" in custom_links 49. text_rendered = Environment().from_string(source=cl.text).render(**context) File "/usr/local/lib/python3.5/dist-packages/jinja2/environment.py" in render 1008. return self.environment.handle_exception(exc_info, True) File "/usr/local/lib/python3.5/dist-packages/jinja2/environment.py" in handle_exception 780. reraise(exc_type, exc_value, tb) File "/usr/local/lib/python3.5/dist-packages/jinja2/_compat.py" in reraise 37. raise value.with_traceback(tb) File "<template>" in top-level template code 1. <source code not available> File "/usr/local/lib/python3.5/dist-packages/jinja2/environment.py" in getattr 430. return getattr(obj, attribute) File "/opt/netbox/netbox/dcim/models.py" in primary_ip 1800. elif self.primary_ip4: File "/usr/local/lib/python3.5/dist-packages/django/db/models/fields/related_descriptors.py" in __get__ 178. rel_obj = self.get_object(instance) File "/usr/local/lib/python3.5/dist-packages/django/db/models/fields/related_descriptors.py" in get_object 298. return super().get_object(instance) File "/usr/local/lib/python3.5/dist-packages/django/db/models/fields/related_descriptors.py" in get_object 145. return qs.get(self.field.get_reverse_related_filter(instance)) File "/usr/local/lib/python3.5/dist-packages/cacheops/query.py" in get 356. return qs._no_monkey.get(qs, *args, **kwargs) File "/usr/local/lib/python3.5/dist-packages/django/db/models/query.py" in get 408. self.model._meta.object_name Exception Type: DoesNotExist at /dcim/devices/37/ Exception Value: IPAddress matching query does not exist. Request information: USER: admin GET: No GET data POST: No POST data FILES: No FILES data ```
Author
Owner

@DanSheps commented on GitHub (Oct 7, 2019):

Thanks @candlerb, appears to be caching related.

Going to re-open this

@DanSheps commented on GitHub (Oct 7, 2019): Thanks @candlerb, appears to be caching related. Going to re-open this
Author
Owner

@jeremystretch commented on GitHub (Mar 23, 2020):

The root issue here is that the custom link template is accessing the device's primary_ip4/primary_ip6 attribute from cache after the IP address has been deleted. The actual database value of this field is null, however the cached value is the primary IP of an IPAddress object which no longer exists, hence the exception.

Note: Since #3461 was implemented, a full exception no longer appears on the page: It is contained by disabling the rendered custom link button. (Hovering over the button reveals the exception message as a tooltip.)

@jeremystretch commented on GitHub (Mar 23, 2020): The root issue here is that the custom link template is accessing the device's `primary_ip4`/`primary_ip6` attribute from cache after the IP address has been deleted. The actual database value of this field is null, however the cached value is the primary IP of an IPAddress object which no longer exists, hence the exception. Note: Since #3461 was implemented, a full exception no longer appears on the page: It is contained by disabling the rendered custom link button. (Hovering over the button reveals the exception message as a tooltip.)
Author
Owner

@lampwins commented on GitHub (May 15, 2020):

The fix for this is refreshingly simple :)

The root cause has to do with the way cacheops handles SET_NULL FK relationships, described in cacheops #348.

The fix I discovered is to simply add the FK relation to the prefetch_related() clause on the queryset which allows cacheops to properly link the relation for invalidation.

Knowing this, going forward we should probably make the best practice to ensure all FK relationships we intend to use in a given context are added to a prefetch. This also likely means we have other "weird" behavior related to this issue yet to be discovered. For instance, in this case, we had solved the symptom of custom links failing to render in #3461, but the issue still existed within the device view template itself, but it turns out the django templating engine suppresses these errors and simply returns an empty string (this was news to me).

Also note that after fixing this in the course of testing, I uncovered another unrelated but similar caching issue.

@lampwins commented on GitHub (May 15, 2020): The fix for this is refreshingly simple :) The root cause has to do with the way cacheops handles `SET_NULL` FK relationships, described in [cacheops #348](https://github.com/Suor/django-cacheops/issues/348). The fix I discovered is to simply add the FK relation to the `prefetch_related()` clause on the queryset which allows cacheops to properly link the relation for invalidation. Knowing this, going forward we should probably make the best practice to ensure all FK relationships we intend to use in a given context are added to a prefetch. This also likely means we have other "weird" behavior related to this issue yet to be discovered. For instance, in this case, we had solved the symptom of custom links failing to render in #3461, but the issue still existed within the device view template itself, but it turns out the django templating engine suppresses these errors and simply returns an empty string (this was news to me). Also note that after fixing this in the course of testing, I uncovered another unrelated but similar caching issue.
Author
Owner

@jeremystretch commented on GitHub (May 15, 2020):

Knowing this, going forward we should probably make the best practice to ensure all FK relationships we intend to use in a given context are added to a prefetch.

This isn't going to be possible under some circumstances, such as when rendering custom export templates. There's no way of knowing ahead of time to what related objects the author will make reference.

@jeremystretch commented on GitHub (May 15, 2020): > Knowing this, going forward we should probably make the best practice to ensure all FK relationships we intend to use in a given context are added to a prefetch. This isn't going to be possible under some circumstances, such as when rendering custom export templates. There's no way of knowing ahead of time to what related objects the author will make reference.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: starred/netbox#2704