Files
VPNaaS-Migrator/ipsec_migrator/sync.py
T
2026-07-21 15:05:52 +03:00

333 lines
14 KiB
Python

"""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()