Bulk-importing pre-existing LAG member interfaces fails if the same LAG interface name is present on multiple devices #8460

Closed
opened 2025-12-29 20:37:01 +01:00 by adam · 8 comments
Owner

Originally created by @pv2b on GitHub (Aug 11, 2023).

NetBox version

v3.5.7

Python version

3.10

Steps to Reproduce

Prerequisites: You'll need two devices, In my case, I called them test1 and test2.

Create two LAG interfaces on these two devices with the same name. You can do this using interface bulk import like this:

device,name,type
test1,lag1,lag
test2,lag1,lag

Then, using bulk import, create an physical interface. Like this:

device,name,type
test1,eth0,100base-fx

Take a note of the ID number of the interface that's just been created, because now we want to use bulk import to join this interface into the LAG:

id,lag
2642,lag1

Expected Behavior

The eth0 interface on test1 is set to join the lag1 interface on test1.

Observed Behavior

Actual result is error message: Record 1 lag: "lag1" is not a unique value for this field; multiple objects were found

I believe this happens because the bulk importer is searching for "lag1" across all of Netbox instead for on the interface. It doesn't seem to happen when creating a new interface, only when updating existing ones!

Also see related discussion: https://github.com/netbox-community/netbox/discussions/8267

Seems another user had the same issue but stopped short of providing a repro, so here I am, providing a repro.

Originally created by @pv2b on GitHub (Aug 11, 2023). ### NetBox version v3.5.7 ### Python version 3.10 ### Steps to Reproduce Prerequisites: You'll need two devices, In my case, I called them test1 and test2. Create two LAG interfaces on these two devices with the same name. You can do this using interface bulk import like this: ``` device,name,type test1,lag1,lag test2,lag1,lag ``` Then, using bulk import, create an physical interface. Like this: ``` device,name,type test1,eth0,100base-fx ``` Take a note of the ID number of the interface that's just been created, because now we want to use bulk import to join this interface into the LAG: ``` id,lag 2642,lag1 ``` ### Expected Behavior The eth0 interface on test1 is set to join the lag1 interface on test1. ### Observed Behavior Actual result is error message: Record 1 lag: "lag1" is not a unique value for this field; multiple objects were found I believe this happens because the bulk importer is searching for "lag1" across all of Netbox instead for on the interface. It doesn't seem to happen when creating a new interface, only when updating existing ones! Also see related discussion: https://github.com/netbox-community/netbox/discussions/8267 Seems another user had the same issue but stopped short of providing a repro, so here I am, providing a repro.
adam added the type: bugstatus: needs ownerpending closureseverity: low labels 2025-12-29 20:37:01 +01:00
adam closed this issue 2025-12-29 20:37:02 +01:00
Author
Owner

@pv2b commented on GitHub (Aug 11, 2023):

This issue looks similar, could be related:
https://github.com/netbox-community/netbox/issues/8546

@pv2b commented on GitHub (Aug 11, 2023): This issue looks similar, could be related: https://github.com/netbox-community/netbox/issues/8546
Author
Owner

@pv2b commented on GitHub (Aug 11, 2023):

Sorry, this issue is bogus, ignore it. The repro case is wrong because "lag" is mistakenly set as the LAG interface name instead of lag1. When correcting the interface name to "lag1" in the repro case, it... works.

There's still a bug here somewhere, I'll re-open or post a new bug once I have a proper repro.

@pv2b commented on GitHub (Aug 11, 2023): Sorry, this issue is bogus, ignore it. The repro case is wrong because "lag" is mistakenly set as the LAG interface name instead of lag1. When correcting the interface name to "lag1" in the repro case, it... works. There's still a bug here somewhere, I'll re-open or post a new bug once I have a proper repro.
Author
Owner

@pv2b commented on GitHub (Aug 11, 2023):

OK, I've narrowed it down, it only seems to happen when bulk-importing and editing EXISTING interfaces, not when bulk-importing entirely new ones. I've edited the bug report and repro accordingly. Sorry for any confusion. Reopening.

@pv2b commented on GitHub (Aug 11, 2023): OK, I've narrowed it down, it only seems to happen when bulk-importing and editing EXISTING interfaces, not when bulk-importing entirely new ones. I've edited the bug report and repro accordingly. Sorry for any confusion. Reopening.
Author
Owner

@pv2b commented on GitHub (Aug 11, 2023):

Related: This API call seems to have a similar problem:

curl -X 'PATCH' \
  'http://netbox.example.com/api/dcim/interfaces/' \
  -H 'accept: application/json' \
  -H 'Content-Type: application/json' \
  -H 'X-CSRFTOKEN: <censored>' \
  -d '[ { "id": 2642, "lag": { "name": "po1" } } ]'

Which gives this response:

{
  "lag": [
    "Multiple objects match the provided attributes: {'name': 'po1'}"
  ]
}
@pv2b commented on GitHub (Aug 11, 2023): Related: This API call seems to have a similar problem: ``` curl -X 'PATCH' \ 'http://netbox.example.com/api/dcim/interfaces/' \ -H 'accept: application/json' \ -H 'Content-Type: application/json' \ -H 'X-CSRFTOKEN: <censored>' \ -d '[ { "id": 2642, "lag": { "name": "po1" } } ]' ``` Which gives this response: ``` { "lag": [ "Multiple objects match the provided attributes: {'name': 'po1'}" ] } ```
Author
Owner

@pv2b commented on GitHub (Aug 11, 2023):

Workaround: Specifying a "device" column along with the "id" seems to make the CSV bulk-import work.

@pv2b commented on GitHub (Aug 11, 2023): Workaround: Specifying a "device" column along with the "id" seems to make the CSV bulk-import work.
Author
Owner

@pv2b commented on GitHub (Sep 7, 2023):

OK, I've spent some time with a debugger on this and I think I have an understanding for why this happens, at least for the API call specified above.

For the API call, the issue is with the implementation of WritableNestedSerializer that simply doesn't have the functionality required.

Specifically, if we look at InterfaceSerializer, which is responsible for serializing/deserializing the API requests we'll find that it has a nested serlaizer for the LAG interface:

class InterfaceSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer):
    # ...
    lag = NestedInterfaceSerializer(required=False, allow_null=True)

If we look closer at NestedInterfaceSerializer we'll find this:

class NestedInterfaceSerializer(WritableNestedSerializer):
    device = NestedDeviceSerializer(read_only=True)
    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail')
    _occupied = serializers.BooleanField(required=False, read_only=True)

    class Meta:
        model = models.Interface
        fields = ['id', 'url', 'display', 'device', 'name', 'cable', '_occupied']

Stepping through the app with a debugger has proved that at some point, we reach the to_internal_value method as defined in WritableNestedSerializer like this:

   def to_internal_value(self, data):

        if data is None:
            return None

        # Dictionary of related object attributes
        if isinstance(data, dict):
            params = dict_to_filter_params(data)
            queryset = self.Meta.model.objects
            try:
                return queryset.get(**params)
            except ObjectDoesNotExist:
                raise ValidationError(f"Related object not found using the provided attributes: {params}")
            except MultipleObjectsReturned:
                raise ValidationError(f"Multiple objects match the provided attributes: {params}")
            except FieldError as e:
                raise ValidationError(e)
# (The rest of the function has been omitted for clarity, it never gets that far! )

So, it doesn't really seem to be looking at self, only looking at data, which in this case is just this:

{'lag': {'name': 'lag1'}}

Then, it's somehow trying to pass this params dictionary into dict_to_filter_params and tries to search the database with this insufficient information and... well... it doesn't succeed because there are multiple objects with the same name.

And the reason it doesn't work isn't really a bug, it's more a matter of missing functionality. Functionality that you as a human user might guess or expect exists, but in fact does not.

As for the solution, I'm not really sure how to approach it. It would be possible to add more functionality to to_internal_value to look at the parent object's device id if it was missing, but this would seem to become fairly messy very quickly. You could just override to_internal_value on the device serializers? Or you might include another field in the Meta class to provide some sort of mapping of what properties to grab from the parent objects? Like, in this case, grab the device ID from the parent serializer if missing. That can be accessed from to_internal_value on the nested serializer using something like self.parent.instance.device.id, but can this be made general somehow?

As for the CSV import case which is what this bug originally tracked, I'm not entirely convinced the problem there has anything to do with the API serializers/deserializers. I've not really been able to penetrate that code just yet, since I focused on the API call which I thought would be easier to debug.

Anyway, my conclusion after digging at this at a while is that this isn't really a bug, it's more a case of a (missing) feature.

Right now I'm pretty lost as to what the correct approach to solve this in a general way that doesn't stink would be, and I don't think I'll be able to get unlost without any help on this, so... unless I hear something else I'm just going to leave it here for now.

