110 lines
4.2 KiB
Python
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")
|