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

110 lines
4.2 KiB
Python

"""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
(`<collection_key>_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")