@pv2b commented on GitHub (Sep 7, 2023): OK, I've spent some time with a debugger on this and I think I have an understanding for why this happens, at least for the API call specified above. For the API call, the issue is with the implementation of WritableNestedSerializer that simply doesn't have the functionality required. Specifically, if we look at InterfaceSerializer, which is responsible for serializing/deserializing the API requests we'll find that it has a nested serlaizer for the LAG interface: ```python class InterfaceSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer): # ... lag = NestedInterfaceSerializer(required=False, allow_null=True) ``` If we look closer at NestedInterfaceSerializer we'll find this: ```python class NestedInterfaceSerializer(WritableNestedSerializer): device = NestedDeviceSerializer(read_only=True) url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail') _occupied = serializers.BooleanField(required=False, read_only=True) class Meta: model = models.Interface fields = ['id', 'url', 'display', 'device', 'name', 'cable', '_occupied'] ``` Stepping through the app with a debugger has proved that at some point, we reach the to_internal_value method as defined in WritableNestedSerializer like this: ```python def to_internal_value(self, data): if data is None: return None # Dictionary of related object attributes if isinstance(data, dict): params = dict_to_filter_params(data) queryset = self.Meta.model.objects try: return queryset.get(**params) except ObjectDoesNotExist: raise ValidationError(f"Related object not found using the provided attributes: {params}") except MultipleObjectsReturned: raise ValidationError(f"Multiple objects match the provided attributes: {params}") except FieldError as e: raise ValidationError(e) # (The rest of the function has been omitted for clarity, it never gets that far! ) ``` So, it doesn't really seem to be looking at `self`, only looking at `data`, which in this case is just this: ```python {'lag': {'name': 'lag1'}} ``` Then, it's somehow trying to pass this params dictionary into `dict_to_filter_params` and tries to search the database with this insufficient information and... well... it doesn't succeed because there are multiple objects with the same name. And the reason it doesn't work isn't really a bug, it's more a matter of missing functionality. Functionality that you as a human user might guess or expect exists, but in fact does not. As for the solution, I'm not really sure how to approach it. It would be possible to add more functionality to `to_internal_value` to look at the parent object's device id if it was missing, but this would seem to become fairly messy very quickly. You could just override `to_internal_value` on the device serializers? Or you might include another field in the Meta class to provide some sort of mapping of what properties to grab from the parent objects? Like, in this case, grab the device ID from the parent serializer if missing. That can be accessed from `to_internal_value` on the nested serializer using something like `self.parent.instance.device.id`, but can this be made general somehow? As for the CSV import case which is what this bug originally tracked, I'm not entirely convinced the problem there has anything to do with the API serializers/deserializers. I've not really been able to penetrate that code just yet, since I focused on the API call which I thought would be easier to debug. Anyway, my conclusion after digging at this at a while is that this isn't really a bug, it's more a case of a (missing) feature. Right now I'm pretty lost as to what the correct approach to solve this in a general way that doesn't stink would be, and I don't think I'll be able to get unlost without any help on this, so... unless I hear something else I'm just going to leave it here for now.
Author
Owner

@github-actions[bot] commented on GitHub (Dec 7, 2023):

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. NetBox is governed by a small group of core maintainers which means not all opened issues may receive direct feedback. Do not attempt to circumvent this process by "bumping" the issue; doing so will result in its immediate closure and you may be barred from participating in any future discussions. Please see our contributing guide.

@github-actions[bot] commented on GitHub (Dec 7, 2023): This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. NetBox is governed by a small group of core maintainers which means not all opened issues may receive direct feedback. **Do not** attempt to circumvent this process by "bumping" the issue; doing so will result in its immediate closure and you may be barred from participating in any future discussions. Please see our [contributing guide](https://github.com/netbox-community/netbox/blob/develop/CONTRIBUTING.md).
Author
Owner

@github-actions[bot] commented on GitHub (Jan 7, 2024):

This issue has been automatically closed due to lack of activity. In an effort to reduce noise, please do not comment any further. Note that the core maintainers may elect to reopen this issue at a later date if deemed necessary.

@github-actions[bot] commented on GitHub (Jan 7, 2024): This issue has been automatically closed due to lack of activity. In an effort to reduce noise, please do not comment any further. Note that the core maintainers may elect to reopen this issue at a later date if deemed necessary.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: starred/netbox#8460