"""STAGE 2-4: collecting existing Sprut objects, comparing/creating missing IKE/IPsec policies, endpoint groups and VPN services, and finally creating IPsec site connections. STAGE 3's four compare_and_create_* functions each return a neutron_id -> sprut_id correspondence dict (replacing the bash originals' `declare -A neutron_to_sprut_*` globals). STAGE 4 looks these up by the raw connection's referenced IDs to build its request body. """ from __future__ import annotations import json from . import audit from .models import MigrationState from .redact import redact_psk from .sprut_client import SprutClient from .tables import print_kv_table def _dump(obj) -> str: return json.dumps(obj, indent=2) def _jq_r_str(value) -> str: """Mirrors `jq -r`: a JSON null stringifies to the 4-char string "null" (not Python None/absence), a JSON boolean stringifies to lowercase "true"/"false" (not Python's "True"/"False" -- confirmed against a real tenant, where Neutron's ipsec site connection ".State" is a genuine JSON boolean), everything else via str(). STAGE 4's request body in the bash original is built entirely with `jq -n --arg`, which means every field -- even ones that look numeric/boolean -- is sent to Sprut as a JSON string. Do not "fix" this to native types.""" if value is None: return "null" if isinstance(value, bool): return "true" if value else "false" return str(value) # -------------------------------------------------------------------------- # STAGE 2: collect existing Sprut objects (always real GETs, even in # dry-run -- dry-run only suppresses non-GET calls). # -------------------------------------------------------------------------- def collect_sprut_state(sprut_client: SprutClient) -> dict: print("Collecting IKE policies from Sprut...") ike_policies = sprut_client.get("/vpn/ikepolicies") print("IKE policies collected:") print(_dump(ike_policies)) print() print("Collecting IPsec policies from Sprut...") ipsec_policies = sprut_client.get("/vpn/ipsecpolicies") print("IPsec policies collected:") print(_dump(ipsec_policies)) print() print("Collecting Endpoint Groups from Sprut...") endpoint_groups = sprut_client.get("/vpn/endpoint-groups") print("Endpoint Groups collected:") print(_dump(endpoint_groups)) print() print("Collecting VPN services from Sprut...") vpn_services = sprut_client.get("/vpn/vpnservices") print("VPN services collected:") print(_dump(vpn_services)) print() print("Collecting IPsec site connections from Sprut...") ipsec_site_connections = sprut_client.get("/vpn/ipsec-site-connections") print("IPsec site connections collected:") print(_dump(redact_psk(ipsec_site_connections))) print() return { "ike_policies": ike_policies, "ipsec_policies": ipsec_policies, "endpoint_groups": endpoint_groups, "vpn_services": vpn_services, "ipsec_site_connections": ipsec_site_connections, } # -------------------------------------------------------------------------- # STAGE 3: compare & create missing objects. Each function is idempotent by # whatever key Sprut exposes (name, router_id, or exact endpoints array). # -------------------------------------------------------------------------- def compare_and_create_ike_policies( state: MigrationState, sprut_client: SprutClient, sprut_ike_policies: dict ) -> dict[str, str]: mapping: dict[str, str] = {} for neutron_id in state.in_scope_ike_policy_ids: neutron_policy = state.neutron_ike_policy_by_id[neutron_id] name = neutron_policy.get("name") sprut_id = next( (p.get("id") for p in sprut_ike_policies.get("ikepolicies", []) if p.get("name") == name), None, ) if not sprut_id: print(f"Creating IKE policy '{name}' in Sprut") request_body = { "ikepolicy": { "name": neutron_policy.get("name"), "phase1_negotiation_mode": neutron_policy.get("phase1_negotiation_mode"), "auth_algorithm": neutron_policy.get("auth_algorithm"), "encryption_algorithm": neutron_policy.get("encryption_algorithm"), "pfs": neutron_policy.get("pfs"), "lifetime": neutron_policy.get("lifetime"), "ike_version": neutron_policy.get("ike_version"), } } response = sprut_client.post("/vpn/ikepolicies", request_body) sprut_id = (response.get("ikepolicy") or {}).get("id") if sprut_client.dry_run and not sprut_id: sprut_id = f"DRY-RUN-{neutron_id}" else: print(f"IKE policy '{name}' already exists in Sprut") mapping[neutron_id] = sprut_id return mapping def compare_and_create_ipsec_policies( state: MigrationState, sprut_client: SprutClient, sprut_ipsec_policies: dict ) -> dict[str, str]: mapping: dict[str, str] = {} for neutron_id in state.in_scope_ipsec_policy_ids: neutron_policy = state.neutron_ipsec_policy_by_id[neutron_id] name = neutron_policy.get("name") sprut_id = next( (p.get("id") for p in sprut_ipsec_policies.get("ipsecpolicies", []) if p.get("name") == name), None, ) if not sprut_id: print(f"Creating IPsec policy '{name}' in Sprut") request_body = { "ipsecpolicy": { "name": neutron_policy.get("name"), "transform_protocol": neutron_policy.get("transform_protocol"), "auth_algorithm": neutron_policy.get("auth_algorithm"), "encryption_algorithm": neutron_policy.get("encryption_algorithm"), "encapsulation_mode": neutron_policy.get("encapsulation_mode"), "pfs": neutron_policy.get("pfs"), "lifetime": neutron_policy.get("lifetime"), } } response = sprut_client.post("/vpn/ipsecpolicies", request_body) sprut_id = (response.get("ipsecpolicy") or {}).get("id") if sprut_client.dry_run and not sprut_id: sprut_id = f"DRY-RUN-{neutron_id}" else: print(f"IPsec policy '{name}' already exists in Sprut") mapping[neutron_id] = sprut_id return mapping def compare_and_create_endpoint_groups( state: MigrationState, sprut_client: SprutClient, sprut_endpoint_groups: dict ) -> dict[str, str]: mapping: dict[str, str] = {} for neutron_id in state.in_scope_endpoint_group_ids: neutron_group = state.neutron_endpoint_group_by_id[neutron_id] name = neutron_group.get("name") converted = audit.resolve_endpoint_addresses(neutron_group, state.subnet_id_to_subnet_address) print(f"Total converted UUIDs in endpoints: {', '.join(converted)}") matching = next( (g for g in sprut_endpoint_groups.get("endpoint_groups", []) if g.get("endpoints") == converted), None, ) sprut_id = matching.get("id") if matching else None print(f"Comparing Neutron endpoint group '{name}' with endpoints: {', '.join(converted)}") if matching: print(f" -> Found corresponding Sprut endpoint group with endpoints: {matching.get('endpoints')}") else: print(" -> No corresponding Sprut endpoint group found for these endpoints") if not sprut_id: print(f"Creating Endpoint Group '{name}' in Sprut") request_body = {"endpoint_group": {"name": name, "endpoints": converted, "type": "cidr"}} print(f"Request body: {json.dumps(request_body)}") response = sprut_client.post("/vpn/endpoint-groups", request_body) sprut_id = (response.get("endpoint_group") or {}).get("id") if sprut_client.dry_run and not sprut_id: sprut_id = f"DRY-RUN-{neutron_id}" print(f"Created Sprut Endpoint group: {sprut_id}") else: print(f"Endpoint Group with matching endpoints already exists in Sprut with id {sprut_id}") mapping[neutron_id] = sprut_id print() return mapping def compare_and_create_vpn_services( state: MigrationState, sprut_client: SprutClient, sprut_vpn_services: dict ) -> dict[str, str]: mapping: dict[str, str] = {} for router_id, vpn_service_id in state.router_id_to_vpn_service_id.items(): advanced_router_id = state.neutron_to_adv_router.get(router_id) sprut_id = next( ( s.get("id") for s in sprut_vpn_services.get("vpnservices", []) if s.get("router_id") == advanced_router_id ), None, ) if not sprut_id: print(f"Creating VPN Service for router '{router_id}' in Sprut") request_body = {"vpnservice": {"router_id": advanced_router_id, "admin_state_up": True}} response = sprut_client.post("/vpn/vpnservices", request_body) sprut_id = (response.get("vpnservice") or {}).get("id") if sprut_client.dry_run and not sprut_id: sprut_id = f"DRY-RUN-{router_id}" else: print(f"VPN Service for router '{router_id}' already exists in Sprut") # Keyed by the neutron VPN-service id (not the router id) -- STAGE 4 # looks this map up by the vpn-service id pulled off each connection. mapping[vpn_service_id] = sprut_id return mapping def compare_and_create_all( state: MigrationState, sprut_client: SprutClient, sprut_state: dict ) -> dict[str, dict[str, str]]: ike_map = compare_and_create_ike_policies(state, sprut_client, sprut_state["ike_policies"]) print_kv_table(ike_map, "Neutron IKE Policy ID", "Sprut IKE Policy ID", title="Neutron to Sprut IKE Policies") ipsec_map = compare_and_create_ipsec_policies(state, sprut_client, sprut_state["ipsec_policies"]) print_kv_table( ipsec_map, "Neutron IPsec Policy ID", "Sprut IPsec Policy ID", title="Neutron to Sprut IPsec Policies" ) endpoint_group_map = compare_and_create_endpoint_groups(state, sprut_client, sprut_state["endpoint_groups"]) print_kv_table( endpoint_group_map, "Neutron Endpoint Group ID", "Sprut Endpoint Group ID", title="Neutron to Sprut Endpoint Groups", ) vpn_service_map = compare_and_create_vpn_services(state, sprut_client, sprut_state["vpn_services"]) print_kv_table( vpn_service_map, "Neutron VPN Service ID", "Sprut VPN Service ID", title="Neutron to Sprut VPN Services" ) return { "ike_policy": ike_map, "ipsec_policy": ipsec_map, "endpoint_group": endpoint_group_map, "vpn_service": vpn_service_map, } # -------------------------------------------------------------------------- # STAGE 4: create IPsec site connections, idempotent by name. # -------------------------------------------------------------------------- def create_ipsec_site_connections( state: MigrationState, sprut_client: SprutClient, sprut_state: dict, correspondence: dict[str, dict[str, str]], ) -> None: sprut_connections = sprut_state["ipsec_site_connections"] for conn_id in state.in_scope_connection_ids: details = state.neutron_connection_by_id[conn_id] name = details.get("name") print(f"Processing IPsec site connection ID: {conn_id}") print("IPsec site connection details:") print(_dump(redact_psk(details))) existing_id = next( ( c.get("id") for c in (sprut_connections.get("ipsec_site_connections") or []) if c.get("name") == name ), None, ) if existing_id: print(f"IPsec site connection '{name}' already exists in Sprut (id {existing_id}) — skipping") print() continue sprut_ipsecpolicy_id = correspondence["ipsec_policy"].get(details.get("ipsecpolicy_id")) sprut_ikepolicy_id = correspondence["ike_policy"].get(details.get("ikepolicy_id")) sprut_local_ep_group_id = correspondence["endpoint_group"].get(details.get("local_ep_group_id")) sprut_peer_ep_group_id = correspondence["endpoint_group"].get(details.get("peer_ep_group_id")) sprut_vpn_service_id = correspondence["vpn_service"].get(details.get("vpnservice_id")) # Every field below is stringified (jq --arg semantics in the bash # original) -- do not switch mtu/admin_state_up to native types. request_body = { "ipsec_site_connection": { "psk": _jq_r_str(details.get("psk")), "initiator": _jq_r_str(details.get("initiator")), "ipsecpolicy_id": _jq_r_str(sprut_ipsecpolicy_id), "admin_state_up": _jq_r_str(details.get("admin_state_up")), "mtu": _jq_r_str(details.get("mtu")), "peer_ep_group_id": _jq_r_str(sprut_peer_ep_group_id), "ikepolicy_id": _jq_r_str(sprut_ikepolicy_id), "vpnservice_id": _jq_r_str(sprut_vpn_service_id), "local_ep_group_id": _jq_r_str(sprut_local_ep_group_id), "peer_address": _jq_r_str(details.get("peer_address")), "peer_id": _jq_r_str(details.get("peer_id")), "name": _jq_r_str(name), } } print(f"Creating IPsec site connection '{name}' in Sprut") print("Executing request with body:") print(_dump(redact_psk(request_body))) response = sprut_client.post("/vpn/ipsec-site-connections", request_body) print("API response:") print(_dump(redact_psk(response))) print()