Repository: nautobot/nautobot

Issue:
Pre-Migration Helper: Validate IPAM Data for Upcoming Restrictions (mixed VRFs on same interface, etc.)



Discussion:

Resolving patch:
diff --git a/nautobot/core/management/commands/pre_migrate.py b/nautobot/core/management/commands/pre_migrate.py
--- a/nautobot/core/management/commands/pre_migrate.py
+++ b/nautobot/core/management/commands/pre_migrate.py
@@ -120,6 +120,58 @@ def check_configcontext_uniqueness():
         )
 
 
+def check_interface_ipaddress_vrf_uniqueness(command):
+    """
+    Check for uniqueness enforcement changes for Interface and VMInterface IPAddress assignments.
+
+    - All IP Addresses assigned to a specific Interface or VMInterface must share the same VRF or have no VRF.
+    """
+
+    failed = False
+
+    command.stdout.write(command.style.WARNING(">>> Running interface/vminterface vrf uniqueness checks..."))
+
+    for ip_address_assignment in (
+        IPAddress.objects.filter(assigned_object_id__isnull=False, assigned_object_type_id__isnull=False)
+        .values("assigned_object_type", "assigned_object_id")
+        .annotate(
+            vrf_count=models.Count("vrf_id", distinct=True),
+            vrf_null_count=models.Count("pk", filter=models.Q(vrf_id__isnull=True)),
+        )
+        .order_by()
+    ):
+        has_null_vrf = 1 if ip_address_assignment["vrf_null_count"] > 0 else 0
+        if ip_address_assignment["vrf_count"] + has_null_vrf > 1:
+            try:
+                assigned_object_type = ContentType.objects.get_for_id(
+                    ip_address_assignment["assigned_object_type"]
+                ).model_class()
+            except ContentType.DoesNotExist:
+                continue
+
+            if assigned_object_type is None:
+                continue  # ContentType.model_class() can return None if an App was removed from the installed_plugins list
+
+            try:
+                assigned_object = assigned_object_type.objects.get(pk=ip_address_assignment["assigned_object_id"])
+            except assigned_object_type.DoesNotExist:
+                continue
+
+            failed = True
+            assigned_object_label = f"{assigned_object} ({getattr(assigned_object, 'parent', assigned_object.id)})"
+            command.stdout.write(
+                command.style.WARNING(
+                    f"{assigned_object_type._meta.label} {assigned_object_label} "
+                    "is associated to IP Addresses with different VRFs."
+                )
+            )
+
+    if failed:
+        raise ValidationError(
+            "You cannot migrate Interfaces or VMInterfaces associated to multiple IP Addresses with different VRFs."
+        )
+
+
 def check_permissions_constraints(command):
     """
     Check for permission constraints that are referencing an object or field
@@ -397,6 +449,11 @@ def handle(self, *args, **options):
             except ValidationError as err:
                 errors.append(err)
 
+        try:
+            check_interface_ipaddress_vrf_uniqueness(self)
+        except ValidationError as err:
+            errors.append(err)
+
         check_permissions_constraints(self)
 
         if errors:


Test patch: