"""REST client for the Neutron API, replacing the `openstack` CLI subprocess wrapper (`openstack_client.py`, removed). Mirrors `SprutClient`'s shape: a single `requests.Session`, token/base URL fixed at construction, `.get()`/`.post()` raising a clear error and exiting on a non-2xx response. Every list function below does exactly one GET and returns the collection unwrapped -- no per-item `show` call. Unlike the `openstack` CLI's table output (which truncates to a handful of display columns unless `--long` is given), Neutron's list endpoints return the full attribute set for every item by default, so the per-item `show` calls the CLI-based version needed are redundant here. """ from __future__ import annotations import sys from typing import Any import requests class NeutronClient: def __init__(self, base_url: str, token: str, timeout: float = 30.0): self.base_url = base_url.rstrip("/") self.timeout = timeout self._session = requests.Session() self._session.headers.update({"X-Auth-Token": token}) def call(self, method: str, path: str, data: dict | None = None) -> Any: url = f"{self.base_url}{path}" try: response = self._session.request( method, url, json=data if data is not None else None, timeout=self.timeout ) except requests.exceptions.RequestException as exc: print(f"Error: Neutron API {method} {url} failed: {exc}", file=sys.stderr) sys.exit(1) if not 200 <= response.status_code < 300: print(f"Error: Neutron API {method} {url} returned HTTP {response.status_code}", file=sys.stderr) print(f"Response: {response.text}", file=sys.stderr) sys.exit(1) if not response.text: return {} return response.json() def get(self, path: str) -> Any: return self.call("GET", path) def get_absolute(self, url: str) -> Any: """Like get(), but for an already-absolute URL (pagination "next" links come back as full URLs, not paths relative to base_url).""" try: response = self._session.get(url, timeout=self.timeout) except requests.exceptions.RequestException as exc: print(f"Error: Neutron API GET {url} failed: {exc}", file=sys.stderr) sys.exit(1) if not 200 <= response.status_code < 300: print(f"Error: Neutron API GET {url} returned HTTP {response.status_code}", file=sys.stderr) print(f"Response: {response.text}", file=sys.stderr) sys.exit(1) return response.json() if response.text else {} def list_all(self, path: str, collection_key: str) -> list[dict]: """GETs a collection and follows Neutron's pagination "next" links (`_links` with `rel: next`) until exhausted. Plain deployments without pagination enabled just return one page with no `_links` key, so this is a no-op there.""" items: list[dict] = [] body = self.get(path) while True: items.extend(body.get(collection_key, [])) links = body.get(f"{collection_key}_links", []) next_url = next((link["href"] for link in links if link.get("rel") == "next"), None) if not next_url: return items body = self.get_absolute(next_url) def router_list(client: NeutronClient) -> list[dict]: return client.list_all("/v2.0/routers", "routers") def subnet_list(client: NeutronClient) -> list[dict]: return client.list_all("/v2.0/subnets", "subnets") def ike_policy_list(client: NeutronClient) -> list[dict]: return client.list_all("/v2.0/vpn/ikepolicies", "ikepolicies") def ipsec_policy_list(client: NeutronClient) -> list[dict]: return client.list_all("/v2.0/vpn/ipsecpolicies", "ipsecpolicies") def endpoint_group_list(client: NeutronClient) -> list[dict]: return client.list_all("/v2.0/vpn/endpoint-groups", "endpoint_groups") def vpn_service_list(client: NeutronClient) -> list[dict]: return client.list_all("/v2.0/vpn/vpnservices", "vpnservices") def ipsec_site_connection_list(client: NeutronClient) -> list[dict]: return client.list_all("/v2.0/vpn/ipsec-site-connections", "ipsec_site_connections")