netbox-community / netbox

The premier source of truth powering network automation. Open source under Apache 2. Try NetBox Cloud free: https://netboxlabs.com/free-netbox-cloud/
http://netboxlabs.com/oss/netbox/
Apache License 2.0
15.96k stars 2.56k forks source link

NoReverseMatch exceptions for plugins without REST API #17722

Open alehaa opened 1 day ago

alehaa commented 1 day ago

Deployment Type

Self-hosted

Triage priority

I volunteer to perform this work (if approved)

NetBox Version

v4.1.3

Python Version

3.11

Steps to Reproduce

  1. Install a NetBox plugin.
  2. Remove API URLs by replacing api/urls.py by this code:

    from netbox.api.routers import NetBoxRouter
    
    router = NetBoxRouter()
    urlpatterns = router.urls
  3. Edit a model of the plugin via UI and save.

Expected Behavior

Observed Behavior

An exception is raised.

Traceback (most recent call last):
  File "/opt/netbox/venv/lib/python3.12/site-packages/rest_framework/relations.py", line 396, in to_representation
    url = self.get_url(value, self.view_name, request, format)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/netbox/netbox/netbox/api/serializers/fields.py", line 37, in get_url
    return self.reverse(view_name, kwargs=kwargs, request=request, format=format)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/netbox/venv/lib/python3.12/site-packages/rest_framework/reverse.py", line 47, in reverse
    url = _reverse(viewname, args, kwargs, request, format, **extra)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/netbox/venv/lib/python3.12/site-packages/rest_framework/reverse.py", line 60, in _reverse
    url = django_reverse(viewname, args=args, kwargs=kwargs, **extra)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/netbox/venv/lib/python3.12/site-packages/django/urls/base.py", line 88, in reverse
    return resolver._reverse_with_prefix(view, prefix, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/netbox/venv/lib/python3.12/site-packages/django/urls/resolvers.py", line 851, in _reverse_with_prefix
    raise NoReverseMatch(msg)
    ^^^^^^^^^^^^^^^^^^^^^^^^^

During handling of the above exception (Reverse for 'domainlistentry-detail' not found. 'domainlistentry-detail' is not a valid view function or pattern name.), another exception occurred:
  File "/opt/netbox/venv/lib/python3.12/site-packages/django/core/handlers/exception.py", line 55, in inner
    response = get_response(request)
               ^^^^^^^^^^^^^^^^^^^^^
  File "/opt/netbox/venv/lib/python3.12/site-packages/django/core/handlers/base.py", line 197, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/netbox/venv/lib/python3.12/site-packages/django/views/generic/base.py", line 104, in view
    return self.dispatch(request, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/netbox/netbox/netbox/views/generic/object_views.py", line 182, in dispatch
    return super().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 125, in dispatch
    return super().dispatch(request, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/netbox/netbox/utilities/views.py", line 39, in dispatch
    return super().dispatch(request, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/netbox/venv/lib/python3.12/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 278, in post
    obj = form.save()
          ^^^^^^^^^^^
  File "/opt/netbox/venv/lib/python3.12/site-packages/django/forms/models.py", line 552, in save
    self.instance.save()
    ^^^^^^^^^^^^^^^^^^^^
  File "/opt/netbox/venv/lib/python3.12/site-packages/django/db/models/base.py", line 822, in save
    self.save_base(
    ^
  File "/opt/netbox/venv/lib/python3.12/site-packages/django/db/models/base.py", line 924, in save_base
    post_save.send(
    ^
  File "/opt/netbox/venv/lib/python3.12/site-packages/django/dispatch/dispatcher.py", line 189, in send
    response = receiver(signal=self, sender=sender, **named)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/netbox/netbox/core/signals.py", line 102, in handle_changed_object
    enqueue_event(queue, instance, request.user, request.id, event_type)
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/netbox/netbox/extras/events.py", line 77, in enqueue_event
    'data': serialize_for_event(instance),
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/netbox/netbox/extras/events.py", line 35, in serialize_for_event
    return serializer.data
           ^^^^^^^^^^^^^^^
  File "/opt/netbox/venv/lib/python3.12/site-packages/rest_framework/serializers.py", line 571, in data
    ret = super().data
          ^^^^^^^^^^^^
  File "/opt/netbox/venv/lib/python3.12/site-packages/rest_framework/serializers.py", line 249, in data
    self._data = self.to_representation(self.instance)
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/netbox/venv/lib/python3.12/site-packages/rest_framework/serializers.py", line 538, in to_representation
    ret[field.field_name] = field.to_representation(attribute)
                            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/netbox/venv/lib/python3.12/site-packages/rest_framework/relations.py", line 411, in to_representation
    raise ImproperlyConfigured(msg % self.view_name)
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Exception Type: ImproperlyConfigured at /plugins/blocklists/domain/lists/entries/add/
Exception Value: Could not resolve URL for hyperlinked relationship using view name "". You may have failed to include the related model in your API, or incorrectly configured the `lookup_field` attribute on this field.
alehaa commented 1 day ago

I believe I can track this down to #15156, where the serializers got automatic URL lookup. The new BaseNetBoxHyperlinkedIdentityField.get_url() method even states:

May raise a NoReverseMatch if the view_name and lookup_field attributes are not configured to correctly match the URL conf.

To solve this easily, I suggest to change the documentation. The serializers are indeed required for NetBox to work properly. However, if someone doesn't want to provide the REST API (or hasn't developed it yet), it is sufficient to simply add url = None to the serializer.

jeremystretch commented 1 day ago

The serializers are indeed required for NetBox to work properly

Maybe we could tweak serialize_for_event() to fall back to using a generic JSON serializer (i.e. serialize_object()) when a custom serializer does not exist for a model.

alehaa commented 16 hours ago

This would be possible. However events may require a defined set of fields that are not provided by the default serialize_object() (e.g. display). Switching the serializer could make things worse if administrators have to worry about what type of payload is being passed.

Maybe we could catch the NoReverseMatch exception in BaseNetBoxHyperlinkedIdentityField.get_url() simply returning no or an empty url if no REST API endpoint is available for this model.