nautobot / nautobot-app-ssot

Single Source of Truth for Nautobot
https://docs.nautobot.com/projects/ssot/en/latest/
Other
34 stars 28 forks source link

VLAN locations are not synchronized #424

Closed steven-douilliet closed 1 month ago

steven-douilliet commented 2 months ago

Environment

Expected Behavior

I expected VLAN synchronization from Infoblox to Nautobot, including:

During synchronization, VLANs should be automatically updated or created with the above attributes.

Except for the VLAN ID and VLAN name, all other attributes are set using extended attributes from Infoblox.

Observed Behavior

VLANs are updated or created, and all attributes are set except for the location. Locations already exist in Nautobot but are not bound to VLANs.

Steps to Reproduce

  1. Create the location from Nautobot.
  2. Create a VLAN on Infoblox.
  3. Set the Location extensible attribute and other attributes.
  4. Run the sync job.

The VLAN will be created based on Infoblox data, but its locations will not be set.

Cause

The root cause is the diffsync model of Nautobot.

Completing the location_id attribute does not associate the location with the VLAN. The VLAN model has a locations field, which is a many-to-many field, and it is here that the location must be set.

Workaround

Create a generic function to process VLAN locations:

def process_vlan_location(diffsync, obj: object, extattrs: dict):
    for attr, attr_value in extattrs.items():  # pylint: disable=too-many-nested-blocks
        if attr_value:
            if attr.lower() in ["site", "facility", "location"]:
                try:
                    location = Location.objects.get(
                        id=diffsync.location_map[attr_value]
                    )
                    obj.locations.add(location)
                except KeyError as err:
                    diffsync.job.logger.warning(
                        f"Unable to find Location {attr_value} for {obj} found in Extensibility Attributes '{attr}'. {err}"
                    )
                except TypeError as err:
                    diffsync.job.logger.warning(
                        f"Cannot set location values {attr_value} for {obj}. Multiple locations are assigned "
                        f"in Extensibility Attributes '{attr}', but multiple location assignments are not "
                        f"supported by Nautobot. {err}"
                    )
                except Exception as err:
                    diffsync.job.logger.warning(
                        f"An error has occured in the after save ! {err}"
                    )

Updated created and updated methods of the NautobotVlan class

class NautobotVlan(Vlan):
    """Nautobot implementation of the Vlan model."""

    @classmethod
    def create(cls, diffsync, ids, attrs):
        """Create VLAN object in Nautobot."""
        _vlan = OrmVlan(
            vid=ids["vid"],
            name=ids["name"],
            status_id=diffsync.status_map[cls.get_vlan_status(attrs["status"])],
            vlan_group_id=diffsync.vlangroup_map[ids["vlangroup"]],
            description=attrs["description"],
        )
        if "ext_attrs" in attrs:
            process_ext_attrs(diffsync=diffsync, obj=_vlan, extattrs=attrs["ext_attrs"])
        try:
            _vlan.validated_save()
            # FIX : Many to Many LocationVLANAssignments
            if "ext_attrs" in attrs:
                process_vlan_location(
                    diffsync=diffsync,
                    obj=_vlan, extattrs=attrs["ext_attrs"]
                )
            if ids["vlangroup"] not in diffsync.vlan_map:
                diffsync.vlan_map[ids["vlangroup"]] = {}
            diffsync.vlan_map[ids["vlangroup"]][_vlan.vid] = _vlan.id
            return super().create(ids=ids, diffsync=diffsync, attrs=attrs)
        except ValidationError as err:
            diffsync.job.logger.warning(f"Unable to create VLAN {ids['name']} {ids['vid']}. {err}")
            return None

    @staticmethod
    def get_vlan_status(status: str) -> str:
        """Return VLAN Status from mapping."""
        statuses = {
            "ASSIGNED": "Active",
            "UNASSIGNED": "Deprecated",
            "RESERVED": "Reserved",
        }
        return statuses[status]

    def update(self, attrs):
        """Update VLAN object in Nautobot."""
        _vlan = OrmVlan.objects.get(id=self.pk)
        if attrs.get("status"):
            _vlan.status_id = self.diffsync.status_map[self.get_vlan_status(attrs["status"])]
        if attrs.get("description"):
            _vlan.description = attrs["description"]
        if "ext_attrs" in attrs:
            process_ext_attrs(diffsync=self.diffsync, obj=_vlan, extattrs=attrs["ext_attrs"])
            # FIX : Many to Many LocationVLANAssignments
            if "location" in attrs["ext_attrs"]:
                _vlan.locations.clear()
                process_vlan_location(
                    diffsync=self.diffsync,
                    obj=_vlan, extattrs=attrs["ext_attrs"]
                )
        if not _vlan.vlan_group.location and _vlan.location:
            _vlan.vlan_group.location = _vlan.location
            _vlan.vlan_group.validated_save()
        try:
            _vlan.validated_save()
        except ValidationError as err:
            self.diffsync.job.logger.warning(f"Unable to update VLAN {_vlan.name} {_vlan.vid}. {err}")
            return None
        return super().update(attrs)
jdrew82 commented 1 month ago

This is already in the code as long as you're using site, facility, or location in your Extensibility Attributes on the VLAN. You can see it being handled here. Are you saying that you aren't seeing this work as expected?

steven-douilliet commented 1 month ago

This is already in the code as long as you're using site, facility, or location in your Extensibility Attributes on the VLAN. You can see it being handled here. Are you saying that you aren't seeing this work as expected?

Hello, yes, I can confirm.

Set the location ID to the obj.location_id attribute won't establish the association between the location and the VLAN due to the location field in the VLAN Model being a many-to-many field:

# It works:
location = Location.objects.get(
    id=diffsync.location_map[attr_value]
)
obj.locations.add(location_obj)

# It doesn't work:
obj.location_id = diffsync.location_map[attr_value]
obj.save()
glennmatthews commented 1 month ago

Possibly related: https://github.com/nautobot/nautobot/pull/5581 (included in 2.2.3 and later)

jdrew82 commented 1 month ago

So the problem is you're expecting every VLAN and Location to be associated together but that wasn't supported until 2.2.3. We can't use that until we have our next big update that forces 2.2.x minimum.

steven-douilliet commented 1 month ago

I updated to version 2.2.4, and locations are now syncing correctly. Thanks!