333 lines
14 KiB
Python
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()
|