build v_1.0.1

This commit is contained in:
2026-07-21 15:05:52 +03:00
commit 6563d755d8
27 changed files with 1974 additions and 0 deletions
+763
View File
@@ -0,0 +1,763 @@
"""STAGE 1: collecting Neutron VPNaaS configuration into an audit JSON file,
plus the --from-audit path that reconstructs the same in-memory state from a
previously written audit file instead of re-querying OpenStack.
This module owns the audit JSON schema definition (to_audit_dict /
load_from_audit_json), so it's defined exactly once and shared by both the
write path (STEP 10) and the read path (--from-audit).
"""
from __future__ import annotations
import json
import os
import re
import sys
from datetime import datetime, timezone
from . import keystone_auth, neutron_client
from .models import MigrationState
from .neutron_client import NeutronClient
from .redact import redact_psk
from .sprut_client import SprutClient
from .tables import print_kv_table
_UUID_LIKE = re.compile(r"^[0-9a-fA-F-]{36}$")
def _dump(obj) -> str:
return json.dumps(obj, indent=2)
def _rule() -> None:
print("*" * 86)
# --------------------------------------------------------------------------
# STEP 1: read the input CSV and build the neutron -> advanced router map.
# A row's advanced_router_id may be blank -- that router will be created in
# Sprut later (STEP 3). Optional 3rd/4th columns (availability_zone,
# flavor) only matter for such rows; they're a hint for that creation,
# used instead of the interactive prompt when present.
# --------------------------------------------------------------------------
def read_input_csv(input_csv: str) -> tuple[dict[str, str], dict[str, dict]]:
print(f"Executing STEP 1: Reading config file {input_csv}")
mapping: dict[str, str] = {}
creation_hints: dict[str, dict] = {}
try:
handle = open(input_csv, encoding="utf-8")
except OSError as exc:
print(f"Error: could not open input file '{input_csv}': {exc}", file=sys.stderr)
sys.exit(1)
with handle:
for raw_line in handle:
line = raw_line.rstrip("\n").rstrip("\r")
fields = [f.strip() for f in line.split(",")]
neutron_router = fields[0] if fields else ""
advanced_router = fields[1] if len(fields) > 1 else ""
availability_zone = fields[2] if len(fields) > 2 else ""
flavor = fields[3] if len(fields) > 3 else ""
if not neutron_router:
continue
mapping[neutron_router] = advanced_router
if not advanced_router:
hint = {}
if availability_zone:
hint["availability_zone"] = availability_zone
if flavor:
hint["flavor"] = flavor
if hint:
creation_hints[neutron_router] = hint
print(
f"Neutron router '{neutron_router}' has no advanced router — "
"a DC Router will be created for it in Sprut.",
file=sys.stderr,
)
print("Reading values from config:")
print()
print_kv_table(
{k: (v or "<to be created>") for k, v in mapping.items()}, "Neutron router", "Advanced Router"
)
print()
print("STEP 1 complete (config read)")
_rule()
return mapping, creation_hints
# --------------------------------------------------------------------------
# STEP 2: verify every Neutron router from the CSV exists in OpenStack.
# --------------------------------------------------------------------------
def check_neutron_routers_exist(state: MigrationState, nc: NeutronClient) -> None:
print("Executing STEP 2: Checking Neutron routers")
listing = neutron_client.router_list(nc)
state.neutron_router_by_id = {entry["id"]: entry for entry in listing}
existing_ids = set(state.neutron_router_by_id)
for neutron_router_id in state.neutron_to_adv_router:
if neutron_router_id not in existing_ids:
# No stderr redirect here -- matches the bash original's
# (inconsistent, but preserved) use of stdout for this message.
print(f"Error: Neutron router ID {neutron_router_id} not found in OpenStack tenant.")
sys.exit(1)
print(f"Neutron router ID {neutron_router_id} exists in OpenStack tenant.")
print("STEP 2 complete (Neutron routers checked)")
_rule()
_DC_FLAVORS = ("basic", "standard", "advanced")
def _require_interactive(neutron_router_id: str, field_name: str) -> None:
if not sys.stdin.isatty():
print(
f"Error: router '{neutron_router_id}' needs a DC Router created but '{field_name}' "
"isn't set in the CSV and stdin is not interactive. Add it as a CSV column "
"(3rd=availability_zone, 4th=flavor) or run interactively.",
file=sys.stderr,
)
sys.exit(1)
def _resolve_az_flavor(neutron_router_id: str, hints: dict[str, dict]) -> tuple[str, str]:
hint = hints.get(neutron_router_id, {})
availability_zone = hint.get("availability_zone") or ""
if not availability_zone:
_require_interactive(neutron_router_id, "availability_zone")
while not availability_zone:
availability_zone = input(f"Router '{neutron_router_id}': enter availability_zone: ").strip()
flavor = hint.get("flavor") or ""
if flavor not in _DC_FLAVORS:
_require_interactive(neutron_router_id, "flavor")
while flavor not in _DC_FLAVORS:
flavor = input(f"Router '{neutron_router_id}': enter flavor ({'/'.join(_DC_FLAVORS)}): ").strip()
return availability_zone, flavor
def _find_internet_network(sprut_client: SprutClient) -> str:
"""Locates Sprut's public network (name "internet"/"Internet"), for
attaching a DC Router's public interface. Looked up once per run,
lazily, only when at least one router actually needs creation.
Deliberately does not resolve or send a subnet_id: Sprut rejects
dc_interface creation with subnet_id set for an external network
("Specifying subnet_id or ip_address for external network is
restricted", confirmed against the real API) -- it auto-assigns one.
"""
networks = sprut_client.get("/networks?limit=10000").get("networks", [])
matches = [n for n in networks if (n.get("name") or "").lower() == "internet"]
if not matches:
print("Error: no Sprut network named 'Internet' found (needed for the public interface).", file=sys.stderr)
sys.exit(1)
if len(matches) > 1:
print(
f"Warning: {len(matches)} Sprut networks named 'Internet' found -- "
f"using the first one ({matches[0].get('id')}).",
file=sys.stderr,
)
return matches[0]["id"]
def _find_or_create_dc_router(
neutron_router_id: str,
name: str,
description: str,
availability_zone: str,
flavor: str,
sprut_client: SprutClient,
existing_dc_routers_by_name: dict[str, str],
) -> str:
sprut_id = existing_dc_routers_by_name.get(name)
if sprut_id:
print(f"DC Router '{name}' already exists in Sprut (id {sprut_id}) -- reusing")
return sprut_id
print(f"Creating DC Router '{name}' in Sprut (az={availability_zone}, flavor={flavor}, enable_snat=true)")
request_body = {
"dc_router": {
"availability_zone": availability_zone,
"flavor": flavor,
"enable_snat": True,
"name": name,
"description": description,
}
}
response = sprut_client.post("/direct_connect/dc_routers", request_body)
sprut_id = (response.get("dc_router") or {}).get("id")
if sprut_client.dry_run and not sprut_id:
sprut_id = f"DRY-RUN-{neutron_router_id}"
return sprut_id
def _find_or_create_dc_interface(
dc_router_id: str, network_id: str, name: str, sprut_client: SprutClient
) -> str:
existing = sprut_client.get("/direct_connect/dc_interfaces").get("dc_interfaces", [])
match = next(
(i for i in existing if i.get("dc_router_id") == dc_router_id and i.get("network_id") == network_id),
None,
)
if match:
print(f"Public interface already attached to DC Router {dc_router_id} (id {match.get('id')}) -- reusing")
return match["id"]
print(f"Attaching public interface (network 'Internet') to DC Router {dc_router_id}")
# subnet_id deliberately omitted -- Sprut rejects it for external
# networks (see _find_internet_network's docstring).
request_body = {
"dc_interface": {
"dc_router_id": dc_router_id,
"network_id": network_id,
"name": name,
}
}
response = sprut_client.post("/direct_connect/dc_interfaces", request_body)
sprut_id = (response.get("dc_interface") or {}).get("id")
if sprut_client.dry_run and not sprut_id:
sprut_id = f"DRY-RUN-iface-{dc_router_id}"
return sprut_id
def _ensure_dc_router(
neutron_router_id: str,
name: str,
description: str,
availability_zone: str,
flavor: str,
sprut_client: SprutClient,
existing_dc_routers_by_name: dict[str, str],
internet_network_id: str,
) -> str:
"""Creates (or reuses, by name) a DC Router and attaches its public
interface. Shared by the fresh-collect path (STEP 3) and the
--from-audit path, which both need identical create-or-reuse logic."""
sprut_id = _find_or_create_dc_router(
neutron_router_id, name, description, availability_zone, flavor, sprut_client, existing_dc_routers_by_name
)
_find_or_create_dc_interface(sprut_id, internet_network_id, f"{name}-public", sprut_client)
existing_dc_routers_by_name[name] = sprut_id
return sprut_id
# --------------------------------------------------------------------------
# STEP 3: verify every advanced router exists in Sprut, using the Keystone
# token issued once up front by collect(). For CSV rows with a blank
# advanced_router_id, either creates the DC Router now (full run) or just
# records what it would need (--audit-only, which must not write to
# Sprut). Returns the SprutClient constructed with that token, reused by
# STAGE 2-4 (the token is never refreshed after this point).
# --------------------------------------------------------------------------
def check_advanced_routers_exist(
state: MigrationState, token: str, sprut_base_url: str, dry_run: bool, audit_only: bool
) -> SprutClient:
print("Executing STEP 3: Checking Advanced routers")
sprut_client = SprutClient(sprut_base_url, token, dry_run)
dc_routers = sprut_client.get("/direct_connect/dc_routers").get("dc_routers", [])
existing_ids = {entry["id"] for entry in dc_routers}
existing_by_name = {entry["name"]: entry["id"] for entry in dc_routers if entry.get("name")}
internet_network_id: str | None = None
for neutron_router_id, advanced_router_id in state.neutron_to_adv_router.items():
if advanced_router_id:
if advanced_router_id not in existing_ids:
print(f"Error: Advanced router ID {advanced_router_id} not found in SDN.")
sys.exit(1)
print(f"Advanced router ID {advanced_router_id} exists in SDN.")
continue
neutron_router = state.neutron_router_by_id.get(neutron_router_id, {})
name = neutron_router.get("name") or neutron_router_id
description = neutron_router.get("description") or ""
availability_zone, flavor = _resolve_az_flavor(neutron_router_id, state.router_creation_hints)
if audit_only:
state.pending_dc_router_by_neutron_id[neutron_router_id] = {
"name": name,
"description": description,
"availability_zone": availability_zone,
"flavor": flavor,
}
print(
f"Neutron router '{neutron_router_id}': no advanced router -- recorded as pending "
"DC Router creation in the audit (no Sprut writes in --audit-only)."
)
continue
if internet_network_id is None:
internet_network_id = _find_internet_network(sprut_client)
state.neutron_to_adv_router[neutron_router_id] = _ensure_dc_router(
neutron_router_id,
name,
description,
availability_zone,
flavor,
sprut_client,
existing_by_name,
internet_network_id,
)
print("STEP 3 complete (Advanced routers checked)")
_rule()
return sprut_client
def recheck_advanced_routers_from_audit(state: MigrationState, sprut_client: SprutClient) -> None:
"""Re-verifies advanced routers recorded in a --from-audit file still
exist in Sprut, since the audit may have been generated earlier. Also
creates any DC Router left pending by a prior --audit-only run, using
the name/description/az/flavor cached in that audit's
"pending_dc_router" block -- Neutron is never re-queried here."""
print("Issuing OpenStack token and re-checking advanced routers in SDN...")
dc_routers = sprut_client.get("/direct_connect/dc_routers").get("dc_routers", [])
existing_ids = {entry["id"] for entry in dc_routers}
existing_by_name = {entry["name"]: entry["id"] for entry in dc_routers if entry.get("name")}
internet_network_id: str | None = None
for neutron_router_id, advanced_router_id in state.neutron_to_adv_router.items():
if advanced_router_id:
if advanced_router_id not in existing_ids:
print(
f"Error: Advanced router ID {advanced_router_id} (from audit file) not found in SDN.",
file=sys.stderr,
)
sys.exit(1)
print(f"Advanced router ID {advanced_router_id} exists in SDN.")
continue
pending = state.pending_dc_router_by_neutron_id.get(neutron_router_id)
if not pending:
print(
f"Error: router '{neutron_router_id}' has no advanced_router_id and no "
"pending_dc_router info in the audit file -- file is incompatible or corrupted.",
file=sys.stderr,
)
sys.exit(1)
if internet_network_id is None:
internet_network_id = _find_internet_network(sprut_client)
state.neutron_to_adv_router[neutron_router_id] = _ensure_dc_router(
neutron_router_id,
pending["name"],
pending.get("description", ""),
pending["availability_zone"],
pending["flavor"],
sprut_client,
existing_by_name,
internet_network_id,
)
# --------------------------------------------------------------------------
# STEP 4-6: IPsec policies, IKE policies, Endpoint groups (project-wide).
# One list call each -- Neutron's list endpoints return full objects, so no
# per-item show call is needed (unlike the old CLI-based version).
# --------------------------------------------------------------------------
def collect_ipsec_policies(state: MigrationState, nc: NeutronClient) -> None:
print("Executing STEP 4: Collecting info about IPsec policies")
for details in neutron_client.ipsec_policy_list(nc):
state.neutron_ipsec_policy_by_id[details["id"]] = details
print("Stored IPsec policies:")
for policy_id, details in state.neutron_ipsec_policy_by_id.items():
print(f"Policy ID: {policy_id}")
print(f"Details: {_dump(details)}")
print()
print("STEP 4 complete: Info about IPsec policies stored")
_rule()
def collect_ike_policies(state: MigrationState, nc: NeutronClient) -> None:
print("Executing STEP 5: Collecting info about IKE policies")
for details in neutron_client.ike_policy_list(nc):
state.neutron_ike_policy_by_id[details["id"]] = details
print("Stored IKE policies:")
for policy_id, details in state.neutron_ike_policy_by_id.items():
print(f"Policy ID: {policy_id}")
print(f"Details: {_dump(details)}")
print()
print("STEP 5 complete: Info about IKE policies stored")
_rule()
def collect_endpoint_groups(state: MigrationState, nc: NeutronClient) -> None:
print("Executing STEP 6: Collecting info about Endpoint Groups")
for details in neutron_client.endpoint_group_list(nc):
eg_id = details["id"]
state.neutron_endpoint_group_by_id[eg_id] = details
if details.get("sdn") == "sprut":
print(f"Endpoint Group {eg_id} has sdn 'sprut' with the following network addresses:")
for address in details.get("endpoints") or []:
print(address)
print("Stored Endpoint Groups:")
for eg_id, details in state.neutron_endpoint_group_by_id.items():
print(f"Endpoint Group ID: {eg_id}")
print(f"Details: {_dump(details)}")
print()
print("STEP 6 complete: Info about Endpoint Groups stored")
_rule()
# --------------------------------------------------------------------------
# STEP 7: subnet id -> CIDR map, excluding ext-subnet* names.
# --------------------------------------------------------------------------
def collect_subnets(state: MigrationState, nc: NeutronClient) -> None:
print("Executing STEP 7: Creating a map of Subnet ID to Subnet Address")
for entry in neutron_client.subnet_list(nc):
name = entry.get("name") or ""
if name.startswith("ext-subnet"):
continue
state.subnet_id_to_subnet_address[entry["id"]] = entry.get("cidr")
print("Stored Subnet ID to Subnet Address mapping:")
print_kv_table(state.subnet_id_to_subnet_address, "Subnet ID", "Subnet Address", width1=36, width2=18)
print()
print("STEP 7 complete: Subnet ID to Subnet Address map created")
_rule()
# --------------------------------------------------------------------------
# STEP 8: router <-> vpn service maps. router_id_to_vpn_service_id is
# filtered to in-scope (CSV) routers; the reverse map is deliberately left
# project-wide since STEP 9 needs it to resolve every connection's owning
# router before in-scope narrowing happens.
# --------------------------------------------------------------------------
def collect_vpn_services(state: MigrationState, nc: NeutronClient) -> None:
print("Executing STEP 8: Collecting info about vpn services")
listing = neutron_client.vpn_service_list(nc)
for entry in listing:
router_id = entry["router_id"]
vpn_id = entry["id"]
state.router_id_to_vpn_service_id.setdefault(router_id, vpn_id)
state.neutron_vpn_service_by_id.setdefault(vpn_id, entry)
print("Remove router_ids that are not in config")
for router_id in list(state.router_id_to_vpn_service_id):
if router_id not in state.neutron_to_adv_router:
del state.router_id_to_vpn_service_id[router_id]
print_kv_table(state.router_id_to_vpn_service_id, "Router ID", "VPN service ID")
print("build backwards dictionary")
for entry in listing:
state.vpn_service_id_to_router_id[entry["id"]] = entry["router_id"]
print_kv_table(state.vpn_service_id_to_router_id, "VPN service ID", "Router ID")
print("STEP 8 complete (Router ID to VPN Service ID map built)")
_rule()
# --------------------------------------------------------------------------
# STEP 9: ipsec site connections + router -> connection-ids map.
# --------------------------------------------------------------------------
def collect_ipsec_site_connections(state: MigrationState, nc: NeutronClient) -> None:
print("Executing STEP 9: Collecting info about ipsec")
for details in neutron_client.ipsec_site_connection_list(nc):
conn_id = details["id"]
vpn_service_id = details.get("vpnservice_id")
router_id = state.vpn_service_id_to_router_id.get(vpn_service_id, "")
state.neutron_router_to_ipsec_ids.setdefault(router_id, []).append(conn_id)
state.neutron_connection_by_id[conn_id] = details
print("Stored IPsec connections:")
for conn_id, details in state.neutron_connection_by_id.items():
print(f"ID: {conn_id}")
print(f"Details: {_dump(redact_psk(details))}")
print()
print("Neutron Router to IPsec IDs mapping:")
for router_id, conn_ids in state.neutron_router_to_ipsec_ids.items():
print(f"Router ID: {router_id}")
print(f"IPsec IDs: {', '.join(conn_ids)}")
print("STEP 9 complete: Info about ipsec stored")
_rule()
# --------------------------------------------------------------------------
# In-scope computation: only routers/objects tied to the input CSV are
# actually created in Sprut by STAGE 3/4, even though STEP 4-9 collect
# project-wide (needed for matching against what Sprut already has).
# --------------------------------------------------------------------------
def compute_in_scope(state: MigrationState) -> None:
for neutron_router_id in state.neutron_to_adv_router:
for conn_id in state.neutron_router_to_ipsec_ids.get(neutron_router_id, []):
if not conn_id:
continue
state.in_scope_connection_ids.add(conn_id)
conn_raw = state.neutron_connection_by_id[conn_id]
ike_id = conn_raw.get("ikepolicy_id")
ipsec_id = conn_raw.get("ipsecpolicy_id")
local_eg_id = conn_raw.get("local_ep_group_id")
peer_eg_id = conn_raw.get("peer_ep_group_id")
if ike_id:
state.in_scope_ike_policy_ids.add(ike_id)
if ipsec_id:
state.in_scope_ipsec_policy_ids.add(ipsec_id)
if local_eg_id:
state.in_scope_endpoint_group_ids.add(local_eg_id)
if peer_eg_id:
state.in_scope_endpoint_group_ids.add(peer_eg_id)
print(f"In-scope IPsec site connections for this migration: {len(state.in_scope_connection_ids)}")
# --------------------------------------------------------------------------
# Endpoint-group helpers, shared by STEP 10 (audit assembly) and STAGE 3
# (endpoint-group compare&create).
# --------------------------------------------------------------------------
def resolve_endpoint_addresses(details: dict, subnet_map: dict[str, str]) -> list[str]:
name = details.get("name")
converted: list[str] = []
for endpoint in details.get("endpoints") or []:
if _UUID_LIKE.match(endpoint):
print(f"Converting subnet UUID {endpoint} in {name} endpoints", file=sys.stderr)
address = subnet_map.get(endpoint)
if address:
print(f"Converted to {address}", file=sys.stderr)
converted.append(address)
else:
print(f"Warning: Subnet ID {endpoint} not found in subnet_id_to_subnet_address map.", file=sys.stderr)
else:
converted.append(endpoint)
return converted
def build_endpoint_group_block(raw: dict | None, subnet_map: dict[str, str]) -> dict | None:
if not raw:
return None
resolved = resolve_endpoint_addresses(raw, subnet_map)
return {
"id": raw.get("id"),
"raw": raw,
"resolved_endpoints": resolved,
"already_migrated": raw.get("sdn") == "sprut",
}
# --------------------------------------------------------------------------
# STEP 10: assemble and write the audit JSON.
# --------------------------------------------------------------------------
def to_audit_dict(state: MigrationState, input_file: str) -> dict:
router_blocks = []
for neutron_router_id, advanced_router_id in state.neutron_to_adv_router.items():
vpn_id = state.router_id_to_vpn_service_id.get(neutron_router_id)
vpn_service_json = state.neutron_vpn_service_by_id.get(vpn_id) if vpn_id else None
connection_blocks = []
for conn_id in state.neutron_router_to_ipsec_ids.get(neutron_router_id, []):
if not conn_id:
continue
conn_raw = state.neutron_connection_by_id[conn_id]
ike_id = conn_raw.get("ikepolicy_id")
ipsec_id = conn_raw.get("ipsecpolicy_id")
local_eg_id = conn_raw.get("local_ep_group_id")
peer_eg_id = conn_raw.get("peer_ep_group_id")
ike_json = state.neutron_ike_policy_by_id.get(ike_id)
ipsec_json = state.neutron_ipsec_policy_by_id.get(ipsec_id)
local_eg_raw = state.neutron_endpoint_group_by_id.get(local_eg_id)
peer_eg_raw = state.neutron_endpoint_group_by_id.get(peer_eg_id)
connection_blocks.append(
{
"id": conn_raw.get("id"),
"raw": conn_raw,
"ike_policy": ike_json,
"ipsec_policy": ipsec_json,
"local_endpoint_group": build_endpoint_group_block(local_eg_raw, state.subnet_id_to_subnet_address),
"peer_endpoint_group": build_endpoint_group_block(peer_eg_raw, state.subnet_id_to_subnet_address),
}
)
router_blocks.append(
{
"neutron_router_id": neutron_router_id,
"advanced_router_id": advanced_router_id or None,
"pending_dc_router": state.pending_dc_router_by_neutron_id.get(neutron_router_id),
"vpn_service": vpn_service_json,
"ipsec_site_connections": connection_blocks,
}
)
return {
"audit_metadata": {
"generated_at": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
"input_file": input_file,
"stage": "STAGE1_AUDIT",
},
"subnets": dict(state.subnet_id_to_subnet_address),
"routers": router_blocks,
}
def write_audit_json(data: dict, output_file: str) -> None:
tmp_path = f"{output_file}.tmp"
with open(tmp_path, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2)
try:
with open(tmp_path, encoding="utf-8") as f:
json.load(f)
except (OSError, json.JSONDecodeError):
print("Error: failed to assemble valid JSON audit output", file=sys.stderr)
os.remove(tmp_path)
sys.exit(1)
os.replace(tmp_path, output_file)
os.chmod(output_file, 0o600)
print(f"Audit JSON written to {output_file} (permissions restricted to owner: contains PSK values)")
# --------------------------------------------------------------------------
# --from-audit: reconstruct MigrationState from a previously written audit
# file, without any OpenStack query. Pure (no network I/O) -- token
# reissuance and the Sprut recheck are the orchestration's job (cli.py),
# via recheck_advanced_routers_from_audit() above.
# --------------------------------------------------------------------------
def load_from_audit_json(audit_file: str) -> MigrationState:
print(f"Loading audit data from {audit_file} (skipping OpenStack collection)")
try:
with open(audit_file, encoding="utf-8") as f:
data = json.load(f)
except (OSError, json.JSONDecodeError):
print(f"Error: '{audit_file}' is not valid JSON.", file=sys.stderr)
sys.exit(1)
schema_stage = (data.get("audit_metadata") or {}).get("stage")
if schema_stage != "STAGE1_AUDIT":
print(
f"Error: '{audit_file}' does not look like a STAGE1 audit file "
"(audit_metadata.stage != STAGE1_AUDIT).",
file=sys.stderr,
)
sys.exit(1)
state = MigrationState()
for subnet_id, subnet_cidr in (data.get("subnets") or {}).items():
if not subnet_id:
continue
state.subnet_id_to_subnet_address[subnet_id] = subnet_cidr
for router_json in data.get("routers") or []:
neutron_router_id = router_json.get("neutron_router_id")
advanced_router_id = router_json.get("advanced_router_id") or ""
if not neutron_router_id:
continue
state.neutron_to_adv_router[neutron_router_id] = advanced_router_id
if not advanced_router_id:
pending = router_json.get("pending_dc_router")
if pending:
state.pending_dc_router_by_neutron_id[neutron_router_id] = pending
vpn_service_json = router_json.get("vpn_service")
if vpn_service_json:
vpn_service_id = vpn_service_json.get("id")
state.router_id_to_vpn_service_id[neutron_router_id] = vpn_service_id
for conn_json in router_json.get("ipsec_site_connections") or []:
conn_id = conn_json.get("id")
if not conn_id:
continue
state.neutron_connection_by_id[conn_id] = conn_json.get("raw")
state.in_scope_connection_ids.add(conn_id)
ike_json = conn_json.get("ike_policy")
if ike_json:
ike_id = ike_json.get("id")
state.neutron_ike_policy_by_id[ike_id] = ike_json
state.in_scope_ike_policy_ids.add(ike_id)
ipsec_json = conn_json.get("ipsec_policy")
if ipsec_json:
ipsec_id = ipsec_json.get("id")
state.neutron_ipsec_policy_by_id[ipsec_id] = ipsec_json
state.in_scope_ipsec_policy_ids.add(ipsec_id)
local_eg_json = conn_json.get("local_endpoint_group")
if local_eg_json:
local_eg_id = local_eg_json.get("id")
state.neutron_endpoint_group_by_id[local_eg_id] = local_eg_json.get("raw")
state.in_scope_endpoint_group_ids.add(local_eg_id)
peer_eg_json = conn_json.get("peer_endpoint_group")
if peer_eg_json:
peer_eg_id = peer_eg_json.get("id")
state.neutron_endpoint_group_by_id[peer_eg_id] = peer_eg_json.get("raw")
state.in_scope_endpoint_group_ids.add(peer_eg_id)
print(
f"Loaded from audit: {len(state.neutron_to_adv_router)} router(s), "
f"{len(state.in_scope_connection_ids)} in-scope IPsec site connection(s)."
)
return state
# --------------------------------------------------------------------------
# Full STAGE 1 orchestration (STEP 1-9 + in-scope computation). STEP 10
# (write) is invoked separately by the caller so --audit-only and the full
# run can share this without duplicating the write step.
# --------------------------------------------------------------------------
def collect(
input_csv: str, sprut_base_url: str, dry_run: bool, audit_only: bool
) -> tuple[MigrationState, SprutClient]:
state = MigrationState()
state.neutron_to_adv_router, state.router_creation_hints = read_input_csv(input_csv)
# Authenticate once, up front: STEP 2 now needs a Neutron token just as
# much as STEP 3 needs one for Sprut, since neither goes through the
# `openstack` CLI's own per-call auth resolution anymore.
token, catalog = keystone_auth.authenticate()
neutron_base_url = keystone_auth.resolve_endpoint(catalog, service_type="network")
nc = NeutronClient(neutron_base_url, token)
check_neutron_routers_exist(state, nc)
sprut_client = check_advanced_routers_exist(state, token, sprut_base_url, dry_run, audit_only)
collect_ipsec_policies(state, nc)
collect_ike_policies(state, nc)
collect_endpoint_groups(state, nc)
collect_subnets(state, nc)
collect_vpn_services(state, nc)
collect_ipsec_site_connections(state, nc)
compute_in_scope(state)
return state, sprut_client