platform: modularize api/gui, add docs-tests-web foundation, and refresh root config
This commit is contained in:
463
selective-vpn-gui/api/transport_clients.py
Normal file
463
selective-vpn-gui/api/transport_clients.py
Normal file
@@ -0,0 +1,463 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict, List, Optional, cast
|
||||
|
||||
from .models import *
|
||||
from .utils import strip_ansi
|
||||
|
||||
|
||||
class TransportClientsApiMixin:
|
||||
def transport_interfaces_get(self) -> TransportInterfacesSnapshot:
|
||||
data = cast(
|
||||
Dict[str, Any],
|
||||
self._json(self._request("GET", "/api/v1/transport/interfaces")) or {},
|
||||
)
|
||||
raw = data.get("items") or []
|
||||
if not isinstance(raw, list):
|
||||
raw = []
|
||||
items: List[TransportInterfaceItem] = []
|
||||
for row in raw:
|
||||
parsed = self._parse_transport_interface_item(row)
|
||||
if parsed is not None:
|
||||
items.append(parsed)
|
||||
return TransportInterfacesSnapshot(
|
||||
ok=bool(data.get("ok", False)),
|
||||
message=strip_ansi(str(data.get("message") or "").strip()),
|
||||
code=str(data.get("code") or "").strip(),
|
||||
count=self._to_int(data.get("count")),
|
||||
items=items,
|
||||
)
|
||||
|
||||
def transport_clients_get(self, enabled_only: bool = False, kind: str = "", include_virtual: bool = False) -> List[TransportClient]:
|
||||
params: Dict[str, Any] = {}
|
||||
if enabled_only:
|
||||
params["enabled_only"] = "true"
|
||||
kind_l = str(kind or "").strip().lower()
|
||||
if kind_l:
|
||||
params["kind"] = kind_l
|
||||
if include_virtual:
|
||||
params["include_virtual"] = "true"
|
||||
|
||||
data = cast(
|
||||
Dict[str, Any],
|
||||
self._json(
|
||||
self._request("GET", "/api/v1/transport/clients", params=(params or None))
|
||||
)
|
||||
or {},
|
||||
)
|
||||
raw = data.get("items") or []
|
||||
if not isinstance(raw, list):
|
||||
raw = []
|
||||
|
||||
out: List[TransportClient] = []
|
||||
for row in raw:
|
||||
item = self._parse_transport_client(row)
|
||||
if item is not None:
|
||||
out.append(item)
|
||||
return out
|
||||
|
||||
def transport_health_refresh(
|
||||
self,
|
||||
*,
|
||||
client_ids: Optional[List[str]] = None,
|
||||
force: bool = False,
|
||||
) -> TransportHealthRefreshResult:
|
||||
payload: Dict[str, Any] = {}
|
||||
ids: List[str] = []
|
||||
for raw in list(client_ids or []):
|
||||
cid = str(raw or "").strip()
|
||||
if cid:
|
||||
ids.append(cid)
|
||||
if ids:
|
||||
payload["client_ids"] = ids
|
||||
if force:
|
||||
payload["force"] = True
|
||||
|
||||
data = cast(
|
||||
Dict[str, Any],
|
||||
self._json(
|
||||
self._request(
|
||||
"POST",
|
||||
"/api/v1/transport/health/refresh",
|
||||
json_body=payload,
|
||||
)
|
||||
)
|
||||
or {},
|
||||
)
|
||||
|
||||
raw_items = data.get("items") or []
|
||||
if not isinstance(raw_items, list):
|
||||
raw_items = []
|
||||
|
||||
items: List[TransportHealthRefreshItem] = []
|
||||
for row in raw_items:
|
||||
if not isinstance(row, dict):
|
||||
continue
|
||||
items.append(
|
||||
TransportHealthRefreshItem(
|
||||
client_id=str(row.get("client_id") or "").strip(),
|
||||
status=str(row.get("status") or "").strip().lower(),
|
||||
queued=bool(row.get("queued", False)),
|
||||
reason=strip_ansi(str(row.get("reason") or "").strip()),
|
||||
)
|
||||
)
|
||||
|
||||
return TransportHealthRefreshResult(
|
||||
ok=bool(data.get("ok", False)),
|
||||
message=strip_ansi(str(data.get("message") or "").strip()),
|
||||
code=str(data.get("code") or "").strip(),
|
||||
count=self._to_int(data.get("count")),
|
||||
queued=self._to_int(data.get("queued")),
|
||||
skipped=self._to_int(data.get("skipped")),
|
||||
items=items,
|
||||
)
|
||||
|
||||
def transport_client_health_get(self, client_id: str) -> TransportClientHealthSnapshot:
|
||||
cid = str(client_id or "").strip()
|
||||
if not cid:
|
||||
raise ValueError("missing transport client id")
|
||||
|
||||
data = cast(
|
||||
Dict[str, Any],
|
||||
self._json(
|
||||
self._request(
|
||||
"GET",
|
||||
f"/api/v1/transport/clients/{cid}/health",
|
||||
)
|
||||
)
|
||||
or {},
|
||||
)
|
||||
|
||||
raw_health = data.get("health") or {}
|
||||
if not isinstance(raw_health, dict):
|
||||
raw_health = {}
|
||||
|
||||
latency_raw = raw_health.get("latency_ms")
|
||||
if latency_raw is None:
|
||||
latency_raw = data.get("latency_ms")
|
||||
|
||||
last_err = (
|
||||
str(raw_health.get("last_error") or "").strip()
|
||||
or str(data.get("last_error") or "").strip()
|
||||
)
|
||||
last_check = (
|
||||
str(raw_health.get("last_check") or "").strip()
|
||||
or str(data.get("last_check") or "").strip()
|
||||
)
|
||||
|
||||
return TransportClientHealthSnapshot(
|
||||
client_id=str(data.get("client_id") or cid).strip(),
|
||||
status=str(data.get("status") or "").strip().lower(),
|
||||
latency_ms=self._to_int(latency_raw),
|
||||
last_error=strip_ansi(last_err),
|
||||
last_check=last_check,
|
||||
)
|
||||
|
||||
def transport_client_create(
|
||||
self,
|
||||
*,
|
||||
client_id: str,
|
||||
kind: str,
|
||||
name: str = "",
|
||||
enabled: bool = True,
|
||||
config: Optional[Dict[str, Any]] = None,
|
||||
) -> CmdResult:
|
||||
cid = str(client_id or "").strip()
|
||||
if not cid:
|
||||
raise ValueError("missing transport client id")
|
||||
k = str(kind or "").strip().lower()
|
||||
if not k:
|
||||
raise ValueError("missing transport client kind")
|
||||
payload: Dict[str, Any] = {
|
||||
"id": cid,
|
||||
"kind": k,
|
||||
"enabled": bool(enabled),
|
||||
}
|
||||
if str(name or "").strip():
|
||||
payload["name"] = str(name).strip()
|
||||
if config is not None:
|
||||
payload["config"] = cast(Dict[str, Any], config)
|
||||
|
||||
data = cast(
|
||||
Dict[str, Any],
|
||||
self._json(
|
||||
self._request(
|
||||
"POST",
|
||||
"/api/v1/transport/clients",
|
||||
json_body=payload,
|
||||
)
|
||||
)
|
||||
or {},
|
||||
)
|
||||
return CmdResult(
|
||||
ok=bool(data.get("ok", False)),
|
||||
message=strip_ansi(str(data.get("message") or "").strip()),
|
||||
exit_code=None,
|
||||
stdout="",
|
||||
stderr="",
|
||||
)
|
||||
|
||||
def transport_client_patch(
|
||||
self,
|
||||
client_id: str,
|
||||
*,
|
||||
name: Optional[str] = None,
|
||||
enabled: Optional[bool] = None,
|
||||
config: Optional[Dict[str, Any]] = None,
|
||||
) -> CmdResult:
|
||||
cid = str(client_id or "").strip()
|
||||
if not cid:
|
||||
raise ValueError("missing transport client id")
|
||||
payload: Dict[str, Any] = {}
|
||||
if name is not None:
|
||||
payload["name"] = str(name).strip()
|
||||
if enabled is not None:
|
||||
payload["enabled"] = bool(enabled)
|
||||
if config is not None:
|
||||
payload["config"] = cast(Dict[str, Any], config)
|
||||
if not payload:
|
||||
raise ValueError("empty patch payload")
|
||||
|
||||
data = cast(
|
||||
Dict[str, Any],
|
||||
self._json(
|
||||
self._request(
|
||||
"PATCH",
|
||||
f"/api/v1/transport/clients/{cid}",
|
||||
json_body=payload,
|
||||
)
|
||||
)
|
||||
or {},
|
||||
)
|
||||
return CmdResult(
|
||||
ok=bool(data.get("ok", False)),
|
||||
message=strip_ansi(str(data.get("message") or "").strip()),
|
||||
exit_code=None,
|
||||
stdout="",
|
||||
stderr="",
|
||||
)
|
||||
|
||||
def transport_client_action(
|
||||
self,
|
||||
client_id: str,
|
||||
action: TransportClientAction,
|
||||
) -> TransportClientActionResult:
|
||||
cid = str(client_id or "").strip()
|
||||
if not cid:
|
||||
raise ValueError("missing transport client id")
|
||||
act = str(action or "").strip().lower()
|
||||
if act not in ("provision", "start", "stop", "restart"):
|
||||
raise ValueError(f"invalid transport action: {action}")
|
||||
|
||||
data = cast(
|
||||
Dict[str, Any],
|
||||
self._json(
|
||||
self._request(
|
||||
"POST",
|
||||
f"/api/v1/transport/clients/{cid}/{act}",
|
||||
)
|
||||
)
|
||||
or {},
|
||||
)
|
||||
|
||||
health_raw = data.get("health") or {}
|
||||
if not isinstance(health_raw, dict):
|
||||
health_raw = {}
|
||||
runtime_raw = data.get("runtime") or {}
|
||||
if not isinstance(runtime_raw, dict):
|
||||
runtime_raw = {}
|
||||
runtime_err_raw = runtime_raw.get("last_error") or {}
|
||||
if not isinstance(runtime_err_raw, dict):
|
||||
runtime_err_raw = {}
|
||||
|
||||
last_error = (
|
||||
str(health_raw.get("last_error") or "").strip()
|
||||
or str(runtime_err_raw.get("message") or "").strip()
|
||||
or str(data.get("stderr") or "").strip()
|
||||
)
|
||||
|
||||
exit_code_val = data.get("exitCode", None)
|
||||
exit_code: Optional[int]
|
||||
try:
|
||||
exit_code = int(exit_code_val) if exit_code_val is not None else None
|
||||
except (TypeError, ValueError):
|
||||
exit_code = None
|
||||
|
||||
return TransportClientActionResult(
|
||||
ok=bool(data.get("ok", False)),
|
||||
message=strip_ansi(str(data.get("message") or "").strip()),
|
||||
code=str(data.get("code") or "").strip(),
|
||||
client_id=str(data.get("client_id") or cid).strip(),
|
||||
kind=str(data.get("kind") or "").strip().lower(),
|
||||
action=str(data.get("action") or act).strip().lower(),
|
||||
status_before=str(data.get("status_before") or "").strip().lower(),
|
||||
status_after=str(data.get("status_after") or "").strip().lower(),
|
||||
last_error=strip_ansi(last_error),
|
||||
exit_code=exit_code,
|
||||
stdout=strip_ansi(str(data.get("stdout") or "")),
|
||||
stderr=strip_ansi(str(data.get("stderr") or "")),
|
||||
)
|
||||
|
||||
def transport_client_delete(
|
||||
self,
|
||||
client_id: str,
|
||||
*,
|
||||
force: bool = False,
|
||||
cleanup: bool = True,
|
||||
) -> CmdResult:
|
||||
cid = str(client_id or "").strip()
|
||||
if not cid:
|
||||
raise ValueError("missing transport client id")
|
||||
params: Dict[str, Any] = {}
|
||||
if force:
|
||||
params["force"] = "true"
|
||||
if not cleanup:
|
||||
params["cleanup"] = "false"
|
||||
data = cast(
|
||||
Dict[str, Any],
|
||||
self._json(
|
||||
self._request(
|
||||
"DELETE",
|
||||
f"/api/v1/transport/clients/{cid}",
|
||||
params=(params or None),
|
||||
)
|
||||
)
|
||||
or {},
|
||||
)
|
||||
return CmdResult(
|
||||
ok=bool(data.get("ok", False)),
|
||||
message=strip_ansi(str(data.get("message") or "").strip()),
|
||||
exit_code=None,
|
||||
stdout="",
|
||||
stderr="",
|
||||
)
|
||||
|
||||
def transport_netns_toggle(
|
||||
self,
|
||||
*,
|
||||
enabled: Optional[bool] = None,
|
||||
client_ids: Optional[List[str]] = None,
|
||||
provision: bool = True,
|
||||
restart_running: bool = True,
|
||||
) -> TransportNetnsToggleResult:
|
||||
payload: Dict[str, Any] = {
|
||||
"provision": bool(provision),
|
||||
"restart_running": bool(restart_running),
|
||||
}
|
||||
if enabled is not None:
|
||||
payload["enabled"] = bool(enabled)
|
||||
if client_ids is not None:
|
||||
payload["client_ids"] = [
|
||||
str(x).strip()
|
||||
for x in (client_ids or [])
|
||||
if str(x).strip()
|
||||
]
|
||||
|
||||
data = cast(
|
||||
Dict[str, Any],
|
||||
self._json(
|
||||
self._request(
|
||||
"POST",
|
||||
"/api/v1/transport/netns/toggle",
|
||||
json_body=payload,
|
||||
)
|
||||
)
|
||||
or {},
|
||||
)
|
||||
|
||||
raw_items = data.get("items") or []
|
||||
if not isinstance(raw_items, list):
|
||||
raw_items = []
|
||||
items: List[TransportNetnsToggleItem] = []
|
||||
for row in raw_items:
|
||||
if not isinstance(row, dict):
|
||||
continue
|
||||
items.append(
|
||||
TransportNetnsToggleItem(
|
||||
ok=bool(row.get("ok", False)),
|
||||
message=strip_ansi(str(row.get("message") or "").strip()),
|
||||
code=str(row.get("code") or "").strip(),
|
||||
client_id=str(row.get("client_id") or "").strip(),
|
||||
kind=str(row.get("kind") or "").strip().lower(),
|
||||
status_before=str(row.get("status_before") or "").strip().lower(),
|
||||
status_after=str(row.get("status_after") or "").strip().lower(),
|
||||
netns_enabled=bool(row.get("netns_enabled", False)),
|
||||
config_updated=bool(row.get("config_updated", False)),
|
||||
provisioned=bool(row.get("provisioned", False)),
|
||||
restarted=bool(row.get("restarted", False)),
|
||||
stdout=strip_ansi(str(row.get("stdout") or "")),
|
||||
stderr=strip_ansi(str(row.get("stderr") or "")),
|
||||
)
|
||||
)
|
||||
|
||||
return TransportNetnsToggleResult(
|
||||
ok=bool(data.get("ok", False)),
|
||||
message=strip_ansi(str(data.get("message") or "").strip()),
|
||||
code=str(data.get("code") or "").strip(),
|
||||
enabled=bool(data.get("enabled", False)),
|
||||
count=self._to_int(data.get("count")),
|
||||
success_count=self._to_int(data.get("success_count")),
|
||||
failure_count=self._to_int(data.get("failure_count")),
|
||||
items=items,
|
||||
)
|
||||
|
||||
def _parse_transport_interface_item(self, row: Any) -> Optional[TransportInterfaceItem]:
|
||||
if not isinstance(row, dict):
|
||||
return None
|
||||
iface_id = str(row.get("id") or "").strip()
|
||||
if not iface_id:
|
||||
return None
|
||||
raw_ids = row.get("client_ids") or []
|
||||
if not isinstance(raw_ids, list):
|
||||
raw_ids = []
|
||||
client_ids = [str(x).strip() for x in raw_ids if str(x).strip()]
|
||||
cfg = row.get("config") or {}
|
||||
if not isinstance(cfg, dict):
|
||||
cfg = {}
|
||||
return TransportInterfaceItem(
|
||||
id=iface_id,
|
||||
name=str(row.get("name") or "").strip(),
|
||||
mode=str(row.get("mode") or "").strip().lower(),
|
||||
runtime_iface=str(row.get("runtime_iface") or "").strip(),
|
||||
netns_name=str(row.get("netns_name") or "").strip(),
|
||||
routing_table=str(row.get("routing_table") or "").strip(),
|
||||
client_ids=client_ids,
|
||||
client_count=self._to_int(row.get("client_count")),
|
||||
up_count=self._to_int(row.get("up_count")),
|
||||
updated_at=str(row.get("updated_at") or "").strip(),
|
||||
config=cast(Dict[str, Any], cfg),
|
||||
)
|
||||
def _parse_transport_client(self, row: Any) -> Optional[TransportClient]:
|
||||
if not isinstance(row, dict):
|
||||
return None
|
||||
cid = str(row.get("id") or "").strip()
|
||||
if not cid:
|
||||
return None
|
||||
raw_health = row.get("health") or {}
|
||||
if not isinstance(raw_health, dict):
|
||||
raw_health = {}
|
||||
raw_caps = row.get("capabilities") or []
|
||||
if not isinstance(raw_caps, list):
|
||||
raw_caps = []
|
||||
raw_cfg = row.get("config") or {}
|
||||
if not isinstance(raw_cfg, dict):
|
||||
raw_cfg = {}
|
||||
return TransportClient(
|
||||
id=cid,
|
||||
name=str(row.get("name") or "").strip(),
|
||||
kind=str(row.get("kind") or "").strip().lower(),
|
||||
enabled=bool(row.get("enabled", False)),
|
||||
status=str(row.get("status") or "").strip().lower(),
|
||||
iface=str(row.get("iface") or "").strip(),
|
||||
routing_table=str(row.get("routing_table") or "").strip(),
|
||||
mark_hex=str(row.get("mark_hex") or "").strip(),
|
||||
priority_base=self._to_int(row.get("priority_base")),
|
||||
capabilities=[str(x).strip() for x in raw_caps if str(x).strip()],
|
||||
health=TransportClientHealth(
|
||||
last_check=str(raw_health.get("last_check") or "").strip(),
|
||||
latency_ms=self._to_int(raw_health.get("latency_ms")),
|
||||
last_error=str(raw_health.get("last_error") or "").strip(),
|
||||
),
|
||||
config=cast(Dict[str, Any], raw_cfg),
|
||||
updated_at=str(row.get("updated_at") or "").strip(),
|
||||
)
|
||||
Reference in New Issue
Block a user