From eb2ebf04e06d22042e0a005cb072d5f8442e064e Mon Sep 17 00:00:00 2001 From: Philippe Parage Date: Thu, 19 Mar 2026 09:48:45 +0100 Subject: [PATCH 01/33] fix(cors): make CORS origin configurable via CORS_ORIGIN_REGEX env var (closes #58) Default unchanged: localhost/127.0.0.1/[::1] only. Set CORS_ORIGIN_REGEX env var to allow additional origins, e.g.: CORS_ORIGIN_REGEX=^https?://(localhost|127\.0\.0\.1|\[::1\]|192\.168\.42\.\d+)(:\d+)?$ --- app/main.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/app/main.py b/app/main.py index e8cfe2e..7c898dd 100644 --- a/app/main.py +++ b/app/main.py @@ -111,16 +111,23 @@ async def lifespan(app: FastAPI): # "https://localhost" # ] +## CORS Configuration +## Set CORS_ORIGIN_REGEX env var to allow additional origins. +## Default: localhost only. Example for lab network: +## CORS_ORIGIN_REGEX=^https?://(localhost|127\.0\.0\.1|\[::1\]|192\.168\.42\.\d+)(:\d+)?$ +cors_origin_regex = os.getenv( + "CORS_ORIGIN_REGEX", + r"^https?://(localhost|127\.0\.0\.1|\[::1\])(:\d+)?$" +) + middleware = [ Middleware( CORSMiddleware, # type: ignore[arg-type] - allow_origin_regex=r"^https?://(localhost|127\.0\.0\.1|\[::1\])(:\d+)?$", - # allow_origins=origins, + allow_origin_regex=cors_origin_regex, allow_credentials=True, allow_methods=["GET","POST","DELETE","OPTIONS"], allow_headers=["Content-Type","Accept","Authorization"], max_age=600, - ) ] From edfb6bed41f6118b8be765e23b18654459b90c31 Mon Sep 17 00:00:00 2001 From: Philippe Parage Date: Thu, 19 Mar 2026 10:05:38 +0100 Subject: [PATCH 02/33] feat(ws): add WebSocket endpoint for real-time VM status streaming New endpoint: ws://host:8000/ws/vm-status?node=X&api_host=X&token_id=X&token_secret=X - Polls Proxmox API directly via httpx (fast, no Ansible overhead) - Sends full VM state on first connect - Sends only diffs (status/CPU changes) on subsequent updates - Filters out templates - 5-second poll interval - Auto-cleanup on disconnect --- app/main.py | 4 ++ app/routes/ws_status.py | 132 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 136 insertions(+) create mode 100644 app/routes/ws_status.py diff --git a/app/main.py b/app/main.py index 7c898dd..ff32589 100644 --- a/app/main.py +++ b/app/main.py @@ -230,6 +230,10 @@ async def validation_exception_handler(request: Request, exc: RequestValidationE app.include_router(api_router) +# WebSocket routes +from app.routes.ws_status import router as ws_router +app.include_router(ws_router) + #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### ### diff --git a/app/routes/ws_status.py b/app/routes/ws_status.py new file mode 100644 index 0000000..335ad4c --- /dev/null +++ b/app/routes/ws_status.py @@ -0,0 +1,132 @@ +""" +WebSocket endpoint for real-time VM status updates. + +Polls Proxmox API directly via httpx (not Ansible — too slow for real-time) +and pushes status changes to connected clients. + +Usage: + ws://host:8000/ws/vm-status?node=pve01&api_host=100.64.0.14:8006&token_id=root@pam!range42-deploy&token_secret=xxx +""" + +import asyncio +import json +import logging +from typing import Dict, Optional + +import httpx +from fastapi import APIRouter, WebSocket, WebSocketDisconnect + +logger = logging.getLogger(__name__) + +router = APIRouter() + +POLL_INTERVAL = 5 # seconds + + +async def fetch_vm_status( + client: httpx.AsyncClient, + api_host: str, + node: str, + token_id: str, + token_secret: str, +) -> list[dict]: + """Fetch VM list directly from Proxmox API (bypasses Ansible for speed).""" + url = f"https://{api_host}/api2/json/nodes/{node}/qemu" + headers = {"Authorization": f"PVEAPIToken={token_id}={token_secret}"} + + try: + resp = await client.get(url, headers=headers, timeout=10) + resp.raise_for_status() + data = resp.json().get("data", []) + return [ + { + "vmid": vm["vmid"], + "name": vm.get("name", ""), + "status": vm.get("status", "unknown"), + "cpu": round(vm.get("cpu", 0) * 100, 1), + "mem": vm.get("mem", 0), + "maxmem": vm.get("maxmem", 0), + "uptime": vm.get("uptime", 0), + "template": vm.get("template", 0), + "tags": vm.get("tags", ""), + } + for vm in data + ] + except Exception as e: + logger.warning(f"[ws] Proxmox poll failed: {e}") + return [] + + +def compute_diff( + prev: Dict[int, dict], current: Dict[int, dict] +) -> Optional[dict]: + """Compare previous and current VM states, return changes.""" + changes = {} + + for vmid, vm in current.items(): + old = prev.get(vmid) + if old is None: + changes[vmid] = {"type": "added", **vm} + elif old["status"] != vm["status"] or abs(old.get("cpu", 0) - vm.get("cpu", 0)) > 2: + changes[vmid] = {"type": "changed", **vm} + + for vmid in prev: + if vmid not in current: + changes[vmid] = {"type": "removed", "vmid": vmid} + + return changes if changes else None + + +@router.websocket("/ws/vm-status") +async def vm_status_websocket(ws: WebSocket): + await ws.accept() + + # Read connection params from query string + params = ws.query_params + node = params.get("node", "pve01") + api_host = params.get("api_host", "") + token_id = params.get("token_id", "") + token_secret = params.get("token_secret", "") + + if not api_host or not token_id or not token_secret: + await ws.send_json({"error": "Missing api_host, token_id, or token_secret query params"}) + await ws.close() + return + + logger.info(f"[ws] Client connected for node={node} via {api_host}") + + prev_state: Dict[int, dict] = {} + + async with httpx.AsyncClient(verify=False) as client: + try: + while True: + vms = await fetch_vm_status(client, api_host, node, token_id, token_secret) + + current_state = {vm["vmid"]: vm for vm in vms if vm.get("template", 0) != 1} + + # First message: send full state + if not prev_state: + await ws.send_json({ + "type": "full", + "vms": list(current_state.values()), + }) + else: + # Subsequent: send only changes + diff = compute_diff(prev_state, current_state) + if diff: + await ws.send_json({ + "type": "diff", + "changes": diff, + }) + + prev_state = current_state + await asyncio.sleep(POLL_INTERVAL) + + except WebSocketDisconnect: + logger.info(f"[ws] Client disconnected for node={node}") + except Exception as e: + logger.error(f"[ws] Error: {e}") + try: + await ws.send_json({"error": str(e)}) + except Exception: + pass From 183748e8310ca847ebf87444db0b6b2e443e879a Mon Sep 17 00:00:00 2001 From: Philippe Parage Date: Thu, 19 Mar 2026 10:56:35 +0100 Subject: [PATCH 03/33] fix(ws): read Proxmox credentials from inventory, not query params MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WebSocket endpoint now reads API host, token ID, and token secret from the backend's own inventory/hosts.yml file. Frontend only needs to connect to ws://backend/ws/vm-status — no credentials in the browser. --- app/routes/ws_status.py | 53 +++++++++++++++++++++++++++++++++-------- 1 file changed, 43 insertions(+), 10 deletions(-) diff --git a/app/routes/ws_status.py b/app/routes/ws_status.py index 335ad4c..88f0e21 100644 --- a/app/routes/ws_status.py +++ b/app/routes/ws_status.py @@ -4,16 +4,23 @@ Polls Proxmox API directly via httpx (not Ansible — too slow for real-time) and pushes status changes to connected clients. +Proxmox credentials are read from the backend's own inventory file +so the frontend never needs to handle API tokens. + Usage: - ws://host:8000/ws/vm-status?node=pve01&api_host=100.64.0.14:8006&token_id=root@pam!range42-deploy&token_secret=xxx + ws://host:8000/ws/vm-status + ws://host:8000/ws/vm-status?node=pve01 """ import asyncio import json import logging +import os +from pathlib import Path from typing import Dict, Optional import httpx +import yaml from fastapi import APIRouter, WebSocket, WebSocketDisconnect logger = logging.getLogger(__name__) @@ -23,6 +30,31 @@ POLL_INTERVAL = 5 # seconds +def load_proxmox_credentials() -> dict: + """Read Proxmox API credentials from the backend's inventory file.""" + inv_dir = os.getenv("API_BACKEND_INVENTORY_DIR", "") + inv_path = Path(inv_dir) / "hosts.yml" if inv_dir else Path("inventory/hosts.yml") + + try: + with open(inv_path) as f: + inv = yaml.safe_load(f) + + # Navigate to proxmox host vars + px = inv.get("all", {}).get("children", {}).get("range42_infrastructure", {}).get("children", {}).get("proxmox", {}).get("hosts", {}) + for host_name, host_vars in px.items(): + if host_vars and host_vars.get("proxmox_api_host"): + return { + "api_host": host_vars["proxmox_api_host"], + "node": host_vars.get("proxmox_node", "pve01"), + "token_id": f"{host_vars.get('proxmox_api_user', 'root@pam')}!{host_vars.get('proxmox_api_token_id', '')}", + "token_secret": host_vars.get("proxmox_api_token_secret", ""), + } + except Exception as e: + logger.error(f"[ws] Failed to load inventory: {e}") + + return {} + + async def fetch_vm_status( client: httpx.AsyncClient, api_host: str, @@ -81,18 +113,19 @@ def compute_diff( async def vm_status_websocket(ws: WebSocket): await ws.accept() - # Read connection params from query string - params = ws.query_params - node = params.get("node", "pve01") - api_host = params.get("api_host", "") - token_id = params.get("token_id", "") - token_secret = params.get("token_secret", "") - - if not api_host or not token_id or not token_secret: - await ws.send_json({"error": "Missing api_host, token_id, or token_secret query params"}) + # Read Proxmox credentials from backend inventory (not from client) + creds = load_proxmox_credentials() + if not creds: + await ws.send_json({"error": "Proxmox credentials not found in backend inventory"}) await ws.close() return + # Allow node override from query string + node = ws.query_params.get("node", creds["node"]) + api_host = creds["api_host"] + token_id = creds["token_id"] + token_secret = creds["token_secret"] + logger.info(f"[ws] Client connected for node={node} via {api_host}") prev_state: Dict[int, dict] = {} From f3621a5019d9d0217006f36aa36c052cf64981f3 Mon Sep 17 00:00:00 2001 From: Philippe Parage Date: Thu, 19 Mar 2026 14:52:44 +0100 Subject: [PATCH 04/33] test: add smoke tests and golden route reference as safety net (#53, #54) --- pytest.ini | 3 + requirements.txt | 3 + tests/__init__.py | 0 tests/conftest.py | 28 ++++ tests/fixtures/routes_golden.json | 242 ++++++++++++++++++++++++++++++ tests/test_api_smoke.py | 48 ++++++ 6 files changed, 324 insertions(+) create mode 100644 pytest.ini create mode 100644 tests/__init__.py create mode 100644 tests/conftest.py create mode 100644 tests/fixtures/routes_golden.json create mode 100644 tests/test_api_smoke.py diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..6f94355 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,3 @@ +[pytest] +testpaths = tests +asyncio_mode = auto diff --git a/requirements.txt b/requirements.txt index 5bb936e..fc72494 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,6 +18,9 @@ setuptools>=70.0.0 wheel>=0.43.0 +pytest==8.3.4 +pytest-asyncio==0.24.0 + # for wazuh ? # pywinrm # requests-ntlm \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..9cfcbb2 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,28 @@ +import json +import os +import pytest +from pathlib import Path +from fastapi.testclient import TestClient + +_PROJECT_ROOT = str(Path(__file__).resolve().parents[1]) + +# Set ALL env vars that are read at module-import time. +# Routes call Path(os.getenv("PROJECT_ROOT_DIR")).resolve() at import time. +os.environ.setdefault("PROJECT_ROOT_DIR", _PROJECT_ROOT) +os.environ.setdefault("API_BACKEND_WWWAPP_PLAYBOOKS_DIR", _PROJECT_ROOT) +os.environ.setdefault("API_BACKEND_PUBLIC_PLAYBOOKS_DIR", _PROJECT_ROOT) +os.environ.setdefault("API_BACKEND_INVENTORY_DIR", str(Path(_PROJECT_ROOT) / "inventory")) + + +@pytest.fixture +def client(): + from app.main import app + return TestClient(app) + + +@pytest.fixture(scope="session") +def openapi_schema(): + from app.main import app + c = TestClient(app) + resp = c.get("/docs/openapi.json") + return resp.json() diff --git a/tests/fixtures/routes_golden.json b/tests/fixtures/routes_golden.json new file mode 100644 index 0000000..fef0a2e --- /dev/null +++ b/tests/fixtures/routes_golden.json @@ -0,0 +1,242 @@ +{ + "/v0/admin/debug/func_test": [ + "POST" + ], + "/v0/admin/debug/ping": [ + "POST" + ], + "/v0/admin/proxmox/firewall/datacenter/disable": [ + "POST" + ], + "/v0/admin/proxmox/firewall/datacenter/enable": [ + "POST" + ], + "/v0/admin/proxmox/firewall/node/disable": [ + "POST" + ], + "/v0/admin/proxmox/firewall/node/enable": [ + "POST" + ], + "/v0/admin/proxmox/firewall/vm/alias/add": [ + "POST" + ], + "/v0/admin/proxmox/firewall/vm/alias/delete": [ + "DELETE" + ], + "/v0/admin/proxmox/firewall/vm/alias/list": [ + "POST" + ], + "/v0/admin/proxmox/firewall/vm/disable": [ + "POST" + ], + "/v0/admin/proxmox/firewall/vm/enable": [ + "POST" + ], + "/v0/admin/proxmox/firewall/vm/rules/apply": [ + "POST" + ], + "/v0/admin/proxmox/firewall/vm/rules/delete": [ + "DELETE" + ], + "/v0/admin/proxmox/firewall/vm/rules/list": [ + "POST" + ], + "/v0/admin/proxmox/network//node/list": [ + "POST" + ], + "/v0/admin/proxmox/network/node/add": [ + "POST" + ], + "/v0/admin/proxmox/network/node/delete": [ + "POST" + ], + "/v0/admin/proxmox/network/vm/add": [ + "POST" + ], + "/v0/admin/proxmox/network/vm/delete": [ + "POST" + ], + "/v0/admin/proxmox/network/vm/list": [ + "POST" + ], + "/v0/admin/proxmox/storage/download_iso": [ + "POST" + ], + "/v0/admin/proxmox/storage/list": [ + "POST" + ], + "/v0/admin/proxmox/storage/storage_name/list_iso": [ + "POST" + ], + "/v0/admin/proxmox/storage/storage_name/list_template": [ + "POST" + ], + "/v0/admin/proxmox/vms/list": [ + "POST" + ], + "/v0/admin/proxmox/vms/list_usage": [ + "POST" + ], + "/v0/admin/proxmox/vms/vm_id/clone": [ + "POST" + ], + "/v0/admin/proxmox/vms/vm_id/config/vm_get_config": [ + "POST" + ], + "/v0/admin/proxmox/vms/vm_id/config/vm_get_config_cdrom": [ + "POST" + ], + "/v0/admin/proxmox/vms/vm_id/config/vm_get_config_cpu": [ + "POST" + ], + "/v0/admin/proxmox/vms/vm_id/config/vm_get_config_ram": [ + "POST" + ], + "/v0/admin/proxmox/vms/vm_id/config/vm_set_tag": [ + "POST" + ], + "/v0/admin/proxmox/vms/vm_id/create": [ + "POST" + ], + "/v0/admin/proxmox/vms/vm_id/delete": [ + "DELETE" + ], + "/v0/admin/proxmox/vms/vm_id/pause": [ + "POST" + ], + "/v0/admin/proxmox/vms/vm_id/resume": [ + "POST" + ], + "/v0/admin/proxmox/vms/vm_id/snapshot/create": [ + "POST" + ], + "/v0/admin/proxmox/vms/vm_id/snapshot/delete": [ + "DELETE" + ], + "/v0/admin/proxmox/vms/vm_id/snapshot/list": [ + "POST" + ], + "/v0/admin/proxmox/vms/vm_id/snapshot/revert": [ + "POST" + ], + "/v0/admin/proxmox/vms/vm_id/start": [ + "POST" + ], + "/v0/admin/proxmox/vms/vm_id/stop": [ + "POST" + ], + "/v0/admin/proxmox/vms/vm_id/stop_force": [ + "POST" + ], + "/v0/admin/proxmox/vms/vm_ids/delete": [ + "DELETE" + ], + "/v0/admin/proxmox/vms/vm_ids/pause": [ + "POST" + ], + "/v0/admin/proxmox/vms/vm_ids/resume": [ + "POST" + ], + "/v0/admin/proxmox/vms/vm_ids/start": [ + "POST" + ], + "/v0/admin/proxmox/vms/vm_ids/stop": [ + "POST" + ], + "/v0/admin/proxmox/vms/vm_ids/stop_force": [ + "POST" + ], + "/v0/admin/run/bundles/core/linux/ubuntu/configure/add-user": [ + "POST" + ], + "/v0/admin/run/bundles/core/linux/ubuntu/install/basic-packages": [ + "POST" + ], + "/v0/admin/run/bundles/core/linux/ubuntu/install/docker": [ + "POST" + ], + "/v0/admin/run/bundles/core/linux/ubuntu/install/docker-compose": [ + "POST" + ], + "/v0/admin/run/bundles/core/linux/ubuntu/install/dot-files": [ + "POST" + ], + "/v0/admin/run/bundles/core/proxmox/configure/default/create-vms-admin": [ + "POST" + ], + "/v0/admin/run/bundles/core/proxmox/configure/default/create-vms-student": [ + "POST" + ], + "/v0/admin/run/bundles/core/proxmox/configure/default/create-vms-vuln": [ + "POST" + ], + "/v0/admin/run/bundles/core/proxmox/configure/default/delete-vms-admin": [ + "DELETE" + ], + "/v0/admin/run/bundles/core/proxmox/configure/default/delete-vms-student": [ + "DELETE" + ], + "/v0/admin/run/bundles/core/proxmox/configure/default/delete-vms-vuln": [ + "DELETE" + ], + "/v0/admin/run/bundles/core/proxmox/configure/default/pause-vms-admin": [ + "POST" + ], + "/v0/admin/run/bundles/core/proxmox/configure/default/pause-vms-student": [ + "POST" + ], + "/v0/admin/run/bundles/core/proxmox/configure/default/pause-vms-vuln": [ + "POST" + ], + "/v0/admin/run/bundles/core/proxmox/configure/default/resume-vms-admin": [ + "POST" + ], + "/v0/admin/run/bundles/core/proxmox/configure/default/resume-vms-student": [ + "POST" + ], + "/v0/admin/run/bundles/core/proxmox/configure/default/resume-vms-vuln": [ + "POST" + ], + "/v0/admin/run/bundles/core/proxmox/configure/default/snapshot/create-vms-admin": [ + "POST" + ], + "/v0/admin/run/bundles/core/proxmox/configure/default/snapshot/create-vms-student": [ + "POST" + ], + "/v0/admin/run/bundles/core/proxmox/configure/default/snapshot/create-vms-vuln": [ + "POST" + ], + "/v0/admin/run/bundles/core/proxmox/configure/default/snapshot/revert-vms-admin": [ + "POST" + ], + "/v0/admin/run/bundles/core/proxmox/configure/default/snapshot/revert-vms-student": [ + "POST" + ], + "/v0/admin/run/bundles/core/proxmox/configure/default/snapshot/revert-vms-vuln": [ + "POST" + ], + "/v0/admin/run/bundles/core/proxmox/configure/default/start-vms-admin": [ + "POST" + ], + "/v0/admin/run/bundles/core/proxmox/configure/default/start-vms-student": [ + "POST" + ], + "/v0/admin/run/bundles/core/proxmox/configure/default/start-vms-vuln": [ + "POST" + ], + "/v0/admin/run/bundles/core/proxmox/configure/default/stop-vms-admin": [ + "POST" + ], + "/v0/admin/run/bundles/core/proxmox/configure/default/stop-vms-student": [ + "POST" + ], + "/v0/admin/run/bundles/core/proxmox/configure/default/stop-vms-vuln": [ + "POST" + ], + "/v0/admin/run/bundles/{bundles_name}/run": [ + "POST" + ], + "/v0/admin/run/scenarios/{scenario_name}/run": [ + "POST" + ] +} \ No newline at end of file diff --git a/tests/test_api_smoke.py b/tests/test_api_smoke.py new file mode 100644 index 0000000..222f978 --- /dev/null +++ b/tests/test_api_smoke.py @@ -0,0 +1,48 @@ +import json +from pathlib import Path + + +def test_app_starts(client): + assert client is not None + + +def test_openapi_schema_loads(client): + resp = client.get("/docs/openapi.json") + assert resp.status_code == 200 + schema = resp.json() + assert schema["info"]["title"] == "CR42 - API" + assert schema["info"]["version"] == "v0.1" + + +def test_swagger_docs_available(client): + resp = client.get("/docs/swagger") + assert resp.status_code == 200 + + +def test_redoc_docs_available(client): + resp = client.get("/docs/redoc") + assert resp.status_code == 200 + + +def test_all_routes_match_golden_reference(client): + golden_path = Path(__file__).parent / "fixtures" / "routes_golden.json" + assert golden_path.exists(), f"Golden reference not found at {golden_path}" + + with open(golden_path) as f: + golden_routes = json.load(f) + + resp = client.get("/docs/openapi.json") + schema = resp.json() + registered = {} + for path, methods in schema.get("paths", {}).items(): + for method in methods: + if method.upper() in ("GET", "POST", "PUT", "DELETE", "PATCH"): + registered.setdefault(path, []).append(method.upper()) + + missing = [] + for path, methods in golden_routes.items(): + for method in methods: + if method not in registered.get(path, []): + missing.append(f"{method} {path}") + + assert not missing, f"Missing {len(missing)} routes:\n" + "\n".join(sorted(missing)) From 2cc39476c6d51a6ec20e4ca1f6ee8d73e37863be Mon Sep 17 00:00:00 2001 From: Philippe Parage Date: Thu, 19 Mar 2026 14:54:41 +0100 Subject: [PATCH 05/33] refactor: add VaultManager class replacing global state (#54) --- app/core/__init__.py | 0 app/core/vault.py | 16 ++++++++++++++++ tests/test_vault.py | 21 +++++++++++++++++++++ 3 files changed, 37 insertions(+) create mode 100644 app/core/__init__.py create mode 100644 app/core/vault.py create mode 100644 tests/test_vault.py diff --git a/app/core/__init__.py b/app/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/core/vault.py b/app/core/vault.py new file mode 100644 index 0000000..2bec680 --- /dev/null +++ b/app/core/vault.py @@ -0,0 +1,16 @@ +"""Vault password management. No global state — uses a class instance.""" + +from pathlib import Path + + +class VaultManager: + """Manages the Ansible vault password file path.""" + + def __init__(self) -> None: + self._vault_pass_path: Path | None = None + + def set_vault_path(self, p: Path | None) -> None: + self._vault_pass_path = p + + def get_vault_path(self) -> Path | None: + return self._vault_pass_path diff --git a/tests/test_vault.py b/tests/test_vault.py new file mode 100644 index 0000000..c621a18 --- /dev/null +++ b/tests/test_vault.py @@ -0,0 +1,21 @@ +from pathlib import Path +from app.core.vault import VaultManager + + +def test_vault_manager_starts_empty(): + vm = VaultManager() + assert vm.get_vault_path() is None + + +def test_vault_manager_set_and_get(): + vm = VaultManager() + p = Path("/tmp/test-vault.txt") + vm.set_vault_path(p) + assert vm.get_vault_path() == p + + +def test_vault_manager_reset(): + vm = VaultManager() + vm.set_vault_path(Path("/tmp/test-vault.txt")) + vm.set_vault_path(None) + assert vm.get_vault_path() is None From ff85436302058ec3f268d9480aa832c1623aa26f Mon Sep 17 00:00:00 2001 From: Philippe Parage Date: Thu, 19 Mar 2026 14:54:45 +0100 Subject: [PATCH 06/33] refactor: add centralized config module (#53, #54) --- app/core/config.py | 46 ++++++++++++++++++++++++++++++++++++++++++++ tests/test_config.py | 38 ++++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+) create mode 100644 app/core/config.py create mode 100644 tests/test_config.py diff --git a/app/core/config.py b/app/core/config.py new file mode 100644 index 0000000..d57168f --- /dev/null +++ b/app/core/config.py @@ -0,0 +1,46 @@ +"""Centralized application configuration. All env vars read here, nowhere else.""" + +import os +from dataclasses import dataclass, field +from pathlib import Path + + +@dataclass(frozen=True) +class Settings: + """Immutable application settings loaded from environment variables.""" + + project_root: Path = field(default_factory=lambda: Path(os.getenv("PROJECT_ROOT_DIR", ".")).resolve()) + + # Playbook paths + wwwapp_playbooks_dir: str = field(default_factory=lambda: os.getenv("API_BACKEND_WWWAPP_PLAYBOOKS_DIR", "")) + public_playbooks_dir: str = field(default_factory=lambda: os.getenv("API_BACKEND_PUBLIC_PLAYBOOKS_DIR", "")) + inventory_dir: str = field(default_factory=lambda: os.getenv("API_BACKEND_INVENTORY_DIR", "")) + vault_file: str = field(default_factory=lambda: os.getenv("API_BACKEND_VAULT_FILE", "")) + + # Vault credentials + vault_password_file: str = field(default_factory=lambda: os.getenv("VAULT_PASSWORD_FILE", "")) + vault_password: str = field(default_factory=lambda: os.getenv("VAULT_PASSWORD", "")) + + # CORS + cors_origin_regex: str = field( + default_factory=lambda: os.getenv( + "CORS_ORIGIN_REGEX", + r"^https?://(localhost|127\.0\.0\.1|\[::1\])(:\d+)?$", + ) + ) + + # Server + host: str = field(default_factory=lambda: os.getenv("HOST", "0.0.0.0")) + port: int = field(default_factory=lambda: int(os.getenv("PORT", "8000"))) + debug: bool = field(default_factory=lambda: os.getenv("DEBUG", "").lower() in ("1", "true", "yes")) + + @property + def playbook_path(self) -> Path: + return self.project_root / "playbooks" / "generic.yml" + + @property + def inventory_name(self) -> str: + return "hosts" + + +settings = Settings() diff --git a/tests/test_config.py b/tests/test_config.py new file mode 100644 index 0000000..a6ac978 --- /dev/null +++ b/tests/test_config.py @@ -0,0 +1,38 @@ +import os +from pathlib import Path + + +def test_settings_loads_from_env(monkeypatch): + monkeypatch.setenv("PROJECT_ROOT_DIR", "/tmp/test-project") + monkeypatch.setenv("CORS_ORIGIN_REGEX", r"^https?://example\.com$") + + import importlib + import app.core.config as config_mod + importlib.reload(config_mod) + from app.core.config import settings + + assert settings.project_root == Path("/tmp/test-project") + assert settings.cors_origin_regex == r"^https?://example\.com$" + + +def test_settings_defaults(monkeypatch): + monkeypatch.setenv("PROJECT_ROOT_DIR", "/tmp/test-project") + monkeypatch.delenv("CORS_ORIGIN_REGEX", raising=False) + + import importlib + import app.core.config as config_mod + importlib.reload(config_mod) + + assert "localhost" in config_mod.Settings().cors_origin_regex + + +def test_settings_playbook_path(monkeypatch): + monkeypatch.setenv("PROJECT_ROOT_DIR", "/tmp/test-project") + + import importlib + import app.core.config as config_mod + importlib.reload(config_mod) + + s = config_mod.Settings() + assert s.playbook_path == Path("/tmp/test-project/playbooks/generic.yml") + assert s.inventory_name == "hosts" From c60aa8835c272c96f168672473b63f7fb658a3d2 Mon Sep 17 00:00:00 2001 From: Philippe Parage Date: Thu, 19 Mar 2026 14:55:49 +0100 Subject: [PATCH 07/33] refactor: add clean runner and extractor in app/core/ (#53, #54) --- app/core/extractor.py | 13 ++++ app/core/runner.py | 150 ++++++++++++++++++++++++++++++++++++++++ tests/test_extractor.py | 32 +++++++++ tests/test_runner.py | 27 ++++++++ 4 files changed, 222 insertions(+) create mode 100644 app/core/extractor.py create mode 100644 app/core/runner.py create mode 100644 tests/test_extractor.py create mode 100644 tests/test_runner.py diff --git a/app/core/extractor.py b/app/core/extractor.py new file mode 100644 index 0000000..4d56198 --- /dev/null +++ b/app/core/extractor.py @@ -0,0 +1,13 @@ +"""Extract structured results from Ansible runner events.""" + + +def extract_action_results(events: list[dict], action_to_search: str) -> list: + """Find all runner_on_ok events containing the specified action key.""" + out = [] + for ev in events: + if ev.get("event") != "runner_on_ok": + continue + res = (ev.get("event_data") or {}).get("res") + if isinstance(res, dict) and action_to_search in res: + out.append(res[action_to_search]) + return out diff --git a/app/core/runner.py b/app/core/runner.py new file mode 100644 index 0000000..8329ed3 --- /dev/null +++ b/app/core/runner.py @@ -0,0 +1,150 @@ +"""Clean playbook runner. Fixes temp-dir leak, removes print debugging, uses VaultManager.""" + +import os +import shutil +import tempfile +from pathlib import Path + +from ansible_runner import run + +from app.core.vault import VaultManager +from app.utils.text_cleaner import strip_ansi + +vault_manager = VaultManager() + + +def build_logs(events) -> tuple[str, str]: + """Build ansible log strings with and without ANSI escape codes.""" + lines = [] + for ev in events: + stdout = ev.get("stdout") + if stdout: + lines.append(stdout) + + text_ansi = "\n".join(lines).strip() + return text_ansi, strip_ansi(text_ansi) + + +def _build_envvars(vm: VaultManager) -> dict: + """Build the Ansible environment variables dict.""" + home_collections = os.path.expanduser("~/.ansible/collections") + sys_collections = "/usr/share/ansible/collections" + coll_paths = f"{home_collections}:{sys_collections}" + + envvars = { + "ANSIBLE_HOST_KEY_CHECKING": "True", + "ANSIBLE_DEPRECATION_WARNINGS": "False", + "ANSIBLE_INVENTORY_ENABLED": "yaml,ini", + "PYTHONWARNINGS": "ignore::DeprecationWarning", + "ANSIBLE_ROLES_PATH": os.environ.get("ANSIBLE_ROLES_PATH", ""), + "ANSIBLE_FILTER_PLUGINS": os.environ.get("ANSIBLE_FILTER_PLUGINS", ""), + "ANSIBLE_COLLECTIONS_PATH": os.environ.get("ANSIBLE_COLLECTIONS_PATH", coll_paths), + "ANSIBLE_COLLECTIONS_PATHS": os.environ.get("ANSIBLE_COLLECTIONS_PATHS", coll_paths), + "ANSIBLE_LIBRARY": os.environ.get("ANSIBLE_LIBRARY", ""), + } + + # Vault password file + if os.getenv("VAULT_PASSWORD_FILE"): + envvars["ANSIBLE_VAULT_PASSWORD_FILE"] = os.environ["VAULT_PASSWORD_FILE"] + elif vm.get_vault_path(): + envvars["ANSIBLE_VAULT_PASSWORD_FILE"] = str(vm.get_vault_path()) + + if os.getenv("ANSIBLE_CONFIG"): + envvars["ANSIBLE_CONFIG"] = os.environ["ANSIBLE_CONFIG"] + + return envvars + + +def _setup_temp_dir( + inventory: Path, playbook: Path, vm: VaultManager, +) -> tuple[Path, Path, Path]: + """Create temp dir, copy playbook tree and inventory, write envvars. + + Returns (tmp_dir, inventory_path_in_tmp, playbook_relative_path). + """ + tmp_dir = Path(tempfile.mkdtemp(prefix="runner-")) + + project_dir = tmp_dir / "project" + inventory_dir = tmp_dir / "inventory" + project_dir.mkdir(parents=True, exist_ok=True) + inventory_dir.mkdir(parents=True, exist_ok=True) + + # Copy the playbook's parent directory into project/ + src_dir = playbook.parent + dst_dir = project_dir / src_dir.name + shutil.copytree(src_dir, dst_dir, dirs_exist_ok=True) + + play_rel = (dst_dir / playbook.name).relative_to(project_dir) + inv_dest = inventory_dir / inventory.name + shutil.copy(inventory, inv_dest) + + # Write envvars file + envvars = _build_envvars(vm) + env_dir = tmp_dir / "env" + env_dir.mkdir(parents=True, exist_ok=True) + env_file = env_dir / "envvars" + env_file.write_text( + "\n".join(f"{k}={v}" for k, v in envvars.items()) + "\n" + ) + + return tmp_dir, inv_dest, play_rel + + +def _build_cmdline( + vm: VaultManager, + cmdline: str | None, + tags: str | None, +) -> str | None: + """Build the ansible-runner cmdline string.""" + if not cmdline: + vf = os.getenv("VAULT_PASSWORD_FILE") + if not vf: + vp = vm.get_vault_path() + vf = str(vp) if vp else None + if vf: + cmdline = f'--vault-password-file "{vf}"' + + vars_file = os.getenv("API_BACKEND_VAULT_FILE") + if vars_file: + cmdline = f'{(cmdline or "").strip()} -e "@{vars_file}"'.strip() + + if tags: + cmdline = f'{(cmdline or "").strip()} --tags {tags}'.strip() + + return cmdline + + +def run_playbook_core( + playbook: Path, + inventory: Path, + limit: str | None = None, + tags: str | None = None, + cmdline: str | None = None, + extravars: dict | None = None, + quiet: bool = False, +) -> tuple[int, list, str, str]: + """Run an Ansible playbook and return (rc, events, log_plain, log_ansi). + + Uses the module-level vault_manager instance. + Cleans up the temp directory in a finally block. + """ + tmp_dir, inv_dest, play_rel = _setup_temp_dir(inventory, playbook, vault_manager) + try: + cmdline = _build_cmdline(vault_manager, cmdline, tags) + + r = run( + private_data_dir=str(tmp_dir), + playbook=str(play_rel), + inventory=str(inv_dest), + streamer="json", + limit=limit, + cmdline=cmdline, + extravars=extravars or {}, + quiet=quiet, + ) + + events = list(r.events) if hasattr(r, "events") else [] + log_ansi, log_plain = build_logs(events) + return r.rc, events, log_plain, log_ansi + finally: + shutil.rmtree(tmp_dir, ignore_errors=True) diff --git a/tests/test_extractor.py b/tests/test_extractor.py new file mode 100644 index 0000000..346f376 --- /dev/null +++ b/tests/test_extractor.py @@ -0,0 +1,32 @@ +from app.core.extractor import extract_action_results + + +def test_extract_finds_matching_action(): + events = [ + {"event": "runner_on_ok", "event_data": {"res": {"vm_list": [{"vmid": 100}]}}}, + {"event": "runner_on_ok", "event_data": {"res": {"other_action": "data"}}}, + ] + result = extract_action_results(events, "vm_list") + assert result == [[{"vmid": 100}]] + + +def test_extract_returns_empty_for_no_match(): + events = [{"event": "runner_on_ok", "event_data": {"res": {"other": "data"}}}] + result = extract_action_results(events, "vm_list") + assert result == [] + + +def test_extract_skips_non_ok_events(): + events = [{"event": "runner_on_failed", "event_data": {"res": {"vm_list": [{"vmid": 100}]}}}] + result = extract_action_results(events, "vm_list") + assert result == [] + + +def test_extract_handles_empty_events(): + assert extract_action_results([], "vm_list") == [] + + +def test_extract_handles_missing_event_data(): + events = [{"event": "runner_on_ok"}] + result = extract_action_results(events, "vm_list") + assert result == [] diff --git a/tests/test_runner.py b/tests/test_runner.py new file mode 100644 index 0000000..b76449a --- /dev/null +++ b/tests/test_runner.py @@ -0,0 +1,27 @@ +from app.core.runner import build_logs + + +def test_build_logs_extracts_stdout(): + events = [ + {"stdout": "PLAY [all] ***"}, + {"stdout": "TASK [ping] ***"}, + {"other": "data"}, + {"stdout": "ok: [host1]"}, + ] + log_ansi, log_plain = build_logs(events) + assert "PLAY [all]" in log_ansi + assert "ok: [host1]" in log_plain + + +def test_build_logs_empty_events(): + log_ansi, log_plain = build_logs([]) + assert log_ansi == "" + assert log_plain == "" + + +def test_build_logs_strips_ansi(): + events = [{"stdout": "\x1b[32mok\x1b[0m: [host1]"}] + log_ansi, log_plain = build_logs(events) + assert "\x1b[32m" in log_ansi + assert "\x1b[32m" not in log_plain + assert "ok" in log_plain From 346b5430640838e3aff50d4c06321c8aa579564b Mon Sep 17 00:00:00 2001 From: Philippe Parage Date: Thu, 19 Mar 2026 14:55:58 +0100 Subject: [PATCH 08/33] refactor: extract exception handlers to app/core/exceptions (#54) --- app/core/exceptions.py | 49 ++++++++++++++++++++++++++++++++++++++++ tests/test_exceptions.py | 31 +++++++++++++++++++++++++ 2 files changed, 80 insertions(+) create mode 100644 app/core/exceptions.py create mode 100644 tests/test_exceptions.py diff --git a/app/core/exceptions.py b/app/core/exceptions.py new file mode 100644 index 0000000..2004155 --- /dev/null +++ b/app/core/exceptions.py @@ -0,0 +1,49 @@ +"""Custom exception handlers for FastAPI.""" + +import json +import logging + +from fastapi import Request +from fastapi.exceptions import RequestValidationError +from fastapi.responses import JSONResponse + +logger = logging.getLogger(__name__) + + +def make_validation_error_detail(err: dict) -> dict: + """Convert a Pydantic validation error to the response format the UI expects.""" + return { + "field": ".".join(str(p) for p in err.get("loc", [])), + "msg": err.get("msg", ""), + "type": err.get("type", ""), + "input": err.get("input", None), + "ctx": err.get("ctx", None), + } + + +async def validation_exception_handler(request: Request, exc: RequestValidationError) -> JSONResponse: + """Verbose 422 handler with debug logging. Matches deployer-ui expected format.""" + logger.error("422 on %s %s %s", request.method, request.url.path, request.url.query) + + try: + raw = await request.body() + if raw: + body_text = raw.decode("utf-8", "ignore") + if body_text.strip(): + try: + parsed = json.loads(body_text) + logger.error("Request body:\n%s", json.dumps(parsed, indent=2, ensure_ascii=False)) + except json.JSONDecodeError: + logger.error("Request body (raw): %s", body_text) + else: + logger.error("Request body: ") + except Exception: + logger.exception("Failed to read request body.") + + details = [] + for err in exc.errors(): + detail = make_validation_error_detail(err) + logger.error("field=%s | msg=%s | type=%s", detail["field"], detail["msg"], detail["type"]) + details.append(detail) + + return JSONResponse(status_code=422, content={"detail": details}) diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py new file mode 100644 index 0000000..f311b34 --- /dev/null +++ b/tests/test_exceptions.py @@ -0,0 +1,31 @@ +from app.core.exceptions import make_validation_error_detail + + +def test_make_validation_error_detail(): + error = { + "loc": ("body", "vm_id"), + "msg": "field required", + "type": "missing", + "input": None, + "ctx": None, + } + detail = make_validation_error_detail(error) + assert detail["field"] == "body.vm_id" + assert detail["msg"] == "field required" + assert detail["type"] == "missing" + + +def test_make_validation_error_detail_empty_loc(): + error = {"loc": (), "msg": "error", "type": "value_error"} + detail = make_validation_error_detail(error) + assert detail["field"] == "" + + +def test_make_validation_error_detail_missing_keys(): + error = {} + detail = make_validation_error_detail(error) + assert detail["field"] == "" + assert detail["msg"] == "" + assert detail["type"] == "" + assert detail["input"] is None + assert detail["ctx"] is None From 0d5725b074f095f3e1f2e3d9b109393f2103170e Mon Sep 17 00:00:00 2001 From: Philippe Parage Date: Thu, 19 Mar 2026 14:57:10 +0100 Subject: [PATCH 09/33] fix: replace broken venv.logger import with proper logging (#54) --- app/utils/checks_inventory.py | 3 ++- app/utils/checks_playbooks.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/utils/checks_inventory.py b/app/utils/checks_inventory.py index ef074d4..27b3bd3 100644 --- a/app/utils/checks_inventory.py +++ b/app/utils/checks_inventory.py @@ -2,7 +2,8 @@ import os from pathlib import Path import re -from venv import logger +import logging +logger = logging.getLogger(__name__) from fastapi import HTTPException diff --git a/app/utils/checks_playbooks.py b/app/utils/checks_playbooks.py index 4d5f8c6..02bc749 100644 --- a/app/utils/checks_playbooks.py +++ b/app/utils/checks_playbooks.py @@ -1,5 +1,6 @@ import re -from venv import logger +import logging +logger = logging.getLogger(__name__) import os from fastapi import HTTPException From 9ea935d8f601794657286b4cd41fe7bb27d46919 Mon Sep 17 00:00:00 2001 From: Philippe Parage Date: Thu, 19 Mar 2026 15:10:19 +0100 Subject: [PATCH 10/33] refactor: consolidate 52 schema files into domain-grouped modules (#53) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add 9 consolidated schema files under app/schemas/: - base.py: ProxmoxBaseRequest added - vms.py: VM list, create, delete, clone, action, mass ops - vm_config.py: VM config get/set (full, cdrom, cpu, ram, tag) - snapshots.py: Snapshot CRUD (create, delete, list, revert) - firewall.py: Rules, aliases, enable/disable at DC/node/VM - network.py: Node and VM network interface operations - storage.py: Storage list, download ISO, list ISO/templates - bundles/__init__.py: Ubuntu + Proxmox default VM bundles - debug/__init__.py: Debug/ping schemas All old class names preserved as backward-compatible aliases. Old files kept in place (cleanup deferred to Task 1.8). 35 new tests in tests/test_schemas.py — all 57 tests pass. --- app/schemas/base.py | 6 + app/schemas/bundles/__init__.py | 854 +++++++++++++++++++++++++ app/schemas/debug/__init__.py | 86 +++ app/schemas/firewall.py | 1049 +++++++++++++++++++++++++++++++ app/schemas/network.py | 582 +++++++++++++++++ app/schemas/snapshots.py | 366 +++++++++++ app/schemas/storage.py | 389 ++++++++++++ app/schemas/vm_config.py | 429 +++++++++++++ app/schemas/vms.py | 711 +++++++++++++++++++++ tests/test_schemas.py | 369 +++++++++++ 10 files changed, 4841 insertions(+) create mode 100644 app/schemas/bundles/__init__.py create mode 100644 app/schemas/debug/__init__.py create mode 100644 app/schemas/firewall.py create mode 100644 app/schemas/network.py create mode 100644 app/schemas/snapshots.py create mode 100644 app/schemas/storage.py create mode 100644 app/schemas/vm_config.py create mode 100644 app/schemas/vms.py create mode 100644 tests/test_schemas.py diff --git a/app/schemas/base.py b/app/schemas/base.py index 4d9d947..936c518 100644 --- a/app/schemas/base.py +++ b/app/schemas/base.py @@ -3,6 +3,12 @@ from pydantic import BaseModel, Field +class ProxmoxBaseRequest(BaseModel): + """Base request model with fields common to all Proxmox operations.""" + proxmox_node: str = Field(..., pattern=r"^[A-Za-z0-9-]*$") + as_json: bool = Field(default=True) + + class PingRequest(BaseModel): hosts: str | None # diff --git a/app/schemas/bundles/__init__.py b/app/schemas/bundles/__init__.py new file mode 100644 index 0000000..31707d4 --- /dev/null +++ b/app/schemas/bundles/__init__.py @@ -0,0 +1,854 @@ +"""Consolidated bundle schemas: Ubuntu packages/configure + Proxmox default VM ops. + +This __init__.py serves double duty: +1. Makes bundles/ a proper Python package so old imports (app.schemas.bundles.core...) keep working. +2. Exposes consolidated schema classes for new code to import from app.schemas.bundles. +""" + +from typing import Dict, List, Literal +from pydantic import BaseModel, Field + + +# =========================================================================== +# Ubuntu Bundles +# =========================================================================== + +# --------------------------------------------------------------------------- +# Add User +# --------------------------------------------------------------------------- + +class BundleAddUserRequest(BaseModel): + + proxmox_node: str = Field( + ..., + # default= "px-testing", + description = "Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$" + ) + + hosts: str = Field( + ..., + description= "Hosts or groups", + pattern = r"^[a-zA-Z0-9._:-]+$" + ) + + #### + + user: str = Field( + ..., + description = "New user", + pattern = r"^[a-z_][a-z0-9_-]*$", + ) + + + password: str = Field( + ..., + description = "New password", + pattern = r"^[A-Za-z0-9@._-]*$" # dangerous chars removed. + ) + + + change_pwd_at_logon : bool = Field( + ..., + description = "Force user to change password on first login" + ) + + shell_path: str = Field( + ..., + description = "Default user shell ", + pattern = r"^/[a-z/]*$" + ) + + model_config = { + "json_schema_extra": { + "example": { + "proxmox_node": "px-testing", + "hosts": "r42.vuln-box-00", + # + "user": "elliot", + "password": "r0b0t_aLd3rs0n", + "change_pwd_at_logon": False, + "shell_path": "/bin/sh", + } + } + } + + +class BundleAddUserItemReply(BaseModel): + + # action: Literal["vm_get_config"] + # source: Literal["proxmox"] + proxmox_node: str + # vm_id: int = Field(..., ge=1) + # vm_name: str + raw_data: str = Field(..., description="Raw string returned by proxmox") + + +class BundleAddUserReply(BaseModel): + + rc: int = Field(0, description="RETURN code (0 = OK)") + result: list[BundleAddUserItemReply] + + model_config = { + "json_schema_extra": { + "example": { + "rc": 0, + "result": [ + { + } + ] + } + } + } + + +# --------------------------------------------------------------------------- +# Install Basic Packages +# --------------------------------------------------------------------------- + +class BundleBasicPackagesRequest(BaseModel): + + proxmox_node: str = Field( + ..., + # default= "px-testing", + description = "Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$" + ) + + hosts: str = Field( + ..., + description= "Hosts or groups", + pattern = r"^[a-zA-Z0-9._:-]+$" + ) + + #### + + install_package_basics : bool = Field( + ..., + description="", + ) + + install_package_firewalls : bool = Field( + ..., + description="", + ) + + install_package_docker : bool = Field( + ..., + description="", + ) + + install_package_docker_compose: bool = Field( + ..., + description="", + ) + + install_package_utils_json : bool = Field( + ..., + description="", + ) + + install_package_utils_network : bool = Field( + ..., + description="", + ) + + #### + + install_ntpclient_and_update_time: bool = Field( + ..., + description="", + ) + + packages_cleaning: bool = Field( + ..., + description="", + ) + + model_config = { + "json_schema_extra": { + "example": { + "proxmox_node": "px-testing", + "hosts": "r42.vuln-box-00", + # + "install_package_basics" : True, + "install_package_firewalls" : False, + "install_package_docker" : False, + "install_package_docker_compose" : False, + "install_package_utils_json" : False, + "install_package_utils_network" : False, + "install_ntpclient_and_update_time" : True, + "packages_cleaning" : True, + + } + } + } + + +class BundleBasicPackagesItemReply(BaseModel): + + # action: Literal["vm_get_config"] + # source: Literal["proxmox"] + proxmox_node: str + # vm_id: int = Field(..., ge=1) + # vm_name: str + raw_data: str = Field(..., description="Raw string returned by proxmox") + + +class BundleBasicPackagesReply(BaseModel): + + rc: int = Field(0, description="RETURN code (0 = OK)") + result: list[BundleBasicPackagesItemReply] + + model_config = { + "json_schema_extra": { + "example": { + "rc": 0, + "result": [ + { + } + ] + } + } + } + + +# --------------------------------------------------------------------------- +# Install Docker +# --------------------------------------------------------------------------- + +class BundleDockerRequest(BaseModel): + + proxmox_node: str = Field( + ..., + # default= "px-testing", + description = "Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$" + ) + + hosts: str = Field( + ..., + description= "Hosts or groups", + pattern = r"^[a-zA-Z0-9._:-]+$" + ) + + #### + + install_package_docker : bool = Field( + ..., + description="", + ) + + #### + + install_ntpclient_and_update_time: bool = Field( + ..., + description="", + ) + + packages_cleaning: bool = Field( + ..., + description="", + ) + + model_config = { + "json_schema_extra": { + "example": { + "proxmox_node": "px-testing", + "hosts": "r42.vuln-box-00", + # + "install_package_docker" : True, + "install_ntpclient_and_update_time" : True, + "packages_cleaning" : True, + + } + } + } + + +class BundleDockerItemReply(BaseModel): + + # action: Literal["vm_get_config"] + # source: Literal["proxmox"] + proxmox_node: str + # vm_id: int = Field(..., ge=1) + # vm_name: str + raw_data: str = Field(..., description="Raw string returned by proxmox") + + +class BundleDockerReply(BaseModel): + + rc: int = Field(0, description="RETURN code (0 = OK)") + result: list[BundleDockerItemReply] + + model_config = { + "json_schema_extra": { + "example": { + "rc": 0, + "result": [ + { + } + ] + } + } + } + + +# --------------------------------------------------------------------------- +# Install Docker Compose +# --------------------------------------------------------------------------- + +class BundleDockerComposeRequest(BaseModel): + + proxmox_node: str = Field( + ..., + # default= "px-testing", + description = "Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$" + ) + + hosts: str = Field( + ..., + description= "Hosts or groups", + pattern = r"^[a-zA-Z0-9._:-]+$" + + ) + + #### + + install_package_docker : bool = Field( + ..., + description="", + ) + + install_package_docker_compose: bool = Field( + ..., + description="", + ) + + #### + + install_ntpclient_and_update_time: bool = Field( + ..., + description="", + ) + + packages_cleaning: bool = Field( + ..., + description="", + ) + + model_config = { + "json_schema_extra": { + "example": { + "proxmox_node": "px-testing", + "hosts": "r42.vuln-box-00", + # + "install_package_docker" : True, + "install_package_docker_compose" : True, + "install_ntpclient_and_update_time" : True, + "packages_cleaning" : True, + + } + } + } + + +class BundleDockerComposeItemReply(BaseModel): + + # action: Literal["vm_get_config"] + # source: Literal["proxmox"] + proxmox_node: str + # vm_id: int = Field(..., ge=1) + # vm_name: str + raw_data: str = Field(..., description="Raw string returned by proxmox") + + +class BundleDockerComposeReply(BaseModel): + + rc: int = Field(0, description="RETURN code (0 = OK)") + result: list[BundleDockerComposeItemReply] + + model_config = { + "json_schema_extra": { + "example": { + "rc": 0, + "result": [ + { + } + ] + } + } + } + + +# --------------------------------------------------------------------------- +# Install Dot Files +# --------------------------------------------------------------------------- + +class BundleDotFilesRequest(BaseModel): + + proxmox_node: str = Field( + ..., + # default= "px-testing", + description = "Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$" + ) + + hosts: str = Field( + ..., + description= "Hosts or groups", + pattern = r"^[a-zA-Z0-9._:-]+$" + ) + + #### + + user: str = Field( + ..., + description="targeted username", + + ) + + install_vim_dot_files: bool = Field( + ..., + description= "Install vim dot file in user directory" + ) + + install_zsh_dot_files: bool = Field( + ..., + description= "Install zsh dot file in user directory" + ) + + apply_for_root: bool = Field( + ..., + description= "Install dot files in /root" + ) + + model_config = { + "json_schema_extra": { + "example": { + "proxmox_node": "px-testing", + "hosts": "r42.vuln-box-00", + # + "user": "jane", + "install_vim_dot_files": True, + "install_zsh_dot_files": True, + "apply_for_root": False, + } + } + } + + +class BundleDotFilesItemReply(BaseModel): + + # action: Literal["vm_get_config"] + # source: Literal["proxmox"] + proxmox_node: str + # vm_id: int = Field(..., ge=1) + # vm_name: str + raw_data: str = Field(..., description="Raw string returned by proxmox") + + +# NOTE: The original file had a bug where the Reply class was also named +# Reply_BundlesCoreLinuxUbuntuInstall_DotFilesItem (same as Item reply). +# We preserve both the Item and the Reply under their correct new names. +class BundleDotFilesReply(BaseModel): + + rc: int = Field(0, description="RETURN code (0 = OK)") + result: list[BundleDotFilesItemReply] + + model_config = { + "json_schema_extra": { + "example": { + "rc": 0, + "result": [ + { + } + ] + } + } + } + + +# =========================================================================== +# Proxmox Bundles -- Default VM Operations +# =========================================================================== + +# --------------------------------------------------------------------------- +# Create Admin VMs (Default) +# --------------------------------------------------------------------------- + +class BundleCreateAdminVmsItemRequest(BaseModel): + + vm_id: int = Field( + ..., + ge=1, + description="Virtual machine id", + ) + + vm_ip: str = Field( + ..., + description="vm ipv4", + pattern=r"^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$" + ) + + vm_description: str = Field( + ..., + strip_whitespace=True, + max_length=200, + # pattern=VM_DESCRIPTION_RE, + description="Description" + ) + +class BundleCreateAdminVmsRequest(BaseModel): + + proxmox_node: str = Field( + ..., + # default= "px-testing", + description = "Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$" + ) + + vms: Dict[str, BundleCreateAdminVmsItemRequest] = Field( + ..., + description="Map - vm override vm_id vm_ip vm_description, ... " + ) + + model_config = { + "json_schema_extra": { + "example": { + "proxmox_node": "px-testing", + "vms": { + "admin-wazuh": { + "vm_id": 1000, + "vm_description": "Wazuh - dashboard", + "vm_ip": "192.168.42.100", + }, + "admin-web-api-kong": { + "vm_id": 1020, + "vm_description": "API gateway", + "vm_ip": "192.168.42.120", + }, + } + } + } + } + + +class BundleCreateAdminVmsItemReply(BaseModel): + + # action: Literal["vm_get_config"] + # source: Literal["proxmox"] + proxmox_node: str + # vm_id: int = Field(..., ge=1) + # vm_name: str + raw_data: str = Field(..., description="Raw string returned by proxmox") + + +class BundleCreateAdminVmsReply(BaseModel): + + rc: int = Field(0, description="RETURN code (0 = OK)") + result: list[BundleCreateAdminVmsItemReply] + + model_config = { + "json_schema_extra": { + "example": { + "rc": 0, + "result": [ + { + } + ] + } + } + } + + +# --------------------------------------------------------------------------- +# Create Student VMs (Default) +# --------------------------------------------------------------------------- + +class BundleCreateStudentVmsItemRequest(BaseModel): + + vm_id: int = Field( + ..., + ge=1, + description="Virtual machine id", + ) + + vm_ip: str = Field( + ..., + description="vm ipv4", + pattern=r"^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$" + ) + + vm_description: str = Field( + ..., + strip_whitespace=True, + max_length=200, + # pattern=VM_DESCRIPTION_RE, + description="Description" + ) + +class BundleCreateStudentVmsRequest(BaseModel): + + proxmox_node: str = Field( + ..., + # default= "px-testing", + description = "Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$" + ) + + vms: Dict[str, BundleCreateStudentVmsItemRequest] = Field( + ..., + description="Map - vm override vm_id vm_ip vm_description, ... " + ) + + + model_config = { + "json_schema_extra": { + "example": { + "proxmox_node": "px-testing", + "vms": { + "student-box-01": { + "vm_id": 3001, + "vm_description": "student R42 student vm", + "vm_ip": "192.168.42.160" , + } + } + } + } + } + + +class BundleCreateStudentVmsItemReply(BaseModel): + + # action: Literal["vm_get_config"] + # source: Literal["proxmox"] + proxmox_node: str + # vm_id: int = Field(..., ge=1) + # vm_name: str + raw_data: str = Field(..., description="Raw string returned by proxmox") + + +class BundleCreateStudentVmsReply(BaseModel): + + rc: int = Field(0, description="RETURN code (0 = OK)") + result: list[BundleCreateStudentVmsItemReply] + + model_config = { + "json_schema_extra": { + "example": { + "rc": 0, + "result": [ + { + } + ] + } + } + } + + +# --------------------------------------------------------------------------- +# Create Vuln VMs (Default) +# --------------------------------------------------------------------------- + +class BundleCreateVulnVmsItemRequest(BaseModel): + + vm_id: int = Field( + ..., + ge=1, + description="Virtual machine id", + ) + + vm_ip: str = Field( + ..., + description="vm ipv4", + pattern=r"^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$" + ) + + vm_description: str = Field( + ..., + strip_whitespace=True, + max_length=200, + # pattern=VM_DESCRIPTION_RE, + description="Description" + ) + +class BundleCreateVulnVmsRequest(BaseModel): + + proxmox_node: str = Field( + ..., + # default= "px-testing", + description = "Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$" + ) + + vms: Dict[str, BundleCreateVulnVmsItemRequest] = Field( + ..., + description="Map - vm override vm_id vm_ip vm_description, ... " + ) + + model_config = { + "json_schema_extra": { + "example": { + "proxmox_node": "px-testing", + "vms": { + "vuln-box-00": { + "vm_id": 4000, + "vm_description": "vulnerable vm 00", + "vm_ip": "192.168.42.170", + }, + } + } + } + } + + +class BundleCreateVulnVmsItemReply(BaseModel): + + # action: Literal["vm_get_config"] + # source: Literal["proxmox"] + proxmox_node: str + # vm_id: int = Field(..., ge=1) + # vm_name: str + raw_data: str = Field(..., description="Raw string returned by proxmox") + + +class BundleCreateVulnVmsReply(BaseModel): + + rc: int = Field(0, description="RETURN code (0 = OK)") + result: list[BundleCreateVulnVmsItemReply] + + model_config = { + "json_schema_extra": { + "example": { + "rc": 0, + "result": [ + { + } + ] + } + } + } + + +# --------------------------------------------------------------------------- +# Revert Snapshot Default (Admin/Vuln/Student VMs) +# --------------------------------------------------------------------------- + +class BundleRevertSnapshotDefaultRequest(BaseModel): + + proxmox_node: str = Field( + ..., + # default= "px-testing", + description = "Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$" + ) + + as_json: bool = Field( + default=True, + description="If true : JSON output else : raw output" + ) + + vm_snapshot_name: str | None = Field( + default=None, + description="Name of the snapshot to create", + pattern=r"^[A-Za-z0-9_-]+$" + ) + # + + model_config = { + "json_schema_extra": { + "example": { + "proxmox_node": "px-testing", + "vm_snapshot_name": "default-snapshot-from-API-220925-1734", + "as_json": True, + + } + } + } + + +# --------------------------------------------------------------------------- +# Start/Stop/Pause/Resume Default (Admin/Vuln/Student VMs) +# --------------------------------------------------------------------------- + +class BundleStartStopDefaultRequest(BaseModel): + + proxmox_node: str = Field( + ..., + # default= "px-testing", + description = "Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$" + ) + + as_json: bool = Field( + default=True, + description="If true : JSON output else : raw output" + ) + # + + model_config = { + "json_schema_extra": { + "example": { + "proxmox_node": "px-testing", + "as_json": True, + + } + } + } + + +# --------------------------------------------------------------------------- +# Backward compatibility -- old names used by current routes +# --------------------------------------------------------------------------- + +# bundles/core/linux/ubuntu/configure/add_user.py +Request_BundlesCoreLinuxUbuntuConfigure_AddUser = BundleAddUserRequest +Reply_BundlesCoreLinuxUbuntuConfigure_AddUserItem = BundleAddUserItemReply +Reply_BundlesCoreLinuxUbuntuConfigure_AddUser = BundleAddUserReply + +# bundles/core/linux/ubuntu/install/basic_packages.py +Request_BundlesCoreLinuxUbuntuInstall_BasicPackages = BundleBasicPackagesRequest +Reply_BundlesCoreLinuxUbuntuInstall_BasicPackagesItem = BundleBasicPackagesItemReply +Reply_BundlesCoreLinuxUbuntuInstall_BasicPackages = BundleBasicPackagesReply + +# bundles/core/linux/ubuntu/install/docker.py +Request_BundlesCoreLinuxUbuntuInstall_Docker = BundleDockerRequest +Reply_BundlesCoreLinuxUbuntuInstall_DockerItem = BundleDockerItemReply +Reply_BundlesCoreLinuxUbuntuInstall_Docker = BundleDockerReply + +# bundles/core/linux/ubuntu/install/docker_compose.py +Request_BundlesCoreLinuxUbuntuInstall_DockerCompose = BundleDockerComposeRequest +Reply_BundlesCoreLinuxUbuntuInstall_DockerComposeItem = BundleDockerComposeItemReply +Reply_BundlesCoreLinuxUbuntuInstall_DockerCompose = BundleDockerComposeReply + +# bundles/core/linux/ubuntu/install/dot_files.py +Request_BundlesCoreLinuxUbuntuInstall_DotFiles = BundleDotFilesRequest +Reply_BundlesCoreLinuxUbuntuInstall_DotFilesItem = BundleDotFilesItemReply +# NOTE: original file had a name collision; both Item and Reply were named +# Reply_BundlesCoreLinuxUbuntuInstall_DotFilesItem. We alias the Reply class too. + +# bundles/core/proxmox/configure/default/vms/create_vms_admin_default.py +Request_BundlesCoreProxmoxConfigureDefaultVms_CreateAdminVmsItem = BundleCreateAdminVmsItemRequest +Request_BundlesCoreProxmoxConfigureDefaultVms_CreateAdminVms = BundleCreateAdminVmsRequest +Reply_BundlesCoreProxmoxConfigureDefaultVms_CreateAdminVmsItem = BundleCreateAdminVmsItemReply +Reply_BundlesCoreProxmoxConfigureDefaultVms_CreateAdminVms = BundleCreateAdminVmsReply + +# bundles/core/proxmox/configure/default/vms/create_vms_student_default.py +Request_BundlesCoreProxmoxConfigureDefaultVms_CreateStudentVmsItem = BundleCreateStudentVmsItemRequest +Request_BundlesCoreProxmoxConfigureDefaultVms_CreateStudentVms = BundleCreateStudentVmsRequest +Reply_BundlesCoreProxmoxConfigureDefaultVms_CreateStudentVmsItem = BundleCreateStudentVmsItemReply +Reply_BundlesCoreProxmoxConfigureDefaultVms_CreateStudentVms = BundleCreateStudentVmsReply + +# bundles/core/proxmox/configure/default/vms/create_vms_vuln_default.py +Request_BundlesCoreProxmoxConfigureDefaultVms_CreateVulnVmsItem = BundleCreateVulnVmsItemRequest +Request_BundlesCoreProxmoxConfigureDefaultVms_CreateVulnVms = BundleCreateVulnVmsRequest +Reply_BundlesCoreProxmoxConfigureDefaultVms_CreateVulnVmsItem = BundleCreateVulnVmsItemReply +Reply_BundlesCoreProxmoxConfigureDefaultVms_CreateVulnVms = BundleCreateVulnVmsReply + +# bundles/core/proxmox/configure/default/vms/revert_snapshot_default.py +Request_BundlesCoreProxmoxConfigureDefaultVms_RevertSnapshotAdminVulnStudentVms = BundleRevertSnapshotDefaultRequest + +# bundles/core/proxmox/configure/default/vms/start_stop_resume_pause_default.py +Request_BundlesCoreProxmoxConfigureDefaultVms_StartStopPauseResumeAdminVulnStudentVms = BundleStartStopDefaultRequest diff --git a/app/schemas/debug/__init__.py b/app/schemas/debug/__init__.py new file mode 100644 index 0000000..21f6735 --- /dev/null +++ b/app/schemas/debug/__init__.py @@ -0,0 +1,86 @@ +"""Consolidated debug schemas: ping request/reply. + +This __init__.py serves double duty: +1. Makes debug/ a proper Python package so old imports (app.schemas.debug.ping) keep working. +2. Exposes consolidated schema classes for new code to import from app.schemas.debug. +""" + +from typing import List +from pydantic import BaseModel, Field + + +# --------------------------------------------------------------------------- +# Debug Ping +# --------------------------------------------------------------------------- + +class DebugPingRequest(BaseModel): + + proxmox_node: str = Field( + ..., + # default="px-testing", + description="Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$" + ) + + as_json: bool | None = Field( + default=False, + description="If true : JSON output else : raw output" + ) + # + + hosts: str | None = Field( + ..., + # default="all", + description="Targeted ansible hosts", + pattern=r"^[A-Za-z0-9\._-]+$" + ) + + + + model_config = { + "json_schema_extra": { + "example": { + "proxmox_node": "px-testing", + "hosts": "all", + "as_json": False + } + } + } + + +class DebugPingReply(BaseModel): + + rc: int = Field( + ..., # mandatory field. + description="Return code of the job (0 = success, >0 = error/warning)" + ) + log_multiline: List[str] = Field( + ..., # mandatory field. + description="Execution log as a list of lines (chronological order)" + ) + + model_config = { + "json_schema_extra": { + "something": { + "rc": 0, + "log_multiline": [ + "PLAY [debug - ping targeted host/group] ****************************************", + "", + "TASK [ansible.builtin.ping] ****************************************************", + "ok: [something-1]", + "", + "PLAY RECAP *********************************************************************", + "something-1 : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0" + ] + } + } + } + + +# --------------------------------------------------------------------------- +# Backward compatibility -- old names used by current routes +# --------------------------------------------------------------------------- + +# debug/ping.py +Request_DebugPing = DebugPingRequest +Reply_DebugPing = DebugPingReply diff --git a/app/schemas/firewall.py b/app/schemas/firewall.py new file mode 100644 index 0000000..93c5ac0 --- /dev/null +++ b/app/schemas/firewall.py @@ -0,0 +1,1049 @@ +"""Consolidated firewall schemas: rules, aliases, enable/disable at DC/node/VM level.""" + +from typing import List, Literal +from pydantic import BaseModel, Field, ConfigDict + + +# --------------------------------------------------------------------------- +# Add Iptables Alias +# --------------------------------------------------------------------------- + +class FirewallAliasAddRequest(BaseModel): + + proxmox_node: str = Field( + ..., + # default= "px-testing", + description = "Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$" + ) + + as_json: bool = Field( + default=True, + description="If true : JSON output else : raw output" + ) + # + + vm_id: str = Field( + ..., + # default="4000", + description="Virtual machine id", + pattern=r"^[0-9]+$" + ) + + vm_fw_alias_name: str = Field( + ..., + description="Firewall alias name", + pattern=r"^[A-Za-z0-9-_]+$", + ) + + vm_fw_alias_cidr: str = Field( + ..., + description="CIDR notation for the alias - eg 192.168.123.0/24", + pattern=r"^[0-9./]+$", + ) + + vm_fw_alias_comment: str | None = Field( + ..., + description="Optional comment for the firewall alias", + pattern=r"^[A-Za-z0-9 _-]*$", + ) + + model_config = { + "json_schema_extra": { + "example": { + "proxmox_node": "px-testing", + "as_json": True, + # + "vm_id":"1000", + # + "vm_fw_alias_name":"test", + "vm_fw_alias_cidr":"192.168.123.0/24", + "vm_fw_alias_comment":"this_comment" + } + } + } + + +class FirewallAliasAddItemReply(BaseModel): + + action: Literal["vm_ListIso_usage"] + source: Literal["proxmox"] + proxmox_node: str + ## + # vm_id: int = Field(..., ge=1) + vm_fw_alias_cidr: str + vm_fw_alias_name: str + vm_id: str + +class FirewallAliasAddReply(BaseModel): + + rc: int = Field(0, description="RETURN code (0 = OK)") + result: list[FirewallAliasAddItemReply] + + model_config = { + "json_schema_extra": { + "example": { + "rc": 0, + "result": [ + { + "action": "firewall_vm_add_iptables_alias", + "source": "proxmox", + "proxmox_node": "px-testing", + ## + "vm_fw_alias_cidr": "192.168.123.0/24", + "vm_fw_alias_name": "test", + "vm_id": "1000" + } + ] + } + } + } + + +# --------------------------------------------------------------------------- +# Apply Iptables Rules +# --------------------------------------------------------------------------- + +class FirewallRuleApplyRequest(BaseModel): + + proxmox_node: str = Field( + ..., + description="Target Proxmox node name.", + pattern=r"^[A-Za-z0-9-]+$", + ) + + as_json: bool = Field( + default=True, + description="If true: return JSON output, otherwise raw output.", + ) + # + + vm_id: str = Field( + ..., + # default="4000", + description="Virtual machine id", + pattern=r"^[0-9]+$" + ) + + vm_fw_action: str = Field( + ..., + description="Firewall action - ACCEPT, DROP, REJECT", + pattern=r"^(ACCEPT|DROP|REJECT)$", + ) + + vm_fw_dport: str = Field( + ..., + description="Destination port or port range", + pattern=r"^[0-9:-]+$", + ) + + vm_fw_enable: int = Field( + ..., + description="Enable flag - 1 = enabled, 0 = disabled", + ) + + vm_fw_proto: str = Field( + ..., + description="Protocol - tcp, udp, icmp", + pattern=r"^[a-zA-Z0-9]+$", + ) + + vm_fw_type: str = Field( + ..., + description="Rule type - in or out", + pattern=r"^(in|out)$", + ) + + vm_fw_log: str | None = Field( + default=None, + description="Optional logging level - info, debug,...", + pattern=r"^[A-Za-z0-9_-]+$", + ) + + vm_fw_iface: str | None = Field( + default=None, + description="Optional network interface name", + pattern=r"^[A-Za-z0-9_-]+$", + ) + + vm_fw_source: str | None = Field( + default=None, + description="Optional source address or CIDR", + pattern=r"^[0-9./]+$", + ) + + vm_fw_dest: str | None = Field( + default=None, + description="Optional destination address or CIDR", + pattern=r"^[0-9./]+$", + ) + + vm_fw_sport: str | None = Field( + default=None, + description="Optional source port or port range", + pattern=r"^[0-9:-]+$", + ) + + vm_fw_comment: str | None = Field( + default=None, + description="Optional comment for the rule", + pattern=r"^[A-Za-z0-9 _-]*$", + ) + + vm_fw_pos: int | None = Field( + default=None, + description="Optional position index rule in the chain.", + ) + + model_config = ConfigDict( + json_schema_extra={ + "example":[ + { + "proxmox_node": "px-node-01", + "as_json": True, + # + "vm_id": "1000", + "vm_fw_action": "ACCEPT", + "vm_fw_type": "in", + "vm_fw_proto": "tcp", + "vm_fw_dport": "22", + "vm_fw_enable": 1, + "vm_fw_iface": "net0", + "vm_fw_source": "192.168.1.0/24", + "vm_fw_dest": "0.0.0.0/0", + "vm_fw_sport": "1024", + "vm_fw_comment": "Test comment", + "vm_fw_pos": 5, + "vm_fw_log": "debug", + }, + ] + } + ) + + +class FirewallRuleApplyItemReply(BaseModel): + + action: Literal["vm_ApplyIptablesRules_usage"] + source: Literal["proxmox"] + proxmox_node: str + ## + # vm_id: int = Field(..., ge=1) + vm_fw_action : str + vm_fw_comment : str + vm_fw_dest : str + vm_fw_dport : str + vm_fw_enable : int + vm_fw_iface : str + vm_fw_log : str + vm_fw_pos : int + vm_fw_proto : str + vm_fw_source : str + vm_fw_sport : str + vm_fw_type : str + vm_id : str + +class FirewallRuleApplyReply(BaseModel): + + rc: int = Field(0, description="RETURN code (0 = OK)") + result: list[FirewallRuleApplyItemReply] + + model_config = { + "json_schema_extra": { + "example": { + "rc": 0, + "result": [ + { + "action": "firewall_vm_apply_iptables_rule", + "proxmox_node": "px-testing", + "source": "proxmox", + # + "vm_fw_action": "ACCEPT", + "vm_fw_dport": "80", + "vm_fw_enable": "1", + "vm_fw_proto": "tcp", + "vm_fw_type": "out", + "vm_id": "100" + }, + ] + } + } + } + + +# --------------------------------------------------------------------------- +# Delete Iptables Alias +# --------------------------------------------------------------------------- + +class FirewallAliasDeleteRequest(BaseModel): + + proxmox_node: str = Field( + ..., + # default= "px-testing", + description = "Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$" + ) + + as_json: bool = Field( + default=True, + description="If true : JSON output else : raw output" + ) + # + + vm_id: str = Field( + ..., + # default="4000", + description="Virtual machine id", + pattern=r"^[0-9]+$" + ) + + vm_fw_alias_name: str = Field( + ..., + description="Firewall alias name", + pattern=r"^[A-Za-z0-9-_]+$", + ) + + model_config = { + "json_schema_extra": { + "example": { + "proxmox_node": "px-testing", + "as_json": True, + # + "vm_id": "1000", + "vm_fw_alias_name": "test", + } + } + } + + +class FirewallAliasDeleteItemReply(BaseModel): + + action: Literal["firewall_vm_delete_iptables_alias"] + source: Literal["proxmox"] + proxmox_node: str + # ## + # vm_id: int = Field(..., ge=1) + vm_fw_alias_name: str + vm_id: int + + +class FirewallAliasDeleteReply(BaseModel): + + rc: int = Field(0, description="RETURN code (0 = OK)") + result: list[FirewallAliasDeleteItemReply] + + model_config = { + "json_schema_extra": { + "example": { + "rc": 0, + "result": [ + { + "action": "firewall_vm_delete_iptables_alias", + "source": "proxmox", + "proxmox_node": "px-testing", + ## + "vm_fw_alias_name": "test", + "vm_id": "1000", + } + ] + } + } + } + + +# --------------------------------------------------------------------------- +# Delete Iptables Rule +# --------------------------------------------------------------------------- + +class FirewallRuleDeleteRequest(BaseModel): + + proxmox_node: str = Field( + ..., + # default= "px-testing", + description = "Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$" + ) + + as_json: bool = Field( + default=True, + description="If true : JSON output else : raw output" + ) + # + + vm_id: str = Field( + ..., + # default="4000", + description="Virtual machine id", + pattern=r"^[0-9]+$" + ) + + vm_fw_pos: int | None = Field( + ..., + description="Optional position index rule in the chain.", + ) + + model_config = { + "json_schema_extra": { + "example": { + "proxmox_node": "px-testing", + "as_json": True, + # + "vm_id":"1000", + "vm_fw_pos":1, + + } + } + } + + +class FirewallRuleDeleteItemReply(BaseModel): + + action: Literal["vm_DeleteIptablesRule_usage"] + source: Literal["proxmox"] + proxmox_node: str + ## + # vm_id: int = Field(..., ge=1) + vm_id: str + vm_fw_pos: int + +class FirewallRuleDeleteReply(BaseModel): + + rc: int = Field(0, description="RETURN code (0 = OK)") + result: list[FirewallRuleDeleteItemReply] + + model_config = { + "json_schema_extra": { + "example": { + "rc": 0, + "result": [ + { + "action": "firewall_vm_delete_iptables_rule", + "source": "proxmox", + "proxmox_node": "px-testing", + ## + "vm_fw_pos": "0", + "vm_id": "1000" + } + ] + } + } + } + + +# --------------------------------------------------------------------------- +# List Iptables Alias +# --------------------------------------------------------------------------- + +class FirewallAliasListRequest(BaseModel): + + proxmox_node: str = Field( + ..., + # default= "px-testing", + description = "Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$" + ) + + as_json: bool = Field( + default=True, + description="If true : JSON output else : raw output" + ) + # + + vm_id: str = Field( + ..., + # default="4000", + description="Virtual machine id", + pattern=r"^[0-9]+$" + ) + + model_config = { + "json_schema_extra": { + "example": { + "proxmox_node": "px-testing", + "as_json": True, + # + "vm_id": "1000", + } + } + } + + +class FirewallAliasListItemReply(BaseModel): + + action: Literal["vm_ListIptablesAlias_usage"] + source: Literal["proxmox"] + proxmox_node: str + ## + # vm_id: int = Field(..., ge=1) + vm_fw_alias_cidr: str + vm_fw_alias_name: int + vm_id: str + +class FirewallAliasListReply(BaseModel): + + rc: int = Field(0, description="RETURN code (0 = OK)") + result: list[FirewallAliasListItemReply] + + model_config = { + "json_schema_extra": { + "example": { + "rc": 0, + "result": [ + { + "action": "firewall_vm_list_iptables_alias", + "source": "proxmox", + "proxmox_node": "px-testing", + ## + "vm_fw_alias_cidr": "192.168.123.0/24", + "vm_fw_alias_name": "test", + "vm_id": "1000" + } + ] + } + } + } + + +# --------------------------------------------------------------------------- +# List Iptables Rules +# --------------------------------------------------------------------------- + +class FirewallRuleListRequest(BaseModel): + + proxmox_node: str = Field( + ..., + # default= "px-testing", + description = "Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$" + ) + + as_json: bool = Field( + default=True, + description="If true : JSON output else : raw output" + ) + # + + vm_id: str = Field( + ..., + # default="4000", + description="Virtual machine id", + pattern=r"^[0-9]+$" + ) + + model_config = { + "json_schema_extra": { + "example": { + "proxmox_node": "px-testing", + "as_json": True, + # + "vm_id": "1000", + + } + } + } + + +class FirewallRuleListItemReply(BaseModel): + + action: Literal["vm_ListIptablesRules_usage"] + source: Literal["proxmox"] + proxmox_node: str + ## + # vm_id: int = Field(..., ge=1) + vm_fw_action: str + vm_fw_comment: str + vm_fw_dest: str + vm_fw_dport: str + vm_fw_enable: int + vm_fw_iface: str + vm_fw_log: str + vm_fw_pos: int + vm_fw_proto: str + vm_fw_source: str + vm_fw_sport: str + vm_fw_type: str + vm_id: str + +class FirewallRuleListReply(BaseModel): + + rc: int = Field(0, description="RETURN code (0 = OK)") + result: list[FirewallRuleListItemReply] + + model_config = { + "json_schema_extra": { + "example": { + "rc": 0, + "result": [ + { + "action": "firewall_vm_list_iptables_rule", + "proxmox_node": "px-testing", + "source": "proxmox", + "vm_fw_action": "ACCEPT", + "vm_fw_enable": 0, + "vm_fw_log": "nolog", + "vm_fw_pos": 0, + "vm_fw_type": "in", + "vm_id": "100" + }, + ] + } + } + } + + +# --------------------------------------------------------------------------- +# Enable / Disable Firewall — Datacenter +# --------------------------------------------------------------------------- + +class FirewallEnableDcRequest(BaseModel): + + proxmox_api_host: str = Field( + ..., + # default= "px-testing", + description = "Proxmox api - ip:port", + pattern=r"^[A-Za-z0-9\.:-]*$" + ) + + as_json: bool = Field( + default=True, + description="If true : JSON output else : raw output" + ) + # + + model_config = { + "json_schema_extra": { + "example": { + "proxmox_node": "px-testing", + "as_json": True, + # + "proxmox_api_host": "127.0.0.1:18007", + } + } + } + + +class FirewallEnableDcItemReply(BaseModel): + + action: Literal["vm_EnableFirewallDc_usage"] + source: Literal["proxmox"] + proxmox_node: str + ## + # vm_id: int = Field(..., ge=1) + proxmox_api_host: str + +class FirewallEnableDcReply(BaseModel): + + rc: int = Field(0, description="RETURN code (0 = OK)") + result: list[FirewallEnableDcItemReply] + + model_config = { + "json_schema_extra": { + "example": { + "rc": 0, + "result": [ + { + "action": "firewall_vm_disable", + "source": "proxmox", + "proxmox_node": "px-testing", + ## + "vm_id": "100", + "vm_firewall": "disable", + "vm_name": "test" + } + ] + } + } + } + + +class FirewallDisableDcRequest(BaseModel): + + proxmox_node: str = Field( + ..., + # default= "px-testing", + description = "Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$" + ) + + as_json: bool = Field( + default=True, + description="If true : JSON output else : raw output" + ) + # + + proxmox_api_host: str = Field( + ..., + # default= "px-testing", + description = "Proxmox api - ip:port", + pattern=r"^[A-Za-z0-9\.:-]*$" + ) + + model_config = { + "json_schema_extra": { + "example": { + "proxmox_node": "px-testing", + "as_json": True, + # + "proxmox_api_host": "127.0.0.1:1234", + + } + } + } + + +class FirewallDisableDcItemReply(BaseModel): + + action: Literal["vm_DisableFirewallDc_usage"] + source: Literal["proxmox"] + proxmox_node: str + ## + +class FirewallDisableDcReply(BaseModel): + + rc: int = Field(0, description="RETURN code (0 = OK)") + result: list[FirewallDisableDcItemReply] + + model_config = { + "json_schema_extra": { + "example": { + "rc": 0, + "result": [ + { + "action": "storage_list_iso", + "source": "proxmox", + "proxmox_node": "px-testing", + ## + } + ] + } + } + } + + +# --------------------------------------------------------------------------- +# Enable / Disable Firewall — Node +# --------------------------------------------------------------------------- + +class FirewallEnableNodeRequest(BaseModel): + + proxmox_node: str = Field( + ..., + # default= "px-testing", + description="Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$" + ) + + as_json: bool = Field( + default=True, + description="If true : JSON output else : raw output" + ) + # + + model_config = { + "json_schema_extra": { + "example": { + "proxmox_node": "px-testing", + "as_json": True + } + } + } + + +class FirewallEnableNodeItemReply(BaseModel): + + action: Literal["vm_EnableFirewallNode_usage"] + source: Literal["proxmox"] + proxmox_node: str + ## + # vm_id: int = Field(..., ge=1) + node_firewall: str + +class FirewallEnableNodeReply(BaseModel): + + rc: int = Field(0, description="RETURN code (0 = OK)") + result: list[FirewallEnableNodeItemReply] + + model_config = { + "json_schema_extra": { + "example": { + "rc": 0, + "result": [ + { + "action": "firewall_node_enable", + "source": "proxmox", + "proxmox_node": "px-testing", + ## + "node_firewall": "enabled", + + } + ] + } + } + } + + +class FirewallDisableNodeRequest(BaseModel): + + proxmox_node: str = Field( + ..., + # default= "px-testing", + description="Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$" + ) + + as_json: bool = Field( + default=True, + description="If true : JSON output else : raw output" + ) + # + + model_config = { + "json_schema_extra": { + "example": { + "proxmox_node": "px-testing", + "as_json": True + } + } + } + + +class FirewallDisableNodeItemReply(BaseModel): + + action: Literal["vm_EnableFirewallNode_usage"] + source: Literal["proxmox"] + proxmox_node: str + ## + # vm_id: int = Field(..., ge=1) + node_firewall: str + +class FirewallDisableNodeReply(BaseModel): + + rc: int = Field(0, description="RETURN code (0 = OK)") + result: list[FirewallDisableNodeItemReply] + + # + # missing feat in role ? + # + model_config = { + "json_schema_extra": { + "example": { + "rc": 0, + "result": [ + { + "action": "firewall_node_enable", + "source": "proxmox", + "proxmox_node": "px-testing", + ## + "node_firewall": "disabled", + } + ] + } + } + } + + +# --------------------------------------------------------------------------- +# Enable / Disable Firewall — VM +# --------------------------------------------------------------------------- + +class FirewallEnableVmRequest(BaseModel): + + proxmox_node: str = Field( + ..., + # default= "px-testing", + description = "Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$" + ) + + as_json: bool = Field( + default=True, + description="If true : JSON output else : raw output" + ) + # + + vm_id: str = Field( + ..., + # default="4000", + description="Virtual machine id", + pattern=r"^[0-9]+$" + ) + + model_config = { + "json_schema_extra": { + "example": { + "proxmox_node": "px-testing", + "as_json": True, + # + "vm_name": "test", + "vm_id": "1000", + } + } + } + + +class FirewallEnableVmItemReply(BaseModel): + + action: Literal["vm_EnableFirewallVm_usage"] + source: Literal["proxmox"] + proxmox_node: str + ## + # vm_id: int = Field(..., ge=1) + vm_id: str + vm_name: str + vm_firewall: str + +class FirewallEnableVmReply(BaseModel): + + rc: int = Field(0, description="RETURN code (0 = OK)") + result: list[FirewallEnableVmItemReply] + + model_config = { + "json_schema_extra": { + "example": { + "rc": 0, + "result": [ + { + "action": "firewall_vm_enable", + "source": "proxmox", + "proxmox_node": "px-testing", + ## + "vm_id": "100", + "vm_firewall": "enabled", + "vm_name": "test" + } + ] + } + } + } + + +class FirewallDisableVmRequest(BaseModel): + + proxmox_node: str = Field( + ..., + # default= "px-testing", + description = "Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$" + ) + + as_json: bool = Field( + default=True, + description="If true : JSON output else : raw output" + ) + # + + vm_id: str = Field( + ..., + # default="4000", + description="Virtual machine id", + pattern=r"^[0-9]+$" + ) + + model_config = { + "json_schema_extra": { + "example": { + "proxmox_node": "px-testing", + "as_json": True, + # + "vm_id": "1000", + } + } + } + + +class FirewallDisableVmItemReply(BaseModel): + + action: Literal["vm_EnableFirewallDc_usage"] + source: Literal["proxmox"] + proxmox_node: str + ## + # vm_id: int = Field(..., ge=1) + proxmox_api_host: str + +class FirewallDisableVmReply(BaseModel): + + rc: int = Field(0, description="RETURN code (0 = OK)") + result: list[FirewallDisableVmItemReply] + + model_config = { + "json_schema_extra": { + "example": { + "rc": 0, + "result": [ + { + "action": "firewall_vm_disable", + "source": "proxmox", + "proxmox_node": "px-testing", + ## + "vm_id": "1000", + "vm_firewall": "disable", + "vm_name": "test" + } + ] + } + } + } + + +# --------------------------------------------------------------------------- +# Backward compatibility -- old names used by current routes +# --------------------------------------------------------------------------- + +# firewall/add_iptable_alias.py +Request_ProxmoxFirewall_AddIptablesAlias = FirewallAliasAddRequest +Reply_ProxmoxFirewallWithStorageName_AddIptablesAliasItem = FirewallAliasAddItemReply +Reply_ProxmoxFirewallWithStorageName_AddIptablesAlias = FirewallAliasAddReply + +# firewall/apply_iptables_rules.py +Request_ProxmoxFirewall_ApplyIptablesRules = FirewallRuleApplyRequest +Reply_ProxmoxFirewallWithStorageName_ApplyIptablesRulesItem = FirewallRuleApplyItemReply +Reply_ProxmoxFirewallWithStorageName_ApplyIptablesRules = FirewallRuleApplyReply + +# firewall/delete_iptables_alias.py +Request_ProxmoxFirewall_DeleteIptablesAlias = FirewallAliasDeleteRequest +Reply_ProxmoxFirewallWithStorageName_DeleteIptablesAliasItem = FirewallAliasDeleteItemReply +Reply_ProxmoxFirewallWithStorageName_DeleteIptablesAlias = FirewallAliasDeleteReply + +# firewall/delete_iptables_rule.py +Request_ProxmoxFirewall_DeleteIptablesRule = FirewallRuleDeleteRequest +Reply_ProxmoxFirewallWithStorageName_DeleteIptablesRuleItem = FirewallRuleDeleteItemReply +Reply_ProxmoxFirewallWithStorageName_DeleteIptablesRule = FirewallRuleDeleteReply + +# firewall/list_iptables_alias.py +Request_ProxmoxFirewall_ListIptablesAlias = FirewallAliasListRequest +Reply_ProxmoxFirewallWithStorageName_ListIptablesAliasItem = FirewallAliasListItemReply +Reply_ProxmoxFirewallWithStorageName_ListIptablesAlias = FirewallAliasListReply + +# firewall/list_iptables_rules.py +Request_ProxmoxFirewall_ListIptablesRules = FirewallRuleListRequest +Reply_ProxmoxFirewallWithStorageName_ListIptablesRulesItem = FirewallRuleListItemReply +Reply_ProxmoxFirewallWithStorageName_ListIptablesRules = FirewallRuleListReply + +# firewall/enable_firewall_dc.py +Request_ProxmoxFirewall_EnableFirewallDc = FirewallEnableDcRequest +Reply_ProxmoxFirewallWithStorageName_EnableFirewallDcItem = FirewallEnableDcItemReply +Reply_ProxmoxFirewallWithStorageName_EnableFirewallDc = FirewallEnableDcReply + +# firewall/disable_firewall_dc.py +Request_ProxmoxFirewall_DisableFirewallDc = FirewallDisableDcRequest +Reply_ProxmoxFirewallWithStorageName_DisableFirewallDcItem = FirewallDisableDcItemReply +Reply_ProxmoxFirewallWithStorageName_DisableFirewallDc = FirewallDisableDcReply + +# firewall/enable_firewall_node.py +Request_ProxmoxFirewall_EnableFirewallNode = FirewallEnableNodeRequest +Reply_ProxmoxFirewallWithStorageName_EnableFirewallNodeItem = FirewallEnableNodeItemReply +Reply_ProxmoxFirewallWithStorageName_EnableFirewallNode = FirewallEnableNodeReply + +# firewall/disable_firewall_node.py +Request_ProxmoxFirewall_DistableFirewallNode = FirewallDisableNodeRequest +Reply_ProxmoxFirewallWithStorageName_DistableFirewallNodeItem = FirewallDisableNodeItemReply +Reply_ProxmoxFirewallWithStorageName_DisableFirewallNode = FirewallDisableNodeReply + +# firewall/enable_firewall_vm.py +Request_ProxmoxFirewall_EnableFirewallVm = FirewallEnableVmRequest +Reply_ProxmoxFirewallWithStorageName_EnableFirewallVmItem = FirewallEnableVmItemReply +Reply_ProxmoxFirewallWithStorageName_EnableFirewallVm = FirewallEnableVmReply + +# firewall/disable_firewall_vm.py +Request_ProxmoxFirewall_DistableFirewallVm = FirewallDisableVmRequest +Reply_ProxmoxFirewallWithStorageName_DistableFirewallVmItem = FirewallDisableVmItemReply +Reply_ProxmoxFirewallWithStorageName_DisableFirewallVm = FirewallDisableVmReply diff --git a/app/schemas/network.py b/app/schemas/network.py new file mode 100644 index 0000000..1c7b231 --- /dev/null +++ b/app/schemas/network.py @@ -0,0 +1,582 @@ +"""Consolidated network schemas: node and VM network interface operations.""" + +from typing import List, Literal +from pydantic import BaseModel, Field + + +# --------------------------------------------------------------------------- +# Node — Add Network Interface +# --------------------------------------------------------------------------- + +class NodeNetworkAddRequest(BaseModel): + + proxmox_node: str = Field( + ..., + # default= "px-testing", + description = "Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$" + ) + + as_json: bool = Field( + default=True, + description="If true : JSON output else : raw output" + ) + + # + + bridge_ports: str | None = Field( + default=None, + description="Bridge ports", + pattern=r"^[a-zA-Z0-9._-]+$" + ) + + iface_name: str | None = Field( + ..., + description="Interface name", + pattern=r"^[a-zA-Z0-9._-]+$" + ) + + iface_type: str | None = Field( + ..., + description="Interface type - ethernet, ovs, bridge", + pattern=r"^[a-zA-Z]+$" + ) + + iface_autostart: int | None = Field( + ..., + description="Autostart flag - 0 = no, 1 = yes" + ) + + ip_address: str | None = Field( + default=None, + description="ipv4 address", + pattern=r"^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$" + ) + + ip_netmask: str | None = Field( + default=None, + description="ipv4 netmask", + pattern=r"^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$|^\/[0-9]{1,2}$" + ) + + ip_gateway: str | None = Field( + default=None, + description="ipv4 gateway", + pattern=r"^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$" + ) + + ovs_bridge: str | None = Field( + default=None, + description="OVS bridge name", + pattern=r"^[a-zA-Z0-9._-]+$" + ) + + model_config = { + "json_schema_extra": { + "example":[ { + "proxmox_node": "px-testing", + "as_json": "true", + # + "iface_name": "vmbr142", + "iface_type": "bridge", + "bridge_ports": "enp87s0", + "iface_autostart": 1, + "ip_address": "192.168.99.2", + "ip_netmask": "255.255.255.0" + }, + ] + } + } + + +class NodeNetworkAddItemReply(BaseModel): + + action: Literal["vm_DeleteIptablesRule_usage"] + source: Literal["proxmox"] + proxmox_node: str + as_json: bool + ## + bridge_ports: str + iface_name: str + iface_type: str + iface_autostart: int + ip_address: str + ip_netmask: str + ip_gateway: str + ovs_bridge: str + + +class NodeNetworkAddReply(BaseModel): + + rc: int = Field(0, description="RETURN code (0 = OK)") + result: list[NodeNetworkAddItemReply] + + model_config = { + "json_schema_extra": { + "example": { + "rc": 0, + "result": [ + { + + "action": "network_add_interfaces_node", + "proxmox_node": "px-testing", + "source": "proxmox", + # + "bridge_ports": "enp87s0", + "iface_autostart": "1", + "iface_name": "vmbr142", + "ip_address": "192.168.99.2", + "ip_netmask": "255.255.255.0", + } + ] + } + } + } + + +# --------------------------------------------------------------------------- +# Node — Delete Network Interface +# --------------------------------------------------------------------------- + +class NodeNetworkDeleteRequest(BaseModel): + + proxmox_node: str = Field( + ..., + # default= "px-testing", + description = "Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$" + ) + # + + iface_name: str | None = Field( + description="Interface name", + pattern=r"^[a-zA-Z0-9._-]+$" + ) + + as_json: bool = Field( + default=True, + description="If true : JSON output else : raw output" + ) + + model_config = { + "json_schema_extra": { + "example": { + "proxmox_node": "px-testing", + "storage_name": "local", + "as_json": True, + # + "iface_name":"vmbr42", + } + } + } + + +class NodeNetworkDeleteItemReply(BaseModel): + + action: Literal["vm_DeleteIptablesRule_usage"] + source: Literal["proxmox"] + proxmox_node: str + ## + iface_name: str + +class NodeNetworkDeleteReply(BaseModel): + + rc: int = Field(0, description="RETURN code (0 = OK)") + result: list[NodeNetworkDeleteItemReply] + + model_config = { + "json_schema_extra": { + "example": { + "rc": 0, + "result": [ + { + "action": "network_delete_interfaces_node", + "source": "proxmox", + "proxmox_node": "px-testing", + ## + "iface_name": "vmbr42", + } + ] + } + } + } + + +# --------------------------------------------------------------------------- +# Node — List Network Interfaces +# --------------------------------------------------------------------------- + +class NodeNetworkListRequest(BaseModel): + + proxmox_node: str = Field( + ..., + # default= "px-testing", + description = "Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$" + ) + + as_json: bool = Field( + default=True, + description="If true : JSON output else : raw output" + ) + # + + model_config = { + "json_schema_extra": { + "example": { + "proxmox_node": "px-testing", + "as_json": True, + } + } + } + + +class NodeNetworkListItemReply(BaseModel): + + action: Literal["vm_DeleteIptablesRule_usage"] + source: Literal["proxmox"] + proxmox_node: str + ## + # vm_id: int = Field(..., ge=1) + vm_id: str + vm_fw_pos: int + +class NodeNetworkListReply(BaseModel): + + rc: int = Field(0, description="RETURN code (0 = OK)") + result: list[NodeNetworkListItemReply] + + model_config = { + "json_schema_extra": { + "example": { + "rc": 0, + "result": [ + { + "action": "network_list_interfaces_node", + "iface": "wlp89s0", + "iface_priority": 8, + "ip_settings_method": "manual", + "ip_settings_method6": "manual", + "proxmox_node": "px-testing", + "source": "proxmox" + }, + ] + } + } + } + + +# --------------------------------------------------------------------------- +# VM — Add Network Interface +# --------------------------------------------------------------------------- + +class VmNetworkAddRequest(BaseModel): + + proxmox_node: str = Field( + ..., + # default= "px-testing", + description = "Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$" + ) + + as_json: bool = Field( + default=True, + description="If true : JSON output else : raw output" + ) + # + + vm_id: str = Field( + ..., + # default="4000", + description="Virtual machine id", + pattern=r"^[0-9]+$" + ) + + # quick classic fields + + iface_model: str | None = Field( + description="Interface model- virtio, e1000, rtl8139", + pattern=r"^[A-Za-z0-9._-]+$" + ) + + iface_bridge: str | None = Field( + description="Bridge name for interface - vmbr0, vmbr142", + pattern=r"^[A-Za-z0-9._-]+$" + ) + + vm_vmnet_id: int | None = Field( + description="Network device index - 0, 1, 2, ..." + ) + + #### below fields to test : + + iface_trunks: bool | None = Field( + description="Enable trunk - allow multiple vlan on interface" + ) + + iface_tag: int | None = Field( + description="VLAN tag id" + ) + + iface_rate: float | None = Field( + description="Limit bandwith - Mbps - 0 to x" + ) + + iface_queues: int | None = Field( + description="Allocated amount allocated tx/rx on interface" + ) + + iface_mtu: int | None = Field( + description="MTU" + ) + + iface_macaddr: str | None = Field( + description="MAC address - hexa format", + pattern = r'^(?:[0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$' + ) + + iface_link_down: bool | None = Field( + description="Force to set down the interface" + ) + + iface_firewall: bool | None = Field( + description="Apply firewall rules on this interface" + ) + + model_config = { + "json_schema_extra": { + "example": { + "proxmox_node": "px-testing", + "storage_name": "local", + "as_json": True, + # + "vm_id": "1000", + "vm_vmnet_id": "1", + "iface_model": "virtio", + "iface_bridge": "vmbr142", + } + } + } + + +class VmNetworkAddItemReply(BaseModel): + + action: Literal["vm_DeleteIptablesRule_usage"] + source: Literal["proxmox"] + proxmox_node: str + ## + # vm_id: int = Field(..., ge=1) + vm_id: str + vm_fw_pos: int + +class VmNetworkAddReply(BaseModel): + + rc: int = Field(0, description="RETURN code (0 = OK)") + result: list[VmNetworkAddItemReply] + + model_config = { + "json_schema_extra": { + "example": { + "rc": 0, + "result": [ + { + "action": "network_add_interfaces_vm", + "source": "proxmox", + "proxmox_node": "px-testing", + ## + "vm_id": "1000", + "iface_model": "virtio", + } + ] + } + } + } + + +# --------------------------------------------------------------------------- +# VM — Delete Network Interface +# --------------------------------------------------------------------------- + +class VmNetworkDeleteRequest(BaseModel): + + proxmox_node: str = Field( + ..., + # default= "px-testing", + description = "Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$" + ) + + as_json: bool = Field( + default=True, + description="If true : JSON output else : raw output" + ) + # + + vm_id: str = Field( + ..., + # default="4000", + description="Virtual machine id", + pattern=r"^[0-9]+$" + ) + + vm_vmnet_id: int | None = Field( + description="Network device index - 0, 1, 2, ..." + ) + + model_config = { + "json_schema_extra": { + "example": { + "proxmox_node": "px-testing", + "storage_name": "local", + "as_json": True, +# + "vm_id":"1000", + "vm_vmnet_id":1, + + } + } + } + + +class VmNetworkDeleteItemReply(BaseModel): + + action: Literal["vm_DeleteIptablesRule_usage"] + source: Literal["proxmox"] + proxmox_node: str + ## + # vm_id: int = Field(..., ge=1) + vm_id: str + vm_fw_pos: int + +class VmNetworkDeleteReply(BaseModel): + + rc: int = Field(0, description="RETURN code (0 = OK)") + result: list[VmNetworkDeleteItemReply] + + model_config = { + "json_schema_extra": { + "example": { + "rc": 0, + "result": [ + { + "action": "network_delete_interfaces_vm", + "source": "proxmox", + "proxmox_node": "px-testing", + ## + "vm_id": "1000", + "iface_model": "virtio" + + } + ] + } + } + } + + +# --------------------------------------------------------------------------- +# VM — List Network Interfaces +# --------------------------------------------------------------------------- + +class VmNetworkListRequest(BaseModel): + + proxmox_node: str = Field( + ..., + # default= "px-testing", + description = "Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$" + ) + + as_json: bool = Field( + default=True, + description="If true : JSON output else : raw output" + ) + # + + vm_id: str = Field( + ..., + # default="4000", + description="Virtual machine id", + pattern=r"^[0-9]+$" + ) + + model_config = { + "json_schema_extra": { + "example": { + "proxmox_node": "px-testing", + "as_json": True, + "vm_id": "1001", + } + } + } + + +class VmNetworkListItemReply(BaseModel): + + action: Literal["vm_DeleteIptablesRule_usage"] + source: Literal["proxmox"] + proxmox_node: str + ## + # vm_id: int = Field(..., ge=1) + vm_id: str + +class VmNetworkListReply(BaseModel): + + rc: int = Field(0, description="RETURN code (0 = OK)") + result: list[VmNetworkListItemReply] + + model_config = { + "json_schema_extra": { + "example": { + "rc": 0, + "result": [ + { + "action": "network_list_interfaces_vm", + "source": "proxmox", + "proxmox_node": "px-testing", + ## + "vm_id": "1000", + "vm_network_bridge": "vmbr0", + "vm_network_device": "net0", + "vm_network_mac": "AA:BB:CC:DD:EE:FF", + "vm_network_type": "virtio" + + } + ] + } + } + } + + +# --------------------------------------------------------------------------- +# Backward compatibility -- old names used by current routes +# --------------------------------------------------------------------------- + +# network/node_name/add_network.py +Request_ProxmoxNetwork_WithNodeName_AddNetworkInterface = NodeNetworkAddRequest +Reply_ProxmoxNetwork_WithNodeName_AddNetworkInterfaceItem = NodeNetworkAddItemReply +Reply_ProxmoxNetwork_WithNodeName_AddNetworkInterface = NodeNetworkAddReply + +# network/node_name/delete_network.py +Request_ProxmoxNetwork_WithNodeName_DeleteInterface = NodeNetworkDeleteRequest +Reply_ProxmoxNetwork_WithNodeName_DeleteInterfaceItem = NodeNetworkDeleteItemReply +Reply_ProxmoxNetwork_WithNodeName_DeleteInterface = NodeNetworkDeleteReply + +# network/node_name/list_network.py +Request_ProxmoxNetwork_WithNodeName_ListInterface = NodeNetworkListRequest +Reply_ProxmoxNetwork_WithNodeName_ListInterfaceItem = NodeNetworkListItemReply +Reply_ProxmoxNetwork_WithNodeName_ListInterface = NodeNetworkListReply + +# network/vm_id/add_network.py +Request_ProxmoxNetwork_WithVmId_AddNetwork = VmNetworkAddRequest +Reply_ProxmoxNetwork_WithVmId_AddNetworkInterfaceItem = VmNetworkAddItemReply +Reply_ProxmoxNetwork_WithVmId_AddNetworkInterface = VmNetworkAddReply + +# network/vm_id/delete_network.py +Request_ProxmoxNetwork_WithVmId_DeleteNetwork = VmNetworkDeleteRequest +Reply_ProxmoxNetwork_WithVmId_DeleteNetworkInterfaceItem = VmNetworkDeleteItemReply +Reply_ProxmoxNetwork_WithVmId_DeleteNetworkInterface = VmNetworkDeleteReply + +# network/vm_id/list_network.py +Request_ProxmoxNetwork_WithVmId_ListNetwork = VmNetworkListRequest +Reply_ProxmoxNetwork_WithVmId_ListNetworkInterfaceItem = VmNetworkListItemReply +Reply_ProxmoxNetwork_WithVmId_ListNetworkInterface = VmNetworkListReply diff --git a/app/schemas/snapshots.py b/app/schemas/snapshots.py new file mode 100644 index 0000000..25ab9e2 --- /dev/null +++ b/app/schemas/snapshots.py @@ -0,0 +1,366 @@ +"""Consolidated snapshot schemas: create, delete, list, revert.""" + +from typing import Literal +from pydantic import BaseModel, Field + + +# --------------------------------------------------------------------------- +# Snapshot Create +# --------------------------------------------------------------------------- + +class SnapshotCreateRequest(BaseModel): + + proxmox_node: str = Field( + ..., + # default= "px-testing", + description = "Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$" + ) + + as_json: bool = Field( + default=True, + description="If true : JSON output else : raw output" + ) + # + + vm_id: str = Field( + ..., + # default="4000", + description="Virtual machine id", + pattern=r"^[0-9]+$" + ) + + vm_snapshot_name: str | None = Field( + default=None, + description="Name of the snapshot to create", + pattern=r"^[A-Za-z0-9_-]+$" + ) + + vm_snapshot_description: str | None = Field( + default=None, + description="Optional description for the snapshot", + pattern=r"^[A-Za-z0-9_-]+$" + ) + + model_config = { + "json_schema_extra": { + "example": { + "proxmox_node": "px-testing", + "vm_id": "1111", + "vm_snapshot_name":"MY_VM_SNAPSHOT", + "vm_snapshot_description":"MY_DESCRIPTION", + "as_json": True + } + } + } + + +class SnapshotCreateItemReply(BaseModel): + + action: Literal["vm_get_config"] + proxmox_node: str + source: Literal["proxmox"] + vm_id: str # int = Field(..., ge=1) + + vm_name: str + vm_snapshot_description: str + vm_snapshot_name: str + + raw_data: str = Field(..., description="Raw string returned by proxmox") + + +class SnapshotCreateReply(BaseModel): + + rc: int = Field(0, description="RETURN code (0 = OK)") + result: list[SnapshotCreateItemReply] + + model_config = { + "json_schema_extra": { + "example": { + "rc": 0, + "result": [ + { + "action": "snapshot_vm_create", + "proxmox_node": "px-testing", + "source": "proxmox", + + "vm_id": "1000", + "vm_name": "admin-wazuh", + "vm_snapshot_description": "MY_DESCRIPTION", + "vm_snapshot_name": "MY_VM_SNAPSHOT", + "raw_data": { "data": "UPID:px-testing:002D5E30:1706941B:68C196E9:qmsnapshot:1000:API_master@pam!API_master:" } + } + ] + } + } + } + + +# --------------------------------------------------------------------------- +# Snapshot Delete +# --------------------------------------------------------------------------- + +class SnapshotDeleteRequest(BaseModel): + + proxmox_node: str = Field( + ..., + # default= "px-testing", + description = "Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$" + ) + + as_json: bool = Field( + default=True, + description="If true : JSON output else : raw output" + ) + # + + vm_id: str = Field( + ..., + # default="4000", + description="Virtual machine id", + pattern=r"^[0-9]+$" + ) + + vm_snapshot_name: str | None = Field( + default=None, + description="Name of the snapshot to delete", + pattern=r"^[A-Za-z0-9_-]+$" + ) + + model_config = { + "json_schema_extra": { + "example": { + "proxmox_node": "px-testing", + "vm_id": "1111", + "vm_snapshot_name": "MY_VM_SNAPSHOT", + "as_json": True + } + } + } + + +class SnapshotDeleteItemReply(BaseModel): + + action: Literal["vm_get_config"] + source: Literal["proxmox"] + # proxmox_node: str + # vm_id: int = Field(..., ge=1) + # vm_name: str + # raw_data: str = Field(..., description="Raw string returned by proxmox") + + +class SnapshotDeleteReply(BaseModel): + + rc: int = Field(0, description="RETURN code (0 = OK)") + result: list[SnapshotDeleteItemReply] + + model_config = { + "json_schema_extra": { + "example": { + "rc": 0, + "result": [ + { + "action": "snapshot_vm_delete", + "proxmox_node": "px-testing", + "source": "proxmox", + + "vm_id": "1000", + "vm_name": "admin-wazuh", + "vm_snapshot_name": "BBBB", + "raw_data": { "data": "UPID:px-testing:002D6878:17077370:68C19925:qmdelsnapshot:1000:API_master@pam!API_master:"}, + } + ] + } + } + } + + +# --------------------------------------------------------------------------- +# Snapshot List +# --------------------------------------------------------------------------- + +class SnapshotListRequest(BaseModel): + + proxmox_node: str = Field( + ..., + # default= "px-testing", + description = "Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$" + ) + + as_json: bool = Field( + default=True, + description="If true : JSON output else : raw output" + ) + # + + vm_id: str = Field( + ..., + # default="4000", + description="Virtual machine id", + pattern=r"^[0-9]+$" + ) + + model_config = { + "json_schema_extra": { + "example": { + "proxmox_node": "px-testing", + "vm_id": "1111" + } + } + } + + +class SnapshotListItemReply(BaseModel): + + action: Literal["vm_get_config"] + source: Literal["proxmox"] + # proxmox_node: str + # vm_id: int = Field(..., ge=1) + # vm_name: str + # raw_data: str = Field(..., description="Raw string returned by proxmox") + + +class SnapshotListReply(BaseModel): + + rc: int = Field(0, description="RETURN code (0 = OK)") + result: list[SnapshotListItemReply] + + model_config = { + "json_schema_extra": { + "example": { + "rc": 0, + + "result": [ + { + "action": "snapshot_vm_list", + "proxmox_node": "px-testing", + "source": "proxmox", + + "vm_id": "1000", + "vm_snapshot_description": "MY_DESCRIPTION", + "vm_snapshot_name": "MY_VM_SNAPSHOT", + "vm_snapshot_parent": "", + "vm_snapshot_time": 1757517545 + }, + { + "action": "snapshot_vm_list", + "proxmox_node": "px-testing", + "source": "proxmox", + + "vm_id": "1000", + "vm_snapshot_description": "You are here!", + "vm_snapshot_name": "current", + "vm_snapshot_parent": "MY_VM_SNAPSHOT", + "vm_snapshot_sha1": "7cc59c988bb8f18601fe076ad239f8b760667270" + } + ] + } + } + } + + +# --------------------------------------------------------------------------- +# Snapshot Revert +# --------------------------------------------------------------------------- + +class SnapshotRevertRequest(BaseModel): + + proxmox_node: str = Field( + ..., + # default= "px-testing", + description = "Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$" + ) + + as_json: bool = Field( + default=True, + description="If true : JSON output else : raw output" + ) + # + + vm_id: str = Field( + ..., + # default="4000", + description="Virtual machine id", + pattern=r"^[0-9]+$" + ) + + vm_snapshot_name: str | None = Field( + default=None, + description="Name of the snapshot to create", + pattern=r"^[A-Za-z0-9_-]+$" + ) + + model_config = { + "json_schema_extra": { + "example": { + "proxmox_node": "px-testing", + "vm_id": "1111", + "vm_snapshot_name":"CCCC", + "as_json": True, + } + } + } + + +class SnapshotRevertItemReply(BaseModel): + + action: Literal["vm_get_config"] + source: Literal["proxmox"] + # proxmox_node: str + # vm_id: int = Field(..., ge=1) + # vm_name: str + # raw_data: str = Field(..., description="Raw string returned by proxmox") + + +class SnapshotRevertReply(BaseModel): + + rc: int = Field(0, description="RETURN code (0 = OK)") + result: list[SnapshotRevertItemReply] + + model_config = { + "json_schema_extra": { + "example": { + "rc": 0, + "result": [ + { + "action": "snapshot_vm_revert", + "source": "proxmox", + "proxmox_node": "px-testing", + + "vm_id": "1000", + "vm_name": "admin-wazuh", + "vm_snapshot_name": "CCCC", + "raw_data": {"data": "UPID:px-testing:002D7C57:17096777:68C19E25:qmrollback:1000:API_master@pam!API_master:"}, + } + ] + } + } + } + + +# --------------------------------------------------------------------------- +# Backward compatibility -- old names used by current routes +# --------------------------------------------------------------------------- + +# vm_id/snapshot/vm_create.py +Request_ProxmoxVmsVMID_CreateSnapshot = SnapshotCreateRequest +Reply_ProxmoxVmsVMID_CreateSnapshotItem = SnapshotCreateItemReply +Reply_ProxmoxVmsVMID_CreateSnapshot = SnapshotCreateReply + +# vm_id/snapshot/vm_delete.py +Request_ProxmoxVmsVMID_DeleteSnapshot = SnapshotDeleteRequest +Reply_ProxmoxVmsVMID_DeleteSnapshotItem = SnapshotDeleteItemReply +Reply_ProxmoxVmsVMID_DeleteSnapshot = SnapshotDeleteReply + +# vm_id/snapshot/vm_list.py +Request_ProxmoxVmsVMID_ListSnapshot = SnapshotListRequest +Reply_ProxmoxVmsVMID_ListSnapshotItem = SnapshotListItemReply +Reply_ProxmoxVmsVMID_ListSnapshot = SnapshotListReply + +# vm_id/snapshot/vm_revert.py +Request_ProxmoxVmsVMID_RevertSnapshot = SnapshotRevertRequest +Reply_ProxmoxVmsVMID_RevertSnapshotItem = SnapshotRevertItemReply +Reply_ProxmoxVmsVMID_RevertSnapshot = SnapshotRevertReply diff --git a/app/schemas/storage.py b/app/schemas/storage.py new file mode 100644 index 0000000..6e9e593 --- /dev/null +++ b/app/schemas/storage.py @@ -0,0 +1,389 @@ +"""Consolidated storage schemas: list, download ISO, list ISO, list templates.""" + +from typing import List, Literal +from pydantic import BaseModel, Field + + +# --------------------------------------------------------------------------- +# Storage List +# --------------------------------------------------------------------------- + +class StorageListRequest(BaseModel): + + proxmox_node: str = Field( + ..., + # default= "px-testing", + description="Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$" + ) + + as_json: bool = Field( + default=True, + description="If true : JSON output else : raw output" + ) + # + + storage_name: str = Field( + ..., + # default= "px-testing", + description="Proxmox storage name", + pattern=r"^[A-Za-z0-9-]*$" + ) + + model_config = { + "json_schema_extra": { + "example": { + "proxmox_node": "px-testing", + "storage_name": "local", + "as_json": True + } + } + } + + +class StorageListItemReply(BaseModel): + + action: Literal["storage_list"] + source: Literal["proxmox"] + proxmox_node: str + # + storage_active: int + storage_content_types: str + storage_is_enable: int + storage_is_share: int + storage_name : str + storage_space_available: int + storage_space_total: int + storage_space_used: int + storage_space_used_fraction: float + storage_type: str + +class StorageListReply(BaseModel): + + rc: int = Field(0, description="RETURN code (0 = OK)") + result: list[StorageListItemReply] + + model_config = { + "json_schema_extra": { + "example": { + "rc": 0, + "result": [ + { + "action": "storage_list", + "proxmox_node": "px-testing", + "source": "proxmox", + # + "storage_active": 1, + "storage_content_types": "images,rootdir", + "storage_is_enable": 1, + "storage_is_share": 0, + "storage_name": "local-lvm", + "storage_space_available": 3600935440666, + "storage_space_total": 3836496314368, + "storage_space_used": 235560873702, + "storage_space_used_fraction": 0.0613999999999491, + "storage_type": "lvmthin" + } + ] + } + } + } + + +# --------------------------------------------------------------------------- +# Download ISO +# --------------------------------------------------------------------------- + +class StorageDownloadIsoRequest(BaseModel): + + proxmox_node: str = Field( + ..., + # default= "px-testing", + description = "Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$" + ) + + as_json: bool = Field( + default=True, + description="If true : JSON output else : raw output" + ) + # + + proxmox_storage: str = Field( + ..., + description="Target Proxmox storage name", + pattern=r"^[A-Za-z0-9-]+$", + ) + + iso_file_content_type: str = Field( + ..., + description="MIME type of the ISO file", + pattern=r"^[A-Za-z0-9-]*$" + # pattern = r"^application/(?:x-)?iso9660-image$", + ) + + iso_file_name: str = Field( + ..., + description="ISO file name - must end with .iso", + pattern=r"^[A-Za-z0-9._-]+\.iso$", + ) + + iso_url: str = Field( + ..., + description="http|https URL where the ISO will be downloaded from.", + pattern=r"^https?://[^\s]+$", + ) + + model_config = { + "json_schema_extra": { + "example": { + "proxmox_node": "px-testing", + "storage_name": "local", + # + "iso_file_content_type": "iso", + "iso_file_name": "ubuntu-24.04-live-server-amd64.iso", + "iso_url": "https://releases.ubuntu.com/24.04/ubuntu-24.04-live-server-amd64.iso", + "as_json": True, + } + } + } + + +class StorageDownloadIsoItemReply(BaseModel): + + action: Literal["storage_download_iso"] + source: Literal["proxmox"] + proxmox_node: str + vm_id: int = Field(..., ge=1) + vm_name: str + # raw_data: str = Field(..., description="Raw string returned by proxmox") + cpu_allocated: int + cpu_current_usage: int + disk_current_usage: int + disk_max: int + disk_read: int + disk_write: int + net_in: int + net_out: int + ram_current_usage: int + ram_max: int + vm_status: str + vm_uptime: int + +class StorageDownloadIsoReply(BaseModel): + + rc: int = Field(0, description="RETURN code (0 = OK)") + result: list[StorageDownloadIsoItemReply] + + model_config = { + "json_schema_extra": { + "example": { + "rc": 0, + "result": [ + { + "cpu_allocated":1, + "cpu_current_usage":0, + "disk_current_usage":0, + "disk_max":34359738368, + "disk_read":0, + "disk_write":0, + "net_in":280531583, + "net_out":6330590, + "ram_current_usage":1910544625, + "ram_max":4294967296, + "vm_id":1020, + "vm_name":"admin-web-api-kong", + "vm_status":"running", + "vm_uptime":79940 + } + ] + } + } + } + + +# --------------------------------------------------------------------------- +# List ISO +# --------------------------------------------------------------------------- + +class StorageListIsoRequest(BaseModel): + + proxmox_node: str = Field( + ..., + # default= "px-testing", + description = "Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$" + ) + + as_json: bool = Field( + default=True, + description="If true : JSON output else : raw output" + ) + # + + storage_name: str = Field( + ..., + # default= "px-testing", + description = "Proxmox storage name", + pattern=r"^[A-Za-z0-9-]*$" + ) + + model_config = { + "json_schema_extra": { + "example": { + "proxmox_node": "px-testing", + "storage_name": "local", + "as_json": True + + } + } + } + + +class StorageListIsoItemReply(BaseModel): + + action: Literal["storage_list_iso"] + source: Literal["proxmox"] + proxmox_node: str + ## + # vm_id: int = Field(..., ge=1) + iso_content: str + iso_ctime: int + iso_format: str + iso_size: int + iso_vol_id: str + local: str + storage_name: str + +class StorageListIsoReply(BaseModel): + + rc: int = Field(0, description="RETURN code (0 = OK)") + result: list[StorageListIsoItemReply] + + model_config = { + "json_schema_extra": { + "example": { + "rc": 0, + "result": [ + { + "action": "storage_list_iso", + "source": "proxmox", + "proxmox_node": "px-testing", + ## + "iso_content": "iso", + "iso_ctime": 1753343734, + "iso_format": "iso", + "iso_size": 614746112, + "iso_vol_id": + "local:iso/noble-server-cloudimg-amd64.img", + "storage_name": "local", + } + ] + } + } + } + + +# --------------------------------------------------------------------------- +# List Templates +# --------------------------------------------------------------------------- + +class StorageListTemplateRequest(BaseModel): + + proxmox_node: str = Field( + ..., + # default= "px-testing", + description = "Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$" + ) + + as_json: bool = Field( + default=True, + description="If true : JSON output else : raw output" + ) + # + + storage_name: str = Field( + ..., + # default= "px-testing", + description = "Proxmox storage name", + pattern=r"^[A-Za-z0-9-]*$" + ) + + model_config = { + "json_schema_extra": { + "example": { + "proxmox_node": "px-testing", + "storage_name": "local", + "as_json": True + + } + } + } + + +class StorageListTemplateItemReply(BaseModel): + + action: Literal["storage_list_template"] + source: Literal["proxmox"] + proxmox_node: str + + # vm_id: int = Field(..., ge=1) + storage_name: str + template_content: str + template_ctime: int + template_format: str + template_size: int + template_vol_id: str + +class StorageListTemplateReply(BaseModel): + + rc: int = Field(0, description="RETURN code (0 = OK)") + result: list[StorageListTemplateItemReply] + + model_config = { + "json_schema_extra": { + "example": { + "rc": 0, + "result": [ + { + "action": "storage_list_template", + "proxmox_node": "px-testing", + "source": "proxmox", + # + "storage_name": "local", + "template_content": "vztmpl", + "template_ctime": 1749734175, + "template_format": "tzst", + "template_size": 126515062, + "template_vol_id": "local:vztmpl/debian-12-standard_12.7-1_amd64.tar.zst" + } + ] + } + } + } + + +# --------------------------------------------------------------------------- +# Backward compatibility -- old names used by current routes +# --------------------------------------------------------------------------- + +# storage/list.py +Request_ProxmoxStorage_List = StorageListRequest +Reply_ProxmoxStorage_ListItem = StorageListItemReply +Reply_ProxmoxStorage_List = StorageListReply + +# storage/download_iso.py +Request_ProxmoxStorage_DownloadIso = StorageDownloadIsoRequest +Reply_ProxmoxStorage_DownloadIsoItem = StorageDownloadIsoItemReply +Reply_ProxmoxStorage_DownloadIso = StorageDownloadIsoReply + +# storage/storage_name/list_iso.py +Request_ProxmoxStorage_ListIso = StorageListIsoRequest +Reply_ProxmoxStorageWithStorageName_ListIsoItem = StorageListIsoItemReply +Reply_ProxmoxStorageWithStorageName_ListIso = StorageListIsoReply + +# storage/storage_name/list_template.py +Request_ProxmoxStorage_ListTemplate = StorageListTemplateRequest +Reply_ProxmoxStorageWithStorageName_ListTemplateItem = StorageListTemplateItemReply +Reply_ProxmoxStorageWithStorageName_ListTemplate = StorageListTemplateReply diff --git a/app/schemas/vm_config.py b/app/schemas/vm_config.py new file mode 100644 index 0000000..fb44612 --- /dev/null +++ b/app/schemas/vm_config.py @@ -0,0 +1,429 @@ +"""Consolidated VM config schemas: get config, get cdrom, get cpu, get ram, set tag.""" + +from typing import Literal +from pydantic import BaseModel, Field + + +# --------------------------------------------------------------------------- +# VM Get Config +# --------------------------------------------------------------------------- + +class VmGetConfigRequest(BaseModel): + + proxmox_node: str = Field( + ..., + # default= "px-testing", + description = "Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$" + ) + + as_json: bool = Field( + default=True, + description="If true : JSON output else : raw output" + ) + # + + vm_id: str = Field( + ..., + # default="4000", + description="Virtual machine id", + pattern=r"^[0-9]+$" + ) + + model_config = { + "json_schema_extra": { + "example": { + "proxmox_node": "px-testing", + "vm_id": "1111", + "as_json": True, + } + } + } + + +class VmGetConfigItemReply(BaseModel): + + action: Literal["vm_get_config"] + source: Literal["proxmox"] + proxmox_node: str + vm_id: int = Field(..., ge=1) + vm_name: str + raw_data: str = Field(..., description="Raw string returned by proxmox") + + +class VmGetConfigReply(BaseModel): + + rc: int = Field(0, description="RETURN code (0 = OK)") + result: list[VmGetConfigItemReply] + + model_config = { + "json_schema_extra": { + "example": { + "rc": 0, + "result": [ + { + "action": "vm_get_config", + "proxmox_node": "px-testing", + "raw_data": { + "data": { + "balloon": 0, + "boot": "c", + "bootdisk": "scsi0", + "cipassword": "**********", + "ciuser": "alice", + "cores": 2, + "cpu": "host", + "digest": "29bec92_redacted", + "ide2": "local:1000/vm-1000-cloudinit.qcow2,media=cdrom,size=4M", + "ipconfig0": "ip=192.168.42.100/24,gw=192.168.42.1", + "memory": "8192", + "meta": "creation-qemu=9.0.2,ctime=1757418890", + "name": "admin-wazuh", + "net0": "virtio=BC:24:11:CB:B3:C7,bridge=vmbr0", + "scsi0": "local-lvm:vm-1000-disk-0,size=64G", + "scsihw": "virtio-scsi-pci", + "serial0": "socket", + "smbios1": "uuid=82c50ddc-a24f-4cbc-a013-c0e846f230fc", + "sockets": 1, + "sshkeys": "ssh-ed25519%20AAAAC....redacted", + "tags": "admin", + "vga": "serial0", + "vmgenid": "c7426562-ad4b-4719-81a1-72328f7ec018" + } + }, + "source": "proxmox", + "vm_id": "1000" + } + ] + } + } + } + + +# --------------------------------------------------------------------------- +# VM Get Config CDROM +# --------------------------------------------------------------------------- + +class VmGetConfigCdromRequest(BaseModel): + + proxmox_node: str = Field( + ..., + # default= "px-testing", + description = "Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$" + ) + + as_json: bool = Field( + default=True, + description="If true : JSON output else : raw output" + ) + # + + vm_id: str = Field( + ..., + # default="4000", + description="Virtual machine id", + pattern=r"^[0-9]+$" + ) + + model_config = { + "json_schema_extra": { + "example": { + "proxmox_node": "px-testing", + "vm_id": "1111", + "as_json": True, + } + } + } + + +class VmGetConfigCdromItemReply(BaseModel): + + action: Literal["vm_get_config_cdrom"] + source: Literal["proxmox"] + proxmox_node : str + vm_id : str # int = Field(..., ge=1) + vm_cdrom_device: str + vm_cdrom_iso : str + vm_cdrom_media : str + vm_cdrom_size : str + + # vm_name: str + # raw_data: str = Field(..., description="Raw string returned by proxmox") + + +class VmGetConfigCdromReply(BaseModel): + + rc: int = Field(0, description="RETURN code (0 = OK)") + result: list[VmGetConfigCdromItemReply] + + model_config = { + "json_schema_extra": { + "example": { + "rc": 0, + "result": [ + { + "action": "vm_get_config_cdrom", + "proxmox_node": "px-testing", + "source": "proxmox", + "vm_cdrom_device": "ide2", + "vm_cdrom_iso": "local:1000/vm-1000-cloudinit.qcow2", + "vm_cdrom_media": "cdrom", + "vm_cdrom_size": "4M", + "vm_id": "1000" + } + ] + } + } + } + + +# --------------------------------------------------------------------------- +# VM Get Config CPU +# --------------------------------------------------------------------------- + +class VmGetConfigCpuRequest(BaseModel): + + proxmox_node: str = Field( + ..., + # default= "px-testing", + description = "Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$" + ) + + as_json: bool = Field( + default=True, + description="If true : JSON output else : raw output" + ) + # + + vm_id: str = Field( + ..., + # default="4000", + description="Virtual machine id", + pattern=r"^[0-9]+$" + ) + + model_config = { + "json_schema_extra": { + "example": { + "proxmox_node": "px-testing", + "vm_id": "1111", + "as_json": True, + } + } + } + + +class VmGetConfigCpuItemReply(BaseModel): + + action: Literal["vm_get_config_cpu"] + source: Literal["proxmox"] + proxmox_node: str + vm_id : str # int = Field(..., ge=1) + vm_arch : str #to fix ? + vm_cores : str #to fix ? + vm_sockets : str #to fix ? + # vm_name: str + # raw_data: str = Field(..., description="Raw string returned by proxmox") + + +class VmGetConfigCpuReply(BaseModel): + + rc: int = Field(0, description="RETURN code (0 = OK)") + result: list[VmGetConfigCpuItemReply] + + model_config = { + "json_schema_extra": { + "example": { + "rc": 0, + "result": [ + { + "action":"vm_get_config_cpu", + "proxmox_node":"px-testing", + "source":"proxmox", + "vm_arch":"host", + "vm_cores":"2", + "vm_id":"1000", + "vm_sockets":"1" + } + ] + } + } + } + + +# --------------------------------------------------------------------------- +# VM Get Config RAM +# --------------------------------------------------------------------------- + +class VmGetConfigRamRequest(BaseModel): + + proxmox_node: str = Field( + ..., + # default= "px-testing", + description = "Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$" + ) + + as_json: bool = Field( + default=True, + description="If true : JSON output else : raw output" + ) + # + + vm_id: str = Field( + ..., + # default="4000", + description="Virtual machine id", + pattern=r"^[0-9]+$" + ) + + model_config = { + "json_schema_extra": { + "example": { + "proxmox_node": "px-testing", + "vm_id": "1111", + "as_json": True, + } + } + } + + +class VmGetConfigRamItemReply(BaseModel): + + action: Literal["vm_get_config_ram"] + source: Literal["proxmox"] + proxmox_node: str + vm_id : str # int = Field(..., ge=1) + vm_ram_allocated: str # wtf... - fix todo + + # vm_name: str + # raw_data: str = Field(..., description="Raw string returned by proxmox") + + +class VmGetConfigRamReply(BaseModel): + + rc: int = Field(0, description="RETURN code (0 = OK)") + result: list[VmGetConfigRamItemReply] + + model_config = { + "json_schema_extra": { + "example": { + "rc": 0, + "result": [ + { + "action": "vm_get_config_ram", + "proxmox_node": "px-testing", + "source": "proxmox", + "vm_id": "1000", + "vm_ram_allocated": "8192" + } + ] + } + } + } + + +# --------------------------------------------------------------------------- +# VM Set Tag +# --------------------------------------------------------------------------- + +class VmSetTagRequest(BaseModel): + + proxmox_node: str = Field( + ..., + # default= "px-testing", + description = "Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$" + ) + as_json: bool = Field( + default=True, + description="If true : JSON output else : raw output" + ) + # + + vm_id: str = Field( + ..., + # default="4000", + description="Virtual machine id", + pattern=r"^[0-9]+$" + ) + + vm_tag_name: str = Field( + ..., + description="Comma separated list of tags to assign to the virtual machine", + pattern=r"^[A-Za-z0-9_, -]+$" + ) + + model_config = { + "json_schema_extra": { + "example": { + "proxmox_node": "px-testing", + "vm_id": "1111", + "vm_tag_name":"group_01,group_02", + "as_json": True, + } + } + } + + +class VmSetTagItemReply(BaseModel): + + action: Literal["vm_get_config"] + source: Literal["proxmox"] + # proxmox_node: str + # vm_id: int = Field(..., ge=1) + # vm_name: str + # raw_data: str = Field(..., description="Raw string returned by proxmox") + + +class VmSetTagReply(BaseModel): + + rc: int = Field(0, description="RETURN code (0 = OK)") + result: list[VmSetTagItemReply] + + model_config = { + "json_schema_extra": { + "example": { + "rc": 0, + "result": [ + { + "action": "vm_set_tag", + "source": "proxmox", + "tags": "group_01,group_02", + } + ] + } + } + } + + +# --------------------------------------------------------------------------- +# Backward compatibility -- old names used by current routes +# --------------------------------------------------------------------------- + +# vm_id/config/vm_get_config.py +Request_ProxmoxVmsVMID_VmGetConfig = VmGetConfigRequest +Reply_ProxmoxVmsVMID_VmGetConfigItem = VmGetConfigItemReply +Reply_ProxmoxVmsVMID_VmGetConfig = VmGetConfigReply + +# vm_id/config/vm_get_config_cdrom.py +Request_ProxmoxVmsVMID_VmGetConfigCdrom = VmGetConfigCdromRequest +Reply_ProxmoxVmsVMID_VmGetConfigCdromItem = VmGetConfigCdromItemReply +Reply_ProxmoxVmsVMID_VmGetConfigCdrom = VmGetConfigCdromReply + +# vm_id/config/vm_get_config_cpu.py +Request_ProxmoxVmsVMID_VmGetConfigCpu = VmGetConfigCpuRequest +Reply_ProxmoxVmsVMID_VmGetConfigCpuItem = VmGetConfigCpuItemReply +Reply_ProxmoxVmsVMID_VmGetConfigCpu = VmGetConfigCpuReply + +# vm_id/config/vm_get_config_ram.py +Request_ProxmoxVmsVMID_VmGetConfigRam = VmGetConfigRamRequest +Reply_ProxmoxVmsVMID_VmGetConfigRamItem = VmGetConfigRamItemReply +Reply_ProxmoxVmsVMID_VmGetConfigRam = VmGetConfigRamReply + +# vm_id/config/vm_set_tag.py +Request_ProxmoxVmsVMID_VmSetTag = VmSetTagRequest +Reply_ProxmoxVmsVMID_VmSetTagItem = VmSetTagItemReply +Reply_ProxmoxVmsVMID_VmSetTag = VmSetTagReply diff --git a/app/schemas/vms.py b/app/schemas/vms.py new file mode 100644 index 0000000..ab4ddd8 --- /dev/null +++ b/app/schemas/vms.py @@ -0,0 +1,711 @@ +"""Consolidated VM schemas: list, create, delete, clone, start/stop, mass ops.""" + +from enum import Enum +from typing import List, Literal +from pydantic import BaseModel, Field + + +# --------------------------------------------------------------------------- +# VM List +# --------------------------------------------------------------------------- + +class VmListRequest(BaseModel): + + proxmox_node: str = Field( + ..., + # default= "px-testing", + description = "Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$" + ) + + as_json: bool = Field( + default=True, + description="If true : JSON output else : raw output" + ) + + model_config = { + "json_schema_extra": { + "example": { + "proxmox_node": "px-testing", + "as_json": True + } + } + } + + +class VmListActionEnum(str, Enum): + + LIST = "vm_list" + START = "vm_start" + STOP = "vm_stop" + RESUME = "vm_resume" + PAUSE = "vm_pause" + STOP_FORCE = "vm_stop_force" + + +class VmListStatusEnum(str, Enum): + + RUNNING = "running" + STOPPED = "stopped" + PAUSED = "paused" + + +class VmListMetaReply(BaseModel): + + cpu_current_usage: int + cpu_allocated: int + disk_current_usage: int + disk_read: int + disk_write: int + disk_max: int + ram_current_usage: int + ram_max: int + net_in: int + net_out: int + + +class VmListInfoReply(BaseModel): + + action: VmListActionEnum + source: str = Field("proxmox", description="data source provider") + proxmox_node: str + vm_name: str + vm_status: VmListStatusEnum + vm_id: int + vm_uptime: int + vm_meta: VmListMetaReply + + +class VmListReply(BaseModel): + + rc: int = Field(..., description="RETURN CODE (0 = OK) ") + result: List[List[ VmListInfoReply]] + + +# --------------------------------------------------------------------------- +# VM List Usage +# --------------------------------------------------------------------------- + +class VmListUsageRequest(BaseModel): + + proxmox_node: str = Field( + ..., + # default= "px-testing", + description = "Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$" + ) + + as_json: bool = Field( + default=True, + description="If true : JSON output else : raw output" + ) + + model_config = { + "json_schema_extra": { + "example": { + "proxmox_node": "px-testing", + "as_json": True + } + } + } + + +class VmListUsageItemReply(BaseModel): + + action: Literal["vm_list_usage"] + source: Literal["proxmox"] + proxmox_node: str + vm_id: int = Field(..., ge=1) + vm_name: str + # raw_data: str = Field(..., description="Raw string returned by proxmox") + cpu_allocated: int + cpu_current_usage: int + disk_current_usage: int + disk_max: int + disk_read: int + disk_write: int + net_in: int + net_out: int + ram_current_usage: int + ram_max: int + vm_status: str + vm_uptime: int + + +class VmListUsageReply(BaseModel): + + rc: int = Field(0, description="RETURN code (0 = OK)") + result: list[VmListUsageItemReply] + + model_config = { + "json_schema_extra": { + "example": { + "rc": 0, + "result": [ + { + "cpu_allocated":1, + "cpu_current_usage":0, + "disk_current_usage":0, + "disk_max":34359738368, + "disk_read":0, + "disk_write":0, + "net_in":280531583, + "net_out":6330590, + "ram_current_usage":1910544625, + "ram_max":4294967296, + "vm_id":1020, + "vm_name":"admin-web-api-kong", + "vm_status":"running", + "vm_uptime":79940 + } + ] + } + } + } + + +# --------------------------------------------------------------------------- +# VM Create +# --------------------------------------------------------------------------- + +class VmCreateRequest(BaseModel): + + proxmox_node: str = Field( + ..., + # default= "px-testing", + description = "Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$" + ) + + as_json: bool = Field( + default=True, + description="If true : JSON output else : raw output" + ) + # + + vm_id: str = Field( + ..., + # default="4000", + description="Virtual machine id", + pattern=r"^[0-9]+$" + ) + + vm_name: str = Field( + ..., + # default="new-vm", + description="Virtual machine meta name", + pattern = r"^[A-Za-z0-9-]*$" + ) + + vm_cpu: str = Field( + ..., + # default= "host", + description='CPU type/model - host)', + pattern=r"^[A-Za-z0-9._-]+$" + ) + + vm_cores: int = Field( + ..., + # default=1, + ge=1, + description="Number of cores per socket" + ) + + vm_sockets: int = Field( + ..., + # default=1, + ge=1, + description="Number of CPU sockets" + ) + + vm_memory: int = Field( + ..., + # default=1024, + ge=128, + description="Memory in MiB" + ) + + vm_disk_size: int | None = Field( + default=None, + ge=1, + description="Disk size in GiB - optional" + ) + + vm_iso: str | None = Field( + default=None, + description="ISO volume path like 'local:iso/xxx.iso' - optional", + pattern=r"^[A-Za-z0-9._-]+:iso/.+\.iso$" + ) + + model_config = { + "json_schema_extra": { + "example": { + "proxmox_node": "px-testing", + "vm_id": "1111", + "vm_name": "new-vm", + "vm_cpu": "host", + "vm_cores": 2, + "vm_sockets": 1, + "vm_memory": 2042, + "vm_disk_size": 42, + "vm_iso": "local:iso/ubuntu-24.04.2-live-server-amd64.iso" + } + } + } + + +class VmCreateItemReply(BaseModel): + + action: Literal["vm_create"] + source: Literal["proxmox"] + proxmox_node: str + vm_id : int = Field(..., ge=1) + vm_name : str + vm_cpu : str + vm_cores : int = Field(..., ge=1) + vm_sockets: int = Field(..., ge=1) + vm_memory : int = Field(..., ge=1) + vm_net0 : str + vm_scsi0 : str + raw_data : str = Field(..., description="Raw string returned by Proxmox") + + +class VmCreateReply(BaseModel): + + rc: int = Field(0, description="RETURN code (0 = OK)") + result: list[VmCreateItemReply] + + model_config = { + "json_schema_extra": { + "example": { + "rc": 0, + "result": [ + { + "action": "vm_create", + "proxmox_node": "px-testing", + "raw_data": "UPID:px-testing:00281144:16855865:68C04C13:qmcreate:9998:API_master@pam!API_master:", + "source": "proxmox", + "vm_cores": 2, + "vm_cpu": "host", + "vm_id": 9998, + "vm_memory": 2042, + "vm_name": "vm-with-local-iso-2", + "vm_net0": "virtio,bridge=vmbr0", + "vm_scsi0": "local-lvm:42,format=raw", + "vm_sockets": 1 + } + ] + } + } + } + + +# --------------------------------------------------------------------------- +# VM Delete +# --------------------------------------------------------------------------- + +class VmDeleteRequest(BaseModel): + + proxmox_node: str = Field( + ..., + # default= "px-testing", + description = "Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$" + ) + + as_json: bool = Field( + default=True, + description="If true : JSON output else : raw output" + ) + # + + vm_id: str = Field( + ..., + # default="4000", + description="Virtual machine id", + pattern=r"^[0-9]+$" + ) + + model_config = { + "json_schema_extra": { + "example": { + "proxmox_node": "px-testing", + "vm_id": "1111", + "as_json": True, + } + } + } + + +class VmDeleteItemReply(BaseModel): + + action: Literal["vm_delete"] + source: Literal["proxmox"] + proxmox_node: str + + vm_id: int = Field(..., ge=1) + vm_name: str + raw_data: str = Field(..., description="Raw string returned by proxmox") + +class VmDeleteReply(BaseModel): + + rc: int = Field(0, description="RETURN code (0 = OK)") + result: list[VmDeleteItemReply] + + model_config = { + "json_schema_extra": { + "example": { + "rc": 0, + "result": [ + { + "action": "vm_delete", + "source": "proxmox", + "proxmox_node": "px-testing", + "vm_id": 1023, + "vm_name": "admin-web-deployer-ui", + "raw_data": "UPID:px-testing:123123:1123D4:68BFF2C7:qmdestroy:1023:API_master@pam!API_master:" + } + ] + } + } + } + + +# --------------------------------------------------------------------------- +# VM Clone +# --------------------------------------------------------------------------- + +class VmCloneRequest(BaseModel): + + proxmox_node: str = Field( + ..., + # default= "px-testing", + description = "Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$" + ) + + as_json: bool = Field( + default=True, + description="If true : JSON output else : raw output" + ) + # + + vm_id: str = Field( + ..., + # default="4000", + description="Virtual machine id", + pattern=r"^[0-9]+$" + ) + + vm_new_id: str = Field( + ..., + # default="5005", + description="New virtual machine id", + pattern=r"^[0-9]+$" + ) + + vm_description: str | None = Field( + default="cloned-vm", + description="Virtual machine meta description field", + pattern = r"^[A-Za-z0-9\s.,_\-]*$" + ) + + vm_name: str = Field( + ..., + # default="new-vm", + description="Virtual machine meta name", + pattern = r"^[A-Za-z0-9-]*$" + ) + + model_config = { + "json_schema_extra": { + "example": { + "proxmox_node": "px-testing", + "vm_id": "2000", + "vm_new_id": "3000", + "vm_name":"test-cloned", + "vm_description":"my description" + } + } + } + + +class VmCloneItemReply(BaseModel): + + action: Literal["vm_clone"] + source: Literal["proxmox"] + proxmox_node: str + vm_id: int = Field(..., ge=1) + vm_id_clone_from: int = Field(..., ge=1) + vm_name: str + vm_description: str + raw_info: str = Field(..., description="Raw string returned by proxmox") + + +class VmCloneReply(BaseModel): + + rc: int = Field(0, description="RETURN code (0 = OK)") + result: list[VmCloneItemReply] + + model_config = { + "json_schema_extra": { + "example": { + "rc": 0, + "result": [ + { + "action": "vm_clone", + "proxmox_node": "px-testing", + "raw_info": { + "data": "UPID:px-testing:0027CE9B:167F1A2C:68C03C17:qmclone:4004:API_master@pam!API_master:"}, + "source": "proxmox", + "vm_description": "my description", + "vm_id": "5004", + "vm_id_clone_from": "4004", + "vm_name": "test-cloned" + } + ] + } + } + } + + +# --------------------------------------------------------------------------- +# VM Action (Start / Stop / Resume / Pause) +# --------------------------------------------------------------------------- + +class VmActionRequest(BaseModel): + + proxmox_node: str = Field( + ..., + # default= "px-testing", + description = "Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$" + ) + + as_json: bool = Field( + default=True, + description="If true : JSON output else : raw output" + ) + # + + vm_id: str = Field( + ..., + # default="1000", + description="Virtual machine id", + pattern=r"^[0-9]+$" + ) + + + model_config = { + "json_schema_extra": { + "example": { + "proxmox_node": "px-testing", + "vm_id": "2000", + } + } + } + + +class VmActionItemReply(BaseModel): + + action: Literal["vm_start", "vm_stop", "vm_resume", "vm_pause", "vm_stop_force" ] + source: Literal["proxmox"] + + proxmox_node: str + vm_id : str # int = Field(..., ge=1) + vm_name : str + + +class VmActionReply(BaseModel): + + rc: int = Field(0, description="RETURN code (0 = OK)") + result: list[VmActionItemReply] + + model_config = { + "json_schema_extra": { + "example": { + "rc": 0, + "result": [ + + { + "action": "vm_delete", + "source": "proxmox", + "proxmox_node": "px-testing", + "vm_id": 4001, + "vm_name": "vuln-box-01", + "raw_data": { + "data": "UPID:px-testing:0033649C:1D2619CC:68D143C5:qmdestroy:4001:API_master@pam!API_master:" + } + }, + { + "action": "vm_delete", + "source": "proxmox", + "proxmox_node": "px-testing", + "vm_id": 4002, + "vm_name": "vuln-box-02", + "raw_data": { + "data": "UPID:px-testing:003364A6:1D261A84:68D143C6:qmdestroy:4002:API_master@pam!API_master:" + } + }, + ] + } + } + } + + +# --------------------------------------------------------------------------- +# Mass Delete +# --------------------------------------------------------------------------- + +class MassDeleteVmItem(BaseModel): + + id: str = Field( + ..., + description="Virtual machine id", + pattern=r"^[0-9]+$" + ) + + name: str = Field( + ..., + description="Virtual machine meta name", + pattern="^[A-Za-z0-9-]+$" # deny void name + # pattern=r"^[A-Za-z0-9-]*$", + ) + + +class MassDeleteRequest(BaseModel): + + proxmox_node: str = Field( + ..., + # default= "px-testing", + description = "Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$" + ) + + as_json: bool = Field( + default=True, + description="If true : JSON output else : raw output" + ) + + vms: List[MassDeleteVmItem] = Field( + ..., + description="List of virtual machine (vm_id + vm_name)", + min_length=1, + ) + + model_config = { + "json_schema_extra": { + "example": { + "proxmox_node": "px-testing", + "as_json": True, + "vms": [ + {"id":"4000", "name":"vuln-box-00"}, + {"id":"4001", "name":"vuln-box-01"}, + {"id":"4002", "name":"vuln-box-02"}, + ], + } + } + } + + +class MassDeleteItemReply(BaseModel): + + action: Literal["vm_start", "vm_stop", "vm_resume", "vm_pause", "vm_stop_force" ] + source: Literal["proxmox"] + + proxmox_node: str + vm_id : str # int = Field(..., ge=1) + # vm_new_id : str # int = Field(..., ge=1) + vm_name : str + vm_status: Literal["running", "stopped", "paused"] + +class MassDeleteReply(BaseModel): + + rc: int = Field(0, description="RETURN code (0 = OK)") + result: list[MassDeleteItemReply] + + +# --------------------------------------------------------------------------- +# Mass Start / Stop / Resume / Pause +# --------------------------------------------------------------------------- + +class MassActionRequest(BaseModel): + + proxmox_node: str = Field( + ..., + # default= "px-testing", + description = "Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$" + ) + + as_json: bool = Field( + default=True, + description="If true : JSON output else : raw output" + ) + # + + vm_ids: List[str] = Field( + ..., + # default="1000", + description="Virtual machine id", + min_items=1, + # pattern=r"^[0-9]+$" + ) + + + model_config = { + "json_schema_extra": { + "example": { + "proxmox_node": "px-testing", + "as_json": True, + "vm_ids": ["4000", "4001"], + } + } + } + + +# --------------------------------------------------------------------------- +# Backward compatibility -- old names used by current routes +# --------------------------------------------------------------------------- + +# vm_list.py +Request_ProxmoxVms_VmList = VmListRequest +Reply_ProxmoxVms_VmList = VmListActionEnum +Reply_ProxmoxVmList_VmStatus = VmListStatusEnum +Reply_ProxmoxVmList_VmMeta = VmListMetaReply +Reply_ProxmoxVmList_VmInfo = VmListInfoReply +Reply_ProxmoxVmList = VmListReply + +# vm_list_usage.py +Request_ProxmoxVms_VmListUsage = VmListUsageRequest +Reply_ProxmoxVms_VmListUsageItem = VmListUsageItemReply +Reply_ProxmoxVms_VmListUsage = VmListUsageReply + +# vm_id/create.py +Request_ProxmoxVmsVMID_Create = VmCreateRequest +Reply_ProxmoxVmsVMID_CreateItem = VmCreateItemReply +Reply_ProxmoxVmsVMID_Create = VmCreateReply + +# vm_id/delete.py +Request_ProxmoxVmsVMID_Delete = VmDeleteRequest +Reply_ProxmoxVmsVMID_DeleteItem = VmDeleteItemReply +Reply_ProxmoxVmsVMID_Delete = VmDeleteReply + +# vm_id/clone.py +Request_ProxmoxVmsVMID_Clone = VmCloneRequest +Reply_ProxmoxVmsVMID_CloneItem = VmCloneItemReply +Reply_ProxmoxVmsVMID_Clone = VmCloneReply + +# vm_id/start_stop_resume_pause.py +Request_ProxmoxVmsVMID_StartStopPauseResume = VmActionRequest +Reply_ProxmoxVmsVMID_StartStopPauseResumeItem = VmActionItemReply +Reply_ProxmoxVmsVMID_StartStopPauseResume = VmActionReply + +# vm_ids/mass_delete.py +vm = MassDeleteVmItem +Request_ProxmoxVmsVmIds_MassDelete = MassDeleteRequest +Reply_ProxmoxVmsVMID_MasseDeleteItem = MassDeleteItemReply +Reply_ProxmoxVmsVmIds_MassDelete = MassDeleteReply + +# vm_ids/mass_start_stop_resume_pause.py +Request_ProxmoxVmsVmIds_MassStartStopPauseResume = MassActionRequest diff --git a/tests/test_schemas.py b/tests/test_schemas.py new file mode 100644 index 0000000..eb33e81 --- /dev/null +++ b/tests/test_schemas.py @@ -0,0 +1,369 @@ +"""Tests for consolidated schema modules — validates fields, patterns, and backward-compat aliases.""" + +from pydantic import ValidationError +import pytest + + +# =========================================================================== +# vms.py +# =========================================================================== + +def test_vm_list_request(): + from app.schemas.vms import VmListRequest + req = VmListRequest(proxmox_node="px-testing") + assert req.proxmox_node == "px-testing" + assert req.as_json is True + + +def test_vm_action_request_validates_vm_id(): + from app.schemas.vms import VmActionRequest + req = VmActionRequest(proxmox_node="px-testing", vm_id="100") + assert req.vm_id == "100" + + +def test_vm_action_request_rejects_invalid_vm_id(): + from app.schemas.vms import VmActionRequest + with pytest.raises(ValidationError): + VmActionRequest(proxmox_node="px-testing", vm_id="abc") + + +def test_vm_create_request(): + from app.schemas.vms import VmCreateRequest + req = VmCreateRequest( + proxmox_node="px-testing", + vm_id="200", + vm_name="test-vm", + vm_cpu="host", + vm_cores=2, + vm_sockets=1, + vm_memory=2048, + ) + assert req.vm_name == "test-vm" + + +def test_vm_create_request_rejects_low_memory(): + from app.schemas.vms import VmCreateRequest + with pytest.raises(ValidationError): + VmCreateRequest( + proxmox_node="px-testing", + vm_id="200", + vm_name="test-vm", + vm_cpu="host", + vm_cores=2, + vm_sockets=1, + vm_memory=64, # below ge=128 + ) + + +def test_proxmox_node_rejects_special_chars(): + from app.schemas.vms import VmListRequest + with pytest.raises(ValidationError): + VmListRequest(proxmox_node="node; rm -rf /") + + +def test_vm_delete_request(): + from app.schemas.vms import VmDeleteRequest + req = VmDeleteRequest(proxmox_node="px-testing", vm_id="1111") + assert req.vm_id == "1111" + + +def test_vm_clone_request(): + from app.schemas.vms import VmCloneRequest + req = VmCloneRequest( + proxmox_node="px-testing", + vm_id="2000", + vm_new_id="3000", + vm_name="test-cloned", + ) + assert req.vm_new_id == "3000" + assert req.vm_description == "cloned-vm" # default + + +def test_mass_delete_request(): + from app.schemas.vms import MassDeleteRequest, MassDeleteVmItem + req = MassDeleteRequest( + proxmox_node="px-testing", + vms=[MassDeleteVmItem(id="4000", name="vuln-box-00")], + ) + assert len(req.vms) == 1 + + +def test_mass_delete_request_rejects_empty_vms(): + from app.schemas.vms import MassDeleteRequest + with pytest.raises(ValidationError): + MassDeleteRequest(proxmox_node="px-testing", vms=[]) + + +# =========================================================================== +# vm_config.py +# =========================================================================== + +def test_vm_get_config_request(): + from app.schemas.vm_config import VmGetConfigRequest + req = VmGetConfigRequest(proxmox_node="px-testing", vm_id="1000") + assert req.as_json is True + + +def test_vm_set_tag_request(): + from app.schemas.vm_config import VmSetTagRequest + req = VmSetTagRequest( + proxmox_node="px-testing", + vm_id="1111", + vm_tag_name="group_01,group_02", + ) + assert "group_01" in req.vm_tag_name + + +def test_vm_set_tag_rejects_bad_pattern(): + from app.schemas.vm_config import VmSetTagRequest + with pytest.raises(ValidationError): + VmSetTagRequest( + proxmox_node="px-testing", + vm_id="1111", + vm_tag_name="tag;evil", + ) + + +# =========================================================================== +# snapshots.py +# =========================================================================== + +def test_snapshot_create_request(): + from app.schemas.snapshots import SnapshotCreateRequest + req = SnapshotCreateRequest( + proxmox_node="px-testing", + vm_id="1000", + vm_snapshot_name="MY_SNAP", + ) + assert req.vm_snapshot_name == "MY_SNAP" + + +def test_snapshot_list_request(): + from app.schemas.snapshots import SnapshotListRequest + req = SnapshotListRequest(proxmox_node="px-testing", vm_id="1000") + assert req.vm_id == "1000" + + +# =========================================================================== +# firewall.py +# =========================================================================== + +def test_firewall_rule_request(): + from app.schemas.firewall import FirewallRuleApplyRequest + req = FirewallRuleApplyRequest( + proxmox_node="px-testing", + vm_id="100", + vm_fw_action="ACCEPT", + vm_fw_type="in", + vm_fw_proto="tcp", + vm_fw_dport="22", + vm_fw_enable=1, + ) + assert req.vm_fw_action == "ACCEPT" + + +def test_firewall_rule_rejects_invalid_action(): + from app.schemas.firewall import FirewallRuleApplyRequest + with pytest.raises(ValidationError): + FirewallRuleApplyRequest( + proxmox_node="px-testing", + vm_id="100", + vm_fw_action="ALLOW", # invalid — must be ACCEPT|DROP|REJECT + vm_fw_type="in", + vm_fw_proto="tcp", + vm_fw_dport="22", + vm_fw_enable=1, + ) + + +def test_firewall_alias_add_request(): + from app.schemas.firewall import FirewallAliasAddRequest + req = FirewallAliasAddRequest( + proxmox_node="px-testing", + vm_id="1000", + vm_fw_alias_name="test", + vm_fw_alias_cidr="192.168.123.0/24", + vm_fw_alias_comment="this_comment", + ) + assert req.vm_fw_alias_cidr == "192.168.123.0/24" + + +# =========================================================================== +# network.py +# =========================================================================== + +def test_node_network_list_request(): + from app.schemas.network import NodeNetworkListRequest + req = NodeNetworkListRequest(proxmox_node="px-testing") + assert req.as_json is True + + +def test_vm_network_list_request(): + from app.schemas.network import VmNetworkListRequest + req = VmNetworkListRequest(proxmox_node="px-testing", vm_id="1001") + assert req.vm_id == "1001" + + +# =========================================================================== +# storage.py +# =========================================================================== + +def test_storage_list_request(): + from app.schemas.storage import StorageListRequest + req = StorageListRequest( + proxmox_node="px-testing", + storage_name="local", + ) + assert req.storage_name == "local" + + +def test_storage_download_iso_request(): + from app.schemas.storage import StorageDownloadIsoRequest + req = StorageDownloadIsoRequest( + proxmox_node="px-testing", + proxmox_storage="local", + iso_file_content_type="iso", + iso_file_name="ubuntu-24.04-live-server-amd64.iso", + iso_url="https://releases.ubuntu.com/24.04/ubuntu-24.04-live-server-amd64.iso", + ) + assert req.iso_file_name.endswith(".iso") + + +# =========================================================================== +# bundles.py +# =========================================================================== + +def test_bundle_add_user_request(): + from app.schemas.bundles import BundleAddUserRequest + req = BundleAddUserRequest( + proxmox_node="px-testing", + hosts="r42.vuln-box-00", + user="elliot", + password="r0b0t_aLd3rs0n", + change_pwd_at_logon=False, + shell_path="/bin/sh", + ) + assert req.user == "elliot" + + +def test_bundle_create_admin_vms_request(): + from app.schemas.bundles import BundleCreateAdminVmsRequest, BundleCreateAdminVmsItemRequest + req = BundleCreateAdminVmsRequest( + proxmox_node="px-testing", + vms={ + "admin-wazuh": BundleCreateAdminVmsItemRequest( + vm_id=1000, + vm_ip="192.168.42.100", + vm_description="Wazuh - dashboard", + ) + }, + ) + assert "admin-wazuh" in req.vms + assert req.vms["admin-wazuh"].vm_id == 1000 + + +# =========================================================================== +# debug.py +# =========================================================================== + +def test_debug_ping_request(): + from app.schemas.debug import DebugPingRequest + req = DebugPingRequest(proxmox_node="px-testing", hosts="all") + assert req.as_json is False # default + + +# =========================================================================== +# base.py — ProxmoxBaseRequest +# =========================================================================== + +def test_proxmox_base_request(): + from app.schemas.base import ProxmoxBaseRequest + req = ProxmoxBaseRequest(proxmox_node="px-testing") + assert req.as_json is True + + +def test_proxmox_base_request_rejects_bad_node(): + from app.schemas.base import ProxmoxBaseRequest + with pytest.raises(ValidationError): + ProxmoxBaseRequest(proxmox_node="node; rm -rf /") + + +# =========================================================================== +# Backward-compatibility aliases +# =========================================================================== + +def test_backward_compat_aliases_vms(): + """Old class names must still be importable.""" + from app.schemas.vms import Request_ProxmoxVms_VmList, VmListRequest + assert Request_ProxmoxVms_VmList is VmListRequest + + from app.schemas.vms import Reply_ProxmoxVmList, VmListReply + assert Reply_ProxmoxVmList is VmListReply + + from app.schemas.vms import Request_ProxmoxVmsVMID_Create, VmCreateRequest + assert Request_ProxmoxVmsVMID_Create is VmCreateRequest + + from app.schemas.vms import Request_ProxmoxVmsVMID_StartStopPauseResume, VmActionRequest + assert Request_ProxmoxVmsVMID_StartStopPauseResume is VmActionRequest + + from app.schemas.vms import Request_ProxmoxVmsVmIds_MassDelete, MassDeleteRequest + assert Request_ProxmoxVmsVmIds_MassDelete is MassDeleteRequest + + +def test_backward_compat_aliases_vm_config(): + from app.schemas.vm_config import Request_ProxmoxVmsVMID_VmGetConfig, VmGetConfigRequest + assert Request_ProxmoxVmsVMID_VmGetConfig is VmGetConfigRequest + + from app.schemas.vm_config import Request_ProxmoxVmsVMID_VmSetTag, VmSetTagRequest + assert Request_ProxmoxVmsVMID_VmSetTag is VmSetTagRequest + + +def test_backward_compat_aliases_snapshots(): + from app.schemas.snapshots import Request_ProxmoxVmsVMID_CreateSnapshot, SnapshotCreateRequest + assert Request_ProxmoxVmsVMID_CreateSnapshot is SnapshotCreateRequest + + from app.schemas.snapshots import Request_ProxmoxVmsVMID_RevertSnapshot, SnapshotRevertRequest + assert Request_ProxmoxVmsVMID_RevertSnapshot is SnapshotRevertRequest + + +def test_backward_compat_aliases_firewall(): + from app.schemas.firewall import Request_ProxmoxFirewall_ApplyIptablesRules, FirewallRuleApplyRequest + assert Request_ProxmoxFirewall_ApplyIptablesRules is FirewallRuleApplyRequest + + from app.schemas.firewall import Request_ProxmoxFirewall_EnableFirewallVm, FirewallEnableVmRequest + assert Request_ProxmoxFirewall_EnableFirewallVm is FirewallEnableVmRequest + + +def test_backward_compat_aliases_network(): + from app.schemas.network import Request_ProxmoxNetwork_WithNodeName_AddNetworkInterface, NodeNetworkAddRequest + assert Request_ProxmoxNetwork_WithNodeName_AddNetworkInterface is NodeNetworkAddRequest + + from app.schemas.network import Request_ProxmoxNetwork_WithVmId_ListNetwork, VmNetworkListRequest + assert Request_ProxmoxNetwork_WithVmId_ListNetwork is VmNetworkListRequest + + +def test_backward_compat_aliases_storage(): + from app.schemas.storage import Request_ProxmoxStorage_List, StorageListRequest + assert Request_ProxmoxStorage_List is StorageListRequest + + from app.schemas.storage import Request_ProxmoxStorage_ListIso, StorageListIsoRequest + assert Request_ProxmoxStorage_ListIso is StorageListIsoRequest + + +def test_backward_compat_aliases_bundles(): + from app.schemas.bundles import Request_BundlesCoreLinuxUbuntuConfigure_AddUser, BundleAddUserRequest + assert Request_BundlesCoreLinuxUbuntuConfigure_AddUser is BundleAddUserRequest + + from app.schemas.bundles import ( + Request_BundlesCoreProxmoxConfigureDefaultVms_CreateAdminVms, + BundleCreateAdminVmsRequest, + ) + assert Request_BundlesCoreProxmoxConfigureDefaultVms_CreateAdminVms is BundleCreateAdminVmsRequest + + +def test_backward_compat_aliases_debug(): + from app.schemas.debug import Request_DebugPing, DebugPingRequest + assert Request_DebugPing is DebugPingRequest + + from app.schemas.debug import Reply_DebugPing, DebugPingReply + assert Reply_DebugPing is DebugPingReply From 8415198551b1a14e7cfc87db78f97e7f4f02a787 Mon Sep 17 00:00:00 2001 From: Philippe Parage Date: Thu, 19 Mar 2026 15:20:27 +0100 Subject: [PATCH 11/33] refactor: consolidate 86 route files into 10 domain modules (#53) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the deeply nested app/routes/v0/ directory tree (86 files) with 10 flat domain-grouped modules under app/routes/: - vms.py — VM list, lifecycle (start/stop/pause/resume), management (create/delete/clone), mass operations - vm_config.py — VM configuration (get config, cdrom, cpu, ram, set tag) - snapshots.py — Snapshot CRUD (list, create, delete, revert) - firewall.py — Firewall aliases, rules, VM/node/DC enable/disable - network.py — VM and node network interface management - storage.py — Storage list, ISO download, ISO/template listing - bundles.py — Core Ubuntu bundles + Proxmox create/start/stop/delete/snapshot bundles - runner.py — Dynamic bundle and scenario runner - debug.py — Ping and test function endpoints - ws_status.py — WebSocket VM status (unchanged) Each domain file uses shared helpers to eliminate the duplicated request_checks/reply_processing/run pattern. The __init__.py wires all routers with their original prefixes. All 80 route paths, HTTP methods, tags, and response models are preserved exactly as before — verified by the golden reference test. --- app/routes/__init__.py | 72 ++++-- app/routes/bundles.py | 438 ++++++++++++++++++++++++++++++++ app/routes/debug.py | 67 +++++ app/routes/firewall.py | 169 ++++++++++++ app/routes/network.py | 103 ++++++++ app/routes/runner.py | 62 +++++ app/routes/snapshots.py | 128 ++++++++++ app/routes/storage.py | 86 +++++++ app/routes/vm_config.py | 129 ++++++++++ app/routes/vms.py | 377 +++++++++++++++++++++++++++ tests/test_routes_registered.py | 27 ++ 11 files changed, 1639 insertions(+), 19 deletions(-) create mode 100644 app/routes/bundles.py create mode 100644 app/routes/debug.py create mode 100644 app/routes/firewall.py create mode 100644 app/routes/network.py create mode 100644 app/routes/runner.py create mode 100644 app/routes/snapshots.py create mode 100644 app/routes/storage.py create mode 100644 app/routes/vm_config.py create mode 100644 app/routes/vms.py create mode 100644 tests/test_routes_registered.py diff --git a/app/routes/__init__.py b/app/routes/__init__.py index 3635a38..5c9f9d5 100644 --- a/app/routes/__init__.py +++ b/app/routes/__init__.py @@ -1,32 +1,66 @@ - from fastapi import APIRouter -# /v0/admin/proxmox/* -from app.routes.admin_proxmox import router as admin_proxmox_routers +from app.routes.vms import vms_router, vm_id_router, vm_ids_router +from app.routes.vm_config import router as vm_config_router +from app.routes.snapshots import router as snapshots_router +from app.routes.firewall import router as firewall_router +from app.routes.network import router as network_router +from app.routes.storage import storage_router, storage_name_router +from app.routes.bundles import router as bundles_router +from app.routes.runner import run_bundle, run_scenario +from app.routes.debug import router as debug_router + +router = APIRouter() # /v0/admin/debug/* -from app.routes.admin_debug import router as admin_debug_routers +router.include_router(debug_router, prefix="/v0/admin/debug") -# /v0/admin/run/* -from app.routes.admin_run import router as admin_run_routers +# /v0/admin/run/bundles/core/* (bundle-specific routes: ubuntu install, proxmox create, etc.) +router.include_router(bundles_router, prefix="/v0/admin/run/bundles") -# /v0/admin/run/actions/core/* -from app.routes.admin_run_bundles_core import router as admin_run_bundles_core_routers +# /v0/admin/run/bundles/{name}/run +_bundles_runner = APIRouter() +_bundles_runner.add_api_route( + "/{bundles_name}/run", run_bundle, methods=["POST"], + summary="Run bundles", + description="Run generic bundles with default (and static) extras_vars ", + tags=["runner"], +) +router.include_router(_bundles_runner, prefix="/v0/admin/run/bundles") -####################################################################################################################### +# /v0/admin/run/scenarios/{name}/run +_scenarios_runner = APIRouter() +_scenarios_runner.add_api_route( + "/{scenario_name}/run", run_scenario, methods=["POST"], + summary="Run scenario", + description="Run generic scenario with default (and static) extras_vars ", + tags=["runner"], +) +router.include_router(_scenarios_runner, prefix="/v0/admin/run/scenarios") -router = APIRouter() +# /v0/admin/proxmox/vms +router.include_router(vms_router, prefix="/v0/admin/proxmox/vms") -# /v0/admin/debug/* -router.include_router(admin_debug_routers) +# /v0/admin/proxmox/vms/vm_id +router.include_router(vm_id_router, prefix="/v0/admin/proxmox/vms/vm_id") + +# /v0/admin/proxmox/vms/vm_ids +router.include_router(vm_ids_router, prefix="/v0/admin/proxmox/vms/vm_ids") + +# /v0/admin/proxmox/vms/vm_id/config +router.include_router(vm_config_router, prefix="/v0/admin/proxmox/vms/vm_id/config") + +# /v0/admin/proxmox/vms/vm_id/snapshot +router.include_router(snapshots_router, prefix="/v0/admin/proxmox/vms/vm_id/snapshot") -# /v0/admin/run/actions/core/* -router.include_router(admin_run_bundles_core_routers) +# /v0/admin/proxmox/storage/storage_name +router.include_router(storage_name_router, prefix="/v0/admin/proxmox/storage/storage_name") -# /v0/run/actions|scenarios/run* -router.include_router(admin_run_routers) +# /v0/admin/proxmox/storage +router.include_router(storage_router, prefix="/v0/admin/proxmox/storage") -# /v0/admin/proxmox/* -router.include_router(admin_proxmox_routers) +# /v0/admin/proxmox/firewall +router.include_router(firewall_router, prefix="/v0/admin/proxmox/firewall") -#### +# /v0/admin/proxmox/network +router.include_router(network_router, prefix="/v0/admin/proxmox/network") diff --git a/app/routes/bundles.py b/app/routes/bundles.py new file mode 100644 index 0000000..06fa18a --- /dev/null +++ b/app/routes/bundles.py @@ -0,0 +1,438 @@ +"""Consolidated bundle routes. + +Replaces: app/routes/v0/admin/bundles/core/linux/ubuntu/**/*.py + app/routes/v0/admin/bundles/proxmox/configure/default/vms/*.py +""" + +import logging +import os +from pathlib import Path +from typing import Any + +from fastapi import APIRouter, HTTPException +from fastapi.responses import JSONResponse + +from app.runner import run_playbook_core +from app.extract_actions import extract_action_results +from app import utils + +# --- Linux/Ubuntu bundle schemas --- +from app.schemas.bundles.core.linux.ubuntu.install.docker import Request_BundlesCoreLinuxUbuntuInstall_Docker, Reply_BundlesCoreLinuxUbuntuInstall_Docker +from app.schemas.bundles.core.linux.ubuntu.install.docker_compose import Request_BundlesCoreLinuxUbuntuInstall_DockerCompose, Reply_BundlesCoreLinuxUbuntuInstall_DockerCompose +from app.schemas.bundles.core.linux.ubuntu.install.basic_packages import Request_BundlesCoreLinuxUbuntuInstall_BasicPackages, Reply_BundlesCoreLinuxUbuntuInstall_BasicPackages +from app.schemas.bundles.core.linux.ubuntu.install.dot_files import Request_BundlesCoreLinuxUbuntuInstall_DotFiles, Reply_BundlesCoreLinuxUbuntuInstall_DotFilesItem +from app.schemas.bundles.core.linux.ubuntu.configure.add_user import Request_BundlesCoreLinuxUbuntuConfigure_AddUser, Reply_BundlesCoreLinuxUbuntuConfigure_AddUser + +# --- Proxmox bundle schemas --- +from app.schemas.bundles.core.proxmox.configure.default.vms.create_vms_admin_default import Request_BundlesCoreProxmoxConfigureDefaultVms_CreateAdminVms, Reply_BundlesCoreProxmoxConfigureDefaultVms_CreateAdminVms +from app.schemas.bundles.core.proxmox.configure.default.vms.create_vms_vuln_default import Request_BundlesCoreProxmoxConfigureDefaultVms_CreateVulnVms, Reply_BundlesCoreProxmoxConfigureDefaultVms_CreateVulnVms +from app.schemas.bundles.core.proxmox.configure.default.vms.create_vms_student_default import Request_BundlesCoreProxmoxConfigureDefaultVms_CreateStudentVms, Reply_BundlesCoreProxmoxConfigureDefaultVms_CreateStudentVms +from app.schemas.bundles.core.proxmox.configure.default.vms.start_stop_resume_pause_default import Request_BundlesCoreProxmoxConfigureDefaultVms_StartStopPauseResumeAdminVulnStudentVms +from app.schemas.bundles.core.proxmox.configure.default.vms.revert_snapshot_default import Request_BundlesCoreProxmoxConfigureDefaultVms_RevertSnapshotAdminVulnStudentVms +from app.schemas.proxmox.vm_id.start_stop_resume_pause import Reply_ProxmoxVmsVMID_StartStopPauseResume +from app.schemas.proxmox.vm_id.snapshot.vm_create import Reply_ProxmoxVmsVMID_CreateSnapshot +from app.schemas.proxmox.vm_id.snapshot.vm_revert import Reply_ProxmoxVmsVMID_RevertSnapshot + +logger = logging.getLogger(__name__) + +PROJECT_ROOT = Path(os.getenv("PROJECT_ROOT_DIR")).resolve() +INVENTORY_NAME = "hosts" + +router = APIRouter() + + +# --------------------------------------------------------------------------- +# Pattern B helpers: simple playbook run (Ubuntu bundles) +# --------------------------------------------------------------------------- + +def _run_bundle_simple(req, action_name: str, extravars: dict) -> JSONResponse: + """Run a single bundle playbook (Pattern B).""" + checked_inventory = utils.resolve_inventory(INVENTORY_NAME) + checked_playbook = utils.resolve_bundles_playbook(action_name, "public_github") + + rc, events, log_plain, _ = run_playbook_core( + checked_playbook, checked_inventory, limit=req.hosts, extravars=extravars, + ) + + payload = {"rc": rc, "log_multiline": log_plain.splitlines()} + return JSONResponse(payload, status_code=200 if rc == 0 else 500) + + +# --------------------------------------------------------------------------- +# Pattern C helpers: multi-step Proxmox bundles +# --------------------------------------------------------------------------- + +def _run_create_vms_bundle(req, action_name: str, request_checks_fn) -> JSONResponse: + """Multi-step create VMs: init.yml per VM, then main.yml (Pattern C).""" + checked_inventory = utils.resolve_inventory(INVENTORY_NAME) + checked_playbook_init = utils.resolve_bundles_playbook_init_file(action_name, "public_github") + + request_checks_fn(req) + + # Phase 1: run init.yml for each VM + for vm_key, item in req.vms.items(): + extravars = { + "proxmox_node": req.proxmox_node, + "global_vm_id": item.vm_id, + "global_vm_ci_ip": str(item.vm_ip), + "global_vm_description": item.vm_description, + } + rc, events, log_plain, _ = run_playbook_core( + checked_playbook_init, checked_inventory, tags=vm_key, extravars=extravars, + ) + if rc != 0: + payload = {"rc": rc, "log_multiline": log_plain.splitlines()} + return JSONResponse(payload, status_code=500) + + # Phase 2: run main.yml + checked_playbook_main = utils.resolve_bundles_playbook(action_name, "public_github") + extravars = {"proxmox_node": req.proxmox_node} + rc, events, log_plain, _ = run_playbook_core( + checked_playbook_main, checked_inventory, extravars=extravars, + ) + payload = {"rc": rc, "log_multiline": log_plain.splitlines()} + return JSONResponse(payload, status_code=200 if rc == 0 else 500) + + +def _run_start_stop_bundle(req, action_name: str, proxmox_vm_action: str) -> JSONResponse: + """Start/stop/pause/resume bundle (Pattern C variant).""" + checked_inventory = utils.resolve_inventory(INVENTORY_NAME) + checked_playbook = utils.resolve_bundles_playbook(action_name, "public_github") + + extravars = {"proxmox_vm_action": proxmox_vm_action} + if req.proxmox_node: + extravars["proxmox_node"] = req.proxmox_node + + rc, events, log_plain, _ = run_playbook_core( + checked_playbook, checked_inventory, limit=req.proxmox_node, extravars=extravars, + ) + + if req.as_json: + result = extract_action_results(events, proxmox_vm_action) + payload = {"rc": rc, "result": result} + else: + payload = {"rc": rc, "log_multiline": log_plain.splitlines()} + + return JSONResponse(payload, status_code=200 if rc == 0 else 500) + + +def _run_delete_bundle(req, action_name: str) -> JSONResponse: + """Delete VMs bundle.""" + checked_inventory = utils.resolve_inventory(INVENTORY_NAME) + checked_playbook = utils.resolve_bundles_playbook(action_name, "public_github") + + extravars = {} + if req.proxmox_node: + extravars["proxmox_node"] = req.proxmox_node + + rc, events, log_plain, _ = run_playbook_core( + checked_playbook, checked_inventory, limit=req.proxmox_node, extravars=extravars, + ) + + if req.as_json: + extravars["proxmox_vm_action"] = "vm_delete" + result = extract_action_results(events, "vm_delete") + payload = {"rc": rc, "result": result} + else: + payload = {"rc": rc, "log_multiline": log_plain.splitlines()} + + return JSONResponse(payload, status_code=200 if rc == 0 else 500) + + +def _run_snapshot_bundle(req, action_name: str, action_key: str) -> JSONResponse: + """Snapshot create/revert bundle.""" + checked_inventory = utils.resolve_inventory(INVENTORY_NAME) + checked_playbook = utils.resolve_bundles_playbook(action_name, "public_github") + + extravars = {} + if req.proxmox_node: + extravars["proxmox_node"] = req.proxmox_node + if hasattr(req, "vm_snapshot_name") and req.vm_snapshot_name is not None: + extravars["VM_SNAPSHOT_NAME"] = req.vm_snapshot_name + + rc, events, log_plain, _ = run_playbook_core( + checked_playbook, checked_inventory, limit=req.proxmox_node, extravars=extravars, + ) + + if req.as_json: + extravars["proxmox_vm_action"] = action_key + result = extract_action_results(events, action_key) + payload = {"rc": rc, "result": result} + else: + payload = {"rc": rc, "log_multiline": log_plain.splitlines()} + + return JSONResponse(payload, status_code=200 if rc == 0 else 500) + + +# =========================================================================== +# Linux/Ubuntu install bundles +# =========================================================================== + +@router.post(path="/core/linux/ubuntu/install/docker", summary="Install docker packages", description="Install and configure docker engine on the target ubuntu system", tags=["bundles - core - ubuntu "], response_model=Reply_BundlesCoreLinuxUbuntuInstall_Docker) +def bundles_core_linux_ubuntu_install_docker(req: Request_BundlesCoreLinuxUbuntuInstall_Docker): + extravars = {} + if req.proxmox_node: + extravars["proxmox_node"] = req.proxmox_node + if req.install_package_docker is not None: + extravars["INSTALL_PACKAGES_DOCKER"] = req.install_package_docker + if req.install_ntpclient_and_update_time is not None: + extravars["INSTALL_PACKAGES_NTP_AND_UPDATE_TIME"] = req.install_ntpclient_and_update_time + if req.packages_cleaning is not None: + extravars["SPECIFIC_PACKAGES_CLEANING"] = req.packages_cleaning + return _run_bundle_simple(req, "core/linux/ubuntu/install/docker", extravars or None) + + +@router.post(path="/core/linux/ubuntu/install/docker-compose", summary="Install docker compose packages", description="Install and configure docker compose on the target ubuntu system", tags=["bundles - core - ubuntu "], response_model=Reply_BundlesCoreLinuxUbuntuInstall_DockerCompose) +def bundles_core_linux_ubuntu_install_docker_compose(req: Request_BundlesCoreLinuxUbuntuInstall_DockerCompose): + extravars = {} + if req.proxmox_node: + extravars["proxmox_node"] = req.proxmox_node + if req.install_package_docker is not None: + extravars["INSTALL_PACKAGES_DOCKER"] = req.install_package_docker + if req.install_package_docker_compose is not None: + extravars["INSTALL_PACKAGES_DOCKER_COMPOSE"] = req.install_package_docker_compose + if req.install_ntpclient_and_update_time is not None: + extravars["INSTALL_PACKAGES_NTP_AND_UPDATE_TIME"] = req.install_ntpclient_and_update_time + if req.packages_cleaning is not None: + extravars["SPECIFIC_PACKAGES_CLEANING"] = req.packages_cleaning + return _run_bundle_simple(req, "core/linux/ubuntu/install/docker", extravars or None) + + +@router.post(path="/core/linux/ubuntu/install/basic-packages", summary="Install basics packages", description="Install and configure a base set of packages on the target Ubuntu system", tags=["bundles - core - ubuntu "], response_model=Reply_BundlesCoreLinuxUbuntuInstall_BasicPackages) +def bundles_core_linux_ubuntu_install_basic_packages(req: Request_BundlesCoreLinuxUbuntuInstall_BasicPackages): + extravars = {} + if req.proxmox_node: + extravars["proxmox_node"] = req.proxmox_node + for field, key in [("install_package_basics", "INSTALL_PACKAGES_BASICS"), ("install_package_firewalls", "INSTALL_PACKAGES_FIREWALLS"), ("install_package_docker", "INSTALL_PACKAGES_DOCKER"), ("install_package_docker_compose", "INSTALL_PACKAGES_DOCKER_COMPOSE"), ("install_package_utils_json", "INSTALL_PACKAGES_UTILS_JSON"), ("install_package_utils_network", "INSTALL_PACKAGES_UTILS_NETWORK"), ("install_ntpclient_and_update_time", "INSTALL_PACKAGES_NTP_AND_UPDATE_TIME"), ("packages_cleaning", "SPECIFIC_PACKAGES_CLEANING")]: + val = getattr(req, field, None) + if val is not None: + extravars[key] = val + return _run_bundle_simple(req, "core/linux/ubuntu/install/basic-packages", extravars or None) + + +@router.post(path="/core/linux/ubuntu/install/dot-files", summary="Install user dotfiles", description="Install and configure generic dotfiles - vimrc, zshrc, etc.", tags=["bundles - core - ubuntu "], response_model=Reply_BundlesCoreLinuxUbuntuInstall_DotFilesItem) +def bundles_core_linux_ubuntu_install_dotfiles(req: Request_BundlesCoreLinuxUbuntuInstall_DotFiles): + extravars = {} + if req.proxmox_node: + extravars["proxmox_node"] = req.proxmox_node + if req.hosts is not None: + extravars["hosts"] = req.hosts + if req.user is not None: + extravars["OPERATOR_USER"] = req.user + if req.install_vim_dot_files is not None: + extravars["INSTALL_VIM_DOTFILES"] = req.install_vim_dot_files + if req.install_zsh_dot_files is not None: + extravars["INSTALL_ZSH_DOTFILES"] = req.install_zsh_dot_files + if req.apply_for_root is not None: + extravars["APPLY_FOR_ROOT"] = req.apply_for_root + return _run_bundle_simple(req, "core/linux/ubuntu/install/dot-files", extravars or None) + + +# =========================================================================== +# Linux/Ubuntu configure bundles +# =========================================================================== + +@router.post(path="/core/linux/ubuntu/configure/add-user", summary="Add system user", description="Create a new user with shell, home and password", tags=["bundles - core - ubuntu "], response_model=Reply_BundlesCoreLinuxUbuntuConfigure_AddUser) +def bundles_core_linux_ubuntu_configure_add_user(req: Request_BundlesCoreLinuxUbuntuConfigure_AddUser): + extravars = {} + if req.proxmox_node: + extravars["proxmox_node"] = req.proxmox_node + if req.user is not None: + extravars["TARGET_USER"] = req.user + if req.password is not None: + extravars["TARGET_PASSWORD"] = req.password + if req.shell_path is not None: + extravars["TARGET_SHELL_PATH"] = req.shell_path + if req.change_pwd_at_logon is not None: + extravars["CHANGE_PWD_AT_LOGON"] = req.change_pwd_at_logon + return _run_bundle_simple(req, "core/linux/ubuntu/configure/add-user", extravars or None) + + +# =========================================================================== +# Proxmox create VMs bundles (Pattern C) +# =========================================================================== + +def _check_admin_vms(req): + if not req.vms or len(req.vms) == 0: + raise HTTPException(status_code=400, detail="Field vms must not be empty") + allowed = {"admin-wazuh", "admin-web-api-kong", "admin-web-builder-api", "admin-web-deployer-ui", "admin-web-emp"} + for r in allowed: + if r not in req.vms: + raise HTTPException(status_code=400, detail=f"Missing required vm key {r}") + for vm in req.vms: + if vm not in allowed: + raise HTTPException(status_code=400, detail=f"Unauthorized vm key {vm}") + for vm_name, vm_spec in req.vms.items(): + if vm_spec.vm_id is None: + raise HTTPException(status_code=400, detail=f"missing key vm_id for {vm_name}") + if vm_spec.vm_description is None: + raise HTTPException(status_code=400, detail=f"missing key vm_description for {vm_name}") + if vm_spec.vm_ip is None: + raise HTTPException(status_code=400, detail=f"missing key vm_ip for {vm_name}") + + +def _check_vuln_vms(req): + if not req.vms or len(req.vms) == 0: + raise HTTPException(status_code=500, detail="Field vms must not be empty") + allowed = {"vuln-box-00", "vuln-box-01", "vuln-box-02", "vuln-box-03", "vuln-box-04"} + for r in allowed: + if r not in req.vms: + raise HTTPException(status_code=500, detail=f"Missing required vm key {r}") + for vm in req.vms: + if vm not in allowed: + raise HTTPException(status_code=500, detail=f"Unauthorized vm key {vm}") + for vm_name, vm_spec in req.vms.items(): + if vm_spec.vm_id is None: + raise HTTPException(status_code=500, detail=f"missing key vm_id for {vm_name}") + if vm_spec.vm_description is None: + raise HTTPException(status_code=500, detail=f"missing key vm_description for {vm_name}") + if vm_spec.vm_ip is None: + raise HTTPException(status_code=500, detail=f"missing key vm_ip for {vm_name}") + + +def _check_student_vms(req): + if not req.vms or len(req.vms) == 0: + raise HTTPException(status_code=400, detail="Field vms must not be empty") + allowed = {"student-box-01"} + for r in allowed: + if r not in req.vms: + raise HTTPException(status_code=400, detail=f"Missing required vm key {r}") + for vm in req.vms: + if vm not in allowed: + raise HTTPException(status_code=400, detail=f"Unauthorized vm key {vm}") + for vm_name, vm_spec in req.vms.items(): + if vm_spec.vm_id is None: + raise HTTPException(status_code=400, detail=f"missing key vm_id for {vm_name}") + if vm_spec.vm_description is None: + raise HTTPException(status_code=400, detail=f"missing key vm_description for {vm_name}") + if vm_spec.vm_ip is None: + raise HTTPException(status_code=400, detail=f"missing key vm_ip for {vm_name}") + + +@router.post(path="/core/proxmox/configure/default/create-vms-admin", summary="Create default admin VMs", description="Create the default set of admin virtual machines for initial configuration in Proxmox", tags=["bundles - core - proxmox - vms - default-configuration - admin"], response_model=Reply_BundlesCoreProxmoxConfigureDefaultVms_CreateAdminVms) +def bundles_proxmox_create_vms_admin(req: Request_BundlesCoreProxmoxConfigureDefaultVms_CreateAdminVms): + return _run_create_vms_bundle(req, "core/proxmox/configure/default/vms/create-vms-admin", _check_admin_vms) + + +@router.post(path="/core/proxmox/configure/default/create-vms-vuln", summary="Create default vulnerable VMs", description="Create the default set of vulnerable virtual machines for initial configuration in Proxmox", tags=["bundles - core - proxmox - vms - default-configuration - vuln"], response_model=Reply_BundlesCoreProxmoxConfigureDefaultVms_CreateVulnVms) +def bundles_proxmox_create_vms_vuln(req: Request_BundlesCoreProxmoxConfigureDefaultVms_CreateVulnVms): + return _run_create_vms_bundle(req, "core/proxmox/configure/default/vms/create-vms-vuln", _check_vuln_vms) + + +@router.post(path="/core/proxmox/configure/default/create-vms-student", summary="Create default student VMs", description="Create the default set of student virtual machines for initial configuration in Proxmox", tags=["bundles - core - proxmox - vms - default-configuration - student"], response_model=Reply_BundlesCoreProxmoxConfigureDefaultVms_CreateStudentVms) +def bundles_proxmox_create_vms_student(req: Request_BundlesCoreProxmoxConfigureDefaultVms_CreateStudentVms): + return _run_create_vms_bundle(req, "core/proxmox/configure/default/vms/create-vms-student", _check_student_vms) + + +# =========================================================================== +# Proxmox start/stop/pause/resume bundles for admin, vuln, student +# =========================================================================== + +_SSPR = { + "admin": "core/proxmox/configure/default/vms/start-stop-pause-resume-vms-admin", + "vuln": "core/proxmox/configure/default/vms/start-stop-pause-resume-vms-vuln", + "student": "core/proxmox/configure/default/vms/start-stop-pause-resume-vms-student", +} + +for _role in ("admin", "vuln", "student"): + for _action, _verb in [("start", "vm_start"), ("stop", "vm_stop"), ("pause", "vm_pause"), ("resume", "vm_resume")]: + _path = f"/core/proxmox/configure/default/{_action}-vms-{_role}" + _tag = f"bundles - core - proxmox - vms - default-configuration - {_role}" + _action_name = _SSPR[_role] + + def _make_handler(_an=_action_name, _v=_verb): + def handler(req: Request_BundlesCoreProxmoxConfigureDefaultVms_StartStopPauseResumeAdminVulnStudentVms): + return _run_start_stop_bundle(req, _an, _v) + return handler + + router.add_api_route( + _path, _make_handler(), methods=["POST"], + summary=f"{_action.capitalize()} {_role} vms ", + description=f"{_action.capitalize()} all {_role} virtual machines", + tags=[_tag], + response_model=Reply_ProxmoxVmsVMID_StartStopPauseResume, + ) + + +# =========================================================================== +# Proxmox delete VMs bundles +# =========================================================================== + +_DELETE_ACTIONS = { + "student": "core/proxmox/configure/default/vms/delete-vms-student", + "vuln": "core/proxmox/configure/default/vms/delete-vms-vuln", + "admin": "core/proxmox/configure/default/vms/delete-vms-admin", +} + +for _role, _an in _DELETE_ACTIONS.items(): + _path = f"/core/proxmox/configure/default/delete-vms-{_role}" + _tag = f"bundles - core - proxmox - vms - default-configuration - {_role}" + + def _make_delete_handler(_an=_an): + def handler(req: Request_BundlesCoreProxmoxConfigureDefaultVms_StartStopPauseResumeAdminVulnStudentVms): + return _run_delete_bundle(req, _an) + return handler + + router.add_api_route( + _path, _make_delete_handler(), methods=["DELETE"], + summary=f"Delete {_role} vms ", + description=f"Delete all {_role} virtual machines", + tags=[_tag], + response_model=Reply_ProxmoxVmsVMID_StartStopPauseResume, + ) + + +# =========================================================================== +# Proxmox snapshot create bundles +# =========================================================================== + +_SNAP_CREATE = { + "student": "core/proxmox/configure/default/vms/snapshot/create-vms-student", + "vuln": "core/proxmox/configure/default/vms/snapshot/create-vms-vuln", + "admin": "core/proxmox/configure/default/vms/snapshot/create-vms-admin", +} + +for _role, _an in _SNAP_CREATE.items(): + _path = f"/core/proxmox/configure/default/snapshot/create-vms-{_role}" + _tag = f"bundles - core - proxmox - vms - default-configuration - {_role}" + + def _make_snap_create_handler(_an=_an): + def handler(req: Request_BundlesCoreProxmoxConfigureDefaultVms_StartStopPauseResumeAdminVulnStudentVms): + return _run_snapshot_bundle(req, _an, "snapshot_vm_create") + return handler + + router.add_api_route( + _path, _make_snap_create_handler(), methods=["POST"], + summary=f"Snapshot {_role} vms ", + description=f"Snapshot all {_role} virtual machines", + tags=[_tag], + response_model=Reply_ProxmoxVmsVMID_CreateSnapshot, + ) + + +# =========================================================================== +# Proxmox snapshot revert bundles +# =========================================================================== + +_SNAP_REVERT = { + "student": "core/proxmox/configure/default/vms/snapshot/revert-vms-student", + "vuln": "core/proxmox/configure/default/vms/snapshot/revert-vms-vuln", + "admin": "core/proxmox/configure/default/vms/snapshot/revert-vms-admin", +} + +for _role, _an in _SNAP_REVERT.items(): + _path = f"/core/proxmox/configure/default/snapshot/revert-vms-{_role}" + _tag = f"bundles - core - proxmox - vms - default-configuration - {_role}" + + def _make_snap_revert_handler(_an=_an): + def handler(req: Request_BundlesCoreProxmoxConfigureDefaultVms_RevertSnapshotAdminVulnStudentVms): + return _run_snapshot_bundle(req, _an, "snapshot_vm_revert") + return handler + + router.add_api_route( + _path, _make_snap_revert_handler(), methods=["POST"], + summary=f"Snapshot {_role} vms ", + description=f"Snapshot all {_role} virtual machines", + tags=[_tag], + response_model=Reply_ProxmoxVmsVMID_RevertSnapshot, + ) diff --git a/app/routes/debug.py b/app/routes/debug.py new file mode 100644 index 0000000..233516f --- /dev/null +++ b/app/routes/debug.py @@ -0,0 +1,67 @@ +"""Consolidated debug routes. + +Replaces: app/routes/v0/debug/ping.py, app/routes/v0/debug/_test_func.py +""" + +import logging +import os +from pathlib import Path +from typing import Any + +from fastapi import APIRouter, HTTPException +from fastapi.responses import JSONResponse + +from app.runner import run_playbook_core +from app.schemas.debug.ping import Request_DebugPing +from app.utils.vm_id_name_resolver import * + +logger = logging.getLogger(__name__) + +PROJECT_ROOT = Path(os.getenv("PROJECT_ROOT_DIR")).resolve() +PLAYBOOK_SRC = PROJECT_ROOT / "playbooks" / "ping.yml" +INVENTORY_SRC = PROJECT_ROOT / "inventory" / "hosts.yml" + +router = APIRouter() + + +@router.post( + path="/ping", + summary="Run Ansible ping utility", + description="This endpoint runs the Ansible ping module to check connectivity with target hosts.", + tags=["runner"], +) +def debug_ping(req: Request_DebugPing): + if not PLAYBOOK_SRC.exists(): + raise HTTPException(status_code=400, detail=f":: MISSING PLAYBOOK : {PLAYBOOK_SRC}") + if not INVENTORY_SRC.exists(): + raise HTTPException(status_code=400, detail=f":: MISSING INVENTORY : {INVENTORY_SRC}") + + extravars = {} + if req.proxmox_node: + extravars["proxmox_node"] = req.proxmox_node + if not extravars: + extravars = None + + rc, events, log_plain, log_ansi = run_playbook_core( + PLAYBOOK_SRC, INVENTORY_SRC, limit=req.hosts, extravars=extravars, + ) + + payload = {"rc": rc, "log_multiline": log_plain.splitlines()} + return JSONResponse(payload, status_code=200 if rc == 0 else 500) + + +@router.post( + path="/func_test", + summary="temp stuff", + description="_testing - tmp ", + tags=["__tmp_testing"], +) +def debug_func_test(): + if not PLAYBOOK_SRC.exists(): + raise HTTPException(status_code=400, detail=f":: MISSING PLAYBOOK : {PLAYBOOK_SRC}") + if not INVENTORY_SRC.exists(): + raise HTTPException(status_code=400, detail=f":: MISSING INVENTORY : {INVENTORY_SRC}") + + out = resolv_id_to_vm_name("px-testing", 1000) + print("GOT :::") + print(out["vm_name"]) diff --git a/app/routes/firewall.py b/app/routes/firewall.py new file mode 100644 index 0000000..183a92e --- /dev/null +++ b/app/routes/firewall.py @@ -0,0 +1,169 @@ +"""Consolidated firewall routes. + +Replaces: app/routes/v0/proxmox/firewall/*.py +""" + +import logging +import os +from pathlib import Path + +from fastapi import APIRouter, HTTPException +from fastapi.responses import JSONResponse + +from app.runner import run_playbook_core +from app.extract_actions import extract_action_results +from app.utils.vm_id_name_resolver import resolv_id_to_vm_name +from app import utils + +from app.schemas.proxmox.firewall.list_iptables_alias import Request_ProxmoxFirewall_ListIptablesAlias, Reply_ProxmoxFirewallWithStorageName_ListIptablesAlias +from app.schemas.proxmox.firewall.add_iptable_alias import Request_ProxmoxFirewall_AddIptablesAlias, Reply_ProxmoxFirewallWithStorageName_AddIptablesAlias +from app.schemas.proxmox.firewall.delete_iptables_alias import Request_ProxmoxFirewall_DeleteIptablesAlias, Reply_ProxmoxFirewallWithStorageName_DeleteIptablesAlias +from app.schemas.proxmox.firewall.list_iptables_rules import Request_ProxmoxFirewall_ListIptablesRules, Reply_ProxmoxFirewallWithStorageName_ListIptablesRules +from app.schemas.proxmox.firewall.apply_iptables_rules import Request_ProxmoxFirewall_ApplyIptablesRules, Reply_ProxmoxFirewallWithStorageName_ApplyIptablesRules +from app.schemas.proxmox.firewall.delete_iptables_rule import Request_ProxmoxFirewall_DeleteIptablesRule +from app.schemas.proxmox.firewall.enable_firewall_vm import Request_ProxmoxFirewall_EnableFirewallVm, Reply_ProxmoxFirewallWithStorageName_EnableFirewallVm +from app.schemas.proxmox.firewall.disable_firewall_vm import Request_ProxmoxFirewall_DistableFirewallVm, Reply_ProxmoxFirewallWithStorageName_DisableFirewallVm +from app.schemas.proxmox.firewall.enable_firewall_node import Request_ProxmoxFirewall_EnableFirewallNode, Reply_ProxmoxFirewallWithStorageName_EnableFirewallNode +from app.schemas.proxmox.firewall.disable_firewall_node import Request_ProxmoxFirewall_DistableFirewallNode, Reply_ProxmoxFirewallWithStorageName_DisableFirewallNode +from app.schemas.proxmox.firewall.enable_firewall_dc import Request_ProxmoxFirewall_EnableFirewallDc, Reply_ProxmoxFirewallWithStorageName_EnableFirewallDc +from app.schemas.proxmox.firewall.disable_firewall_dc import Request_ProxmoxFirewall_DisableFirewallDc, Reply_ProxmoxFirewallWithStorageName_DisableFirewallDc + +logger = logging.getLogger(__name__) + +PROJECT_ROOT = Path(os.getenv("PROJECT_ROOT_DIR")).resolve() +INVENTORY_NAME = "hosts" +PLAYBOOK_SRC = PROJECT_ROOT / "playbooks" / "generic.yml" + +router = APIRouter() + + +def _run_fw(req, action: str, extravars: dict) -> JSONResponse: + extravars["proxmox_vm_action"] = action + extravars["hosts"] = "proxmox" + if not PLAYBOOK_SRC.exists(): + raise HTTPException(status_code=400, detail=f":: err - MISSING PLAYBOOK : {PLAYBOOK_SRC}") + inventory = utils.resolve_inventory(INVENTORY_NAME) + rc, events, log_plain, _ = run_playbook_core(PLAYBOOK_SRC, inventory, limit=extravars["hosts"], extravars=extravars) + if req.as_json: + payload = {"rc": rc, "result": extract_action_results(events, action)} + else: + payload = {"rc": rc, "log_multiline": log_plain.splitlines()} + return JSONResponse(payload, status_code=200 if rc == 0 else 500) + + +# --- Alias routes --- + +@router.post(path="/vm/alias/list", summary="List VM firewall aliases", description="List firewall aliases for a specific virtual machine", tags=["proxmox - firewall"], response_model=Reply_ProxmoxFirewallWithStorageName_ListIptablesAlias, response_description="Details of the VM firewall aliases") +def proxmox_vm_alias_list(req: Request_ProxmoxFirewall_ListIptablesAlias): + extravars = {"proxmox_node": req.proxmox_node} + if req.vm_id: + extravars["vm_id"] = req.vm_id + return _run_fw(req, "firewall_vm_list_iptables_alias", extravars) + + +@router.post(path="/vm/alias/add", summary="Add a firewall alias", description="Add a new alias to the Proxmox firewall - IPs, subnets/networks, hostnames", tags=["proxmox - firewall"], response_model=Reply_ProxmoxFirewallWithStorageName_AddIptablesAlias, response_description="Information about the created firewall alias") +def proxmox_firewall_vm_alias_add(req: Request_ProxmoxFirewall_AddIptablesAlias): + extravars = {"proxmox_node": req.proxmox_node} + if req.vm_id is not None: + extravars["vm_id"] = req.vm_id + if req.vm_fw_alias_name is not None: + extravars["vm_fw_alias_name"] = req.vm_fw_alias_name + if req.vm_fw_alias_cidr is not None: + extravars["vm_fw_alias_cidr"] = req.vm_fw_alias_cidr + if req.vm_fw_alias_comment is not None: + extravars["vm_fw_alias_comment"] = req.vm_fw_alias_comment + return _run_fw(req, "firewall_vm_add_iptables_alias", extravars) + + +@router.delete(path="/vm/alias/delete", summary="Delete a firewall alias", description="Remove an existing alias from the proxmox firewall", tags=["proxmox - firewall"], response_model=Reply_ProxmoxFirewallWithStorageName_DeleteIptablesAlias, response_description="Details of the deleted firewall alias") +def proxmox_firewall_vm_alias_delete(req: Request_ProxmoxFirewall_DeleteIptablesAlias): + extravars = {"proxmox_node": req.proxmox_node} + if req.vm_id is not None: + extravars["vm_id"] = req.vm_id + if req.vm_fw_alias_name is not None: + extravars["vm_fw_alias_name"] = req.vm_fw_alias_name + return _run_fw(req, "firewall_vm_delete_iptables_alias", extravars) + + +# --- Rules routes --- + +@router.post(path="/vm/rules/list", summary="List VM firewall rules", description="List firewall rules for a specific virtual machine", tags=["proxmox - firewall"], response_model=Reply_ProxmoxFirewallWithStorageName_ListIptablesRules, response_description="Details of the VM firewall rules") +def proxmox_vm_rules_list(req: Request_ProxmoxFirewall_ListIptablesRules): + extravars = {"proxmox_node": req.proxmox_node} + if req.vm_id: + extravars["vm_id"] = req.vm_id + return _run_fw(req, "firewall_vm_list_iptables_rule", extravars) + + +@router.post(path="/vm/rules/apply", summary="Apply firewall rules", description="Apply the received firewall rules to the proxmox firewall", tags=["proxmox - firewall"], response_model=Reply_ProxmoxFirewallWithStorageName_ApplyIptablesRules, response_description="Details of the applied firewall rules") +def proxmox_firewall_vm_rules_add(req: Request_ProxmoxFirewall_ApplyIptablesRules): + extravars = {"proxmox_node": req.proxmox_node} + if req.vm_id is not None: + extravars["vm_id"] = req.vm_id + for field in ("vm_fw_action", "vm_fw_dport", "vm_fw_enable", "vm_fw_proto", "vm_fw_type", "vm_fw_log", "vm_fw_iface", "vm_fw_source", "vm_fw_dest", "vm_fw_sport", "vm_fw_comment", "vm_fw_pos"): + val = getattr(req, field, None) + if val is not None: + extravars[field] = val + return _run_fw(req, "firewall_vm_apply_iptables_rule", extravars) + + +@router.delete(path="/vm/rules/delete", summary="Delete a firewall rule", description="Remove an existing rule from the proxmox firewall configuration", tags=["proxmox - firewall"], response_model=Request_ProxmoxFirewall_DeleteIptablesRule, response_description="Details of the deleted firewall rule.") +def proxmox_firewall_vm_rules_delete(req: Request_ProxmoxFirewall_DeleteIptablesRule): + extravars = {"proxmox_node": req.proxmox_node} + if req.vm_id is not None: + extravars["vm_id"] = req.vm_id + if req.vm_fw_pos is not None: + extravars["vm_fw_pos"] = req.vm_fw_pos + return _run_fw(req, "firewall_vm_delete_iptables_rule", extravars) + + +# --- VM enable/disable --- + +@router.post(path="/vm/enable", summary="Enable VM firewall", description="Enable the proxmox firewall for a specific virtual machine", tags=["proxmox - firewall"], response_model=Reply_ProxmoxFirewallWithStorageName_EnableFirewallVm, response_description="Details of the enabled VM firewall") +def proxmox_firewall_vm_enable(req: Request_ProxmoxFirewall_EnableFirewallVm): + extravars = {"proxmox_node": req.proxmox_node} + if req.vm_id: + extravars["vm_id"] = req.vm_id + extravars["vm_name"] = resolv_id_to_vm_name(extravars["proxmox_node"], extravars["vm_id"]) + return _run_fw(req, "firewall_vm_enable", extravars) + + +@router.post(path="/vm/disable", summary="Disable VM firewall", description="Disable the proxmox firewall for a specific virtual machine", tags=["proxmox - firewall"], response_model=Reply_ProxmoxFirewallWithStorageName_DisableFirewallVm, response_description="Details of the disabled VM firewall") +def proxmox_firewall_vm_disable(req: Request_ProxmoxFirewall_DistableFirewallVm): + extravars = {"proxmox_node": req.proxmox_node} + if req.vm_id: + extravars["vm_id"] = req.vm_id + extravars["vm_name"] = resolv_id_to_vm_name(extravars["proxmox_node"], extravars["vm_id"]) + return _run_fw(req, "firewall_vm_disable", extravars) + + +# --- Node enable/disable --- + +@router.post(path="/node/enable", summary="Enable node firewall", description="Enable the proxmox firewall on a specific node", tags=["proxmox - firewall"], response_model=Reply_ProxmoxFirewallWithStorageName_EnableFirewallNode, response_description="Details of the enabled node firewall") +def proxmox_firewall_node_enable(req: Request_ProxmoxFirewall_EnableFirewallNode): + extravars = {"proxmox_node": req.proxmox_node} + return _run_fw(req, "firewall_node_enable", extravars) + + +@router.post(path="/node/disable", summary="Disable node firewall", description="Disable the proxmox firewall on a specific node", tags=["proxmox - firewall"], response_model=Reply_ProxmoxFirewallWithStorageName_DisableFirewallNode, response_description="Details of the disabled node firewall") +def proxmox_firewall_node_disable(req: Request_ProxmoxFirewall_DistableFirewallNode): + extravars = {"proxmox_node": req.proxmox_node} + return _run_fw(req, "firewall_node_disable", extravars) + + +# --- Datacenter enable/disable --- + +@router.post(path="/datacenter/enable", summary="Enable datacenter firewall", description="Enable the proxmox firewall at the datacenter level", tags=["proxmox - firewall"], response_model=Reply_ProxmoxFirewallWithStorageName_EnableFirewallDc, response_description="Details of the enabled datacenter firewall") +def proxmox_firewall_dc_enable(req: Request_ProxmoxFirewall_EnableFirewallDc): + extravars = {} + if req.proxmox_api_host: + extravars["proxmox_api_host"] = req.proxmox_api_host + return _run_fw(req, "firewall_dc_enable", extravars) + + +@router.post(path="/datacenter/disable", summary="Disable datacenter firewall", description="Disable the proxmox firewall at the datacenter level", tags=["proxmox - firewall"], response_model=Reply_ProxmoxFirewallWithStorageName_DisableFirewallDc, response_description="Details of the disabled datacenter firewall") +def proxmox_firewall_dc_disable(req: Request_ProxmoxFirewall_DisableFirewallDc): + extravars = {} + if req.proxmox_api_host: + extravars["proxmox_api_host"] = req.proxmox_api_host + return _run_fw(req, "firewall_dc_disable", extravars) diff --git a/app/routes/network.py b/app/routes/network.py new file mode 100644 index 0000000..fdc30b6 --- /dev/null +++ b/app/routes/network.py @@ -0,0 +1,103 @@ +"""Consolidated network routes. + +Replaces: app/routes/v0/proxmox/network/vm/*.py, network/node/*.py +""" + +import logging +import os +from pathlib import Path + +from fastapi import APIRouter, HTTPException +from fastapi.responses import JSONResponse + +from app.runner import run_playbook_core +from app.extract_actions import extract_action_results +from app import utils + +from app.schemas.proxmox.network.vm_id.add_network import Request_ProxmoxNetwork_WithVmId_AddNetwork, Reply_ProxmoxNetwork_WithVmId_AddNetworkInterface +from app.schemas.proxmox.network.vm_id.delete_network import Request_ProxmoxNetwork_WithVmId_DeleteNetwork, Reply_ProxmoxNetwork_WithVmId_DeleteNetworkInterface +from app.schemas.proxmox.network.vm_id.list_network import Request_ProxmoxNetwork_WithVmId_ListNetwork, Reply_ProxmoxNetwork_WithVmId_ListNetworkInterface +from app.schemas.proxmox.network.node_name.add_network import Request_ProxmoxNetwork_WithNodeName_AddNetworkInterface, Reply_ProxmoxNetwork_WithNodeName_AddNetworkInterface +from app.schemas.proxmox.network.node_name.delete_network import Request_ProxmoxNetwork_WithNodeName_DeleteInterface, Reply_ProxmoxNetwork_WithNodeName_DeleteInterface +from app.schemas.proxmox.network.node_name.list_network import Request_ProxmoxNetwork_WithNodeName_ListInterface, Reply_ProxmoxNetwork_WithNodeName_ListInterface + +logger = logging.getLogger(__name__) + +PROJECT_ROOT = Path(os.getenv("PROJECT_ROOT_DIR")).resolve() +INVENTORY_NAME = "hosts" +PLAYBOOK_SRC = PROJECT_ROOT / "playbooks" / "generic.yml" + +router = APIRouter() + + +def _run_net(req, action: str, extravars: dict) -> JSONResponse: + extravars["proxmox_vm_action"] = action + extravars["hosts"] = "proxmox" + if not PLAYBOOK_SRC.exists(): + raise HTTPException(status_code=400, detail=f":: err - MISSING PLAYBOOK : {PLAYBOOK_SRC}") + inventory = utils.resolve_inventory(INVENTORY_NAME) + rc, events, log_plain, _ = run_playbook_core(PLAYBOOK_SRC, inventory, limit=extravars["hosts"], extravars=extravars) + if req.as_json: + payload = {"rc": rc, "result": extract_action_results(events, action)} + else: + payload = {"rc": rc, "log_multiline": log_plain.splitlines()} + return JSONResponse(payload, status_code=200 if rc == 0 else 500) + + +# --- VM network routes --- + +@router.post(path="/vm/add", summary="Add VM network interface", description="Create and attach a new network interface to a Proxmox VM.", tags=["proxmox - network - vm"], response_model=Reply_ProxmoxNetwork_WithVmId_AddNetworkInterface, response_description="Information about the added network interface.") +def proxmox_network_vm_add_interface(req: Request_ProxmoxNetwork_WithVmId_AddNetwork): + extravars = {"proxmox_node": req.proxmox_node} + if req.vm_id is not None: + extravars["vm_id"] = req.vm_id + for field in ("iface_model", "iface_bridge", "vm_vmnet_id", "iface_trunks", "iface_tag", "iface_rate", "iface_queues", "iface_mtu", "iface_macaddr", "iface_link_down", "iface_firewall"): + val = getattr(req, field, None) + if val is not None: + extravars[field] = val + return _run_net(req, "network_add_interfaces_vm", extravars) + + +@router.post(path="/vm/delete", summary="Delete VM network interface", description="Remove a network interface from a Proxmox VM.", tags=["proxmox - network - vm"], response_model=Reply_ProxmoxNetwork_WithVmId_DeleteNetworkInterface, response_description="Information about the deleted network interface.") +def proxmox_network_vm_delete_interface(req: Request_ProxmoxNetwork_WithVmId_DeleteNetwork): + extravars = {"proxmox_node": req.proxmox_node} + if req.vm_id is not None: + extravars["vm_id"] = req.vm_id + if req.vm_vmnet_id is not None: + extravars["vm_vmnet_id"] = req.vm_vmnet_id + return _run_net(req, "network_delete_interfaces_vm", extravars) + + +@router.post(path="/vm/list", summary="List VM network interfaces", description="Retrieve all network interfaces attached to a Proxmox VM.", tags=["proxmox - network - vm"], response_model=Reply_ProxmoxNetwork_WithVmId_ListNetworkInterface, response_description="List of VM network interfaces.") +def proxmox_network_vm_list_interface(req: Request_ProxmoxNetwork_WithVmId_ListNetwork): + extravars = {"proxmox_node": req.proxmox_node} + if req.vm_id is not None: + extravars["vm_id"] = req.vm_id + return _run_net(req, "network_list_interfaces_vm", extravars) + + +# --- Node network routes --- + +@router.post(path="/node/add", summary="Add node network interface", description="Create and attach a new network interface to a Proxmox node.", tags=["proxmox - network - node"], response_model=Reply_ProxmoxNetwork_WithNodeName_AddNetworkInterface, response_description="Information about the added network interface.") +def proxmox_network_node_add_interface(req: Request_ProxmoxNetwork_WithNodeName_AddNetworkInterface): + extravars = {"proxmox_node": req.proxmox_node} + for field in ("bridge_ports", "iface_name", "iface_type", "iface_autostart", "ip_address", "ip_netmask", "ip_gateway", "ovs_bridge"): + val = getattr(req, field, None) + if val is not None: + extravars[field] = val + return _run_net(req, "network_add_interfaces_node", extravars) + + +@router.post(path="/node/delete", summary="Delete node network interface", description="Remove a network interface from a Proxmox node.", tags=["proxmox - network - node"], response_model=Reply_ProxmoxNetwork_WithNodeName_DeleteInterface, response_description="Information about the deleted network interface.") +def proxmox_network_node_delete_interface(req: Request_ProxmoxNetwork_WithNodeName_DeleteInterface): + extravars = {"proxmox_node": req.proxmox_node} + if req.iface_name is not None: + extravars["iface_name"] = req.iface_name + return _run_net(req, "network_delete_interfaces_node", extravars) + + +# NOTE: original path has double slash "//node/list" - preserving exactly +@router.post(path="//node/list", summary="List node network interfaces", description="Retrieve all network interfaces configured on a Proxmox node.", tags=["proxmox - network - node"], response_model=Reply_ProxmoxNetwork_WithNodeName_ListInterface, response_description="List of node network interfaces.") +def proxmox_network_node_list_interface(req: Request_ProxmoxNetwork_WithNodeName_ListInterface): + extravars = {"proxmox_node": req.proxmox_node} + return _run_net(req, "network_list_interfaces_node", extravars) diff --git a/app/routes/runner.py b/app/routes/runner.py new file mode 100644 index 0000000..6824a2f --- /dev/null +++ b/app/routes/runner.py @@ -0,0 +1,62 @@ +"""Consolidated dynamic runner routes. + +Replaces: app/routes/v0/run/bundles/actions_run.py + app/routes/v0/run/scenarios/scenarios_run.py +""" + +import logging +import os +from pathlib import Path +from typing import Any + +from fastapi import APIRouter +from fastapi.responses import JSONResponse + +from app.runner import run_playbook_core +from app.schemas.debug.ping import Request_DebugPing +from app import utils + +logger = logging.getLogger(__name__) + +PROJECT_ROOT = Path(os.getenv("PROJECT_ROOT_DIR")).resolve() +INVENTORY_NAME = "hosts" + +router = APIRouter() + + +def _run_generic(req, name: str, resolver_fn) -> JSONResponse: + checked_inventory = utils.resolve_inventory(INVENTORY_NAME) + checked_playbook = resolver_fn(name, "public_github") + + extravars = {} + if req.proxmox_node: + extravars["proxmox_node"] = req.proxmox_node + if not extravars: + extravars = None + + rc, events, log_plain, _ = run_playbook_core( + checked_playbook, checked_inventory, limit=req.hosts, extravars=extravars, + ) + + payload = {"rc": rc, "log_multiline": log_plain.splitlines()} + return JSONResponse(payload, status_code=200 if rc == 0 else 500) + + +@router.post( + path="/{bundles_name}/run", + summary="Run bundles", + description="Run generic bundles with default (and static) extras_vars ", + tags=["runner"], +) +def run_bundle(bundles_name: str, req: Request_DebugPing): + return _run_generic(req, bundles_name, utils.resolve_bundles_playbook) + + +@router.post( + path="/{scenario_name}/run", + summary="Run scenario", + description="Run generic scenario with default (and static) extras_vars ", + tags=["runner"], +) +def run_scenario(scenario_name: str, req: Request_DebugPing): + return _run_generic(req, scenario_name, utils.resolve_scenarios_playbook) diff --git a/app/routes/snapshots.py b/app/routes/snapshots.py new file mode 100644 index 0000000..75703cf --- /dev/null +++ b/app/routes/snapshots.py @@ -0,0 +1,128 @@ +"""Consolidated snapshot routes. + +Replaces: app/routes/v0/proxmox/vms/vm_id/snapshots/*.py +""" + +import logging +import os +from pathlib import Path + +from fastapi import APIRouter, HTTPException +from fastapi.responses import JSONResponse + +from app.runner import run_playbook_core +from app.extract_actions import extract_action_results +from app.utils.vm_id_name_resolver import resolv_id_to_vm_name +from app import utils + +from app.schemas.proxmox.vm_id.snapshot.vm_list import Request_ProxmoxVmsVMID_ListSnapshot, Reply_ProxmoxVmsVMID_ListSnapshot +from app.schemas.proxmox.vm_id.snapshot.vm_create import Request_ProxmoxVmsVMID_CreateSnapshot, Reply_ProxmoxVmsVMID_CreateSnapshot +from app.schemas.proxmox.vm_id.snapshot.vm_delete import Request_ProxmoxVmsVMID_DeleteSnapshot, Reply_ProxmoxVmsVMID_DeleteSnapshot +from app.schemas.proxmox.vm_id.snapshot.vm_revert import Request_ProxmoxVmsVMID_RevertSnapshot, Reply_ProxmoxVmsVMID_RevertSnapshot + +logger = logging.getLogger(__name__) + +PROJECT_ROOT = Path(os.getenv("PROJECT_ROOT_DIR")).resolve() +INVENTORY_NAME = "hosts" +PLAYBOOK_SRC = PROJECT_ROOT / "playbooks" / "generic.yml" + +router = APIRouter() + + +def _run_snapshot_action(req, action: str, extravars: dict) -> JSONResponse: + extravars["proxmox_vm_action"] = action + extravars["hosts"] = "proxmox" + + if not PLAYBOOK_SRC.exists(): + raise HTTPException(status_code=400, detail=f":: err - MISSING PLAYBOOK : {PLAYBOOK_SRC}") + + inventory = utils.resolve_inventory(INVENTORY_NAME) + + rc, events, log_plain, log_ansi = run_playbook_core( + PLAYBOOK_SRC, inventory, limit=extravars["hosts"], extravars=extravars, + ) + + if req.as_json: + result = extract_action_results(events, action) + payload = {"rc": rc, "result": result} + else: + payload = {"rc": rc, "log_multiline": log_plain.splitlines()} + + return JSONResponse(payload, status_code=200 if rc == 0 else 500) + + +@router.post( + path="/list", + summary="List a snapshot for a VM", + description="List snapshot of the specified virtual machine (VM).", + tags=["proxmox - vm snapshots"], + response_model=Reply_ProxmoxVmsVMID_ListSnapshot, + response_description="Snapshot list result", +) +def proxmox_vms_vm_id_list_snapshot(req: Request_ProxmoxVmsVMID_ListSnapshot): + extravars = {} + if req.vm_id is not None: + extravars["vm_id"] = req.vm_id + if req.proxmox_node: + extravars["proxmox_node"] = req.proxmox_node + return _run_snapshot_action(req, "snapshot_vm_list", extravars) + + +@router.post( + path="/create", + summary="Create a snapshot for a VM", + description="Creates a snapshot of the specified virtual machine (VM).", + tags=["proxmox - vm snapshots"], + response_model=Reply_ProxmoxVmsVMID_CreateSnapshot, + response_description="Snapshot creation result", +) +def proxmox_vms_vm_id_create_snapshot(req: Request_ProxmoxVmsVMID_CreateSnapshot): + extravars = {"proxmox_node": req.proxmox_node} + if req.vm_id is not None: + extravars["vm_id"] = req.vm_id + extravars["vm_name"] = resolv_id_to_vm_name(extravars["proxmox_node"], extravars["vm_id"]) + if req.vm_snapshot_name is not None: + extravars["vm_snapshot_name"] = req.vm_snapshot_name + if req.vm_snapshot_description is not None: + extravars["vm_snapshot_description"] = req.vm_snapshot_description + return _run_snapshot_action(req, "snapshot_vm_create", extravars) + + +@router.delete( + path="/delete", + summary="Delete a snapshot for a VM", + description="Delete a snapshot of the specified virtual machine (VM).", + tags=["proxmox - vm snapshots"], + response_model=Reply_ProxmoxVmsVMID_DeleteSnapshot, + response_description="Snapshot delete result", +) +def proxmox_vms_vm_id_delete_snapshot(req: Request_ProxmoxVmsVMID_DeleteSnapshot): + extravars = {} + if req.proxmox_node: + extravars["proxmox_node"] = req.proxmox_node + if req.vm_id is not None: + extravars["vm_id"] = req.vm_id + extravars["vm_name"] = resolv_id_to_vm_name(extravars["proxmox_node"], extravars["vm_id"]) + if req.vm_snapshot_name: + extravars["vm_snapshot_name"] = req.vm_snapshot_name + return _run_snapshot_action(req, "snapshot_vm_delete", extravars) + + +@router.post( + path="/revert", + summary="Revert a VM to a snapshot", + description="Reverts the specified virtual machine (VM) to the given snapshot.", + tags=["proxmox - vm snapshots"], + response_model=Reply_ProxmoxVmsVMID_RevertSnapshot, + response_description="Snapshot revert result", +) +def proxmox_vms_vm_id_revert_snapshot(req: Request_ProxmoxVmsVMID_RevertSnapshot): + extravars = {} + if req.proxmox_node: + extravars["proxmox_node"] = req.proxmox_node + if req.vm_id is not None: + extravars["vm_id"] = req.vm_id + extravars["vm_name"] = resolv_id_to_vm_name(extravars["proxmox_node"], extravars["vm_id"]) + if req.vm_snapshot_name is not None: + extravars["vm_snapshot_name"] = req.vm_snapshot_name + return _run_snapshot_action(req, "snapshot_vm_revert", extravars) diff --git a/app/routes/storage.py b/app/routes/storage.py new file mode 100644 index 0000000..07ede5f --- /dev/null +++ b/app/routes/storage.py @@ -0,0 +1,86 @@ +"""Consolidated storage routes. + +Replaces: app/routes/v0/proxmox/storage/*.py +""" + +import logging +import os +from pathlib import Path + +from fastapi import APIRouter, HTTPException +from fastapi.responses import JSONResponse + +from app.runner import run_playbook_core +from app.extract_actions import extract_action_results +from app import utils + +from app.schemas.proxmox.storage.list import Request_ProxmoxStorage_List, Reply_ProxmoxStorage_ListItem +from app.schemas.proxmox.storage.download_iso import Request_ProxmoxStorage_DownloadIso, Reply_ProxmoxStorage_DownloadIsoItem +from app.schemas.proxmox.storage.storage_name.list_iso import Request_ProxmoxStorage_ListIso, Reply_ProxmoxStorageWithStorageName_ListIsoItem +from app.schemas.proxmox.storage.storage_name.list_template import Request_ProxmoxStorage_ListTemplate, Reply_ProxmoxStorageWithStorageName_ListTemplate + +logger = logging.getLogger(__name__) + +PROJECT_ROOT = Path(os.getenv("PROJECT_ROOT_DIR")).resolve() +INVENTORY_NAME = "hosts" +PLAYBOOK_SRC = PROJECT_ROOT / "playbooks" / "generic.yml" + +# Two routers for different prefixes +storage_router = APIRouter() +storage_name_router = APIRouter() + + +def _run_storage(req, action: str, extravars: dict) -> JSONResponse: + extravars["proxmox_vm_action"] = action + extravars["hosts"] = "proxmox" + if not PLAYBOOK_SRC.exists(): + raise HTTPException(status_code=400, detail=f":: err - MISSING PLAYBOOK : {PLAYBOOK_SRC}") + inventory = utils.resolve_inventory(INVENTORY_NAME) + rc, events, log_plain, _ = run_playbook_core(PLAYBOOK_SRC, inventory, limit=extravars["hosts"], extravars=extravars) + if req.as_json: + payload = {"rc": rc, "result": extract_action_results(events, action)} + else: + payload = {"rc": rc, "log_multiline": log_plain.splitlines()} + return JSONResponse(payload, status_code=200 if rc == 0 else 500) + + +# --- /v0/admin/proxmox/storage/ --- + +@storage_router.post(path="/list", summary="Retrieve configuration of a VM", description="Returns the configuration details of the specified virtual machine (VM).", tags=["proxmox - storage"], response_model=Reply_ProxmoxStorage_ListItem, response_description="VM configuration details") +def proxmox_storage_list(req: Request_ProxmoxStorage_List): + extravars = {"proxmox_node": req.proxmox_node} + if req.storage_name is not None: + extravars["storage_name"] = req.storage_name + return _run_storage(req, "storage_list", extravars) + + +@storage_router.post(path="/download_iso", summary="Retrieve configuration of a VM", description="Returns the configuration details of the specified virtual machine (VM).", tags=["proxmox - storage"], response_model=Reply_ProxmoxStorage_DownloadIsoItem, response_description="VM configuration details") +def proxmox_storage_download_iso(req: Request_ProxmoxStorage_DownloadIso): + extravars = {"proxmox_node": req.proxmox_node} + if req.proxmox_storage is not None: + extravars["proxmox_storage"] = req.proxmox_storage + if req.iso_file_content_type is not None: + extravars["iso_file_content_type"] = req.iso_file_content_type + if req.iso_file_name is not None: + extravars["iso_file_name"] = req.iso_file_name + if req.iso_url is not None: + extravars["iso_url"] = req.iso_url + return _run_storage(req, "storage_download_iso", extravars) + + +# --- /v0/admin/proxmox/storage/storage_name/ --- + +@storage_name_router.post(path="/list_iso", summary="Retrieve configuration of a VM", description="Returns the configuration details of the specified virtual machine (VM).", tags=["proxmox - storage"], response_model=Reply_ProxmoxStorageWithStorageName_ListIsoItem, response_description="VM configuration details") +def proxmox_storage_with_storage_name_list_iso(req: Request_ProxmoxStorage_ListIso): + extravars = {"proxmox_node": req.proxmox_node} + if req.storage_name is not None: + extravars["storage_name"] = req.storage_name + return _run_storage(req, "storage_list_iso", extravars) + + +@storage_name_router.post(path="/list_template", summary="Retrieve configuration of a VM", description="Returns the configuration details of the specified virtual machine (VM).", tags=["proxmox - storage"], response_model=Reply_ProxmoxStorageWithStorageName_ListTemplate, response_description="VM configuration details") +def proxmox_storage_with_storage_name_list_template(req: Request_ProxmoxStorage_ListTemplate): + extravars = {"proxmox_node": req.proxmox_node} + if req.storage_name is not None: + extravars["storage_name"] = req.storage_name + return _run_storage(req, "storage_list_template", extravars) diff --git a/app/routes/vm_config.py b/app/routes/vm_config.py new file mode 100644 index 0000000..3fc1a1b --- /dev/null +++ b/app/routes/vm_config.py @@ -0,0 +1,129 @@ +"""Consolidated VM configuration routes. + +Replaces: app/routes/v0/proxmox/vms/vm_id/config/*.py +""" + +import logging +import os +from pathlib import Path + +from fastapi import APIRouter, HTTPException +from fastapi.responses import JSONResponse + +from app.runner import run_playbook_core +from app.extract_actions import extract_action_results +from app import utils + +from app.schemas.proxmox.vm_id.config.vm_get_config import Request_ProxmoxVmsVMID_VmGetConfig, Reply_ProxmoxVmsVMID_VmGetConfig +from app.schemas.proxmox.vm_id.config.vm_get_config_cdrom import Request_ProxmoxVmsVMID_VmGetConfigCdrom, Reply_ProxmoxVmsVMID_VmGetConfigCdrom +from app.schemas.proxmox.vm_id.config.vm_get_config_cpu import Request_ProxmoxVmsVMID_VmGetConfigCpu, Reply_ProxmoxVmsVMID_VmGetConfigCpu +from app.schemas.proxmox.vm_id.config.vm_get_config_ram import Request_ProxmoxVmsVMID_VmGetConfigRam, Reply_ProxmoxVmsVMID_VmGetConfigRam +from app.schemas.proxmox.vm_id.config.vm_set_tag import Request_ProxmoxVmsVMID_VmSetTag, Reply_ProxmoxVmsVMID_VmSetTag + +logger = logging.getLogger(__name__) + +PROJECT_ROOT = Path(os.getenv("PROJECT_ROOT_DIR")).resolve() +INVENTORY_NAME = "hosts" +PLAYBOOK_SRC = PROJECT_ROOT / "playbooks" / "generic.yml" + +router = APIRouter() + + +def _run_config_action(req, action: str, extravars: dict) -> JSONResponse: + """Common pattern for VM config routes.""" + extravars["proxmox_vm_action"] = action + extravars["hosts"] = "proxmox" + + if not PLAYBOOK_SRC.exists(): + raise HTTPException(status_code=400, detail=f":: err - MISSING PLAYBOOK : {PLAYBOOK_SRC}") + + inventory = utils.resolve_inventory(INVENTORY_NAME) + + rc, events, log_plain, log_ansi = run_playbook_core( + PLAYBOOK_SRC, inventory, limit=extravars["hosts"], extravars=extravars, + ) + + if req.as_json: + result = extract_action_results(events, action) + payload = {"rc": rc, "result": result} + else: + payload = {"rc": rc, "log_multiline": log_plain.splitlines()} + + return JSONResponse(payload, status_code=200 if rc == 0 else 500) + + +@router.post( + path="/vm_get_config", + summary="Retrieve configuration of a VM", + description="Returns the configuration details of the specified virtual machine (VM).", + tags=["proxmox - vm configuration"], + response_model=Reply_ProxmoxVmsVMID_VmGetConfig, + response_description="VM configuration details", +) +def proxmox_vms_vm_id_vm_get_config(req: Request_ProxmoxVmsVMID_VmGetConfig): + extravars = {"proxmox_node": req.proxmox_node} + if req.vm_id is not None: + extravars["vm_id"] = req.vm_id + return _run_config_action(req, "vm_get_config", extravars) + + +@router.post( + path="/vm_get_config_cdrom", + summary="Get cdrom configuration of a VM", + description="Returns the cdrom configuration details of the specified virtual machine (VM).", + tags=["proxmox - vm configuration"], + response_model=Reply_ProxmoxVmsVMID_VmGetConfigCdrom, + response_description="cdrom configuration details", +) +def proxmox_vms_vm_id_vm_get_config_cdrom(req: Request_ProxmoxVmsVMID_VmGetConfigCdrom): + extravars = {"proxmox_node": req.proxmox_node} + if req.vm_id is not None: + extravars["vm_id"] = req.vm_id + return _run_config_action(req, "vm_get_config_cdrom", extravars) + + +@router.post( + path="/vm_get_config_cpu", + summary="Get cpu configuration of a VM", + description="Returns the cpu configuration details of the specified virtual machine (VM).", + tags=["proxmox - vm configuration"], + response_model=Reply_ProxmoxVmsVMID_VmGetConfigCpu, + response_description="cpu configuration details", +) +def proxmox_vms_vm_id_vm_get_config_cpu(req: Request_ProxmoxVmsVMID_VmGetConfigCpu): + extravars = {"proxmox_node": req.proxmox_node} + if req.vm_id is not None: + extravars["vm_id"] = req.vm_id + return _run_config_action(req, "vm_get_config_cpu", extravars) + + +@router.post( + path="/vm_get_config_ram", + summary="Get ram configuration of a VM", + description="Returns the ram configuration details of the specified virtual machine (VM).", + tags=["proxmox - vm configuration"], + response_model=Reply_ProxmoxVmsVMID_VmGetConfigRam, + response_description="ram configuration details", +) +def proxmox_vms_vm_id_vm_get_config_ram(req: Request_ProxmoxVmsVMID_VmGetConfigRam): + extravars = {"proxmox_node": req.proxmox_node} + if req.vm_id is not None: + extravars["vm_id"] = req.vm_id + return _run_config_action(req, "vm_get_config_ram", extravars) + + +@router.post( + path="/vm_set_tag", + summary="Retrieve configuration of a VM", + description="Returns the configuration details of the specified virtual machine (VM).", + tags=["proxmox - vm configuration"], + response_model=Reply_ProxmoxVmsVMID_VmSetTag, + response_description="VM configuration details", +) +def proxmox_vms_vm_id_vm_set_tags(req: Request_ProxmoxVmsVMID_VmSetTag): + extravars = {"proxmox_node": req.proxmox_node} + if req.vm_id is not None: + extravars["vm_id"] = req.vm_id + if req.proxmox_node: + extravars["vm_tag_name"] = req.vm_tag_name + return _run_config_action(req, "vm_set_tag", extravars) diff --git a/app/routes/vms.py b/app/routes/vms.py new file mode 100644 index 0000000..ba3aceb --- /dev/null +++ b/app/routes/vms.py @@ -0,0 +1,377 @@ +"""Consolidated VM lifecycle routes. + +Replaces: app/routes/v0/proxmox/vms/list.py, list_usage.py, + vm_id/{start,stop,stop_force,pause,resume,create,delete,clone}.py, + vm_ids/{mass_start_stop_pause_resume,mass_delete}.py +""" + +import logging +import os +from pathlib import Path +from typing import Any + +from fastapi import APIRouter, HTTPException +from fastapi.responses import JSONResponse + +from app.runner import run_playbook_core +from app.extract_actions import extract_action_results +from app.utils.vm_id_name_resolver import resolv_id_to_vm_name +from app import utils + +from app.schemas.proxmox.vm_list import Request_ProxmoxVms_VmList, Reply_ProxmoxVmList +from app.schemas.proxmox.vm_list_usage import Request_ProxmoxVms_VmListUsage, Reply_ProxmoxVms_VmListUsage +from app.schemas.proxmox.vm_id.start_stop_resume_pause import ( + Request_ProxmoxVmsVMID_StartStopPauseResume, + Reply_ProxmoxVmsVMID_StartStopPauseResume, +) +from app.schemas.proxmox.vm_id.create import Request_ProxmoxVmsVMID_Create, Reply_ProxmoxVmsVMID_Create +from app.schemas.proxmox.vm_id.delete import Request_ProxmoxVmsVMID_Delete, Reply_ProxmoxVmsVMID_Delete +from app.schemas.proxmox.vm_id.clone import Request_ProxmoxVmsVMID_Clone, Reply_ProxmoxVmsVMID_Clone +from app.schemas.proxmox.vm_ids.mass_start_stop_resume_pause import Request_ProxmoxVmsVmIds_MassStartStopPauseResume +from app.schemas.proxmox.vm_ids.mass_delete import Request_ProxmoxVmsVmIds_MassDelete, Reply_ProxmoxVmsVmIds_MassDelete + +logger = logging.getLogger(__name__) + +PROJECT_ROOT = Path(os.getenv("PROJECT_ROOT_DIR")).resolve() +INVENTORY_NAME = "hosts" +PLAYBOOK_SRC = PROJECT_ROOT / "playbooks" / "generic.yml" + +# --------------------------------------------------------------------------- +# Shared helpers +# --------------------------------------------------------------------------- + +def _run_proxmox_action(req, action: str, extravars: dict) -> JSONResponse: + """Common pattern for all standard Proxmox action routes.""" + extravars["proxmox_vm_action"] = action + extravars["hosts"] = "proxmox" + + if not PLAYBOOK_SRC.exists(): + raise HTTPException(status_code=400, detail=f":: err - MISSING PLAYBOOK : {PLAYBOOK_SRC}") + + inventory = utils.resolve_inventory(INVENTORY_NAME) + + rc, events, log_plain, log_ansi = run_playbook_core( + PLAYBOOK_SRC, + inventory, + limit=extravars["hosts"], + extravars=extravars, + ) + + if req.as_json: + result = extract_action_results(events, action) + payload = {"rc": rc, "result": result} + else: + payload = {"rc": rc, "log_multiline": log_plain.splitlines()} + + return JSONResponse(payload, status_code=200 if rc == 0 else 500) + + +# =========================================================================== +# Router 1: /v0/admin/proxmox/vms (list, list_usage) +# =========================================================================== +vms_router = APIRouter() + + +@vms_router.post( + path="/list", + summary="List VMs and LXC containers", + description="This endpoint retrieves all virtual machines (VMs) and LXC containers from Proxmox.", + tags=["proxmox - usage"], + response_model=Reply_ProxmoxVmList, + response_description="List VM result", +) +def proxmox_vms_list(req: Request_ProxmoxVms_VmList): + extravars = {} + if req.proxmox_node: + extravars["proxmox_node"] = req.proxmox_node + return _run_proxmox_action(req, "vm_list", extravars) + + +@vms_router.post( + path="/list_usage", + summary="Retrieve current resource usage of VMs and LXC containers", + description="Returns the current RAM, disk, and CPU usage for all virtual machines (VMs) and LXC containers.", + tags=["proxmox - usage"], + response_model=Reply_ProxmoxVms_VmListUsage, + response_description="Resource usage details", +) +def proxmox_vms_list_usage(req: Request_ProxmoxVms_VmListUsage): + extravars = {} + if req.proxmox_node: + extravars["proxmox_node"] = req.proxmox_node + return _run_proxmox_action(req, "vm_list_usage", extravars) + + +# =========================================================================== +# Router 2: /v0/admin/proxmox/vms/vm_id (lifecycle + management) +# =========================================================================== +vm_id_router = APIRouter() + + +@vm_id_router.post( + path="/start", + summary="Start a specific VM", + description="This endpoint start the target virtual machine (VM).", + tags=["proxmox - vm lifecycle"], + response_model=Reply_ProxmoxVmsVMID_StartStopPauseResume, + response_description="Start result", +) +def proxmox_vms_vm_id_start(req: Request_ProxmoxVmsVMID_StartStopPauseResume): + extravars = {"proxmox_node": req.proxmox_node} + if req.vm_id is not None: + extravars["vm_id"] = req.vm_id + return _run_proxmox_action(req, "vm_start", extravars) + + +@vm_id_router.post( + path="/stop", + summary="Stop a specific VM", + description="This endpoint stop the target virtual machine (VM).", + tags=["proxmox - vm lifecycle"], + response_model=Reply_ProxmoxVmsVMID_StartStopPauseResume, + response_description="Start result", +) +def proxmox_vms_vm_id_stop(req: Request_ProxmoxVmsVMID_StartStopPauseResume): + extravars = {"proxmox_node": req.proxmox_node} + if req.vm_id is not None: + extravars["vm_id"] = req.vm_id + return _run_proxmox_action(req, "vm_stop", extravars) + + +@vm_id_router.post( + path="/stop_force", + summary="Force stop a specific VM", + description="This endpoint force stop the target virtual machine (VM).", + tags=["proxmox - vm lifecycle"], + response_model=Reply_ProxmoxVmsVMID_StartStopPauseResume, + response_description="Start result", +) +def proxmox_vms_vm_id_stop_force(req: Request_ProxmoxVmsVMID_StartStopPauseResume): + extravars = {"proxmox_node": req.proxmox_node} + if req.vm_id is not None: + extravars["vm_id"] = req.vm_id + return _run_proxmox_action(req, "vm_stop_force", extravars) + + +@vm_id_router.post( + path="/pause", + summary="Pause a specific VM", + description="This endpoint pauses the target virtual machine (VM).", + tags=["proxmox - vm lifecycle"], + response_model=Reply_ProxmoxVmsVMID_StartStopPauseResume, + response_description="Start result", +) +def proxmox_vms_vm_id_pause(req: Request_ProxmoxVmsVMID_StartStopPauseResume): + extravars = {"proxmox_node": req.proxmox_node} + if req.vm_id is not None: + extravars["vm_id"] = req.vm_id + return _run_proxmox_action(req, "vm_pause", extravars) + + +@vm_id_router.post( + path="/resume", + summary="Resume a specific VM", + description="This endpoint resume the target virtual machine (VM).", + tags=["proxmox - vm lifecycle"], + response_model=Reply_ProxmoxVmsVMID_StartStopPauseResume, + response_description="Start result", +) +def proxmox_vms_vm_id_resume(req: Request_ProxmoxVmsVMID_StartStopPauseResume): + extravars = {"proxmox_node": req.proxmox_node} + if req.vm_id is not None: + extravars["vm_id"] = req.vm_id + return _run_proxmox_action(req, "vm_resume", extravars) + + +@vm_id_router.post( + path="/create", + summary="Create a specific VM", + description="This endpoint create the target virtual machine (VM).", + tags=["proxmox - vm management"], + response_model=Reply_ProxmoxVmsVMID_Create, + response_description="Delete result", +) +def proxmox_vms_vm_id_create(req: Request_ProxmoxVmsVMID_Create): + extravars = {"proxmox_node": req.proxmox_node} + if req.vm_id is not None: + extravars["vm_id"] = req.vm_id + if req.vm_name is not None: + extravars["vm_name"] = req.vm_name + if req.vm_cpu is not None: + extravars["vm_cpu"] = req.vm_cpu + if req.vm_cores is not None: + extravars["vm_cores"] = req.vm_cores + if req.vm_sockets is not None: + extravars["vm_sockets"] = req.vm_sockets + if req.vm_memory is not None: + extravars["vm_memory"] = req.vm_memory + if req.vm_disk_size is not None: + extravars["vm_disk_size"] = req.vm_disk_size + if req.vm_iso is not None: + extravars["vm_iso"] = req.vm_iso + return _run_proxmox_action(req, "vm_create", extravars) + + +@vm_id_router.delete( + path="/delete", + summary="Delete a specific VM", + description="This endpoint delete the target virtual machine (VM).", + tags=["proxmox - vm management"], + response_model=Reply_ProxmoxVmsVMID_Delete, + response_description="Delete result", +) +def proxmox_vms_vm_id_delete(req: Request_ProxmoxVmsVMID_Delete): + extravars = {"proxmox_node": req.proxmox_node} + if req.vm_id is not None: + extravars["vm_id"] = req.vm_id + extravars["vm_name"] = resolv_id_to_vm_name(extravars["proxmox_node"], extravars["vm_id"]) + return _run_proxmox_action(req, "vm_delete", extravars) + + +@vm_id_router.post( + path="/clone", + summary="Clone a specific VM", + description="This endpoint clone the target virtual machine (VM).", + tags=["proxmox - vm management"], + response_model=Reply_ProxmoxVmsVMID_Clone, + response_description="Delete result", +) +def proxmox_vms_vm_id_clone(req: Request_ProxmoxVmsVMID_Clone): + extravars = {"proxmox_node": req.proxmox_node} + if req.vm_id is not None: + extravars["vm_id"] = req.vm_id + extravars["vm_name"] = resolv_id_to_vm_name(extravars["proxmox_node"], extravars["vm_id"]) + if req.vm_new_id is not None: + extravars["vm_new_id"] = req.vm_new_id + if req.vm_name is not None: + extravars["vm_name"] = req.vm_name + if req.vm_description is not None: + extravars["vm_description"] = req.vm_description + return _run_proxmox_action(req, "vm_clone", extravars) + + +# =========================================================================== +# Router 3: /v0/admin/proxmox/vms/vm_ids (mass operations) +# =========================================================================== +vm_ids_router = APIRouter() + + +def _run_mass_action(req, action_name: str, proxmox_vm_action: str) -> JSONResponse: + """Helper for mass start/stop/pause/resume.""" + checked_inventory_filepath = utils.resolve_inventory(INVENTORY_NAME) + checked_playbook_filepath = utils.resolve_bundles_playbook(action_name, "public_github") + + extravars = {} + extravars["PROXMOX_VM_ACTION"] = proxmox_vm_action + if req.proxmox_node: + extravars["PROXMOX_NODE"] = req.proxmox_node + if req.vm_ids: + extravars["VM_IDS"] = req.vm_ids + + rc, events, log_plain, log_ansi = run_playbook_core( + checked_playbook_filepath, + checked_inventory_filepath, + limit=req.proxmox_node, + extravars=extravars, + ) + + if req.as_json: + result = extract_action_results(events, proxmox_vm_action) + payload = {"rc": rc, "result": result} + else: + payload = {"rc": rc, "log_multiline": log_plain.splitlines()} + + return JSONResponse(payload, status_code=200 if rc == 0 else 500) + + +_MASS_ACTION_NAME = "core/proxmox/configure/default/vms/start-stop-pause-resume-vms-vuln" + + +@vm_ids_router.post( + path="/stop", + summary="Mass stop vms ", + description="Stop all specified virtual machines", + tags=["proxmox - vm lifecycle"], + response_model=Reply_ProxmoxVmsVMID_StartStopPauseResume, +) +def proxmox_vms_vm_ids_mass_stop(req: Request_ProxmoxVmsVmIds_MassStartStopPauseResume): + return _run_mass_action(req, _MASS_ACTION_NAME, "vm_stop") + + +@vm_ids_router.post( + path="/stop_force", + summary="Mass force stop vms ", + description="Force stop all specified virtual machines", + tags=["proxmox - vm lifecycle"], + response_model=Reply_ProxmoxVmsVMID_StartStopPauseResume, +) +def proxmox_vms_vm_ids_mass_stop_force(req: Request_ProxmoxVmsVmIds_MassStartStopPauseResume): + return _run_mass_action(req, _MASS_ACTION_NAME, "vm_stop_force") + + +@vm_ids_router.post( + path="/start", + summary="Mass start vms ", + description="Start all specified virtual machines", + tags=["proxmox - vm lifecycle"], + response_model=Reply_ProxmoxVmsVMID_StartStopPauseResume, +) +def proxmox_vms_vm_ids_mass_start(req: Request_ProxmoxVmsVmIds_MassStartStopPauseResume): + return _run_mass_action(req, _MASS_ACTION_NAME, "vm_start") + + +@vm_ids_router.post( + path="/pause", + summary="Mass pause vms ", + description="Pause all specified virtual machines", + tags=["proxmox - vm lifecycle"], + response_model=Reply_ProxmoxVmsVMID_StartStopPauseResume, +) +def proxmox_vms_vm_ids_mass_pause(req: Request_ProxmoxVmsVmIds_MassStartStopPauseResume): + return _run_mass_action(req, _MASS_ACTION_NAME, "vm_pause") + + +@vm_ids_router.post( + path="/resume", + summary="Mass resume vms ", + description="Resume all specified virtual machines", + tags=["proxmox - vm lifecycle"], + response_model=Reply_ProxmoxVmsVMID_StartStopPauseResume, +) +def proxmox_vms_vm_ids_mass_resume(req: Request_ProxmoxVmsVmIds_MassStartStopPauseResume): + return _run_mass_action(req, _MASS_ACTION_NAME, "vm_resume") + + +@vm_ids_router.delete( + path="/delete", + summary="Mass delete vms ", + description="Delete all specified virtual machines", + tags=["proxmox - vm lifecycle"], + response_model=Reply_ProxmoxVmsVmIds_MassDelete, +) +def proxmox_vms_vm_ids_mass_delete(req: Request_ProxmoxVmsVmIds_MassDelete): + action_name = "core/proxmox/configure/default/vms/delete-vms-vuln" + checked_inventory_filepath = utils.resolve_inventory(INVENTORY_NAME) + checked_playbook_filepath = utils.resolve_bundles_playbook(action_name, "public_github") + + extravars = {} + if req.proxmox_node: + extravars["PROXMOX_NODE"] = req.proxmox_node + if getattr(req, "vms", None): + extravars["VMS"] = [{"ID": vm.id, "NAME": vm.name} for vm in req.vms] + + rc, events, log_plain, log_ansi = run_playbook_core( + checked_playbook_filepath, + checked_inventory_filepath, + limit=req.proxmox_node, + extravars=extravars, + ) + + if req.as_json: + extravars["proxmox_vm_action"] = "vm_delete" + result = extract_action_results(events, "vm_delete") + payload = {"rc": rc, "result": result} + else: + payload = {"rc": rc, "log_multiline": log_plain.splitlines()} + + return JSONResponse(payload, status_code=200 if rc == 0 else 500) diff --git a/tests/test_routes_registered.py b/tests/test_routes_registered.py new file mode 100644 index 0000000..c97a458 --- /dev/null +++ b/tests/test_routes_registered.py @@ -0,0 +1,27 @@ +"""Verify all routes from the golden reference are registered after consolidation.""" + +import json +from pathlib import Path + + +def test_all_routes_preserved(client): + golden_path = Path(__file__).parent / "fixtures" / "routes_golden.json" + with open(golden_path) as f: + golden_routes = json.load(f) + + resp = client.get("/docs/openapi.json") + schema = resp.json() + + registered = {} + for path, methods in schema.get("paths", {}).items(): + for method in methods: + if method.upper() in ("GET", "POST", "PUT", "DELETE", "PATCH"): + registered.setdefault(path, []).append(method.upper()) + + missing = [] + for path, methods in golden_routes.items(): + for method in methods: + if method not in registered.get(path, []): + missing.append(f"{method} {path}") + + assert not missing, f"Missing {len(missing)} routes:\n" + "\n".join(sorted(missing)) From dd62295a0d3102d4933ccda9e4d22d1a2a85f2d4 Mon Sep 17 00:00:00 2001 From: Philippe Parage Date: Thu, 19 Mar 2026 15:22:57 +0100 Subject: [PATCH 12/33] refactor: rewrite main.py with application factory pattern (#54) --- app/main.py | 296 +++++++++----------------------------- tests/test_app_factory.py | 5 + 2 files changed, 69 insertions(+), 232 deletions(-) create mode 100644 tests/test_app_factory.py diff --git a/app/main.py b/app/main.py index ff32589..a04ecc1 100644 --- a/app/main.py +++ b/app/main.py @@ -1,255 +1,87 @@ -#!/usr/bin/python +"""FastAPI application factory for the Range42 Backend API.""" -import os, shutil, stat, tempfile import logging -import json +import os +import shutil +import stat +import tempfile +from contextlib import asynccontextmanager from pathlib import Path -from fastapi import FastAPI, Request +from fastapi import FastAPI from fastapi.exceptions import RequestValidationError -from fastapi.responses import JSONResponse -from app.routes import router as api_router -from contextlib import asynccontextmanager - from starlette.middleware import Middleware from starlette.middleware.cors import CORSMiddleware -# # - -from . import vault - - -debug = True -# debug = False - - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### -# LIFESPAN ACTIONS : -# -# ACTIONS -# - unlock vault using VAULT_PASSWORD_FILE (filepath) OR VAULT_PASSWORD -# - -_VAULT_TMP_DIR: Path | None = None - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - -async def use_vault_password(vault_password: str): - global _VAULT_TMP_DIR - # create temp dir with prefix - _VAULT_TMP_DIR = Path(tempfile.mkdtemp(prefix="vault-")) - - # write / chmod vault pass file in temp dir. - f = _VAULT_TMP_DIR / "vault_pass.txt" - f.write_text(vault_password) - os.chmod(f, stat.S_IRUSR | stat.S_IWUSR) # 0600 - - # vault.set_vault_path(f) - vault.set_vault_path(f) - - print(":: lifespan :: use - VAULT_PASSWORD") - - -async def use_vault_password_file(vault_password_file: str): - p = Path(vault_password_file) - if not p.exists(): - raise RuntimeError(f":: err - vault password file not found ! :: {p}") - - vault.set_vault_path(p) +from app.core.config import settings +from app.core.exceptions import validation_exception_handler +from app.core.runner import vault_manager +from app.routes import router as api_router +from app.routes.ws_status import router as ws_router - print(f":: lifespan :: use - VAULT_PASSWORD_FILE={p}") +logger = logging.getLogger(__name__) -#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### @asynccontextmanager async def lifespan(app: FastAPI): - - global _VAULT_TMP_DIR - - vault_password_file = os.getenv("VAULT_PASSWORD_FILE") - vault_password = os.getenv("VAULT_PASSWORD") - - #### #### #### #### #### #### #### #### #### #### #### #### - if vault_password_file: - await use_vault_password_file(vault_password_file) - - elif vault_password: - await use_vault_password(vault_password) + """Manage vault password lifecycle: setup on startup, cleanup on shutdown.""" + tmp_dir: Path | None = None + + if settings.vault_password_file: + p = Path(settings.vault_password_file) + if not p.exists(): + raise RuntimeError(f"Vault password file not found: {p}") + vault_manager.set_vault_path(p) + logger.info("Using VAULT_PASSWORD_FILE=%s", p) + + elif settings.vault_password: + tmp_dir = Path(tempfile.mkdtemp(prefix="vault-")) + f = tmp_dir / "vault_pass.txt" + f.write_text(settings.vault_password) + os.chmod(f, stat.S_IRUSR | stat.S_IWUSR) + vault_manager.set_vault_path(f) + logger.info("Using VAULT_PASSWORD (temp file)") else: - print(":: lifespan :: no vault password provided !") + logger.warning("No vault password provided") - #### #### #### #### #### #### #### #### #### #### #### #### try: - yield # - + yield finally: - if _VAULT_TMP_DIR and _VAULT_TMP_DIR.exists(): - shutil.rmtree(_VAULT_TMP_DIR, ignore_errors=True) - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### -# START FAST API -#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - -logger = logging.getLogger(__name__) - -### swagger meta - -tags_metadata = [ - {"name": "proxmox", "description": "proxmox lifecycle actions"}, - {"name": "docker" , "description": "docker lifecycle actions"}, -] - - -### CORS - -# origins = [ -# "http://127.0.0.1", -# "https://127.0.0.1", -# "http://localhost", -# "https://localhost" -# ] - -## CORS Configuration -## Set CORS_ORIGIN_REGEX env var to allow additional origins. -## Default: localhost only. Example for lab network: -## CORS_ORIGIN_REGEX=^https?://(localhost|127\.0\.0\.1|\[::1\]|192\.168\.42\.\d+)(:\d+)?$ -cors_origin_regex = os.getenv( - "CORS_ORIGIN_REGEX", - r"^https?://(localhost|127\.0\.0\.1|\[::1\])(:\d+)?$" -) - -middleware = [ - Middleware( - CORSMiddleware, # type: ignore[arg-type] - allow_origin_regex=cors_origin_regex, - allow_credentials=True, - allow_methods=["GET","POST","DELETE","OPTIONS"], - allow_headers=["Content-Type","Accept","Authorization"], - max_age=600, + if tmp_dir and tmp_dir.exists(): + shutil.rmtree(tmp_dir, ignore_errors=True) + + +def create_app() -> FastAPI: + """Application factory. Creates and configures the FastAPI application.""" + middleware = [ + Middleware( + CORSMiddleware, + allow_origin_regex=settings.cors_origin_regex, + allow_credentials=True, + allow_methods=["GET", "POST", "DELETE", "OPTIONS"], + allow_headers=["Content-Type", "Accept", "Authorization"], + max_age=600, + ) + ] + + _app = FastAPI( + title="CR42 - API", + lifespan=lifespan, + docs_url="/docs/swagger", + redoc_url="/docs/redoc", + openapi_url="/docs/openapi.json", + version="v0.1", + license_info={"name": "GPLv3"}, + contact={"email": "info@digisquad.com"}, + middleware=middleware, ) -] - -# - -app = FastAPI( - title = "CR42 - API", - lifespan = lifespan, - # - docs_url = "/docs/swagger", - redoc_url = "/docs/redoc", - openapi_url = "/docs/openapi.json", - version = "v0.1", - # - license_info = { "name": "GPLv3" }, - contact = { "email": "info@digisquad.com" }, - # - middleware = middleware, - - # - # docs_url=None, # to disable - swagger ui - default location /docs - # redoc_url=None, # to disable - redoc - default location /redoc - # openapi_url=None # to disable - openapi json - default location /openapi.json - # -) - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### -# -# disable the following in production to reduce attack surface with errors. -# - -if debug: - @app.exception_handler(RequestValidationError) - async def validation_exception_handler(request: Request, exc: RequestValidationError): - """ schema validation handler with debug output """ - - # request context : method + url_path + url_query - logger.error(" :::: 422 on %s %s %s", request.method, request.url.path, request.url.query) - - body_text = "" - - try: - raw = await request.body() - - if raw: - body_text = raw.decode("utf-8", "ignore") - - except Exception: - logger.exception(" :::: Failed to read request body.") - - #### #### Get errors details - json or raw format - - if body_text.strip(): - - try: - parsed = json.loads(body_text) - logger.error(" :::: Request - body - pretty_json :\n%s", json.dumps(parsed, indent=2, ensure_ascii=False)) - - except json.JSONDecodeError: - logger.error(" :::: Request - body - raw : %s", body_text) - - else: - logger.error(" :::: Request body: ") - - - #### Post rendering error msg. - - details = [] - - for err in exc.errors(): - - err_loc = ".".join(str(p) for p in err.get("loc", [])) - err_msg = err.get("msg", "") - err_type = err.get("type", "") - err_input = err.get("input", None) - err_ctx = err.get("ctx", None) - - logger.error(" :::: field=%s | msg=%s | type=%s", err_loc, err_msg, err_type) - - if err_input is not None: - logger.error(" :::: input=%r", err_input) - - if err_ctx: - logger.error(" :::: ctx=%s", err_ctx) - - details.append({ - "field": err_loc, - "msg": err_msg, - "type": err_type, - "input": err_input, - "ctx": err_ctx, - }) - - return JSONResponse(status_code=422, content={"detail": details}) - - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### -# include routers -#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - -app.include_router(api_router) - -# WebSocket routes -from app.routes.ws_status import router as ws_router -app.include_router(ws_router) - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - -### -### SHOW DEBUG ROUTE :: list all routes ### -### - -# for r in app.router.routes: -for r in app.routes: - - try: - # GET HTTP VERBS - verbs = ",".join(sorted(r.methods)) + _app.add_exception_handler(RequestValidationError, validation_exception_handler) + _app.include_router(api_router) + _app.include_router(ws_router) - except Exception: - verbs = "" + return _app - print(f" :: routes :: {verbs:16s} {getattr(r, 'path', '')}") +app = create_app() diff --git a/tests/test_app_factory.py b/tests/test_app_factory.py new file mode 100644 index 0000000..4dbba15 --- /dev/null +++ b/tests/test_app_factory.py @@ -0,0 +1,5 @@ +def test_create_app_returns_fastapi_instance(): + from app.main import create_app + app = create_app() + assert app.title == "CR42 - API" + assert app.version == "v0.1" From cf35bbc11088937ea9b4d83a961d90e6211ddaa0 Mon Sep 17 00:00:00 2001 From: Philippe Parage Date: Thu, 19 Mar 2026 15:27:36 +0100 Subject: [PATCH 13/33] chore: remove old nested files replaced by consolidated modules (#53) Delete 120+ old route/schema/core files that are fully replaced by the new consolidated domain modules from Tasks 1.3-1.7. Update all imports in the new route files to point to app.core.runner, app.core.extractor, and the flat app.schemas.* modules instead of the old deeply nested paths. Deleted: - app/routes/v0/ (entire deeply nested route tree) - app/routes/admin_proxmox.py, admin_run.py, admin_run_bundles_core.py, admin_debug.py - app/schemas/proxmox/ (old nested schema tree) - app/schemas/bundles/core/ (old nested bundle schemas) - app/schemas/debug/ping.py (old debug schema) - app/vault/ (replaced by app/core/vault.py) - app/runner.py (replaced by app/core/runner.py) - app/extract_actions.py (replaced by app/core/extractor.py) All 59 tests pass before and after deletion. --- .gitignore | 5 +- app/extract_actions.py | 27 - app/routes/admin_debug.py | 25 - app/routes/admin_proxmox.py | 189 -- app/routes/admin_run.py | 20 - app/routes/admin_run_bundles_core.py | 66 - app/routes/bundles.py | 34 +- app/routes/debug.py | 4 +- app/routes/firewall.py | 30 +- app/routes/network.py | 18 +- app/routes/runner.py | 4 +- app/routes/snapshots.py | 14 +- app/routes/storage.py | 14 +- app/routes/v0/__init__.py | 0 app/routes/v0/admin/__init__.py | 0 app/routes/v0/admin/bundles/__init__.py | 0 .../core/linux/ubuntu/configure/add_user.py | 138 -- .../linux/ubuntu/install/basic_packages.py | 148 -- .../core/linux/ubuntu/install/docker.py | 145 -- .../linux/ubuntu/install/docker_compose.py | 145 -- .../core/linux/ubuntu/install/dot_files.py | 134 -- .../v0/admin/bundles/proxmox/__init__.py | 0 .../bundles/proxmox/configure/__init__.py | 0 .../proxmox/configure/default/__init__.py | 0 .../proxmox/configure/default/vms/__init__.py | 0 .../default/vms/create_vms_admin_default.py | 206 -- .../default/vms/create_vms_student_default.py | 202 -- .../default/vms/create_vms_vuln_default.py | 206 -- .../delete_vms_admin_vuln_student_default.py | 153 -- .../default/vms/snapshot/__init__.py | 0 .../create_vms_admin_vuln_student_default.py | 154 -- .../revert_vms_admin_vuln_student_default.py | 159 -- .../vms/start_stop_pause_resume_admin.py | 210 -- .../vms/start_stop_pause_resume_student.py | 210 -- .../vms/start_stop_pause_resume_vuln.py | 210 -- app/routes/v0/debug/__init__.py | 0 app/routes/v0/debug/_test_func.py | 56 - app/routes/v0/debug/ping.py | 107 - app/routes/v0/proxmox/__init__.py | 0 app/routes/v0/proxmox/firewall/__init__.py | 0 .../v0/proxmox/firewall/add_iptable_alias.py | 160 -- .../proxmox/firewall/apply_iptables_rules.py | 186 -- .../proxmox/firewall/delete_iptables_alias.py | 155 -- .../proxmox/firewall/delete_iptables_rule.py | 154 -- .../proxmox/firewall/disable_firewall_dc.py | 150 -- .../proxmox/firewall/disable_firewall_node.py | 147 -- .../proxmox/firewall/disable_firewall_vm.py | 154 -- .../v0/proxmox/firewall/enable_firewall_dc.py | 150 -- .../proxmox/firewall/enable_firewall_node.py | 147 -- .../v0/proxmox/firewall/enable_firewall_vm.py | 157 -- .../proxmox/firewall/list_iptables_alias.py | 150 -- .../proxmox/firewall/list_iptables_rules.py | 150 -- .../v0/proxmox/network/node/add_network.py | 174 -- .../v0/proxmox/network/node/delete_network.py | 153 -- .../v0/proxmox/network/node/list_network.py | 150 -- .../v0/proxmox/network/vm/add_network.py | 189 -- .../v0/proxmox/network/vm/delete_network.py | 153 -- .../v0/proxmox/network/vm/list_network.py | 150 -- app/routes/v0/proxmox/storage/__init__.py | 0 app/routes/v0/proxmox/storage/download_iso.py | 167 -- app/routes/v0/proxmox/storage/list.py | 150 -- .../proxmox/storage/storage_name/__init__.py | 0 .../proxmox/storage/storage_name/list_iso.py | 166 -- .../storage/storage_name/list_template.py | 163 -- app/routes/v0/proxmox/vms/__init__.py | 0 app/routes/v0/proxmox/vms/list.py | 154 -- app/routes/v0/proxmox/vms/list_usage.py | 156 -- app/routes/v0/proxmox/vms/vm_id/__init__.py | 0 app/routes/v0/proxmox/vms/vm_id/clone.py | 167 -- .../proxmox/vms/vm_id/config/vm_get_config.py | 156 -- .../vms/vm_id/config/vm_get_config_cdrom.py | 155 -- .../vms/vm_id/config/vm_get_config_cpu.py | 155 -- .../vms/vm_id/config/vm_get_config_ram.py | 155 -- .../v0/proxmox/vms/vm_id/config/vm_set_tag.py | 159 -- app/routes/v0/proxmox/vms/vm_id/create.py | 173 -- app/routes/v0/proxmox/vms/vm_id/delete.py | 153 -- app/routes/v0/proxmox/vms/vm_id/pause.py | 158 -- app/routes/v0/proxmox/vms/vm_id/resume.py | 156 -- .../proxmox/vms/vm_id/snapshots/vm_create.py | 160 -- .../proxmox/vms/vm_id/snapshots/vm_delete.py | 166 -- .../v0/proxmox/vms/vm_id/snapshots/vm_list.py | 155 -- .../proxmox/vms/vm_id/snapshots/vm_revert.py | 162 -- app/routes/v0/proxmox/vms/vm_id/start.py | 151 -- app/routes/v0/proxmox/vms/vm_id/stop.py | 154 -- app/routes/v0/proxmox/vms/vm_id/stop_force.py | 154 -- app/routes/v0/proxmox/vms/vm_ids/__init__.py | 0 .../v0/proxmox/vms/vm_ids/mass_delete.py | 142 -- .../vm_ids/mass_start_stop_pause_resume.py | 221 -- app/routes/v0/run/__init__.py | 0 app/routes/v0/run/bundles/__init__.py | 0 app/routes/v0/run/bundles/actions_run.py | 112 - app/routes/v0/run/scenarios/__init__.py | 0 app/routes/v0/run/scenarios/scenarios_run.py | 106 - app/routes/vm_config.py | 16 +- app/routes/vms.py | 23 +- app/runner.py | 246 -- .../core/linux/ubuntu/configure/add_user.py | 113 - .../linux/ubuntu/install/basic_packages.py | 131 -- .../core/linux/ubuntu/install/docker.py | 131 -- .../linux/ubuntu/install/docker_compose.py | 132 -- .../core/linux/ubuntu/install/dot_files.py | 105 - .../default/vms/create_vms_admin_default.py | 140 -- .../default/vms/create_vms_student_default.py | 109 - .../default/vms/create_vms_vuln_default.py | 128 -- .../default/vms/revert_snapshot_default.py | 52 - .../vms/start_stop_resume_pause_default.py | 45 - app/schemas/debug/ping.py | 78 - .../proxmox/firewall/add_iptable_alias.py | 103 - .../proxmox/firewall/apply_iptables_rules.py | 253 --- .../proxmox/firewall/delete_iptables_alias.py | 87 - .../proxmox/firewall/delete_iptables_rule.py | 86 - .../proxmox/firewall/disable_firewall_dc.py | 82 - .../proxmox/firewall/disable_firewall_node.py | 70 - .../proxmox/firewall/disable_firewall_vm.py | 85 - .../proxmox/firewall/enable_firewall_dc.py | 71 - .../proxmox/firewall/enable_firewall_node.py | 72 - .../proxmox/firewall/enable_firewall_vm.py | 100 - .../proxmox/firewall/list_iptables_alias.py | 81 - .../proxmox/firewall/list_iptables_rules.py | 125 -- .../proxmox/network/node_name/add_network.py | 155 -- .../network/node_name/delete_network.py | 78 - .../proxmox/network/node_name/list_network.py | 148 -- .../proxmox/network/vm_id/add_network.py | 139 -- .../proxmox/network/vm_id/delete_network.py | 87 - .../proxmox/network/vm_id/list_network.py | 81 - app/schemas/proxmox/storage/__init__.py | 0 app/schemas/proxmox/storage/download_iso.py | 119 - app/schemas/proxmox/storage/list.py | 93 - .../proxmox/storage/storage_name/list_iso.py | 89 - .../storage/storage_name/list_template.py | 87 - app/schemas/proxmox/vm_id/clone.py | 111 - .../proxmox/vm_id/config/vm_get_config.py | 104 - .../vm_id/config/vm_get_config_cdrom.py | 86 - .../proxmox/vm_id/config/vm_get_config_cpu.py | 83 - .../proxmox/vm_id/config/vm_get_config_ram.py | 80 - .../proxmox/vm_id/config/vm_set_tag.py | 89 - app/schemas/proxmox/vm_id/create.py | 161 -- app/schemas/proxmox/vm_id/delete.py | 80 - .../proxmox/vm_id/snapshot/vm_create.py | 101 - .../proxmox/vm_id/snapshot/vm_delete.py | 89 - app/schemas/proxmox/vm_id/snapshot/vm_list.py | 94 - .../proxmox/vm_id/snapshot/vm_revert.py | 89 - .../proxmox/vm_id/start_stop_resume_pause.py | 98 - app/schemas/proxmox/vm_ids/mass_delete.py | 79 - .../vm_ids/mass_start_stop_resume_pause.py | 45 - app/schemas/proxmox/vm_list.py | 86 - app/schemas/proxmox/vm_list_usage.py | 90 - app/utils/vm_id_name_resolver.py | 4 +- app/vault/__init__.py | 4 - app/vault/vault.py | 12 - ...-03-19-backend-api-restructure-refactor.md | 1979 +++++++++++++++++ 151 files changed, 2068 insertions(+), 15324 deletions(-) delete mode 100644 app/extract_actions.py delete mode 100644 app/routes/admin_debug.py delete mode 100644 app/routes/admin_proxmox.py delete mode 100644 app/routes/admin_run.py delete mode 100644 app/routes/admin_run_bundles_core.py delete mode 100644 app/routes/v0/__init__.py delete mode 100644 app/routes/v0/admin/__init__.py delete mode 100644 app/routes/v0/admin/bundles/__init__.py delete mode 100644 app/routes/v0/admin/bundles/core/linux/ubuntu/configure/add_user.py delete mode 100644 app/routes/v0/admin/bundles/core/linux/ubuntu/install/basic_packages.py delete mode 100644 app/routes/v0/admin/bundles/core/linux/ubuntu/install/docker.py delete mode 100644 app/routes/v0/admin/bundles/core/linux/ubuntu/install/docker_compose.py delete mode 100644 app/routes/v0/admin/bundles/core/linux/ubuntu/install/dot_files.py delete mode 100644 app/routes/v0/admin/bundles/proxmox/__init__.py delete mode 100644 app/routes/v0/admin/bundles/proxmox/configure/__init__.py delete mode 100644 app/routes/v0/admin/bundles/proxmox/configure/default/__init__.py delete mode 100644 app/routes/v0/admin/bundles/proxmox/configure/default/vms/__init__.py delete mode 100644 app/routes/v0/admin/bundles/proxmox/configure/default/vms/create_vms_admin_default.py delete mode 100644 app/routes/v0/admin/bundles/proxmox/configure/default/vms/create_vms_student_default.py delete mode 100644 app/routes/v0/admin/bundles/proxmox/configure/default/vms/create_vms_vuln_default.py delete mode 100644 app/routes/v0/admin/bundles/proxmox/configure/default/vms/delete_vms_admin_vuln_student_default.py delete mode 100644 app/routes/v0/admin/bundles/proxmox/configure/default/vms/snapshot/__init__.py delete mode 100644 app/routes/v0/admin/bundles/proxmox/configure/default/vms/snapshot/create_vms_admin_vuln_student_default.py delete mode 100644 app/routes/v0/admin/bundles/proxmox/configure/default/vms/snapshot/revert_vms_admin_vuln_student_default.py delete mode 100644 app/routes/v0/admin/bundles/proxmox/configure/default/vms/start_stop_pause_resume_admin.py delete mode 100644 app/routes/v0/admin/bundles/proxmox/configure/default/vms/start_stop_pause_resume_student.py delete mode 100644 app/routes/v0/admin/bundles/proxmox/configure/default/vms/start_stop_pause_resume_vuln.py delete mode 100644 app/routes/v0/debug/__init__.py delete mode 100644 app/routes/v0/debug/_test_func.py delete mode 100644 app/routes/v0/debug/ping.py delete mode 100644 app/routes/v0/proxmox/__init__.py delete mode 100644 app/routes/v0/proxmox/firewall/__init__.py delete mode 100644 app/routes/v0/proxmox/firewall/add_iptable_alias.py delete mode 100644 app/routes/v0/proxmox/firewall/apply_iptables_rules.py delete mode 100644 app/routes/v0/proxmox/firewall/delete_iptables_alias.py delete mode 100644 app/routes/v0/proxmox/firewall/delete_iptables_rule.py delete mode 100644 app/routes/v0/proxmox/firewall/disable_firewall_dc.py delete mode 100644 app/routes/v0/proxmox/firewall/disable_firewall_node.py delete mode 100644 app/routes/v0/proxmox/firewall/disable_firewall_vm.py delete mode 100644 app/routes/v0/proxmox/firewall/enable_firewall_dc.py delete mode 100644 app/routes/v0/proxmox/firewall/enable_firewall_node.py delete mode 100644 app/routes/v0/proxmox/firewall/enable_firewall_vm.py delete mode 100644 app/routes/v0/proxmox/firewall/list_iptables_alias.py delete mode 100644 app/routes/v0/proxmox/firewall/list_iptables_rules.py delete mode 100644 app/routes/v0/proxmox/network/node/add_network.py delete mode 100644 app/routes/v0/proxmox/network/node/delete_network.py delete mode 100644 app/routes/v0/proxmox/network/node/list_network.py delete mode 100644 app/routes/v0/proxmox/network/vm/add_network.py delete mode 100644 app/routes/v0/proxmox/network/vm/delete_network.py delete mode 100644 app/routes/v0/proxmox/network/vm/list_network.py delete mode 100644 app/routes/v0/proxmox/storage/__init__.py delete mode 100644 app/routes/v0/proxmox/storage/download_iso.py delete mode 100644 app/routes/v0/proxmox/storage/list.py delete mode 100644 app/routes/v0/proxmox/storage/storage_name/__init__.py delete mode 100644 app/routes/v0/proxmox/storage/storage_name/list_iso.py delete mode 100644 app/routes/v0/proxmox/storage/storage_name/list_template.py delete mode 100644 app/routes/v0/proxmox/vms/__init__.py delete mode 100644 app/routes/v0/proxmox/vms/list.py delete mode 100644 app/routes/v0/proxmox/vms/list_usage.py delete mode 100644 app/routes/v0/proxmox/vms/vm_id/__init__.py delete mode 100644 app/routes/v0/proxmox/vms/vm_id/clone.py delete mode 100644 app/routes/v0/proxmox/vms/vm_id/config/vm_get_config.py delete mode 100644 app/routes/v0/proxmox/vms/vm_id/config/vm_get_config_cdrom.py delete mode 100644 app/routes/v0/proxmox/vms/vm_id/config/vm_get_config_cpu.py delete mode 100644 app/routes/v0/proxmox/vms/vm_id/config/vm_get_config_ram.py delete mode 100644 app/routes/v0/proxmox/vms/vm_id/config/vm_set_tag.py delete mode 100644 app/routes/v0/proxmox/vms/vm_id/create.py delete mode 100644 app/routes/v0/proxmox/vms/vm_id/delete.py delete mode 100644 app/routes/v0/proxmox/vms/vm_id/pause.py delete mode 100644 app/routes/v0/proxmox/vms/vm_id/resume.py delete mode 100644 app/routes/v0/proxmox/vms/vm_id/snapshots/vm_create.py delete mode 100644 app/routes/v0/proxmox/vms/vm_id/snapshots/vm_delete.py delete mode 100644 app/routes/v0/proxmox/vms/vm_id/snapshots/vm_list.py delete mode 100644 app/routes/v0/proxmox/vms/vm_id/snapshots/vm_revert.py delete mode 100644 app/routes/v0/proxmox/vms/vm_id/start.py delete mode 100644 app/routes/v0/proxmox/vms/vm_id/stop.py delete mode 100644 app/routes/v0/proxmox/vms/vm_id/stop_force.py delete mode 100644 app/routes/v0/proxmox/vms/vm_ids/__init__.py delete mode 100644 app/routes/v0/proxmox/vms/vm_ids/mass_delete.py delete mode 100644 app/routes/v0/proxmox/vms/vm_ids/mass_start_stop_pause_resume.py delete mode 100644 app/routes/v0/run/__init__.py delete mode 100644 app/routes/v0/run/bundles/__init__.py delete mode 100644 app/routes/v0/run/bundles/actions_run.py delete mode 100644 app/routes/v0/run/scenarios/__init__.py delete mode 100644 app/routes/v0/run/scenarios/scenarios_run.py delete mode 100644 app/runner.py delete mode 100644 app/schemas/bundles/core/linux/ubuntu/configure/add_user.py delete mode 100644 app/schemas/bundles/core/linux/ubuntu/install/basic_packages.py delete mode 100644 app/schemas/bundles/core/linux/ubuntu/install/docker.py delete mode 100644 app/schemas/bundles/core/linux/ubuntu/install/docker_compose.py delete mode 100644 app/schemas/bundles/core/linux/ubuntu/install/dot_files.py delete mode 100644 app/schemas/bundles/core/proxmox/configure/default/vms/create_vms_admin_default.py delete mode 100644 app/schemas/bundles/core/proxmox/configure/default/vms/create_vms_student_default.py delete mode 100644 app/schemas/bundles/core/proxmox/configure/default/vms/create_vms_vuln_default.py delete mode 100644 app/schemas/bundles/core/proxmox/configure/default/vms/revert_snapshot_default.py delete mode 100644 app/schemas/bundles/core/proxmox/configure/default/vms/start_stop_resume_pause_default.py delete mode 100644 app/schemas/debug/ping.py delete mode 100644 app/schemas/proxmox/firewall/add_iptable_alias.py delete mode 100644 app/schemas/proxmox/firewall/apply_iptables_rules.py delete mode 100644 app/schemas/proxmox/firewall/delete_iptables_alias.py delete mode 100644 app/schemas/proxmox/firewall/delete_iptables_rule.py delete mode 100644 app/schemas/proxmox/firewall/disable_firewall_dc.py delete mode 100644 app/schemas/proxmox/firewall/disable_firewall_node.py delete mode 100644 app/schemas/proxmox/firewall/disable_firewall_vm.py delete mode 100644 app/schemas/proxmox/firewall/enable_firewall_dc.py delete mode 100644 app/schemas/proxmox/firewall/enable_firewall_node.py delete mode 100644 app/schemas/proxmox/firewall/enable_firewall_vm.py delete mode 100644 app/schemas/proxmox/firewall/list_iptables_alias.py delete mode 100644 app/schemas/proxmox/firewall/list_iptables_rules.py delete mode 100644 app/schemas/proxmox/network/node_name/add_network.py delete mode 100644 app/schemas/proxmox/network/node_name/delete_network.py delete mode 100644 app/schemas/proxmox/network/node_name/list_network.py delete mode 100644 app/schemas/proxmox/network/vm_id/add_network.py delete mode 100644 app/schemas/proxmox/network/vm_id/delete_network.py delete mode 100644 app/schemas/proxmox/network/vm_id/list_network.py delete mode 100644 app/schemas/proxmox/storage/__init__.py delete mode 100644 app/schemas/proxmox/storage/download_iso.py delete mode 100644 app/schemas/proxmox/storage/list.py delete mode 100644 app/schemas/proxmox/storage/storage_name/list_iso.py delete mode 100644 app/schemas/proxmox/storage/storage_name/list_template.py delete mode 100644 app/schemas/proxmox/vm_id/clone.py delete mode 100644 app/schemas/proxmox/vm_id/config/vm_get_config.py delete mode 100644 app/schemas/proxmox/vm_id/config/vm_get_config_cdrom.py delete mode 100644 app/schemas/proxmox/vm_id/config/vm_get_config_cpu.py delete mode 100644 app/schemas/proxmox/vm_id/config/vm_get_config_ram.py delete mode 100644 app/schemas/proxmox/vm_id/config/vm_set_tag.py delete mode 100644 app/schemas/proxmox/vm_id/create.py delete mode 100644 app/schemas/proxmox/vm_id/delete.py delete mode 100644 app/schemas/proxmox/vm_id/snapshot/vm_create.py delete mode 100644 app/schemas/proxmox/vm_id/snapshot/vm_delete.py delete mode 100644 app/schemas/proxmox/vm_id/snapshot/vm_list.py delete mode 100644 app/schemas/proxmox/vm_id/snapshot/vm_revert.py delete mode 100644 app/schemas/proxmox/vm_id/start_stop_resume_pause.py delete mode 100644 app/schemas/proxmox/vm_ids/mass_delete.py delete mode 100644 app/schemas/proxmox/vm_ids/mass_start_stop_resume_pause.py delete mode 100644 app/schemas/proxmox/vm_list.py delete mode 100644 app/schemas/proxmox/vm_list_usage.py delete mode 100644 app/vault/__init__.py delete mode 100644 app/vault/vault.py create mode 100644 docs/plans/2026-03-19-backend-api-restructure-refactor.md diff --git a/.gitignore b/.gitignore index 11a392a..e9204cd 100644 --- a/.gitignore +++ b/.gitignore @@ -43,9 +43,12 @@ out/ *.log logs/ -# misc +# misc *.swp *.swo *.tmp *.bak +# claude +CLAUDE.md + diff --git a/app/extract_actions.py b/app/extract_actions.py deleted file mode 100644 index da19934..0000000 --- a/app/extract_actions.py +++ /dev/null @@ -1,27 +0,0 @@ - - -### TEMP EXTRACTION - -def extract_action_results(events, action_to_search: str) -> list: - """ - extract actions data if runner_on_ok. - """ - - out = [] - for ev in events: - # print (ev) - if ev.get("event") == "runner_on_ok": - res = (ev.get("event_data") or {}).get("res") - if isinstance(res, dict): - if action_to_search in res: - out.append(res[action_to_search]) - - # elif "msg" in res and isinstance(res["msg"], str): - # try: - # j = json.loads(res["msg"]) - # if isinstance(j, dict) and action in j: - # out.append(j[action_to_search]) - # except Exception: - # pass - - return out \ No newline at end of file diff --git a/app/routes/admin_debug.py b/app/routes/admin_debug.py deleted file mode 100644 index bbaba40..0000000 --- a/app/routes/admin_debug.py +++ /dev/null @@ -1,25 +0,0 @@ - - - -from fastapi import APIRouter - -# -# debug routes -# -from app.routes.v0.debug.ping import router as debug_ping -from app.routes.v0.debug._test_func import router as debug_test_func - - - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - -router = APIRouter() - -# -# temp -router.include_router(debug_test_func, prefix="/v0/admin/debug") - -# -# /v0/admin/debug/ - issue #1 -# -router.include_router(debug_ping, prefix="/v0/admin/debug") \ No newline at end of file diff --git a/app/routes/admin_proxmox.py b/app/routes/admin_proxmox.py deleted file mode 100644 index 0c296e0..0000000 --- a/app/routes/admin_proxmox.py +++ /dev/null @@ -1,189 +0,0 @@ -from fastapi import APIRouter - -# -# vm list and usage - issue #2 -# -from app.routes.v0.proxmox.vms.list import router as proxmox_vms_list_router -from app.routes.v0.proxmox.vms.list_usage import router as proxmox_vms_list_usage_router - -# -# proxmox vm life cycle mgmt - issue #3 -# - -from app.routes.v0.proxmox.vms.vm_id.start import router as proxmox_vms_vm_id_start_router -from app.routes.v0.proxmox.vms.vm_id.stop import router as proxmox_vms_vm_id_stop_router -from app.routes.v0.proxmox.vms.vm_id.pause import router as proxmox_vms_vm_id_pause_router -from app.routes.v0.proxmox.vms.vm_id.resume import router as proxmox_vms_vm_id_resume_router -from app.routes.v0.proxmox.vms.vm_id.stop_force import router as proxmox_vms_vm_id_stop_force_router - -# -#vm create delete clone - issue #3 - -from app.routes.v0.proxmox.vms.vm_id.create import router as proxmox_vms_vm_id_create_router -from app.routes.v0.proxmox.vms.vm_id.delete import router as proxmox_vms_vm_id_delete_router -from app.routes.v0.proxmox.vms.vm_id.clone import router as proxmox_vms_vm_id_clone_router - -# -#config - issue #5 -# -from app.routes.v0.proxmox.vms.vm_id.config.vm_get_config import router as proxmox_vms_vm_id_vm_get_config_router -from app.routes.v0.proxmox.vms.vm_id.config.vm_get_config_cdrom import router as proxmox_vms_vm_id_vm_get_config_cdrom_router -from app.routes.v0.proxmox.vms.vm_id.config.vm_get_config_cpu import router as proxmox_vms_vm_id_vm_get_config_cpu_router -from app.routes.v0.proxmox.vms.vm_id.config.vm_get_config_ram import router as proxmox_vms_vm_id_vm_get_config_ram_router -from app.routes.v0.proxmox.vms.vm_id.config.vm_set_tag import router as proxmox_vms_vm_id_vm_set_tags_router - -# -#snapshot - issue #9 -# -from app.routes.v0.proxmox.vms.vm_id.snapshots.vm_create import router as proxmox_vms_vm_id_vm_snapshot_create_router -from app.routes.v0.proxmox.vms.vm_id.snapshots.vm_list import router as proxmox_vms_vm_id_vm_snapshot_list_router -from app.routes.v0.proxmox.vms.vm_id.snapshots.vm_delete import router as proxmox_vms_vm_id_vm_snapshot_delete_router -from app.routes.v0.proxmox.vms.vm_id.snapshots.vm_revert import router as proxmox_vms_vm_id_vm_snapshot_revert_router - -# -# storage - issue #14 -# -from app.routes.v0.proxmox.storage.storage_name.list_iso import router as proxmox_storage_with_storage_name_list_iso_router -from app.routes.v0.proxmox.storage.storage_name.list_template import router as proxmox_storage_with_storage_name_list_template_router - -from app.routes.v0.proxmox.storage.list import router as proxmox_storage_list_router -from app.routes.v0.proxmox.storage.download_iso import router as proxmox_storage_download_iso - -# -# firewall - issue #13 -# -from app.routes.v0.proxmox.firewall.add_iptable_alias import router as proxmox_firewall_add_iptables_alias_router -from app.routes.v0.proxmox.firewall.apply_iptables_rules import router as proxmox_firewall_apply_iptables_rules_router - -from app.routes.v0.proxmox.firewall.delete_iptables_alias import router as proxmox_firewall_delete_iptables_alias_router -from app.routes.v0.proxmox.firewall.delete_iptables_rule import router as proxmox_firewall_delete_iptables_rule_router - -from app.routes.v0.proxmox.firewall.disable_firewall_dc import router as proxmox_firewall_disable_firewall_dc_router -from app.routes.v0.proxmox.firewall.disable_firewall_node import router as proxmox_firewall_disable_firewall_node_router -from app.routes.v0.proxmox.firewall.disable_firewall_vm import router as proxmox_firewall_disable_firewall_vm_router - -from app.routes.v0.proxmox.firewall.enable_firewall_dc import router as proxmox_firewall_enable_firewall_dc_router -from app.routes.v0.proxmox.firewall.enable_firewall_node import router as proxmox_firewall_enable_firewall_node_router -from app.routes.v0.proxmox.firewall.enable_firewall_vm import router as proxmox_firewall_enable_firewall_vm_router - -from app.routes.v0.proxmox.firewall.list_iptables_alias import router as proxmox_firewall_list_iptables_alias_router -from app.routes.v0.proxmox.firewall.list_iptables_rules import router as proxmox_firewall_list_iptables_rules_router - - -# -# network - issue #11 -# -from app.routes.v0.proxmox.network.vm.add_network import router as proxmox_network_vm_add_network_router -from app.routes.v0.proxmox.network.vm.delete_network import router as proxmox_network_vm_delete_network_router -from app.routes.v0.proxmox.network.vm.list_network import router as proxmox_network_vm_list_network_router - -from app.routes.v0.proxmox.network.node.add_network import router as proxmox_network_node_add_network_router -from app.routes.v0.proxmox.network.node.delete_network import router as proxmox_network_node_delete_network_router -from app.routes.v0.proxmox.network.node.list_network import router as proxmox_network_node_list_network_router - - -# mass start,stop,pause,resume - - -from app.routes.v0.proxmox.vms.vm_ids.mass_start_stop_pause_resume import router as proxmox_vms_vm_id_mass_start_stop_pause_resume_router -from app.routes.v0.proxmox.vms.vm_ids.mass_delete import router as proxmox_vms_vm_id_mass_delete_router -# - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### -#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### -#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - -router = APIRouter() - - -# -# /v0/admin/proxmox/vms - issue #3 -# -router.include_router(proxmox_vms_list_router, prefix="/v0/admin/proxmox/vms") -router.include_router(proxmox_vms_list_usage_router, prefix="/v0/admin/proxmox/vms") - -# -# /v0/admin/proxmox/vms/vm_id/ - issue #3 -# -router.include_router(proxmox_vms_vm_id_start_router, prefix="/v0/admin/proxmox/vms/vm_id") -router.include_router(proxmox_vms_vm_id_stop_router, prefix="/v0/admin/proxmox/vms/vm_id") -router.include_router(proxmox_vms_vm_id_stop_force_router, prefix="/v0/admin/proxmox/vms/vm_id") -router.include_router(proxmox_vms_vm_id_pause_router, prefix="/v0/admin/proxmox/vms/vm_id") -router.include_router(proxmox_vms_vm_id_resume_router, prefix="/v0/admin/proxmox/vms/vm_id") - - -# -# /v0/admin/proxmox/vms/vm_ids/ - issue #3 derived from #30 -# -router.include_router(proxmox_vms_vm_id_mass_start_stop_pause_resume_router, prefix="/v0/admin/proxmox/vms/vm_ids") -router.include_router(proxmox_vms_vm_id_mass_delete_router, prefix="/v0/admin/proxmox/vms/vm_ids") - - -# -# /v0/admin/proxmox/firewall/ - issue #6 -# -router.include_router(proxmox_vms_vm_id_create_router, prefix="/v0/admin/proxmox/vms/vm_id") -router.include_router(proxmox_vms_vm_id_clone_router, prefix="/v0/admin/proxmox/vms/vm_id") -router.include_router(proxmox_vms_vm_id_delete_router, prefix="/v0/admin/proxmox/vms/vm_id") - -# -# /v0/admin/proxmox/firewall/ - issue #5 -# -router.include_router(proxmox_vms_vm_id_vm_get_config_router, prefix="/v0/admin/proxmox/vms/vm_id/config") -router.include_router(proxmox_vms_vm_id_vm_get_config_cdrom_router, prefix="/v0/admin/proxmox/vms/vm_id/config") -router.include_router(proxmox_vms_vm_id_vm_get_config_cpu_router, prefix="/v0/admin/proxmox/vms/vm_id/config") -router.include_router(proxmox_vms_vm_id_vm_get_config_ram_router, prefix="/v0/admin/proxmox/vms/vm_id/config") - -# -# /v0/admin/proxmox/firewall/ - issue #7 -# -router.include_router(proxmox_vms_vm_id_vm_set_tags_router, prefix="/v0/admin/proxmox/vms/vm_id/config") - -# -# /v0/admin/proxmox/firewall/ - issue #9 -# -router.include_router(proxmox_vms_vm_id_vm_snapshot_list_router, prefix="/v0/admin/proxmox/vms/vm_id/snapshot") -router.include_router(proxmox_vms_vm_id_vm_snapshot_create_router, prefix="/v0/admin/proxmox/vms/vm_id/snapshot") -router.include_router(proxmox_vms_vm_id_vm_snapshot_delete_router, prefix="/v0/admin/proxmox/vms/vm_id/snapshot") -router.include_router(proxmox_vms_vm_id_vm_snapshot_revert_router, prefix="/v0/admin/proxmox/vms/vm_id/snapshot") - -# -# /v0/admin/proxmox/storage/ - issue #17 -# -router.include_router(proxmox_storage_with_storage_name_list_iso_router, prefix="/v0/admin/proxmox/storage/storage_name") -router.include_router(proxmox_storage_with_storage_name_list_template_router, prefix="/v0/admin/proxmox/storage/storage_name") - -router.include_router(proxmox_storage_list_router, prefix="/v0/admin/proxmox/storage") -router.include_router(proxmox_storage_download_iso, prefix="/v0/admin/proxmox/storage") - -# -# /v0/admin/proxmox/firewall/ - issue #13, #12 -# -router.include_router(proxmox_firewall_list_iptables_alias_router, prefix="/v0/admin/proxmox/firewall") -router.include_router(proxmox_firewall_add_iptables_alias_router, prefix="/v0/admin/proxmox/firewall") -router.include_router(proxmox_firewall_delete_iptables_alias_router, prefix="/v0/admin/proxmox/firewall") - -router.include_router(proxmox_firewall_list_iptables_rules_router, prefix="/v0/admin/proxmox/firewall") -router.include_router(proxmox_firewall_apply_iptables_rules_router, prefix="/v0/admin/proxmox/firewall") -router.include_router(proxmox_firewall_delete_iptables_rule_router, prefix="/v0/admin/proxmox/firewall") - -router.include_router(proxmox_firewall_enable_firewall_vm_router, prefix="/v0/admin/proxmox/firewall") -router.include_router(proxmox_firewall_disable_firewall_vm_router, prefix="/v0/admin/proxmox/firewall") - -router.include_router(proxmox_firewall_enable_firewall_node_router, prefix="/v0/admin/proxmox/firewall") -router.include_router(proxmox_firewall_disable_firewall_node_router, prefix="/v0/admin/proxmox/firewall") - -router.include_router(proxmox_firewall_enable_firewall_dc_router, prefix="/v0/admin/proxmox/firewall") -router.include_router(proxmox_firewall_disable_firewall_dc_router, prefix="/v0/admin/proxmox/firewall") - -# -# /v0/admin/proxmox/firewall/ - issue #10, #11 -# -router.include_router(proxmox_network_vm_add_network_router, prefix="/v0/admin/proxmox/network") -router.include_router(proxmox_network_vm_delete_network_router, prefix="/v0/admin/proxmox/network") -router.include_router(proxmox_network_vm_list_network_router, prefix="/v0/admin/proxmox/network") - -router.include_router(proxmox_network_node_add_network_router, prefix="/v0/admin/proxmox/network") -router.include_router(proxmox_network_node_delete_network_router, prefix="/v0/admin/proxmox/network") -router.include_router(proxmox_network_node_list_network_router, prefix="/v0/admin/proxmox/network") diff --git a/app/routes/admin_run.py b/app/routes/admin_run.py deleted file mode 100644 index d717485..0000000 --- a/app/routes/admin_run.py +++ /dev/null @@ -1,20 +0,0 @@ - - -from fastapi import APIRouter - -# -# runner routes - generic -# -from app.routes.v0.run.bundles.actions_run import router as bundles_run_router -from app.routes.v0.run.scenarios.scenarios_run import router as scenarios_run_router - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - -router = APIRouter() - -# -# /v0/run/catalog/ - issue #15 -# -router.include_router(bundles_run_router, prefix="/v0/admin/run/bundles") -router.include_router(scenarios_run_router, prefix="/v0/admin/run/scenarios") - diff --git a/app/routes/admin_run_bundles_core.py b/app/routes/admin_run_bundles_core.py deleted file mode 100644 index 595ae3a..0000000 --- a/app/routes/admin_run_bundles_core.py +++ /dev/null @@ -1,66 +0,0 @@ -from fastapi import APIRouter - -# -# runner routes - bundles/core/linux/ubuntu/configure/* -# - -from app.routes.v0.admin.bundles.core.linux.ubuntu.configure.add_user import router as bundles_core_linux_ubuntu_configure_add_user_router - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - -# -# runner routes - bundles/core/linux/ubuntu/install/* -# -from app.routes.v0.admin.bundles.core.linux.ubuntu.install.dot_files import router as bundles_core_linux_ubuntu_install_dotfiles_router -from app.routes.v0.admin.bundles.core.linux.ubuntu.install.docker_compose import router as bundles_core_linux_ubuntu_install_docker_router -from app.routes.v0.admin.bundles.core.linux.ubuntu.install.docker import router as bundles_core_linux_ubuntu_install_docker_compose_router -from app.routes.v0.admin.bundles.core.linux.ubuntu.install.basic_packages import router as bundles_core_linux_ubuntu_install_basic_packages_router - - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - -router = APIRouter() - -# -# /v0/admin/run/bundles/core/linux/ubuntu/install/ -# -router.include_router(bundles_core_linux_ubuntu_install_dotfiles_router, prefix="/v0/admin/run/bundles") #ISSUE-28 -router.include_router(bundles_core_linux_ubuntu_install_docker_router, prefix="/v0/admin/run/bundles") #ISSUE-22 -router.include_router(bundles_core_linux_ubuntu_install_docker_compose_router, prefix="/v0/admin/run/bundles") #ISSUE-21 -router.include_router(bundles_core_linux_ubuntu_install_basic_packages_router, prefix="/v0/admin/run/bundles") #ISSUE-28 - -# -# /v0/admin/run/bundles/core/linux/ubuntu/configure/ -# -router.include_router(bundles_core_linux_ubuntu_configure_add_user_router, prefix="/v0/admin/run/bundles") #ISSUE-19 - - -# -# proxmox/configure/default/create-vms* -# - -from app.routes.v0.admin.bundles.proxmox.configure.default.vms.create_vms_admin_default import router as bundles_proxmox_configure_default_vms_create_admin_default_router # -from app.routes.v0.admin.bundles.proxmox.configure.default.vms.create_vms_vuln_default import router as bundles_proxmox_configure_default_vms_create_vuln_default_router # -from app.routes.v0.admin.bundles.proxmox.configure.default.vms.create_vms_student_default import router as bundles_proxmox_configure_default_vms_create_student_default_router # -from app.routes.v0.admin.bundles.proxmox.configure.default.vms.start_stop_pause_resume_admin import router as bundles_proxmox_configure_default_vms_start_stop_resume_pause_default_admin_router # ISSUE 30 -from app.routes.v0.admin.bundles.proxmox.configure.default.vms.start_stop_pause_resume_vuln import router as bundles_proxmox_configure_default_vms_start_stop_resume_pause_default_vuln_router # -from app.routes.v0.admin.bundles.proxmox.configure.default.vms.start_stop_pause_resume_student import router as bundles_proxmox_configure_default_vms_start_stop_resume_pause_default_student_router # - -from app.routes.v0.admin.bundles.proxmox.configure.default.vms.delete_vms_admin_vuln_student_default import router as bundles_proxmox_configure_default_vms_delete_vms_admin_vuln_student_default_router # - -from app.routes.v0.admin.bundles.proxmox.configure.default.vms.snapshot.create_vms_admin_vuln_student_default import router as bundles_proxmox_configure_default_vms_create_vms_admin_vuln_student_default_router # ISSUE 30 -from app.routes.v0.admin.bundles.proxmox.configure.default.vms.snapshot.revert_vms_admin_vuln_student_default import router as bundles_proxmox_configure_default_vms_revert_vms_admin_vuln_student_default_router # - -router.include_router(bundles_proxmox_configure_default_vms_create_admin_default_router, prefix="/v0/admin/run/bundles") # -router.include_router(bundles_proxmox_configure_default_vms_create_vuln_default_router, prefix="/v0/admin/run/bundles") # ISSUE 30 -router.include_router(bundles_proxmox_configure_default_vms_create_student_default_router, prefix="/v0/admin/run/bundles") # - -# mass start, stop, pause, resume :: admin | student | vuln vms -router.include_router(bundles_proxmox_configure_default_vms_start_stop_resume_pause_default_admin_router, prefix="/v0/admin/run/bundles") # -router.include_router(bundles_proxmox_configure_default_vms_start_stop_resume_pause_default_vuln_router, prefix="/v0/admin/run/bundles") # ISSUE 30 -router.include_router(bundles_proxmox_configure_default_vms_start_stop_resume_pause_default_student_router, prefix="/v0/admin/run/bundles") # - -router.include_router(bundles_proxmox_configure_default_vms_delete_vms_admin_vuln_student_default_router, prefix="/v0/admin/run/bundles") # - -router.include_router(bundles_proxmox_configure_default_vms_create_vms_admin_vuln_student_default_router, prefix="/v0/admin/run/bundles") # ISSUE 30 -router.include_router(bundles_proxmox_configure_default_vms_revert_vms_admin_vuln_student_default_router, prefix="/v0/admin/run/bundles") # \ No newline at end of file diff --git a/app/routes/bundles.py b/app/routes/bundles.py index 06fa18a..bb14ace 100644 --- a/app/routes/bundles.py +++ b/app/routes/bundles.py @@ -12,26 +12,26 @@ from fastapi import APIRouter, HTTPException from fastapi.responses import JSONResponse -from app.runner import run_playbook_core -from app.extract_actions import extract_action_results +from app.core.runner import run_playbook_core +from app.core.extractor import extract_action_results from app import utils # --- Linux/Ubuntu bundle schemas --- -from app.schemas.bundles.core.linux.ubuntu.install.docker import Request_BundlesCoreLinuxUbuntuInstall_Docker, Reply_BundlesCoreLinuxUbuntuInstall_Docker -from app.schemas.bundles.core.linux.ubuntu.install.docker_compose import Request_BundlesCoreLinuxUbuntuInstall_DockerCompose, Reply_BundlesCoreLinuxUbuntuInstall_DockerCompose -from app.schemas.bundles.core.linux.ubuntu.install.basic_packages import Request_BundlesCoreLinuxUbuntuInstall_BasicPackages, Reply_BundlesCoreLinuxUbuntuInstall_BasicPackages -from app.schemas.bundles.core.linux.ubuntu.install.dot_files import Request_BundlesCoreLinuxUbuntuInstall_DotFiles, Reply_BundlesCoreLinuxUbuntuInstall_DotFilesItem -from app.schemas.bundles.core.linux.ubuntu.configure.add_user import Request_BundlesCoreLinuxUbuntuConfigure_AddUser, Reply_BundlesCoreLinuxUbuntuConfigure_AddUser - -# --- Proxmox bundle schemas --- -from app.schemas.bundles.core.proxmox.configure.default.vms.create_vms_admin_default import Request_BundlesCoreProxmoxConfigureDefaultVms_CreateAdminVms, Reply_BundlesCoreProxmoxConfigureDefaultVms_CreateAdminVms -from app.schemas.bundles.core.proxmox.configure.default.vms.create_vms_vuln_default import Request_BundlesCoreProxmoxConfigureDefaultVms_CreateVulnVms, Reply_BundlesCoreProxmoxConfigureDefaultVms_CreateVulnVms -from app.schemas.bundles.core.proxmox.configure.default.vms.create_vms_student_default import Request_BundlesCoreProxmoxConfigureDefaultVms_CreateStudentVms, Reply_BundlesCoreProxmoxConfigureDefaultVms_CreateStudentVms -from app.schemas.bundles.core.proxmox.configure.default.vms.start_stop_resume_pause_default import Request_BundlesCoreProxmoxConfigureDefaultVms_StartStopPauseResumeAdminVulnStudentVms -from app.schemas.bundles.core.proxmox.configure.default.vms.revert_snapshot_default import Request_BundlesCoreProxmoxConfigureDefaultVms_RevertSnapshotAdminVulnStudentVms -from app.schemas.proxmox.vm_id.start_stop_resume_pause import Reply_ProxmoxVmsVMID_StartStopPauseResume -from app.schemas.proxmox.vm_id.snapshot.vm_create import Reply_ProxmoxVmsVMID_CreateSnapshot -from app.schemas.proxmox.vm_id.snapshot.vm_revert import Reply_ProxmoxVmsVMID_RevertSnapshot +from app.schemas.bundles import ( + Request_BundlesCoreLinuxUbuntuInstall_Docker, Reply_BundlesCoreLinuxUbuntuInstall_Docker, + Request_BundlesCoreLinuxUbuntuInstall_DockerCompose, Reply_BundlesCoreLinuxUbuntuInstall_DockerCompose, + Request_BundlesCoreLinuxUbuntuInstall_BasicPackages, Reply_BundlesCoreLinuxUbuntuInstall_BasicPackages, + Request_BundlesCoreLinuxUbuntuInstall_DotFiles, Reply_BundlesCoreLinuxUbuntuInstall_DotFilesItem, + Request_BundlesCoreLinuxUbuntuConfigure_AddUser, Reply_BundlesCoreLinuxUbuntuConfigure_AddUser, + # --- Proxmox bundle schemas --- + Request_BundlesCoreProxmoxConfigureDefaultVms_CreateAdminVms, Reply_BundlesCoreProxmoxConfigureDefaultVms_CreateAdminVms, + Request_BundlesCoreProxmoxConfigureDefaultVms_CreateVulnVms, Reply_BundlesCoreProxmoxConfigureDefaultVms_CreateVulnVms, + Request_BundlesCoreProxmoxConfigureDefaultVms_CreateStudentVms, Reply_BundlesCoreProxmoxConfigureDefaultVms_CreateStudentVms, + Request_BundlesCoreProxmoxConfigureDefaultVms_StartStopPauseResumeAdminVulnStudentVms, + Request_BundlesCoreProxmoxConfigureDefaultVms_RevertSnapshotAdminVulnStudentVms, +) +from app.schemas.vms import Reply_ProxmoxVmsVMID_StartStopPauseResume +from app.schemas.snapshots import Reply_ProxmoxVmsVMID_CreateSnapshot, Reply_ProxmoxVmsVMID_RevertSnapshot logger = logging.getLogger(__name__) diff --git a/app/routes/debug.py b/app/routes/debug.py index 233516f..82fce05 100644 --- a/app/routes/debug.py +++ b/app/routes/debug.py @@ -11,8 +11,8 @@ from fastapi import APIRouter, HTTPException from fastapi.responses import JSONResponse -from app.runner import run_playbook_core -from app.schemas.debug.ping import Request_DebugPing +from app.core.runner import run_playbook_core +from app.schemas.debug import Request_DebugPing from app.utils.vm_id_name_resolver import * logger = logging.getLogger(__name__) diff --git a/app/routes/firewall.py b/app/routes/firewall.py index 183a92e..32a187a 100644 --- a/app/routes/firewall.py +++ b/app/routes/firewall.py @@ -10,23 +10,25 @@ from fastapi import APIRouter, HTTPException from fastapi.responses import JSONResponse -from app.runner import run_playbook_core -from app.extract_actions import extract_action_results +from app.core.runner import run_playbook_core +from app.core.extractor import extract_action_results from app.utils.vm_id_name_resolver import resolv_id_to_vm_name from app import utils -from app.schemas.proxmox.firewall.list_iptables_alias import Request_ProxmoxFirewall_ListIptablesAlias, Reply_ProxmoxFirewallWithStorageName_ListIptablesAlias -from app.schemas.proxmox.firewall.add_iptable_alias import Request_ProxmoxFirewall_AddIptablesAlias, Reply_ProxmoxFirewallWithStorageName_AddIptablesAlias -from app.schemas.proxmox.firewall.delete_iptables_alias import Request_ProxmoxFirewall_DeleteIptablesAlias, Reply_ProxmoxFirewallWithStorageName_DeleteIptablesAlias -from app.schemas.proxmox.firewall.list_iptables_rules import Request_ProxmoxFirewall_ListIptablesRules, Reply_ProxmoxFirewallWithStorageName_ListIptablesRules -from app.schemas.proxmox.firewall.apply_iptables_rules import Request_ProxmoxFirewall_ApplyIptablesRules, Reply_ProxmoxFirewallWithStorageName_ApplyIptablesRules -from app.schemas.proxmox.firewall.delete_iptables_rule import Request_ProxmoxFirewall_DeleteIptablesRule -from app.schemas.proxmox.firewall.enable_firewall_vm import Request_ProxmoxFirewall_EnableFirewallVm, Reply_ProxmoxFirewallWithStorageName_EnableFirewallVm -from app.schemas.proxmox.firewall.disable_firewall_vm import Request_ProxmoxFirewall_DistableFirewallVm, Reply_ProxmoxFirewallWithStorageName_DisableFirewallVm -from app.schemas.proxmox.firewall.enable_firewall_node import Request_ProxmoxFirewall_EnableFirewallNode, Reply_ProxmoxFirewallWithStorageName_EnableFirewallNode -from app.schemas.proxmox.firewall.disable_firewall_node import Request_ProxmoxFirewall_DistableFirewallNode, Reply_ProxmoxFirewallWithStorageName_DisableFirewallNode -from app.schemas.proxmox.firewall.enable_firewall_dc import Request_ProxmoxFirewall_EnableFirewallDc, Reply_ProxmoxFirewallWithStorageName_EnableFirewallDc -from app.schemas.proxmox.firewall.disable_firewall_dc import Request_ProxmoxFirewall_DisableFirewallDc, Reply_ProxmoxFirewallWithStorageName_DisableFirewallDc +from app.schemas.firewall import ( + Request_ProxmoxFirewall_ListIptablesAlias, Reply_ProxmoxFirewallWithStorageName_ListIptablesAlias, + Request_ProxmoxFirewall_AddIptablesAlias, Reply_ProxmoxFirewallWithStorageName_AddIptablesAlias, + Request_ProxmoxFirewall_DeleteIptablesAlias, Reply_ProxmoxFirewallWithStorageName_DeleteIptablesAlias, + Request_ProxmoxFirewall_ListIptablesRules, Reply_ProxmoxFirewallWithStorageName_ListIptablesRules, + Request_ProxmoxFirewall_ApplyIptablesRules, Reply_ProxmoxFirewallWithStorageName_ApplyIptablesRules, + Request_ProxmoxFirewall_DeleteIptablesRule, + Request_ProxmoxFirewall_EnableFirewallVm, Reply_ProxmoxFirewallWithStorageName_EnableFirewallVm, + Request_ProxmoxFirewall_DistableFirewallVm, Reply_ProxmoxFirewallWithStorageName_DisableFirewallVm, + Request_ProxmoxFirewall_EnableFirewallNode, Reply_ProxmoxFirewallWithStorageName_EnableFirewallNode, + Request_ProxmoxFirewall_DistableFirewallNode, Reply_ProxmoxFirewallWithStorageName_DisableFirewallNode, + Request_ProxmoxFirewall_EnableFirewallDc, Reply_ProxmoxFirewallWithStorageName_EnableFirewallDc, + Request_ProxmoxFirewall_DisableFirewallDc, Reply_ProxmoxFirewallWithStorageName_DisableFirewallDc, +) logger = logging.getLogger(__name__) diff --git a/app/routes/network.py b/app/routes/network.py index fdc30b6..5ec52a6 100644 --- a/app/routes/network.py +++ b/app/routes/network.py @@ -10,16 +10,18 @@ from fastapi import APIRouter, HTTPException from fastapi.responses import JSONResponse -from app.runner import run_playbook_core -from app.extract_actions import extract_action_results +from app.core.runner import run_playbook_core +from app.core.extractor import extract_action_results from app import utils -from app.schemas.proxmox.network.vm_id.add_network import Request_ProxmoxNetwork_WithVmId_AddNetwork, Reply_ProxmoxNetwork_WithVmId_AddNetworkInterface -from app.schemas.proxmox.network.vm_id.delete_network import Request_ProxmoxNetwork_WithVmId_DeleteNetwork, Reply_ProxmoxNetwork_WithVmId_DeleteNetworkInterface -from app.schemas.proxmox.network.vm_id.list_network import Request_ProxmoxNetwork_WithVmId_ListNetwork, Reply_ProxmoxNetwork_WithVmId_ListNetworkInterface -from app.schemas.proxmox.network.node_name.add_network import Request_ProxmoxNetwork_WithNodeName_AddNetworkInterface, Reply_ProxmoxNetwork_WithNodeName_AddNetworkInterface -from app.schemas.proxmox.network.node_name.delete_network import Request_ProxmoxNetwork_WithNodeName_DeleteInterface, Reply_ProxmoxNetwork_WithNodeName_DeleteInterface -from app.schemas.proxmox.network.node_name.list_network import Request_ProxmoxNetwork_WithNodeName_ListInterface, Reply_ProxmoxNetwork_WithNodeName_ListInterface +from app.schemas.network import ( + Request_ProxmoxNetwork_WithVmId_AddNetwork, Reply_ProxmoxNetwork_WithVmId_AddNetworkInterface, + Request_ProxmoxNetwork_WithVmId_DeleteNetwork, Reply_ProxmoxNetwork_WithVmId_DeleteNetworkInterface, + Request_ProxmoxNetwork_WithVmId_ListNetwork, Reply_ProxmoxNetwork_WithVmId_ListNetworkInterface, + Request_ProxmoxNetwork_WithNodeName_AddNetworkInterface, Reply_ProxmoxNetwork_WithNodeName_AddNetworkInterface, + Request_ProxmoxNetwork_WithNodeName_DeleteInterface, Reply_ProxmoxNetwork_WithNodeName_DeleteInterface, + Request_ProxmoxNetwork_WithNodeName_ListInterface, Reply_ProxmoxNetwork_WithNodeName_ListInterface, +) logger = logging.getLogger(__name__) diff --git a/app/routes/runner.py b/app/routes/runner.py index 6824a2f..1239cbe 100644 --- a/app/routes/runner.py +++ b/app/routes/runner.py @@ -12,8 +12,8 @@ from fastapi import APIRouter from fastapi.responses import JSONResponse -from app.runner import run_playbook_core -from app.schemas.debug.ping import Request_DebugPing +from app.core.runner import run_playbook_core +from app.schemas.debug import Request_DebugPing from app import utils logger = logging.getLogger(__name__) diff --git a/app/routes/snapshots.py b/app/routes/snapshots.py index 75703cf..8959bc1 100644 --- a/app/routes/snapshots.py +++ b/app/routes/snapshots.py @@ -10,15 +10,17 @@ from fastapi import APIRouter, HTTPException from fastapi.responses import JSONResponse -from app.runner import run_playbook_core -from app.extract_actions import extract_action_results +from app.core.runner import run_playbook_core +from app.core.extractor import extract_action_results from app.utils.vm_id_name_resolver import resolv_id_to_vm_name from app import utils -from app.schemas.proxmox.vm_id.snapshot.vm_list import Request_ProxmoxVmsVMID_ListSnapshot, Reply_ProxmoxVmsVMID_ListSnapshot -from app.schemas.proxmox.vm_id.snapshot.vm_create import Request_ProxmoxVmsVMID_CreateSnapshot, Reply_ProxmoxVmsVMID_CreateSnapshot -from app.schemas.proxmox.vm_id.snapshot.vm_delete import Request_ProxmoxVmsVMID_DeleteSnapshot, Reply_ProxmoxVmsVMID_DeleteSnapshot -from app.schemas.proxmox.vm_id.snapshot.vm_revert import Request_ProxmoxVmsVMID_RevertSnapshot, Reply_ProxmoxVmsVMID_RevertSnapshot +from app.schemas.snapshots import ( + Request_ProxmoxVmsVMID_ListSnapshot, Reply_ProxmoxVmsVMID_ListSnapshot, + Request_ProxmoxVmsVMID_CreateSnapshot, Reply_ProxmoxVmsVMID_CreateSnapshot, + Request_ProxmoxVmsVMID_DeleteSnapshot, Reply_ProxmoxVmsVMID_DeleteSnapshot, + Request_ProxmoxVmsVMID_RevertSnapshot, Reply_ProxmoxVmsVMID_RevertSnapshot, +) logger = logging.getLogger(__name__) diff --git a/app/routes/storage.py b/app/routes/storage.py index 07ede5f..4e744ce 100644 --- a/app/routes/storage.py +++ b/app/routes/storage.py @@ -10,14 +10,16 @@ from fastapi import APIRouter, HTTPException from fastapi.responses import JSONResponse -from app.runner import run_playbook_core -from app.extract_actions import extract_action_results +from app.core.runner import run_playbook_core +from app.core.extractor import extract_action_results from app import utils -from app.schemas.proxmox.storage.list import Request_ProxmoxStorage_List, Reply_ProxmoxStorage_ListItem -from app.schemas.proxmox.storage.download_iso import Request_ProxmoxStorage_DownloadIso, Reply_ProxmoxStorage_DownloadIsoItem -from app.schemas.proxmox.storage.storage_name.list_iso import Request_ProxmoxStorage_ListIso, Reply_ProxmoxStorageWithStorageName_ListIsoItem -from app.schemas.proxmox.storage.storage_name.list_template import Request_ProxmoxStorage_ListTemplate, Reply_ProxmoxStorageWithStorageName_ListTemplate +from app.schemas.storage import ( + Request_ProxmoxStorage_List, Reply_ProxmoxStorage_ListItem, + Request_ProxmoxStorage_DownloadIso, Reply_ProxmoxStorage_DownloadIsoItem, + Request_ProxmoxStorage_ListIso, Reply_ProxmoxStorageWithStorageName_ListIsoItem, + Request_ProxmoxStorage_ListTemplate, Reply_ProxmoxStorageWithStorageName_ListTemplate, +) logger = logging.getLogger(__name__) diff --git a/app/routes/v0/__init__.py b/app/routes/v0/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/routes/v0/admin/__init__.py b/app/routes/v0/admin/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/routes/v0/admin/bundles/__init__.py b/app/routes/v0/admin/bundles/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/routes/v0/admin/bundles/core/linux/ubuntu/configure/add_user.py b/app/routes/v0/admin/bundles/core/linux/ubuntu/configure/add_user.py deleted file mode 100644 index 1b52c77..0000000 --- a/app/routes/v0/admin/bundles/core/linux/ubuntu/configure/add_user.py +++ /dev/null @@ -1,138 +0,0 @@ -from typing import Any -import logging -from fastapi import APIRouter, HTTPException -from fastapi.responses import JSONResponse - -from app.schemas.bundles.core.linux.ubuntu.configure.add_user import Request_BundlesCoreLinuxUbuntuConfigure_AddUser -from app.schemas.bundles.core.linux.ubuntu.configure.add_user import Reply_BundlesCoreLinuxUbuntuConfigure_AddUser - -from app.runner import run_playbook_core # , extract_action_results -from app.extract_actions import extract_action_results -from app import utils -from pathlib import Path -import os - -# -# ISSUE - #19 -# - -debug = 0 - -router = APIRouter() -logger = logging.getLogger(__name__) - -# PROJECT_ROOT = Path(__file__).resolve().parents[5] -PROJECT_ROOT = Path(os.getenv("PROJECT_ROOT_DIR")).resolve() -INVENTORY_NAME = "hosts" - -# -# => /v0/admin/run/actions/core/linux/ubuntu/configure/add-user -# -@router.post( - path="/core/linux/ubuntu/configure/add-user", - summary="Add system user", - description="Create a new user with shell, home and password", - tags=["bundles - core - ubuntu "], - response_model=Reply_BundlesCoreLinuxUbuntuConfigure_AddUser, - # response_description="AAAAAAAAAAAAAAAAAA", -) -def bundles_core_linux_ubuntu_install_dotfiles_router( req: Request_BundlesCoreLinuxUbuntuConfigure_AddUser): - - action_name = "core/linux/ubuntu/configure/add-user" - # - checked_inventory_filepath = utils.resolve_inventory(INVENTORY_NAME) - # checked_playbook_filepath = utils.resolve_actions_playbook(action_name, "public_github") - checked_playbook_filepath = utils.resolve_bundles_playbook(action_name, "public_github") - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - if debug ==1: - print(":: ACTION_NAME", action_name) - print(":: REQUEST ::", req.model_dump()) - print(f":: PROJECT_ROOT :: {PROJECT_ROOT} ") - print(f":: checked_inventory_filepath :: {checked_inventory_filepath} ") - print(f":: checked_playbook_filepath :: {checked_playbook_filepath} ") - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - extravars = request_checks(req) - - #### - - rc, events, log_plain, log_ansi = run_playbook_core( - checked_playbook_filepath, - checked_inventory_filepath, - limit=req.hosts, - extravars=extravars, - ) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - payload = reply_processing(log_plain, rc) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - if rc == 0: - status = 200 - else: - status = 500 - - return JSONResponse(payload, status_code=status) - - -def reply_processing(log_plain: str, - rc - ) -> dict[str, list[str] | Any]: - - """ reply post-processing - for ansible raw output """ - - payload = {"rc": rc, "log_multiline": log_plain.splitlines()} - - return payload - - -def request_checks(req: Request_BundlesCoreLinuxUbuntuConfigure_AddUser) -> dict[Any, Any]: - """ request checks """ - - extravars = {} - - if req.proxmox_node: - extravars["proxmox_node"] = req.proxmox_node - - # moved to --limit switch - # if req.hosts is not None: - # extravars["hosts"] = req.hosts - - ##### - - if req.user is not None: - extravars["TARGET_USER"] = req.user - - if req.password is not None: - extravars["TARGET_PASSWORD"] = req.password - - if req.shell_path is not None: - extravars["TARGET_SHELL_PATH"] = req.shell_path - - if req.change_pwd_at_logon is not None: - extravars["CHANGE_PWD_AT_LOGON"] = req.change_pwd_at_logon - - # nothing : - - if not extravars: - extravars = None - - if debug == 1: - print(f", extra vars : {extravars}") - - - # extravars["hosts"] = "proxmox" - - return extravars - - - diff --git a/app/routes/v0/admin/bundles/core/linux/ubuntu/install/basic_packages.py b/app/routes/v0/admin/bundles/core/linux/ubuntu/install/basic_packages.py deleted file mode 100644 index 633799f..0000000 --- a/app/routes/v0/admin/bundles/core/linux/ubuntu/install/basic_packages.py +++ /dev/null @@ -1,148 +0,0 @@ -from typing import Any -import logging -from fastapi import APIRouter, HTTPException -from fastapi.responses import JSONResponse - -from app.schemas.bundles.core.linux.ubuntu.install.basic_packages import Request_BundlesCoreLinuxUbuntuInstall_BasicPackages -from app.schemas.bundles.core.linux.ubuntu.install.basic_packages import Reply_BundlesCoreLinuxUbuntuInstall_BasicPackages - -from app.runner import run_playbook_core # , extract_action_results -from app.extract_actions import extract_action_results -from app import utils -from pathlib import Path -import os - -# -# ISSUE - #20 -# - -debug = 0 - -router = APIRouter() -logger = logging.getLogger(__name__) - -# PROJECT_ROOT = Path(__file__).resolve().parents[5] -PROJECT_ROOT = Path(os.getenv("PROJECT_ROOT_DIR")).resolve() -INVENTORY_NAME = "hosts" - -# -# => /v0/admin/run/actions/core/linux/ubuntu/install/basic-packages -# -@router.post( - path="/core/linux/ubuntu/install/basic-packages", - summary="Install basics packages", - description="Install and configure a base set of packages on the target Ubuntu system", - tags=["bundles - core - ubuntu "], - response_model=Reply_BundlesCoreLinuxUbuntuInstall_BasicPackages, - # response_description="AAAAAAAAAAAAAAAAAA", -) -def bundles_core_linux_ubuntu_install_basic_package_router( req: Request_BundlesCoreLinuxUbuntuInstall_BasicPackages): - - action_name = "core/linux/ubuntu/install/basic-packages" - # - checked_inventory_filepath = utils.resolve_inventory(INVENTORY_NAME) - # checked_playbook_filepath = utils.resolve_actions_playbook(action_name, "public_github") - checked_playbook_filepath = utils.resolve_bundles_playbook(action_name, "public_github") - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - if debug ==1: - print(":: ACTION_NAME", action_name) - print(":: REQUEST ::", req.model_dump()) - print(f":: PROJECT_ROOT :: {PROJECT_ROOT} ") - print(f":: checked_inventory_filepath :: {checked_inventory_filepath} ") - print(f":: checked_playbook_filepath :: {checked_playbook_filepath} ") - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - extravars = request_checks(req) - - #### - - rc, events, log_plain, log_ansi = run_playbook_core( - checked_playbook_filepath, - checked_inventory_filepath, - limit=req.hosts, - extravars=extravars, - ) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - payload = reply_processing(log_plain, rc) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - if rc == 0: - status = 200 - else: - status = 500 - - return JSONResponse(payload, status_code=status) - - -def reply_processing(log_plain: str, - rc - ) -> dict[str, list[str] | Any]: - - """ reply post-processing - for ansible raw output """ - - payload = {"rc": rc, "log_multiline": log_plain.splitlines()} - - return payload - - -def request_checks(req: Request_BundlesCoreLinuxUbuntuInstall_BasicPackages) -> dict[Any, Any]: - """ request checks """ - - extravars = {} - - if req.proxmox_node: - extravars["proxmox_node"] = req.proxmox_node - - # moved to --limit switch - # if req.hosts is not None: - # extravars["hosts"] = req.hosts - - # - - if req.install_package_basics is not None: - extravars["INSTALL_PACKAGES_BASICS"] = req.install_package_basics - - if req.install_package_firewalls is not None: - extravars["INSTALL_PACKAGES_FIREWALLS"] = req.install_package_firewalls - - if req.install_package_docker is not None: - extravars["INSTALL_PACKAGES_DOCKER"] = req.install_package_docker - - if req.install_package_docker_compose is not None: - extravars["INSTALL_PACKAGES_DOCKER_COMPOSE"] = req.install_package_docker_compose - - if req.install_package_utils_json is not None: - extravars["INSTALL_PACKAGES_UTILS_JSON"] = req.install_package_utils_json - - if req.install_package_utils_network is not None: - extravars["INSTALL_PACKAGES_UTILS_NETWORK"] = req.install_package_utils_network - - if req.install_ntpclient_and_update_time is not None: - extravars["INSTALL_PACKAGES_NTP_AND_UPDATE_TIME"] = req.install_ntpclient_and_update_time - - if req.packages_cleaning is not None: - extravars["SPECIFIC_PACKAGES_CLEANING"] = req.packages_cleaning - - - # nothing : - if not extravars: - extravars = None - - if debug == 1: - print(f", extra vars : {extravars}") - - - # extravars["hosts"] = "proxmox" - - return extravars - diff --git a/app/routes/v0/admin/bundles/core/linux/ubuntu/install/docker.py b/app/routes/v0/admin/bundles/core/linux/ubuntu/install/docker.py deleted file mode 100644 index 6e2e668..0000000 --- a/app/routes/v0/admin/bundles/core/linux/ubuntu/install/docker.py +++ /dev/null @@ -1,145 +0,0 @@ -from typing import Any -import logging -from fastapi import APIRouter, HTTPException -from fastapi.responses import JSONResponse - -from app.schemas.bundles.core.linux.ubuntu.install.docker import Request_BundlesCoreLinuxUbuntuInstall_Docker -from app.schemas.bundles.core.linux.ubuntu.install.docker import Reply_BundlesCoreLinuxUbuntuInstall_Docker - -from app.runner import run_playbook_core # , extract_action_results -from app.extract_actions import extract_action_results -from app import utils -from pathlib import Path -import os - -# -# ISSUE - #21 -# - -debug = 0 - -router = APIRouter() -logger = logging.getLogger(__name__) - -# PROJECT_ROOT = Path(__file__).resolve().parents[5] -PROJECT_ROOT = Path(os.getenv("PROJECT_ROOT_DIR")).resolve() -INVENTORY_NAME = "hosts" - -# -# => /v0/admin/run/actions/core/linux/ubuntu/install/docker -# -@router.post( - path="/core/linux/ubuntu/install/docker", - summary="Install docker packages", - description="Install and configure docker engine on the target ubuntu system", - tags=["bundles - core - ubuntu "], - response_model=Reply_BundlesCoreLinuxUbuntuInstall_Docker, - # response_description="AAAAAAAAAAAAAAAAAA", -) -def bundles_core_linux_ubuntu_install_docker_router(req: Request_BundlesCoreLinuxUbuntuInstall_Docker): - - action_name = "core/linux/ubuntu/install/docker" - # - checked_inventory_filepath = utils.resolve_inventory(INVENTORY_NAME) -# checked_playbook_filepath = utils.resolve_actions_playbook(action_name, "public_github") - checked_playbook_filepath = utils.resolve_bundles_playbook(action_name, "public_github") - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - if debug == 1: - print(":: ACTION_NAME", action_name) - print(":: REQUEST ::", req.model_dump()) - print(f":: PROJECT_ROOT :: {PROJECT_ROOT} ") - print(f":: checked_inventory_filepath :: {checked_inventory_filepath} ") - print(f":: checked_playbook_filepath :: {checked_playbook_filepath} ") - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - extravars = request_checks(req) - - #### - - rc, events, log_plain, log_ansi = run_playbook_core( - checked_playbook_filepath, - checked_inventory_filepath, - limit=req.hosts, - extravars=extravars, - ) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - payload = reply_processing(log_plain, rc) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - if rc == 0: - status = 200 - else: - status = 500 - - return JSONResponse(payload, status_code=status) - - -def reply_processing(log_plain: str, - rc - ) -> dict[str, list[str] | Any]: - """ reply post-processing - for ansible raw output """ - - payload = {"rc": rc, "log_multiline": log_plain.splitlines()} - - return payload - - -def request_checks(req: Request_BundlesCoreLinuxUbuntuInstall_Docker) -> dict[Any, Any]: - """ request checks """ - - extravars = {} - - if req.proxmox_node: - extravars["proxmox_node"] = req.proxmox_node - - # moved to --limit switch - # if req.hosts is not None: - # extravars["hosts"] = req.hosts - - # - - # if req.install_package_basics is not None: - # extravars["INSTALL_PACKAGES_BASICS"] = req.install_package_basics - # - # if req.install_package_firewalls is not None: - # extravars["INSTALL_PACKAGES_FIREWALLS"] = req.install_package_firewalls - - if req.install_package_docker is not None: - extravars["INSTALL_PACKAGES_DOCKER"] = req.install_package_docker - - # if req.install_package_docker_compose is not None: - # extravars["INSTALL_PACKAGES_DOCKER_COMPOSE"] = req.install_package_docker_compose - - # if req.install_package_utils_json is not None: - # extravars["INSTALL_PACKAGES_UTILS_JSON"] = req.install_package_utils_json - # - # if req.install_package_utils_network is not None: - # extravars["INSTALL_PACKAGES_UTILS_NETWORK"] = req.install_package_utils_network - - if req.install_ntpclient_and_update_time is not None: - extravars["INSTALL_PACKAGES_NTP_AND_UPDATE_TIME"] = req.install_ntpclient_and_update_time - - if req.packages_cleaning is not None: - extravars["SPECIFIC_PACKAGES_CLEANING"] = req.packages_cleaning - - # nothing : - if not extravars: - extravars = None - - if debug == 1: - print(f", extra vars : {extravars}") - - # extravars["hosts"] = "proxmox" - - return extravars - diff --git a/app/routes/v0/admin/bundles/core/linux/ubuntu/install/docker_compose.py b/app/routes/v0/admin/bundles/core/linux/ubuntu/install/docker_compose.py deleted file mode 100644 index d2dc411..0000000 --- a/app/routes/v0/admin/bundles/core/linux/ubuntu/install/docker_compose.py +++ /dev/null @@ -1,145 +0,0 @@ -from typing import Any -import logging -from fastapi import APIRouter, HTTPException -from fastapi.responses import JSONResponse - -from app.schemas.bundles.core.linux.ubuntu.install.docker_compose import Request_BundlesCoreLinuxUbuntuInstall_DockerCompose -from app.schemas.bundles.core.linux.ubuntu.install.docker_compose import Reply_BundlesCoreLinuxUbuntuInstall_DockerCompose - -from app.runner import run_playbook_core # , extract_action_results -from app.extract_actions import extract_action_results -from app import utils -from pathlib import Path -import os - -# -# ISSUE - #22 -# - -debug = 0 - -router = APIRouter() -logger = logging.getLogger(__name__) - -# PROJECT_ROOT = Path(__file__).resolve().parents[5] -PROJECT_ROOT = Path(os.getenv("PROJECT_ROOT_DIR")).resolve() -INVENTORY_NAME = "hosts" - -# -# => /v0/admin/run/actions/core/linux/ubuntu/install/docker-compose -# -@router.post( - path="/core/linux/ubuntu/install/docker-compose", - summary="Install docker compose packages", - description="Install and configure docker compose on the target ubuntu system", - tags=["bundles - core - ubuntu "], - response_model=Reply_BundlesCoreLinuxUbuntuInstall_DockerCompose, - # response_description="AAAAAAAAAAAAAAAAAA", -) -def bundles_core_linux_ubuntu_install_docker_compose_router(req: Request_BundlesCoreLinuxUbuntuInstall_DockerCompose): - - action_name = "core/linux/ubuntu/install/docker" - # - checked_inventory_filepath = utils.resolve_inventory(INVENTORY_NAME) -# checked_playbook_filepath = utils.resolve_actions_playbook(action_name, "public_github") - checked_playbook_filepath = utils.resolve_bundles_playbook(action_name, "public_github") - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - if debug == 1: - print(":: ACTION_NAME", action_name) - print(":: REQUEST ::", req.model_dump()) - print(f":: PROJECT_ROOT :: {PROJECT_ROOT} ") - print(f":: checked_inventory_filepath :: {checked_inventory_filepath} ") - print(f":: checked_playbook_filepath :: {checked_playbook_filepath} ") - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - extravars = request_checks(req) - - #### - - rc, events, log_plain, log_ansi = run_playbook_core( - checked_playbook_filepath, - checked_inventory_filepath, - limit=req.hosts, - extravars=extravars, - ) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - payload = reply_processing(log_plain, rc) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - if rc == 0: - status = 200 - else: - status = 500 - - return JSONResponse(payload, status_code=status) - - -def reply_processing(log_plain: str, - rc - ) -> dict[str, list[str] | Any]: - """ reply post-processing - for ansible raw output """ - - payload = {"rc": rc, "log_multiline": log_plain.splitlines()} - - return payload - - -def request_checks(req: Request_BundlesCoreLinuxUbuntuInstall_DockerCompose) -> dict[Any, Any]: - """ request checks """ - - extravars = {} - - if req.proxmox_node: - extravars["proxmox_node"] = req.proxmox_node - - # moved to --limit switch - # if req.hosts is not None: - # extravars["hosts"] = req.hosts - - # - - # if req.install_package_basics is not None: - # extravars["INSTALL_PACKAGES_BASICS"] = req.install_package_basics - # - # if req.install_package_firewalls is not None: - # extravars["INSTALL_PACKAGES_FIREWALLS"] = req.install_package_firewalls - - if req.install_package_docker is not None: - extravars["INSTALL_PACKAGES_DOCKER"] = req.install_package_docker - - if req.install_package_docker_compose is not None: - extravars["INSTALL_PACKAGES_DOCKER_COMPOSE"] = req.install_package_docker_compose - - # if req.install_package_utils_json is not None: - # extravars["INSTALL_PACKAGES_UTILS_JSON"] = req.install_package_utils_json - # - # if req.install_package_utils_network is not None: - # extravars["INSTALL_PACKAGES_UTILS_NETWORK"] = req.install_package_utils_network - - if req.install_ntpclient_and_update_time is not None: - extravars["INSTALL_PACKAGES_NTP_AND_UPDATE_TIME"] = req.install_ntpclient_and_update_time - - if req.packages_cleaning is not None: - extravars["SPECIFIC_PACKAGES_CLEANING"] = req.packages_cleaning - - # nothing : - if not extravars: - extravars = None - - if debug == 1: - print(f", extra vars : {extravars}") - - # extravars["hosts"] = "proxmox" - - return extravars - diff --git a/app/routes/v0/admin/bundles/core/linux/ubuntu/install/dot_files.py b/app/routes/v0/admin/bundles/core/linux/ubuntu/install/dot_files.py deleted file mode 100644 index b24dc52..0000000 --- a/app/routes/v0/admin/bundles/core/linux/ubuntu/install/dot_files.py +++ /dev/null @@ -1,134 +0,0 @@ -from typing import Any -import logging -from fastapi import APIRouter, HTTPException -from fastapi.responses import JSONResponse - -from app.schemas.bundles.core.linux.ubuntu.install.dot_files import Request_BundlesCoreLinuxUbuntuInstall_DotFiles -from app.schemas.bundles.core.linux.ubuntu.install.dot_files import Reply_BundlesCoreLinuxUbuntuInstall_DotFilesItem - -from app.runner import run_playbook_core # , extract_action_results -from app.extract_actions import extract_action_results -from app import utils -from pathlib import Path -import os - -# -# ISSUE - #28 -# - -debug = 0 - -router = APIRouter() -logger = logging.getLogger(__name__) - -# PROJECT_ROOT = Path(__file__).resolve().parents[5] -PROJECT_ROOT = Path(os.getenv("PROJECT_ROOT_DIR")).resolve() -INVENTORY_NAME = "hosts" - -# -# => /v0/admin/run/actions/core/linux/ubuntu/install/dot-files -# -@router.post( - path="/core/linux/ubuntu/install/dot-files", - summary="Install user dotfiles", - description="Install and configure generic dotfiles - vimrc, zshrc, etc.", - tags=["bundles - core - ubuntu "], - response_model=Reply_BundlesCoreLinuxUbuntuInstall_DotFilesItem, - # response_description="AAAAAAAAAAAAAAAAAA", -) -def bundles_core_linux_ubuntu_install_dotfiles_router( req: Request_BundlesCoreLinuxUbuntuInstall_DotFiles): - - action_name = "core/linux/ubuntu/install/dot-files" - # - checked_inventory_filepath = utils.resolve_inventory(INVENTORY_NAME) - # checked_playbook_filepath = utils.resolve_actions_playbook(action_name, "public_github") - checked_playbook_filepath = utils.resolve_bundles_playbook(action_name, "public_github") - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - if debug ==1: - print(":: ACTION_NAME", action_name) - print(":: REQUEST ::", req.model_dump()) - print(f":: PROJECT_ROOT :: {PROJECT_ROOT} ") - print(f":: checked_inventory_filepath :: {checked_inventory_filepath} ") - print(f":: checked_playbook_filepath :: {checked_playbook_filepath} ") - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - extravars = request_checks(req) - - #### - - rc, events, log_plain, log_ansi = run_playbook_core( - checked_playbook_filepath, - checked_inventory_filepath, - limit=req.hosts, - extravars=extravars, - ) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - payload = reply_processing(log_plain, rc) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - if rc == 0: - status = 200 - else: - status = 500 - - return JSONResponse(payload, status_code=status) - - -def reply_processing(log_plain: str, - rc - ) -> dict[str, list[str] | Any]: - - """ reply post-processing - for ansible raw output """ - - payload = {"rc": rc, "log_multiline": log_plain.splitlines()} - - return payload - - -def request_checks(req: Request_BundlesCoreLinuxUbuntuInstall_DotFiles) -> dict[Any, Any]: - """ request checks """ - - extravars = {} - - if req.proxmox_node: - extravars["proxmox_node"] = req.proxmox_node - - if req.hosts is not None : - extravars["hosts"] = req.hosts - - # - - if req.user is not None: - extravars["OPERATOR_USER"] = req.user - - if req.install_vim_dot_files is not None: - extravars["INSTALL_VIM_DOTFILES"] = req.install_vim_dot_files - - if req.install_zsh_dot_files is not None: - extravars["INSTALL_ZSH_DOTFILES"] = req.install_zsh_dot_files - - if req.apply_for_root is not None : - extravars["APPLY_FOR_ROOT"] = req.apply_for_root - - # nothing : - if not extravars: - extravars = None - - if debug == 1: - print(f", extra vars : {extravars}") - - - # extravars["hosts"] = "proxmox" - - return extravars - diff --git a/app/routes/v0/admin/bundles/proxmox/__init__.py b/app/routes/v0/admin/bundles/proxmox/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/routes/v0/admin/bundles/proxmox/configure/__init__.py b/app/routes/v0/admin/bundles/proxmox/configure/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/routes/v0/admin/bundles/proxmox/configure/default/__init__.py b/app/routes/v0/admin/bundles/proxmox/configure/default/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/routes/v0/admin/bundles/proxmox/configure/default/vms/__init__.py b/app/routes/v0/admin/bundles/proxmox/configure/default/vms/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/routes/v0/admin/bundles/proxmox/configure/default/vms/create_vms_admin_default.py b/app/routes/v0/admin/bundles/proxmox/configure/default/vms/create_vms_admin_default.py deleted file mode 100644 index 119bbb3..0000000 --- a/app/routes/v0/admin/bundles/proxmox/configure/default/vms/create_vms_admin_default.py +++ /dev/null @@ -1,206 +0,0 @@ -from typing import Any, Dict -import logging -from fastapi import APIRouter, HTTPException -from fastapi.responses import JSONResponse - -from app.schemas.bundles.core.proxmox.configure.default.vms.create_vms_admin_default import Request_BundlesCoreProxmoxConfigureDefaultVms_CreateAdminVms -from app.schemas.bundles.core.proxmox.configure.default.vms.create_vms_admin_default import Reply_BundlesCoreProxmoxConfigureDefaultVms_CreateAdminVms - -from app.runner import run_playbook_core # , extract_action_results -from app.extract_actions import extract_action_results -from app import utils -from pathlib import Path -import os - -# -# ISSUE - #30 -# - -debug = 0 - -router = APIRouter() -logger = logging.getLogger(__name__) - -# PROJECT_ROOT = Path(__file__).resolve().parents[5] -PROJECT_ROOT = Path(os.getenv("PROJECT_ROOT_DIR")).resolve() -INVENTORY_NAME = "hosts" - - -# -# => /v0/admin/run/bundles/core/proxmox/configure/vms/create-admin -# -@router.post( - path="/core/proxmox/configure/default/create-vms-admin", - summary="Create default admin VMs", - description="Create the default set of admin virtual machines for initial configuration in Proxmox", - # tags=["bundles", "core", "proxmox", "default-configuration"] - tags=["bundles - core - proxmox - vms - default-configuration - admin"], - response_model=Reply_BundlesCoreProxmoxConfigureDefaultVms_CreateAdminVms, - # response_description="AAAAAAAAAAAAAAAAAA", -) -def bundles_proxmox_configure_default_vms_create_vms_admin( - req: Request_BundlesCoreProxmoxConfigureDefaultVms_CreateAdminVms): - action_name = "core/proxmox/configure/default/vms/create-vms-admin" - - checked_inventory_filepath = utils.resolve_inventory(INVENTORY_NAME) - # checked_playbook_filepath = utils.resolve_bundles_playbook(action_name, "public_github") - checked_playbook_filepath = utils.resolve_bundles_playbook_init_file(action_name, "public_github") - - # results: list[Dict[str, Any]] = [] - - if debug == 1: - print(":: ACTION_NAME", action_name) - print(":: REQUEST ::", req.model_dump()) - print(f":: PROJECT_ROOT :: {PROJECT_ROOT}") - print(f":: INVENTORY :: {checked_inventory_filepath}") - print(f":: PLAYBOOK :: {checked_playbook_filepath}") - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - request_checks(req) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - # - # run init.yml - we must create vm first. - # - - for vm_key, item in req.vms.items(): - - extravars = { - "proxmox_node": req.proxmox_node, - "global_vm_id": item.vm_id, - "global_vm_ci_ip": str(item.vm_ip), - "global_vm_description": item.vm_description, - } - - if debug == 1 : - - print() - print(extravars["proxmox_node"]) - print("::: vm_key", vm_key, [vm_key]) - print("::: vm_de", extravars["global_vm_description"]) - print("::: vm_id", extravars["global_vm_id"]) - print("::: vm_ip", extravars["global_vm_ci_ip"]) - print() - - # NOTE: on passe --tags=vm_key ; pas besoin de --limit ici - rc, events, log_plain, log_ansi = run_playbook_core( - checked_playbook_filepath, - checked_inventory_filepath, - tags=vm_key, - extravars=extravars, - # limit=req.hosts, # we use tags for init.yml ! - ) - - # results.append({ - # "proxmox_node": req.proxmox_node, - # "raw_data": (log_plain or "") - # }) - - if rc != 0: - status = 500 - payload = reply_processing(log_plain, rc) - return JSONResponse(payload, status_code=status) - - # - # run main.yml - # - - checked_inventory_filepath = utils.resolve_inventory(INVENTORY_NAME) - checked_playbook_filepath = utils.resolve_bundles_playbook(action_name, "public_github") - - extravars = { - "proxmox_node": req.proxmox_node, - # "global_vm_id": item.vm_id, - # "global_vm_ci_ip": str(item.vm_ip), - # "global_vm_description": item.vm_description, - } - - # limit_admin = "r42-admin" - - - rc, events, log_plain, log_ansi = run_playbook_core( - checked_playbook_filepath, - checked_inventory_filepath, - extravars=extravars, - # limit=limit_admin, # useless - restricted in playbook - # tags=vm_key, - - ) - - # results.append({ - # "proxmox_node": req.proxmox_node, - # "raw_data": (log_plain or "") - # }) - - payload = reply_processing(log_plain, rc) - - - if rc == 0: - status = 200 - else: - status = 500 - - return JSONResponse(payload, status_code=status) - - -def reply_processing(log_plain: str, rc) -> dict[str, list[str] | Any]: - """ reply post-processing - for ansible raw output """ - - payload = {"rc": rc, "log_multiline": log_plain.splitlines()} - - return payload - - -def request_checks(req: Request_BundlesCoreProxmoxConfigureDefaultVms_CreateAdminVms) -> dict[Any, Any]: - """ request checks """ - - extravars = {} - - if req.proxmox_node: - extravars["proxmox_node"] = req.proxmox_node - - ##### - - if not req.vms or len(req.vms) == 0: - raise HTTPException(status_code=400, detail="Field vms must not be empty") - - allowed_vms = { - "admin-wazuh", - "admin-web-api-kong", - "admin-web-builder-api", - "admin-web-deployer-ui", - "admin-web-emp", - } - - for required in allowed_vms: - if required not in req.vms: - raise HTTPException(status_code=400, detail=f"Missing required vm key {required}") - - for vm in req.vms: - if vm not in allowed_vms: - raise HTTPException(status_code=400, detail=f"Unauthorized vm key {vm}" ) - - # - - for vm_name, vm_spec in req.vms.items(): - - if vm_spec.vm_id is None: - raise HTTPException(status_code=400, detail=f"missing key vm_id for {vm_name}") - - if vm_spec.vm_description is None: - raise HTTPException(status_code=400, detail=f"missing key vm_description for {vm_name}") - - if vm_spec.vm_ip is None: - raise HTTPException(status_code=400, detail=f"missing key vm_ip for {vm_name}") - - # nothing : - - if not extravars: - extravars = None - - if debug == 1: - print(f", extra vars : {extravars}") - - return extravars diff --git a/app/routes/v0/admin/bundles/proxmox/configure/default/vms/create_vms_student_default.py b/app/routes/v0/admin/bundles/proxmox/configure/default/vms/create_vms_student_default.py deleted file mode 100644 index 4e9730f..0000000 --- a/app/routes/v0/admin/bundles/proxmox/configure/default/vms/create_vms_student_default.py +++ /dev/null @@ -1,202 +0,0 @@ -from typing import Any, Dict -import logging -from fastapi import APIRouter, HTTPException -from fastapi.responses import JSONResponse - -from app.schemas.bundles.core.proxmox.configure.default.vms.create_vms_student_default import Request_BundlesCoreProxmoxConfigureDefaultVms_CreateStudentVms -from app.schemas.bundles.core.proxmox.configure.default.vms.create_vms_student_default import Reply_BundlesCoreProxmoxConfigureDefaultVms_CreateStudentVms - -from app.runner import run_playbook_core # , extract_action_results -from app.extract_actions import extract_action_results -from app import utils -from pathlib import Path -import os - -# -# ISSUE - #30 -# - -debug = 0 - -router = APIRouter() -logger = logging.getLogger(__name__) - -# PROJECT_ROOT = Path(__file__).resolve().parents[5] -PROJECT_ROOT = Path(os.getenv("PROJECT_ROOT_DIR")).resolve() -INVENTORY_NAME = "hosts" - - -# -# => /v0/admin/run/bundles/core/proxmox/configure/vms/create-vms-student -# -@router.post( - path="/core/proxmox/configure/default/create-vms-student", - summary="Create default student VMs", - description="Create the default set of student virtual machines for initial configuration in Proxmox", - # tags=["bundles", "core", "proxmox", "default-configuration"] - tags=["bundles - core - proxmox - vms - default-configuration - student"], - response_model=Reply_BundlesCoreProxmoxConfigureDefaultVms_CreateStudentVms, - # response_description="AAAAAAAAAAAAAAAAAA", -) -def bundles_proxmox_configure_default_vms_create_vms_student( - req: Request_BundlesCoreProxmoxConfigureDefaultVms_CreateStudentVms): - action_name = "core/proxmox/configure/default/vms/create-vms-student" - - checked_inventory_filepath = utils.resolve_inventory(INVENTORY_NAME) - # checked_playbook_filepath = utils.resolve_bundles_playbook(action_name, "public_github") - checked_playbook_filepath = utils.resolve_bundles_playbook_init_file(action_name, "public_github") - - # results: list[Dict[str, Any]] = [] - - if debug == 1: - print(":: ACTION_NAME", action_name) - print(":: REQUEST ::", req.model_dump()) - print(f":: PROJECT_ROOT :: {PROJECT_ROOT}") - print(f":: INVENTORY :: {checked_inventory_filepath}") - print(f":: PLAYBOOK :: {checked_playbook_filepath}") - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - request_checks(req) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - # - # run init.yml - we must create vm first. - # - - for vm_key, item in req.vms.items(): - - extravars = { - "proxmox_node": req.proxmox_node, - "global_vm_id": item.vm_id, - "global_vm_ci_ip": str(item.vm_ip), - "global_vm_description": item.vm_description, - } - - if debug == 1 : - - print() - print(extravars["proxmox_node"]) - print("::: vm_key", vm_key, [vm_key]) - print("::: vm_de", extravars["global_vm_description"]) - print("::: vm_id", extravars["global_vm_id"]) - print("::: vm_ip", extravars["global_vm_ci_ip"]) - print() - - # NOTE: on passe --tags=vm_key ; pas besoin de --limit ici - rc, events, log_plain, log_ansi = run_playbook_core( - checked_playbook_filepath, - checked_inventory_filepath, - tags=vm_key, - extravars=extravars, - # limit=req.hosts, # we use tags for init.yml ! - ) - - # results.append({ - # "proxmox_node": req.proxmox_node, - # "raw_data": (log_plain or "") - # }) - - if rc != 0: - status = 500 - payload = reply_processing(log_plain, rc) - return JSONResponse(payload, status_code=status) - - # - # run main.yml - # - - checked_inventory_filepath = utils.resolve_inventory(INVENTORY_NAME) - checked_playbook_filepath = utils.resolve_bundles_playbook(action_name, "public_github") - - extravars = { - "proxmox_node": req.proxmox_node, - # "global_vm_id": item.vm_id, - # "global_vm_ci_ip": str(item.vm_ip), - # "global_vm_description": item.vm_description, - } - - # limit_admin = "r42-admin" - - - rc, events, log_plain, log_ansi = run_playbook_core( - checked_playbook_filepath, - checked_inventory_filepath, - extravars=extravars, - # limit=limit_admin, # useless - restricted in playbook - # tags=vm_key, - - ) - - # results.append({ - # "proxmox_node": req.proxmox_node, - # "raw_data": (log_plain or "") - # }) - - payload = reply_processing(log_plain, rc) - - - if rc == 0: - status = 200 - else: - status = 500 - - return JSONResponse(payload, status_code=status) - - -def reply_processing(log_plain: str, rc) -> dict[str, list[str] | Any]: - """ reply post-processing - for ansible raw output """ - - payload = {"rc": rc, "log_multiline": log_plain.splitlines()} - - return payload - - -def request_checks(req: Request_BundlesCoreProxmoxConfigureDefaultVms_CreateStudentVms) -> dict[Any, Any]: - """ request checks """ - - extravars = {} - - if req.proxmox_node: - extravars["proxmox_node"] = req.proxmox_node - - ##### - - if not req.vms or len(req.vms) == 0: - raise HTTPException(status_code=400, detail="Field vms must not be empty") - - allowed_vms = { - "student-box-01" - } - - for required in allowed_vms: - if required not in req.vms: - raise HTTPException(status_code=400, detail=f"Missing required vm key {required}") - - for vm in req.vms: - if vm not in allowed_vms: - raise HTTPException(status_code=400, detail=f"Unauthorized vm key {vm}" ) - - # - - for vm_name, vm_spec in req.vms.items(): - - if vm_spec.vm_id is None: - raise HTTPException(status_code=400, detail=f"missing key vm_id for {vm_name}") - - if vm_spec.vm_description is None: - raise HTTPException(status_code=400, detail=f"missing key vm_description for {vm_name}") - - if vm_spec.vm_ip is None: - raise HTTPException(status_code=400, detail=f"missing key vm_ip for {vm_name}") - - # nothing : - - if not extravars: - extravars = None - - if debug == 1: - print(f", extra vars : {extravars}") - - return extravars diff --git a/app/routes/v0/admin/bundles/proxmox/configure/default/vms/create_vms_vuln_default.py b/app/routes/v0/admin/bundles/proxmox/configure/default/vms/create_vms_vuln_default.py deleted file mode 100644 index 3d35f9d..0000000 --- a/app/routes/v0/admin/bundles/proxmox/configure/default/vms/create_vms_vuln_default.py +++ /dev/null @@ -1,206 +0,0 @@ -from typing import Any, Dict -import logging -from fastapi import APIRouter, HTTPException -from fastapi.responses import JSONResponse - -from app.schemas.bundles.core.proxmox.configure.default.vms.create_vms_vuln_default import Request_BundlesCoreProxmoxConfigureDefaultVms_CreateVulnVms -from app.schemas.bundles.core.proxmox.configure.default.vms.create_vms_vuln_default import Reply_BundlesCoreProxmoxConfigureDefaultVms_CreateVulnVms - -from app.runner import run_playbook_core # , extract_action_results -from app.extract_actions import extract_action_results -from app import utils -from pathlib import Path -import os - -# -# ISSUE - #30 -# - -debug = 1 - -router = APIRouter() -logger = logging.getLogger(__name__) - -# PROJECT_ROOT = Path(__file__).resolve().parents[5] -PROJECT_ROOT = Path(os.getenv("PROJECT_ROOT_DIR")).resolve() -INVENTORY_NAME = "hosts" - - -# -# => /v0/admin/run/bundles/core/proxmox/configure/vms/create-vms-vuln -# -@router.post( - path="/core/proxmox/configure/default/create-vms-vuln", - summary="Create default vulnerable VMs", - description="Create the default set of vulnerable virtual machines for initial configuration in Proxmox", - # tags=["bundles", "core", "proxmox", "default-configuration"] - tags=["bundles - core - proxmox - vms - default-configuration - vuln"], - response_model=Reply_BundlesCoreProxmoxConfigureDefaultVms_CreateVulnVms, - # response_description="AAAAAAAAAAAAAAAAAA", -) -def bundles_proxmox_configure_default_vms_create_vms_vuln( - req: Request_BundlesCoreProxmoxConfigureDefaultVms_CreateVulnVms): - action_name = "core/proxmox/configure/default/vms/create-vms-vuln" - - checked_inventory_filepath = utils.resolve_inventory(INVENTORY_NAME) - # checked_playbook_filepath = utils.resolve_bundles_playbook(action_name, "public_github") - checked_playbook_filepath = utils.resolve_bundles_playbook_init_file(action_name, "public_github") - - # results: list[Dict[str, Any]] = [] - - if debug == 1: - print(":: ACTION_NAME", action_name) - print(":: REQUEST ::", req.model_dump()) - print(f":: PROJECT_ROOT :: {PROJECT_ROOT}") - print(f":: INVENTORY :: {checked_inventory_filepath}") - print(f":: PLAYBOOK :: {checked_playbook_filepath}") - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - request_checks(req) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - # - # run init.yml - we must create vm first. - # - - for vm_key, item in req.vms.items(): - - extravars = { - "proxmox_node": req.proxmox_node, - "global_vm_id": item.vm_id, - "global_vm_ci_ip": str(item.vm_ip), - "global_vm_description": item.vm_description, - } - - if debug == 1 : - - print() - print(extravars["proxmox_node"]) - print("::: vm_key", vm_key, [vm_key]) - print("::: vm_de", extravars["global_vm_description"]) - print("::: vm_id", extravars["global_vm_id"]) - print("::: vm_ip", extravars["global_vm_ci_ip"]) - print() - - - rc, events, log_plain, log_ansi = run_playbook_core( - checked_playbook_filepath, - checked_inventory_filepath, - tags=vm_key, - extravars=extravars, - # limit=req.hosts, # we use tags for init.yml ! - ) - - # results.append({ - # "proxmox_node": req.proxmox_node, - # "raw_data": (log_plain or "") - # }) - - if rc != 0: - status = 500 - payload = reply_processing(log_plain, rc) - return JSONResponse(payload, status_code=status) - - # - # run main.yml - # - - checked_inventory_filepath = utils.resolve_inventory(INVENTORY_NAME) - checked_playbook_filepath = utils.resolve_bundles_playbook(action_name, "public_github") - - extravars = { - "proxmox_node": req.proxmox_node, - # "global_vm_id": item.vm_id, - # "global_vm_ci_ip": str(item.vm_ip), - # "global_vm_description": item.vm_description, - } - - # limit_admin = "r42-admin" - - # NOTE: on passe --tags=vm_key ; pas besoin de --limit ici - rc, events, log_plain, log_ansi = run_playbook_core( - checked_playbook_filepath, - checked_inventory_filepath, - extravars=extravars, - # limit=limit_admin, # useless - restricted in playbook - # tags=vm_key, - - ) - - # results.append({ - # "proxmox_node": req.proxmox_node, - # "raw_data": (log_plain or "") - # }) - - payload = reply_processing(log_plain, rc) - - - if rc == 0: - status = 200 - else: - status = 500 - - return JSONResponse(payload, status_code=status) - - -def reply_processing(log_plain: str, rc ) -> dict[str, list[str] | Any]: - """ reply post-processing - for ansible raw output """ - - payload = {"rc": rc, "log_multiline": log_plain.splitlines()} - - return payload - - -def request_checks(req: Request_BundlesCoreProxmoxConfigureDefaultVms_CreateVulnVms) -> dict[Any, Any]: - """ request checks """ - - extravars = {} - - if req.proxmox_node: - extravars["proxmox_node"] = req.proxmox_node - - ##### - - if not req.vms or len(req.vms) == 0: - raise HTTPException(status_code=500, detail="Field vms must not be empty") - - allowed_vms = { - "vuln-box-00", - "vuln-box-01", - "vuln-box-02", - "vuln-box-03", - "vuln-box-04", - } - - for required in allowed_vms: - if required not in req.vms: - raise HTTPException(status_code=500, detail=f"Missing required vm key {required}") - - for vm in req.vms: - if vm not in allowed_vms: - raise HTTPException(status_code=500, detail=f"Unauthorized vm key {vm}" ) - - # - - for vm_name, vm_spec in req.vms.items(): - - if vm_spec.vm_id is None: - raise HTTPException(status_code=500, detail=f"missing key vm_id for {vm_name}") - - if vm_spec.vm_description is None: - raise HTTPException(status_code=500, detail=f"missing key vm_description for {vm_name}") - - if vm_spec.vm_ip is None: - raise HTTPException(status_code=500, detail=f"missing key vm_ip for {vm_name}") - - # nothing : - - if not extravars: - extravars = None - - if debug == 1: - print(f", extra vars : {extravars}") - - return extravars diff --git a/app/routes/v0/admin/bundles/proxmox/configure/default/vms/delete_vms_admin_vuln_student_default.py b/app/routes/v0/admin/bundles/proxmox/configure/default/vms/delete_vms_admin_vuln_student_default.py deleted file mode 100644 index c1dc1df..0000000 --- a/app/routes/v0/admin/bundles/proxmox/configure/default/vms/delete_vms_admin_vuln_student_default.py +++ /dev/null @@ -1,153 +0,0 @@ -from typing import Any -import logging -from fastapi import APIRouter, HTTPException -from fastapi.responses import JSONResponse - -from app.schemas.bundles.core.proxmox.configure.default.vms.start_stop_resume_pause_default import Request_BundlesCoreProxmoxConfigureDefaultVms_StartStopPauseResumeAdminVulnStudentVms -from app.schemas.proxmox.vm_id.start_stop_resume_pause import Reply_ProxmoxVmsVMID_StartStopPauseResume - -from app.runner import run_playbook_core # , extract_action_results -from app.extract_actions import extract_action_results -from app import utils -from pathlib import Path -import os - -# -# ISSUE - #30 -# - -debug = 0 - -router = APIRouter() -logger = logging.getLogger(__name__) - -# PROJECT_ROOT = Path(__file__).resolve().parents[5] -PROJECT_ROOT = Path(os.getenv("PROJECT_ROOT_DIR")).resolve() -INVENTORY_NAME = "hosts" - -# -# => /v0/admin/run/bundles/core/proxmox/configure/default/delete-vms-student -# -@router.delete( - path="/core/proxmox/configure/default/delete-vms-student", - summary="Delete student vms ", - description="Delete all vulnerable virtual machines", - tags=["bundles - core - proxmox - vms - default-configuration - student"], - response_model=Reply_ProxmoxVmsVMID_StartStopPauseResume, -) -def bundles_proxmox_configure_default_vms_stop_vms_student(req: Request_BundlesCoreProxmoxConfigureDefaultVms_StartStopPauseResumeAdminVulnStudentVms): - - return bundles_proxmox_configure_default_vms_delete(req, "core/proxmox/configure/default/vms/delete-vms-student") -# -# => /v0/admin/run/bundles/core/proxmox/configure/default/delete-vms-vuln -# -@router.delete( - path="/core/proxmox/configure/default/delete-vms-vuln", - summary="Delete student vms ", - description="Delete all vulnerable virtual machines", - tags=["bundles - core - proxmox - vms - default-configuration - vuln"], - response_model=Reply_ProxmoxVmsVMID_StartStopPauseResume, -) -def bundles_proxmox_configure_default_vms_stop_vms_vuln(req: Request_BundlesCoreProxmoxConfigureDefaultVms_StartStopPauseResumeAdminVulnStudentVms): - - return bundles_proxmox_configure_default_vms_delete(req, "core/proxmox/configure/default/vms/delete-vms-vuln") - -# -# => /v0/admin/run/bundles/core/proxmox/configure/default/admin-vms-admin -# -@router.delete( - path="/core/proxmox/configure/default/delete-vms-admin", - summary="Delete admin vms ", - description="Delete all admin virtual machines", - tags=["bundles - core - proxmox - vms - default-configuration - admin"], - response_model=Reply_ProxmoxVmsVMID_StartStopPauseResume, -) -def bundles_proxmox_configure_default_vms_stop_vms_admin(req: Request_BundlesCoreProxmoxConfigureDefaultVms_StartStopPauseResumeAdminVulnStudentVms): - - return bundles_proxmox_configure_default_vms_delete(req, "core/proxmox/configure/default/vms/delete-vms-admin" ) - -####################################################################################################################### - -def bundles_proxmox_configure_default_vms_delete( - req: Request_BundlesCoreProxmoxConfigureDefaultVms_StartStopPauseResumeAdminVulnStudentVms, - action_name: str) -> JSONResponse: - - checked_inventory_filepath = utils.resolve_inventory(INVENTORY_NAME) - checked_playbook_filepath = utils.resolve_bundles_playbook(action_name, "public_github") - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - extravars = request_checks(req) - - #### - - rc, events, log_plain, log_ansi = run_playbook_core( - checked_playbook_filepath, - checked_inventory_filepath, - limit=req.proxmox_node, - extravars=extravars, - ) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - payload = reply_processing(events, extravars, log_plain, rc, req) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - if rc == 0: - status = 200 - else: - status = 500 - - return JSONResponse(payload, status_code=status) - - -def reply_processing(events: list[dict] | list[Any], - extravars: dict[Any, Any], - log_plain: str, - rc, - req: Request_BundlesCoreProxmoxConfigureDefaultVms_StartStopPauseResumeAdminVulnStudentVms) -> dict[str, list | Any]: - - """ reply post-processing - json or ansible raw output """ - - if req.as_json: ##### OUTPUT TYPE - as_json=True - - # do not remove this. We want only vm_delete json line from the role (not the vm_stop_force !) - extravars["proxmox_vm_action"] = "vm_delete" - # - - action = extravars["proxmox_vm_action"] - result = extract_action_results(events, action) - - payload = { - "rc": rc, - "result": result - # "action": action, - } # raw - - else: #### OUTPUT AS TEXT - as_json=False - - payload = {"rc": rc, "log_multiline": log_plain.splitlines()} - return payload - - - -def request_checks(req: Request_BundlesCoreProxmoxConfigureDefaultVms_StartStopPauseResumeAdminVulnStudentVms) -> dict[Any, Any]: - """ request checks """ - - extravars = {} - - if req.proxmox_node: - extravars["proxmox_node"] = req.proxmox_node - - # nothing : - if not extravars: - extravars = None - - if debug == 1: - print(f", extra vars : {extravars}") - - # extravars["hosts"] = "proxmox" - - return extravars - diff --git a/app/routes/v0/admin/bundles/proxmox/configure/default/vms/snapshot/__init__.py b/app/routes/v0/admin/bundles/proxmox/configure/default/vms/snapshot/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/routes/v0/admin/bundles/proxmox/configure/default/vms/snapshot/create_vms_admin_vuln_student_default.py b/app/routes/v0/admin/bundles/proxmox/configure/default/vms/snapshot/create_vms_admin_vuln_student_default.py deleted file mode 100644 index f54002d..0000000 --- a/app/routes/v0/admin/bundles/proxmox/configure/default/vms/snapshot/create_vms_admin_vuln_student_default.py +++ /dev/null @@ -1,154 +0,0 @@ -from typing import Any -import logging -from fastapi import APIRouter, HTTPException -from fastapi.responses import JSONResponse - -from app.schemas.bundles.core.proxmox.configure.default.vms.start_stop_resume_pause_default import Request_BundlesCoreProxmoxConfigureDefaultVms_StartStopPauseResumeAdminVulnStudentVms -# from app.schemas.proxmox.vm_id.start_stop_resume_pause import Reply_ProxmoxVmsVMID_StartStopPauseResume -from app.schemas.proxmox.vm_id.snapshot.vm_create import Reply_ProxmoxVmsVMID_CreateSnapshot - -from app.runner import run_playbook_core # , extract_action_results -from app.extract_actions import extract_action_results -from app import utils -from pathlib import Path -import os - -# -# ISSUE - #30 -# - -debug = 0 - -router = APIRouter() -logger = logging.getLogger(__name__) - -# PROJECT_ROOT = Path(__file__).resolve().parents[5] -PROJECT_ROOT = Path(os.getenv("PROJECT_ROOT_DIR")).resolve() -INVENTORY_NAME = "hosts" - -# -# => /v0/admin/run/bundles/core/proxmox/configure/default/snapshot-vms-student -# -@router.post( - path="/core/proxmox/configure/default/snapshot/create-vms-student", - summary="Snapshot student vms ", - description="Snapshot all vulnerable virtual machines", - tags=["bundles - core - proxmox - vms - default-configuration - student"], - response_model=Reply_ProxmoxVmsVMID_CreateSnapshot, -) -def bundles_proxmox_configure_default_vms_create_snapshot_vms_student(req: Request_BundlesCoreProxmoxConfigureDefaultVms_StartStopPauseResumeAdminVulnStudentVms): - - return bundles_proxmox_configure_default_vms_create_snapshot(req, "core/proxmox/configure/default/vms/snapshot/create-vms-student") -# -# => /v0/admin/run/bundles/core/proxmox/configure/default/snapshot-vms-vuln -# -@router.post( - path="/core/proxmox/configure/default/snapshot/create-vms-vuln", - summary="Snapshot student vms ", - description="Snapshot all vulnerable virtual machines", - tags=["bundles - core - proxmox - vms - default-configuration - vuln"], - response_model=Reply_ProxmoxVmsVMID_CreateSnapshot, -) -def bundles_proxmox_configure_default_vms_create_snapshot_vms_vuln(req: Request_BundlesCoreProxmoxConfigureDefaultVms_StartStopPauseResumeAdminVulnStudentVms): - - return bundles_proxmox_configure_default_vms_create_snapshot(req, "core/proxmox/configure/default/vms/snapshot/create-vms-vuln") - -# -# => /v0/admin/run/bundles/core/proxmox/configure/default/admin-vms-admin -# -@router.post( - path="/core/proxmox/configure/default/snapshot/create-vms-admin", - summary="Snapshot admin vms ", - description="Snapshot all admin virtual machines", - tags=["bundles - core - proxmox - vms - default-configuration - admin"], - response_model=Reply_ProxmoxVmsVMID_CreateSnapshot, -) -def bundles_proxmox_configure_default_vms_create_snapshot_vms_admin(req: Request_BundlesCoreProxmoxConfigureDefaultVms_StartStopPauseResumeAdminVulnStudentVms): - - return bundles_proxmox_configure_default_vms_create_snapshot(req, "core/proxmox/configure/default/vms/snapshot/create-vms-admin" ) - -####################################################################################################################### - -def bundles_proxmox_configure_default_vms_create_snapshot( - req: Request_BundlesCoreProxmoxConfigureDefaultVms_StartStopPauseResumeAdminVulnStudentVms, - action_name: str) -> JSONResponse: - - checked_inventory_filepath = utils.resolve_inventory(INVENTORY_NAME) - checked_playbook_filepath = utils.resolve_bundles_playbook(action_name, "public_github") - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - extravars = request_checks(req) - - #### - - rc, events, log_plain, log_ansi = run_playbook_core( - checked_playbook_filepath, - checked_inventory_filepath, - limit=req.proxmox_node, - extravars=extravars, - ) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - payload = reply_processing(events, extravars, log_plain, rc, req) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - if rc == 0: - status = 200 - else: - status = 500 - - return JSONResponse(payload, status_code=status) - - -def reply_processing(events: list[dict] | list[Any], - extravars: dict[Any, Any], - log_plain: str, - rc, - req: Request_BundlesCoreProxmoxConfigureDefaultVms_StartStopPauseResumeAdminVulnStudentVms) -> dict[str, list | Any]: - - """ reply post-processing - json or ansible raw output """ - - if req.as_json: ##### OUTPUT TYPE - as_json=True - - # do not remove this. We want only vm_delete json line from the role (not the vm_stop_force !) - extravars["proxmox_vm_action"] = "snapshot_vm_create" - # - - action = extravars["proxmox_vm_action"] - result = extract_action_results(events, action) - - payload = { - "rc": rc, - "result": result - # "action": action, - } # raw - - else: #### OUTPUT AS TEXT - as_json=False - - payload = {"rc": rc, "log_multiline": log_plain.splitlines()} - return payload - - - -def request_checks(req: Request_BundlesCoreProxmoxConfigureDefaultVms_StartStopPauseResumeAdminVulnStudentVms) -> dict[Any, Any]: - """ request checks """ - - extravars = {} - - if req.proxmox_node: - extravars["proxmox_node"] = req.proxmox_node - - # nothing : - if not extravars: - extravars = None - - if debug == 1: - print(f", extra vars : {extravars}") - - # extravars["hosts"] = "proxmox" - - return extravars - diff --git a/app/routes/v0/admin/bundles/proxmox/configure/default/vms/snapshot/revert_vms_admin_vuln_student_default.py b/app/routes/v0/admin/bundles/proxmox/configure/default/vms/snapshot/revert_vms_admin_vuln_student_default.py deleted file mode 100644 index 249e198..0000000 --- a/app/routes/v0/admin/bundles/proxmox/configure/default/vms/snapshot/revert_vms_admin_vuln_student_default.py +++ /dev/null @@ -1,159 +0,0 @@ -from typing import Any -import logging -from fastapi import APIRouter, HTTPException -from fastapi.responses import JSONResponse - -# from app.schemas.bundles.core.proxmox.configure.default.vms.start_stop_resume_pause_default import Request_BundlesCoreProxmoxConfigureDefaultVms_StartStopPauseResumeAdminVulnStudentVms -from app.schemas.bundles.core.proxmox.configure.default.vms.revert_snapshot_default import Request_BundlesCoreProxmoxConfigureDefaultVms_RevertSnapshotAdminVulnStudentVms -from app.schemas.proxmox.vm_id.snapshot.vm_revert import Reply_ProxmoxVmsVMID_RevertSnapshot - -from app.utils.vm_id_name_resolver import resolv_id_to_vm_name - -from app.runner import run_playbook_core # , extract_action_results -from app.extract_actions import extract_action_results -from app import utils -from pathlib import Path -import os - -# -# ISSUE - #30 -# - -debug = 0 - -router = APIRouter() -logger = logging.getLogger(__name__) - -# PROJECT_ROOT = Path(__file__).resolve().parents[5] -PROJECT_ROOT = Path(os.getenv("PROJECT_ROOT_DIR")).resolve() -INVENTORY_NAME = "hosts" - -# -# => /v0/admin/run/bundles/core/proxmox/configure/default/snapshot-revert-vms-student -# -@router.post( - path="/core/proxmox/configure/default/snapshot/revert-vms-student", - summary="Snapshot student vms ", - description="Snapshot all vulnerable virtual machines", - tags=["bundles - core - proxmox - vms - default-configuration - student"], - response_model=Reply_ProxmoxVmsVMID_RevertSnapshot, -) -def bundles_proxmox_configure_default_vms_stop_vms_student(req: Request_BundlesCoreProxmoxConfigureDefaultVms_RevertSnapshotAdminVulnStudentVms): - - return bundles_proxmox_configure_default_vms_revert_snapshot(req, "core/proxmox/configure/default/vms/snapshot/revert-vms-student") -# -# => /v0/admin/run/bundles/core/proxmox/configure/default/snapshot-revert-vms-vuln -# -@router.post( - path="/core/proxmox/configure/default/snapshot/revert-vms-vuln", - summary="Snapshot student vms ", - description="Snapshot all vulnerable virtual machines", - tags=["bundles - core - proxmox - vms - default-configuration - vuln"], - response_model=Reply_ProxmoxVmsVMID_RevertSnapshot, -) -def bundles_proxmox_configure_default_vms_stop_vms_vuln(req: Request_BundlesCoreProxmoxConfigureDefaultVms_RevertSnapshotAdminVulnStudentVms): - - return bundles_proxmox_configure_default_vms_revert_snapshot(req, "core/proxmox/configure/default/vms/snapshot/revert-vms-vuln") - -# -# => /v0/admin/run/bundles/core/proxmox/configure/default/admin-vms-admin -# -@router.post( - path="/core/proxmox/configure/default/snapshot/revert-vms-admin", - summary="Snapshot admin vms ", - description="Snapshot all admin virtual machines", - tags=["bundles - core - proxmox - vms - default-configuration - admin"], - response_model=Reply_ProxmoxVmsVMID_RevertSnapshot, -) -def bundles_proxmox_configure_default_vms_stop_vms_admin(req: Request_BundlesCoreProxmoxConfigureDefaultVms_RevertSnapshotAdminVulnStudentVms): - - return bundles_proxmox_configure_default_vms_revert_snapshot(req, "core/proxmox/configure/default/vms/snapshot/revert-vms-admin" ) - -####################################################################################################################### - -def bundles_proxmox_configure_default_vms_revert_snapshot( - req: Request_BundlesCoreProxmoxConfigureDefaultVms_RevertSnapshotAdminVulnStudentVms, - action_name: str) -> JSONResponse: - - checked_inventory_filepath = utils.resolve_inventory(INVENTORY_NAME) - checked_playbook_filepath = utils.resolve_bundles_playbook(action_name, "public_github") - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - extravars = request_checks(req) - - #### - - rc, events, log_plain, log_ansi = run_playbook_core( - checked_playbook_filepath, - checked_inventory_filepath, - limit=req.proxmox_node, - extravars=extravars, - ) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - payload = reply_processing(events, extravars, log_plain, rc, req) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - if rc == 0: - status = 200 - else: - status = 500 - - return JSONResponse(payload, status_code=status) - - -def reply_processing(events: list[dict] | list[Any], - extravars: dict[Any, Any], - log_plain: str, - rc, - req: Request_BundlesCoreProxmoxConfigureDefaultVms_RevertSnapshotAdminVulnStudentVms) -> dict[str, list | Any]: - - """ reply post-processing - json or ansible raw output """ - - if req.as_json: ##### OUTPUT TYPE - as_json=True - - # do not remove this. We want only vm_delete json line from the role (not the vm_stop_force !) - extravars["proxmox_vm_action"] = "snapshot_vm_revert" - # - - action = extravars["proxmox_vm_action"] - result = extract_action_results(events, action) - - payload = { - "rc": rc, - "result": result - # "action": action, - } # raw - - else: #### OUTPUT AS TEXT - as_json=False - - payload = {"rc": rc, "log_multiline": log_plain.splitlines()} - return payload - - - -def request_checks(req: Request_BundlesCoreProxmoxConfigureDefaultVms_RevertSnapshotAdminVulnStudentVms) -> dict[Any, Any]: - """ request checks """ - - extravars = {} - - if req.proxmox_node: - extravars["proxmox_node"] = req.proxmox_node - - if req.vm_snapshot_name is not None: - extravars["VM_SNAPSHOT_NAME"] = req.vm_snapshot_name - - # nothing : - if not extravars: - extravars = None - - if debug == 1: - print(f", extra vars : {extravars}") - - # extravars["hosts"] = "proxmox" - - return extravars - diff --git a/app/routes/v0/admin/bundles/proxmox/configure/default/vms/start_stop_pause_resume_admin.py b/app/routes/v0/admin/bundles/proxmox/configure/default/vms/start_stop_pause_resume_admin.py deleted file mode 100644 index 1d21746..0000000 --- a/app/routes/v0/admin/bundles/proxmox/configure/default/vms/start_stop_pause_resume_admin.py +++ /dev/null @@ -1,210 +0,0 @@ -from typing import Any -import logging -from fastapi import APIRouter, HTTPException -from fastapi.responses import JSONResponse - -from app.schemas.bundles.core.proxmox.configure.default.vms.start_stop_resume_pause_default import Request_BundlesCoreProxmoxConfigureDefaultVms_StartStopPauseResumeAdminVulnStudentVms -from app.schemas.proxmox.vm_id.start_stop_resume_pause import Reply_ProxmoxVmsVMID_StartStopPauseResume - -from app.runner import run_playbook_core # , extract_action_results -from app.extract_actions import extract_action_results -from app import utils -from pathlib import Path -import os - -# -# ISSUE - #30 -# - -debug = 0 - -router = APIRouter() -logger = logging.getLogger(__name__) - -# PROJECT_ROOT = Path(__file__).resolve().parents[5] -PROJECT_ROOT = Path(os.getenv("PROJECT_ROOT_DIR")).resolve() -INVENTORY_NAME = "hosts" - -# -# => /v0/admin/run/bundles/core/proxmox/configure/default/start-vms-admin -# -@router.post( - path="/core/proxmox/configure/default/start-vms-admin", - summary="Start student vms ", - description="Start all vulnerable virtual machines", - tags=["bundles - core - proxmox - vms - default-configuration - admin"], - response_model=Reply_ProxmoxVmsVMID_StartStopPauseResume, -) -def bundles_proxmox_configure_default_vms_stop_vms_vuln(req: Request_BundlesCoreProxmoxConfigureDefaultVms_StartStopPauseResumeAdminVulnStudentVms): - - return bundles_proxmox_configure_default_vms_run(req, - "core/proxmox/configure/default/vms/start-stop-pause-resume-vms-admin", - "vm_start" - ) - -####################################################################################################################### - -# -# => /v0/admin/run/bundles/core/proxmox/configure/default/stop-vms-admin -# -@router.post( - path="/core/proxmox/configure/default/stop-vms-admin", - summary="Stop student vms ", - description="Stop all vulnerable virtual machines", - tags=["bundles - core - proxmox - vms - default-configuration - admin"], - response_model=Reply_ProxmoxVmsVMID_StartStopPauseResume, -) -def bundles_proxmox_configure_default_vms_stop_vms_vuln(req: Request_BundlesCoreProxmoxConfigureDefaultVms_StartStopPauseResumeAdminVulnStudentVms): - - return bundles_proxmox_configure_default_vms_run(req, "core/proxmox/configure/default/vms/start-stop-pause-resume-vms-admin", - "vm_stop" - ) - - -####################################################################################################################### - -# -# => /v0/admin/run/bundles/core/proxmox/configure/default/pause-vms-admin -# -@router.post( - path="/core/proxmox/configure/default/pause-vms-admin", - summary="Pause student vms ", - description="Pause all vulnerable virtual machines", - tags=["bundles - core - proxmox - vms - default-configuration - admin"], - response_model=Reply_ProxmoxVmsVMID_StartStopPauseResume, -) -def bundles_proxmox_configure_default_vms_stop_vms_vuln(req: Request_BundlesCoreProxmoxConfigureDefaultVms_StartStopPauseResumeAdminVulnStudentVms): - - return bundles_proxmox_configure_default_vms_run(req, "core/proxmox/configure/default/vms/start-stop-pause-resume-vms-admin", - "vm_pause" - ) - - -####################################################################################################################### - -# -# => /v0/admin/run/bundles/core/proxmox/configure/default/resume-vms-admin -# -@router.post( - path="/core/proxmox/configure/default/resume-vms-admin", - summary="Resume student vms ", - description="Resume all vulnerable virtual machines", - tags=["bundles - core - proxmox - vms - default-configuration - admin"], - response_model=Reply_ProxmoxVmsVMID_StartStopPauseResume, -) -def bundles_proxmox_configure_default_vms_stop_vms_vuln(req: Request_BundlesCoreProxmoxConfigureDefaultVms_StartStopPauseResumeAdminVulnStudentVms): - - return bundles_proxmox_configure_default_vms_run(req, "core/proxmox/configure/default/vms/start-stop-pause-resume-vms-admin", - "vm_resume" - ) - - -def bundles_proxmox_configure_default_vms_run( - req: Request_BundlesCoreProxmoxConfigureDefaultVms_StartStopPauseResumeAdminVulnStudentVms, - action_name: str, - proxmox_vm_action: str) -> JSONResponse: - - # - checked_inventory_filepath = utils.resolve_inventory(INVENTORY_NAME) - # checked_playbook_filepath = utils.resolve_actions_playbook(action_name, "public_github") - checked_playbook_filepath = utils.resolve_bundles_playbook(action_name, "public_github") - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - if debug == 1: - print(":: ACTION_NAME", action_name) - print(":: REQUEST ::", req.model_dump()) - print(f":: PROJECT_ROOT :: {PROJECT_ROOT} ") - print(f":: checked_inventory_filepath :: {checked_inventory_filepath} ") - print(f":: checked_playbook_filepath :: {checked_playbook_filepath} ") - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - extravars = request_checks(req, proxmox_vm_action) - - #### - - rc, events, log_plain, log_ansi = run_playbook_core( - checked_playbook_filepath, - checked_inventory_filepath, - limit=req.proxmox_node, - extravars=extravars, - ) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - payload = reply_processing(events, extravars, log_plain, rc, req) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - if rc == 0: - status = 200 - else: - status = 500 - - return JSONResponse(payload, status_code=status) - - -def reply_processing(events: list[dict] | list[Any], - extravars: dict[Any, Any], - log_plain: str, - rc, - req: Request_BundlesCoreProxmoxConfigureDefaultVms_StartStopPauseResumeAdminVulnStudentVms) -> dict[str, list | Any]: - - """ reply post-processing - json or ansible raw output """ - - if req.as_json: - - ##### OUTPUT TYPE - as_json=True - ##### - - action = extravars["proxmox_vm_action"] - result = extract_action_results(events, action) - - payload = { - "rc": rc, - "result": result - # "action": action, - } # raw - - # payload = {"rc": rc, "action": action, "result": events} - else: - #### - #### OUTPUT AS TEXT - as_json=False - #### - - # payload = {"rc": rc, "log_plain": log_plain, "log_multiline": log_plain.splitlines()} - payload = {"rc": rc, "log_multiline": log_plain.splitlines()} - return payload - - - -def request_checks(req: Request_BundlesCoreProxmoxConfigureDefaultVms_StartStopPauseResumeAdminVulnStudentVms, - proxmox_vm_action: str) -> dict[Any, Any]: - """ request checks """ - - extravars = {} - - extravars["proxmox_vm_action"] = proxmox_vm_action # "vm_stop_force" - - if req.proxmox_node: - extravars["proxmox_node"] = req.proxmox_node - # - # if req.vm_ids: - # extravars["vm_ids"] = req.vm_ids - - # nothing : - if not extravars: - extravars = None - - if debug == 1: - print(f", extra vars : {extravars}") - - # extravars["hosts"] = "proxmox" - - return extravars - diff --git a/app/routes/v0/admin/bundles/proxmox/configure/default/vms/start_stop_pause_resume_student.py b/app/routes/v0/admin/bundles/proxmox/configure/default/vms/start_stop_pause_resume_student.py deleted file mode 100644 index 831f0f2..0000000 --- a/app/routes/v0/admin/bundles/proxmox/configure/default/vms/start_stop_pause_resume_student.py +++ /dev/null @@ -1,210 +0,0 @@ -from typing import Any -import logging -from fastapi import APIRouter, HTTPException -from fastapi.responses import JSONResponse - -from app.schemas.bundles.core.proxmox.configure.default.vms.start_stop_resume_pause_default import Request_BundlesCoreProxmoxConfigureDefaultVms_StartStopPauseResumeAdminVulnStudentVms -from app.schemas.proxmox.vm_id.start_stop_resume_pause import Reply_ProxmoxVmsVMID_StartStopPauseResume - -from app.runner import run_playbook_core # , extract_action_results -from app.extract_actions import extract_action_results -from app import utils -from pathlib import Path -import os - -# -# ISSUE - #30 -# - -debug = 0 - -router = APIRouter() -logger = logging.getLogger(__name__) - -# PROJECT_ROOT = Path(__file__).resolve().parents[5] -PROJECT_ROOT = Path(os.getenv("PROJECT_ROOT_DIR")).resolve() -INVENTORY_NAME = "hosts" - -# -# => /v0/admin/run/bundles/core/proxmox/configure/default/start-vms-vuln -# -@router.post( - path="/core/proxmox/configure/default/start-vms-student", - summary="Start student vms ", - description="Start all vulnerable virtual machines", - tags=["bundles - core - proxmox - vms - default-configuration - student"], - response_model=Reply_ProxmoxVmsVMID_StartStopPauseResume, -) -def bundles_proxmox_configure_default_vms_stop_vms_vuln(req: Request_BundlesCoreProxmoxConfigureDefaultVms_StartStopPauseResumeAdminVulnStudentVms): - - return bundles_proxmox_configure_default_vms_run(req, - "core/proxmox/configure/default/vms/start-stop-pause-resume-vms-student", - "vm_start" - ) - -####################################################################################################################### - -# -# => /v0/admin/run/bundles/core/proxmox/configure/default/stop-vms-student -# -@router.post( - path="/core/proxmox/configure/default/stop-vms-student", - summary="Stop student vms ", - description="Stop all vulnerable virtual machines", - tags=["bundles - core - proxmox - vms - default-configuration - student"], - response_model=Reply_ProxmoxVmsVMID_StartStopPauseResume, -) -def bundles_proxmox_configure_default_vms_stop_vms_vuln(req: Request_BundlesCoreProxmoxConfigureDefaultVms_StartStopPauseResumeAdminVulnStudentVms): - - return bundles_proxmox_configure_default_vms_run(req, "core/proxmox/configure/default/vms/start-stop-pause-resume-vms-student", - "vm_stop" - ) - - -####################################################################################################################### - -# -# => /v0/admin/run/bundles/core/proxmox/configure/default/pause-vms-student -# -@router.post( - path="/core/proxmox/configure/default/pause-vms-student", - summary="Pause student vms ", - description="Pause all vulnerable virtual machines", - tags=["bundles - core - proxmox - vms - default-configuration - student"], - response_model=Reply_ProxmoxVmsVMID_StartStopPauseResume, -) -def bundles_proxmox_configure_default_vms_stop_vms_vuln(req: Request_BundlesCoreProxmoxConfigureDefaultVms_StartStopPauseResumeAdminVulnStudentVms): - - return bundles_proxmox_configure_default_vms_run(req, "core/proxmox/configure/default/vms/start-stop-pause-resume-vms-student", - "vm_pause" - ) - - -####################################################################################################################### - -# -# => /v0/admin/run/bundles/core/proxmox/configure/default/resume-vms-student -# -@router.post( - path="/core/proxmox/configure/default/resume-vms-student", - summary="Resume student vms ", - description="Resume all vulnerable virtual machines", - tags=["bundles - core - proxmox - vms - default-configuration - student"], - response_model=Reply_ProxmoxVmsVMID_StartStopPauseResume, -) -def bundles_proxmox_configure_default_vms_stop_vms_vuln(req: Request_BundlesCoreProxmoxConfigureDefaultVms_StartStopPauseResumeAdminVulnStudentVms): - - return bundles_proxmox_configure_default_vms_run(req, "core/proxmox/configure/default/vms/start-stop-pause-resume-vms-student", - "vm_resume" - ) - - -def bundles_proxmox_configure_default_vms_run( - req: Request_BundlesCoreProxmoxConfigureDefaultVms_StartStopPauseResumeAdminVulnStudentVms, - action_name: str, - proxmox_vm_action: str) -> JSONResponse: - - # - checked_inventory_filepath = utils.resolve_inventory(INVENTORY_NAME) - # checked_playbook_filepath = utils.resolve_actions_playbook(action_name, "public_github") - checked_playbook_filepath = utils.resolve_bundles_playbook(action_name, "public_github") - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - if debug == 1: - print(":: ACTION_NAME", action_name) - print(":: REQUEST ::", req.model_dump()) - print(f":: PROJECT_ROOT :: {PROJECT_ROOT} ") - print(f":: checked_inventory_filepath :: {checked_inventory_filepath} ") - print(f":: checked_playbook_filepath :: {checked_playbook_filepath} ") - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - extravars = request_checks(req, proxmox_vm_action) - - #### - - rc, events, log_plain, log_ansi = run_playbook_core( - checked_playbook_filepath, - checked_inventory_filepath, - limit=req.proxmox_node, - extravars=extravars, - ) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - payload = reply_processing(events, extravars, log_plain, rc, req) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - if rc == 0: - status = 200 - else: - status = 500 - - return JSONResponse(payload, status_code=status) - - -def reply_processing(events: list[dict] | list[Any], - extravars: dict[Any, Any], - log_plain: str, - rc, - req: Request_BundlesCoreProxmoxConfigureDefaultVms_StartStopPauseResumeAdminVulnStudentVms) -> dict[str, list | Any]: - - """ reply post-processing - json or ansible raw output """ - - if req.as_json: - - ##### OUTPUT TYPE - as_json=True - ##### - - action = extravars["proxmox_vm_action"] - result = extract_action_results(events, action) - - payload = { - "rc": rc, - "result": result - # "action": action, - } # raw - - # payload = {"rc": rc, "action": action, "result": events} - else: - #### - #### OUTPUT AS TEXT - as_json=False - #### - - # payload = {"rc": rc, "log_plain": log_plain, "log_multiline": log_plain.splitlines()} - payload = {"rc": rc, "log_multiline": log_plain.splitlines()} - return payload - - - -def request_checks(req: Request_BundlesCoreProxmoxConfigureDefaultVms_StartStopPauseResumeAdminVulnStudentVms, - proxmox_vm_action: str) -> dict[Any, Any]: - """ request checks """ - - extravars = {} - - extravars["proxmox_vm_action"] = proxmox_vm_action # "vm_stop_force" - - if req.proxmox_node: - extravars["proxmox_node"] = req.proxmox_node - # - # if req.vm_ids: - # extravars["vm_ids"] = req.vm_ids - - # nothing : - if not extravars: - extravars = None - - if debug == 1: - print(f", extra vars : {extravars}") - - # extravars["hosts"] = "proxmox" - - return extravars - diff --git a/app/routes/v0/admin/bundles/proxmox/configure/default/vms/start_stop_pause_resume_vuln.py b/app/routes/v0/admin/bundles/proxmox/configure/default/vms/start_stop_pause_resume_vuln.py deleted file mode 100644 index f7c9a3a..0000000 --- a/app/routes/v0/admin/bundles/proxmox/configure/default/vms/start_stop_pause_resume_vuln.py +++ /dev/null @@ -1,210 +0,0 @@ -from typing import Any -import logging -from fastapi import APIRouter, HTTPException -from fastapi.responses import JSONResponse - -from app.schemas.bundles.core.proxmox.configure.default.vms.start_stop_resume_pause_default import Request_BundlesCoreProxmoxConfigureDefaultVms_StartStopPauseResumeAdminVulnStudentVms -from app.schemas.proxmox.vm_id.start_stop_resume_pause import Reply_ProxmoxVmsVMID_StartStopPauseResume - -from app.runner import run_playbook_core # , extract_action_results -from app.extract_actions import extract_action_results -from app import utils -from pathlib import Path -import os - -# -# ISSUE - #30 -# - -debug = 0 - -router = APIRouter() -logger = logging.getLogger(__name__) - -# PROJECT_ROOT = Path(__file__).resolve().parents[5] -PROJECT_ROOT = Path(os.getenv("PROJECT_ROOT_DIR")).resolve() -INVENTORY_NAME = "hosts" - -# -# => /v0/admin/run/bundles/core/proxmox/configure/default/start-vms-vuln -# -@router.post( - path="/core/proxmox/configure/default/start-vms-vuln", - summary="Start vulnerable vms ", - description="Start all vulnerable virtual machines", - tags=["bundles - core - proxmox - vms - default-configuration - vuln"], - response_model=Reply_ProxmoxVmsVMID_StartStopPauseResume, -) -def bundles_proxmox_configure_default_vms_stop_vms_vuln(req: Request_BundlesCoreProxmoxConfigureDefaultVms_StartStopPauseResumeAdminVulnStudentVms): - - return bundles_proxmox_configure_default_vms_run(req, - "core/proxmox/configure/default/vms/start-stop-pause-resume-vms-vuln", - "vm_start" - ) - -####################################################################################################################### - -# -# => /v0/admin/run/bundles/core/proxmox/configure/default/stop-vms-vuln -# -@router.post( - path="/core/proxmox/configure/default/stop-vms-vuln", - summary="Stop vulnerable vms ", - description="Stop all vulnerable virtual machines", - tags=["bundles - core - proxmox - vms - default-configuration - vuln"], - response_model=Reply_ProxmoxVmsVMID_StartStopPauseResume, -) -def bundles_proxmox_configure_default_vms_stop_vms_vuln(req: Request_BundlesCoreProxmoxConfigureDefaultVms_StartStopPauseResumeAdminVulnStudentVms): - - return bundles_proxmox_configure_default_vms_run(req, "core/proxmox/configure/default/vms/start-stop-pause-resume-vms-vuln", - "vm_stop" - ) - - -####################################################################################################################### - -# -# => /v0/admin/run/bundles/core/proxmox/configure/default/pause-vms-vuln -# -@router.post( - path="/core/proxmox/configure/default/pause-vms-vuln", - summary="Pause vulnerable vms ", - description="Pause all vulnerable virtual machines", - tags=["bundles - core - proxmox - vms - default-configuration - vuln"], - response_model=Reply_ProxmoxVmsVMID_StartStopPauseResume, -) -def bundles_proxmox_configure_default_vms_stop_vms_vuln(req: Request_BundlesCoreProxmoxConfigureDefaultVms_StartStopPauseResumeAdminVulnStudentVms): - - return bundles_proxmox_configure_default_vms_run(req, "core/proxmox/configure/default/vms/start-stop-pause-resume-vms-vuln", - "vm_pause" - ) - - -####################################################################################################################### - -# -# => /v0/admin/run/bundles/core/proxmox/configure/default/resume-vms-vuln -# -@router.post( - path="/core/proxmox/configure/default/resume-vms-vuln", - summary="Resume vulnerable vms ", - description="Resume all vulnerable virtual machines", - tags=["bundles - core - proxmox - vms - default-configuration - vuln"], - response_model=Reply_ProxmoxVmsVMID_StartStopPauseResume, -) -def bundles_proxmox_configure_default_vms_stop_vms_vuln(req: Request_BundlesCoreProxmoxConfigureDefaultVms_StartStopPauseResumeAdminVulnStudentVms): - - return bundles_proxmox_configure_default_vms_run(req, "core/proxmox/configure/default/vms/start-stop-pause-resume-vms-vuln", - "vm_resume" - ) - - -def bundles_proxmox_configure_default_vms_run( - req: Request_BundlesCoreProxmoxConfigureDefaultVms_StartStopPauseResumeAdminVulnStudentVms, - action_name: str, - proxmox_vm_action: str) -> JSONResponse: - - # - checked_inventory_filepath = utils.resolve_inventory(INVENTORY_NAME) - # checked_playbook_filepath = utils.resolve_actions_playbook(action_name, "public_github") - checked_playbook_filepath = utils.resolve_bundles_playbook(action_name, "public_github") - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - if debug == 1: - print(":: ACTION_NAME", action_name) - print(":: REQUEST ::", req.model_dump()) - print(f":: PROJECT_ROOT :: {PROJECT_ROOT} ") - print(f":: checked_inventory_filepath :: {checked_inventory_filepath} ") - print(f":: checked_playbook_filepath :: {checked_playbook_filepath} ") - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - extravars = request_checks(req, proxmox_vm_action) - - #### - - rc, events, log_plain, log_ansi = run_playbook_core( - checked_playbook_filepath, - checked_inventory_filepath, - limit=req.proxmox_node, - extravars=extravars, - ) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - payload = reply_processing(events, extravars, log_plain, rc, req) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - if rc == 0: - status = 200 - else: - status = 500 - - return JSONResponse(payload, status_code=status) - - -def reply_processing(events: list[dict] | list[Any], - extravars: dict[Any, Any], - log_plain: str, - rc, - req: Request_BundlesCoreProxmoxConfigureDefaultVms_StartStopPauseResumeAdminVulnStudentVms) -> dict[str, list | Any]: - - """ reply post-processing - json or ansible raw output """ - - if req.as_json: - - ##### OUTPUT TYPE - as_json=True - ##### - - action = extravars["proxmox_vm_action"] - result = extract_action_results(events, action) - - payload = { - "rc": rc, - "result": result - # "action": action, - } # raw - - # payload = {"rc": rc, "action": action, "result": events} - else: - #### - #### OUTPUT AS TEXT - as_json=False - #### - - # payload = {"rc": rc, "log_plain": log_plain, "log_multiline": log_plain.splitlines()} - payload = {"rc": rc, "log_multiline": log_plain.splitlines()} - return payload - - - -def request_checks(req: Request_BundlesCoreProxmoxConfigureDefaultVms_StartStopPauseResumeAdminVulnStudentVms, - proxmox_vm_action: str) -> dict[Any, Any]: - """ request checks """ - - extravars = {} - - extravars["proxmox_vm_action"] = proxmox_vm_action # "vm_stop_force" - - if req.proxmox_node: - extravars["proxmox_node"] = req.proxmox_node - # - # if req.vm_ids: - # extravars["vm_ids"] = req.vm_ids - - # nothing : - if not extravars: - extravars = None - - if debug == 1: - print(f", extra vars : {extravars}") - - # extravars["hosts"] = "proxmox" - - return extravars - diff --git a/app/routes/v0/debug/__init__.py b/app/routes/v0/debug/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/routes/v0/debug/_test_func.py b/app/routes/v0/debug/_test_func.py deleted file mode 100644 index 224e1a6..0000000 --- a/app/routes/v0/debug/_test_func.py +++ /dev/null @@ -1,56 +0,0 @@ - -from fastapi import APIRouter, HTTPException -from app.utils.vm_id_name_resolver import * - -import os - -# -# ISSUE - #18 -# - - -router = APIRouter() - -# PROJECT_ROOT = Path(__file__).resolve().parents[4] -PROJECT_ROOT = Path(os.getenv("PROJECT_ROOT_DIR")).resolve() -PLAYBOOK_SRC = PROJECT_ROOT / "playbooks" / "ping.yml" -INVENTORY_SRC = PROJECT_ROOT / "inventory" / "hosts.yml" - - -debug = 0 - -@router.post( - path="/func_test", - summary="temp stuff", - description="_testing - tmp ", - tags=["__tmp_testing"], -) - -def debug_ping(): - - if debug ==1: - # print(":: REQUEST ::", req.dict()) - print(f":: PROJECT_ROOT :: {PROJECT_ROOT} ") - print(f":: PLAYBOOK_SRC :: {PLAYBOOK_SRC} ") - print(f":: INVENTORY_SRC :: {INVENTORY_SRC} ") - - if not PLAYBOOK_SRC.exists(): - raise HTTPException(status_code=400, detail=f":: MISSING PLAYBOOK : {PLAYBOOK_SRC}") - if not INVENTORY_SRC.exists(): - raise HTTPException(status_code=400, detail=f":: MISSING INVENTORY : {INVENTORY_SRC}") - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - print("------------------------------") - # out = resolv_id_to_vm_name("px-testing", 29999) # must raise error 500 - out = resolv_id_to_vm_name("px-testing", 1000) - print ("GOT :::") - print (out["vm_name"]) - - print("------------------------------") - out = resolv_id_to_vm_name("px-testing", "1000") - print ("GOT :::") - print (out["vm_name"]) - print("------------------------------") diff --git a/app/routes/v0/debug/ping.py b/app/routes/v0/debug/ping.py deleted file mode 100644 index 50a2b8a..0000000 --- a/app/routes/v0/debug/ping.py +++ /dev/null @@ -1,107 +0,0 @@ -from typing import Any - -from fastapi import APIRouter, HTTPException -from fastapi.responses import JSONResponse - -from app.runner import run_playbook_core # , extract_action_results -from app.schemas.debug.ping import Request_DebugPing - -from pathlib import Path -import os - -# -# ISSUE - #1 -# - - -router = APIRouter() - -# PROJECT_ROOT = Path(__file__).resolve().parents[4] -PROJECT_ROOT = Path(os.getenv("PROJECT_ROOT_DIR")).resolve() -PLAYBOOK_SRC = PROJECT_ROOT / "playbooks" / "ping.yml" -INVENTORY_SRC = PROJECT_ROOT / "inventory" / "hosts.yml" - - -debug = 0 - -@router.post( - path="/ping", - summary="Run Ansible ping utility", - description="This endpoint runs the Ansible ping module to check connectivity with target hosts.", - tags=["runner"], -) - -def debug_ping(req: Request_DebugPing): - - if debug ==1: - print(":: REQUEST ::", req.model_dump()) - print(f":: PROJECT_ROOT :: {PROJECT_ROOT} ") - print(f":: PLAYBOOK_SRC :: {PLAYBOOK_SRC} ") - print(f":: INVENTORY_SRC :: {INVENTORY_SRC} ") - - if not PLAYBOOK_SRC.exists(): - raise HTTPException(status_code=400, detail=f":: MISSING PLAYBOOK : {PLAYBOOK_SRC}") - if not INVENTORY_SRC.exists(): - raise HTTPException(status_code=400, detail=f":: MISSING INVENTORY : {INVENTORY_SRC}") - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - extravars = request_checks(req) - - #### - - rc, events, log_plain, log_ansi = run_playbook_core( - PLAYBOOK_SRC, - INVENTORY_SRC, - limit=req.hosts, - extravars=extravars, - ) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - payload = reply_processing(log_plain, rc) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - if rc == 0: - status = 200 - else: - status = 500 - - return JSONResponse(payload, status_code=status) - - -def reply_processing(log_plain: str, - rc - ) -> dict[str, list[str] | Any]: - - """ reply post-processing - for ansible raw output """ - - payload = {"rc": rc, "log_multiline": log_plain.splitlines()} - - return payload - - -def request_checks(req: Request_DebugPing) -> dict[Any, Any]: - """ requet checks """ - - extravars = {} - - if req.proxmox_node: - extravars["proxmox_node"] = req.proxmox_node - - # nothing : - if not extravars: - extravars = None - - if debug == 1: - print(f", extra vars : {extravars}") - - return extravars - diff --git a/app/routes/v0/proxmox/__init__.py b/app/routes/v0/proxmox/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/routes/v0/proxmox/firewall/__init__.py b/app/routes/v0/proxmox/firewall/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/routes/v0/proxmox/firewall/add_iptable_alias.py b/app/routes/v0/proxmox/firewall/add_iptable_alias.py deleted file mode 100644 index e80cff2..0000000 --- a/app/routes/v0/proxmox/firewall/add_iptable_alias.py +++ /dev/null @@ -1,160 +0,0 @@ -from typing import Any -import logging -from fastapi import APIRouter, HTTPException -from fastapi.responses import JSONResponse - -from app.schemas.proxmox.firewall.add_iptable_alias import Request_ProxmoxFirewall_AddIptablesAlias -from app.schemas.proxmox.firewall.add_iptable_alias import Reply_ProxmoxFirewallWithStorageName_AddIptablesAlias - -from app.runner import run_playbook_core # , extract_action_results -from app.extract_actions import extract_action_results -from app import utils -from pathlib import Path -import os - -# -# ISSUE - #12 -# - -debug = 0 - -router = APIRouter() -logger = logging.getLogger(__name__) - -# PROJECT_ROOT = Path(__file__).resolve().parents[6] -PROJECT_ROOT = Path(os.getenv("PROJECT_ROOT_DIR")).resolve() -INVENTORY_NAME = "hosts" -PLAYBOOK_SRC = PROJECT_ROOT / "playbooks" / "generic.yml" -# INVENTORY_SRC = PROJECT_ROOT / "inventory" / "hosts.yml" - -# -# => /v0/admin/proxmox/firewall/alias/add -# -@router.post( - path="/vm/alias/add", - summary="Add a firewall alias", - description="Add a new alias to the Proxmox firewall - IPs, subnets/networks, hostnames", - tags=["proxmox - firewall"], - response_model=Reply_ProxmoxFirewallWithStorageName_AddIptablesAlias, - response_description="Information about the created firewall alias", -) -def proxmox_firewall_vm_alias_add(req: Request_ProxmoxFirewall_AddIptablesAlias): - - if not PLAYBOOK_SRC.exists(): - err = f":: err - MISSING PLAYBOOK : {PLAYBOOK_SRC}" - logging.error(err) - raise HTTPException(status_code=400, detail=err) - - checked_playbook_filepath = PLAYBOOK_SRC - checked_inventory_filepath = utils.resolve_inventory(INVENTORY_NAME) - - - if debug ==1: - print("") - print(":: REQUEST ::", req.dict()) - print(f":: checked_inventory_filepath :: {checked_inventory_filepath} ") - print(f":: checked_playbook_filepath :: {checked_playbook_filepath} ") - print("") - - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - extravars = request_checks(req) - - #### - - rc, events, log_plain, log_ansi = run_playbook_core( - checked_playbook_filepath, - checked_inventory_filepath, - # limit=req.hosts, - limit=extravars["hosts"], - extravars=extravars, - ) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - payload = reply_processing(events, extravars, log_plain, rc, req) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - if rc == 0: - status = 200 - else: - status = 500 - - return JSONResponse(payload, status_code=status) - - -def reply_processing(events: list[dict] | list[Any], - extravars: dict[Any, Any], - log_plain: str, - rc, - req: Request_ProxmoxFirewall_AddIptablesAlias) -> dict[str, list | Any]: - - """ reply post-processing - json or ansible raw output """ - - if req.as_json: - - ##### OUTPUT TYPE - as_json=True - ##### - - action = extravars["proxmox_vm_action"] - result = extract_action_results(events, action) - - payload = { - "rc": rc, - "result": result - # "action": action, - } # raw - - # payload = {"rc": rc, "action": action, "result": events} - else: - #### - #### OUTPUT AS TEXT - as_json=False - #### - - # payload = {"rc": rc, "log_plain": log_plain, "log_multiline": log_plain.splitlines()} - payload = {"rc": rc, "log_multiline": log_plain.splitlines()} - return payload - - -def request_checks(req: Request_ProxmoxFirewall_AddIptablesAlias) -> dict[Any, Any]: - """ request checks """ - - extravars = {} - extravars["proxmox_vm_action"] = "firewall_vm_add_iptables_alias" - - if req.proxmox_node: - extravars["proxmox_node"] = req.proxmox_node - - if req.vm_id is not None: - extravars["vm_id"] = req.vm_id - - #### - - if req.vm_fw_alias_name is not None: - extravars["vm_fw_alias_name"] = req.vm_fw_alias_name - - if req.vm_fw_alias_cidr is not None: - extravars["vm_fw_alias_cidr"] = req.vm_fw_alias_cidr - - if req.vm_fw_alias_comment is not None: - extravars["vm_fw_alias_comment"] = req.vm_fw_alias_comment - - - # nothing : - if not extravars: - extravars = None - - if debug == 1: - print(f", extra vars : {extravars}") - - extravars["hosts"] = "proxmox" - return extravars - diff --git a/app/routes/v0/proxmox/firewall/apply_iptables_rules.py b/app/routes/v0/proxmox/firewall/apply_iptables_rules.py deleted file mode 100644 index e08ee6c..0000000 --- a/app/routes/v0/proxmox/firewall/apply_iptables_rules.py +++ /dev/null @@ -1,186 +0,0 @@ -from typing import Any -import logging -from fastapi import APIRouter, HTTPException -from fastapi.responses import JSONResponse - -from app.schemas.proxmox.firewall.apply_iptables_rules import Request_ProxmoxFirewall_ApplyIptablesRules -from app.schemas.proxmox.firewall.apply_iptables_rules import Reply_ProxmoxFirewallWithStorageName_ApplyIptablesRules - -from app.runner import run_playbook_core # , extract_action_results -from app.extract_actions import extract_action_results -from app import utils -from pathlib import Path -import os - -# -# ISSUE - #12 -# - -debug = 0 - -router = APIRouter() -logger = logging.getLogger(__name__) - -# PROJECT_ROOT = Path(__file__).resolve().parents[6] -PROJECT_ROOT = Path(os.getenv("PROJECT_ROOT_DIR")).resolve() -INVENTORY_NAME = "hosts" -PLAYBOOK_SRC = PROJECT_ROOT / "playbooks" / "generic.yml" -# INVENTORY_SRC = PROJECT_ROOT / "inventory" / "hosts.yml" - -# -# => /v0/admin/proxmox/firewall/vm/rules/apply ---- should rename to rules/add ? -# -@router.post( - path="/vm/rules/apply", - summary="Apply firewall rules", - description="Apply the received firewall rules to the proxmox firewall", - tags=["proxmox - firewall"], - response_model=Reply_ProxmoxFirewallWithStorageName_ApplyIptablesRules, - response_description="Details of the applied firewall rules", -) -def proxmox_firewall_vm_rules_add(req: Request_ProxmoxFirewall_ApplyIptablesRules): - - if not PLAYBOOK_SRC.exists(): - err = f":: err - MISSING PLAYBOOK : {PLAYBOOK_SRC}" - logging.error(err) - raise HTTPException(status_code=400, detail=err) - - checked_playbook_filepath = PLAYBOOK_SRC - checked_inventory_filepath = utils.resolve_inventory(INVENTORY_NAME) - - - if debug ==1: - print("") - print(":: REQUEST ::", req.dict()) - print(f":: checked_inventory_filepath :: {checked_inventory_filepath} ") - print(f":: checked_playbook_filepath :: {checked_playbook_filepath} ") - print("") - - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - extravars = request_checks(req) - - #### - - rc, events, log_plain, log_ansi = run_playbook_core( - checked_playbook_filepath, - checked_inventory_filepath, - # limit=req.hosts, - limit=extravars["hosts"], - extravars=extravars, - ) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - payload = reply_processing(events, extravars, log_plain, rc, req) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - if rc == 0: - status = 200 - else: - status = 500 - - return JSONResponse(payload, status_code=status) - - -def reply_processing(events: list[dict] | list[Any], - extravars: dict[Any, Any], - log_plain: str, - rc, - req: Request_ProxmoxFirewall_ApplyIptablesRules) -> dict[str, list | Any]: - - """ reply post-processing - json or ansible raw output """ - - if req.as_json: - - ##### OUTPUT TYPE - as_json=True - ##### - - action = extravars["proxmox_vm_action"] - result = extract_action_results(events, action) - - payload = { - "rc": rc, - "result": result - # "action": action, - } # raw - - # payload = {"rc": rc, "action": action, "result": events} - else: - #### - #### OUTPUT AS TEXT - as_json=False - #### - - # payload = {"rc": rc, "log_plain": log_plain, "log_multiline": log_plain.splitlines()} - payload = {"rc": rc, "log_multiline": log_plain.splitlines()} - return payload - - -def request_checks(req: Request_ProxmoxFirewall_ApplyIptablesRules) -> dict[Any, Any]: - """ request checks """ - - extravars = {} - extravars["proxmox_vm_action"] = "firewall_vm_apply_iptables_rule" - - if req.proxmox_node: - extravars["proxmox_node"] = req.proxmox_node - - if req.vm_id is not None: - extravars["vm_id"] = req.vm_id - - #### - - if req.vm_fw_action is not None: - extravars["vm_fw_action"] = req.vm_fw_action - - if req.vm_fw_dport is not None: - extravars["vm_fw_dport"] = req.vm_fw_dport - - if req.vm_fw_enable is not None: - extravars["vm_fw_enable"] = req.vm_fw_enable - - if req.vm_fw_proto is not None: - extravars["vm_fw_proto"] = req.vm_fw_proto - - if req.vm_fw_type is not None: - extravars["vm_fw_type"] = req.vm_fw_type - - if req.vm_fw_log is not None: - extravars["vm_fw_log"] = req.vm_fw_log - - if req.vm_fw_iface is not None: - extravars["vm_fw_iface"] = req.vm_fw_iface - - if req.vm_fw_source is not None: - extravars["vm_fw_source"] = req.vm_fw_source - - if req.vm_fw_dest is not None: - extravars["vm_fw_dest"] = req.vm_fw_dest - - if req.vm_fw_sport is not None: - extravars["vm_fw_sport"] = req.vm_fw_sport - - if req.vm_fw_comment is not None: - extravars["vm_fw_comment"] = req.vm_fw_comment - - if req.vm_fw_pos is not None: - extravars["vm_fw_pos"] = req.vm_fw_pos - - # nothing : - if not extravars: - extravars = None - - if debug == 1: - print(f", extra vars : {extravars}") - - extravars["hosts"] = "proxmox" - return extravars - diff --git a/app/routes/v0/proxmox/firewall/delete_iptables_alias.py b/app/routes/v0/proxmox/firewall/delete_iptables_alias.py deleted file mode 100644 index ac92671..0000000 --- a/app/routes/v0/proxmox/firewall/delete_iptables_alias.py +++ /dev/null @@ -1,155 +0,0 @@ -from typing import Any -import logging -from fastapi import APIRouter, HTTPException -from fastapi.responses import JSONResponse - -from app.schemas.proxmox.firewall.delete_iptables_alias import Request_ProxmoxFirewall_DeleteIptablesAlias -from app.schemas.proxmox.firewall.delete_iptables_alias import Reply_ProxmoxFirewallWithStorageName_DeleteIptablesAlias - -from app.runner import run_playbook_core # , extract_action_results -from app.extract_actions import extract_action_results -from app import utils -from pathlib import Path -import os - -# -# ISSUE - #12 -# - -debug = 0 - -router = APIRouter() -logger = logging.getLogger(__name__) - -# PROJECT_ROOT = Path(__file__).resolve().parents[6] -PROJECT_ROOT = Path(os.getenv("PROJECT_ROOT_DIR")).resolve() -INVENTORY_NAME = "hosts" -PLAYBOOK_SRC = PROJECT_ROOT / "playbooks" / "generic.yml" -# INVENTORY_SRC = PROJECT_ROOT / "inventory" / "hosts.yml" - -# -# => /v0/admin/proxmox/firewall/vm/alias/delete -# -@router.delete( - path="/vm/alias/delete", - summary="Delete a firewall alias", - description="Remove an existing alias from the proxmox firewall", - tags=["proxmox - firewall"], - response_model=Reply_ProxmoxFirewallWithStorageName_DeleteIptablesAlias, - response_description="Details of the deleted firewall alias", -) -def proxmox_firewall_vm_alias_delete(req: Request_ProxmoxFirewall_DeleteIptablesAlias): - - if not PLAYBOOK_SRC.exists(): - err = f":: err - MISSING PLAYBOOK : {PLAYBOOK_SRC}" - logging.error(err) - raise HTTPException(status_code=400, detail=err) - - checked_playbook_filepath = PLAYBOOK_SRC - checked_inventory_filepath = utils.resolve_inventory(INVENTORY_NAME) - - - if debug ==1: - print("") - print(":: REQUEST ::", req.dict()) - print(f":: checked_inventory_filepath :: {checked_inventory_filepath} ") - print(f":: checked_playbook_filepath :: {checked_playbook_filepath} ") - print("") - - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - extravars = request_checks(req) - - #### - - rc, events, log_plain, log_ansi = run_playbook_core( - checked_playbook_filepath, - checked_inventory_filepath, - # limit=req.hosts, - limit=extravars["hosts"], - extravars=extravars, - ) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - payload = reply_processing(events, extravars, log_plain, rc, req) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - if rc == 0: - status = 200 - else: - status = 500 - - return JSONResponse(payload, status_code=status) - - -def reply_processing(events: list[dict] | list[Any], - extravars: dict[Any, Any], - log_plain: str, - rc, - req: Request_ProxmoxFirewall_DeleteIptablesAlias) -> dict[str, list | Any]: - - """ reply post-processing - json or ansible raw output """ - - if req.as_json: - - ##### OUTPUT TYPE - as_json=True - ##### - - action = extravars["proxmox_vm_action"] - result = extract_action_results(events, action) - - payload = { - "rc": rc, - "result": result - # "action": action, - } # raw - - # payload = {"rc": rc, "action": action, "result": events} - else: - #### - #### OUTPUT AS TEXT - as_json=False - #### - - # payload = {"rc": rc, "log_plain": log_plain, "log_multiline": log_plain.splitlines()} - payload = {"rc": rc, "log_multiline": log_plain.splitlines()} - return payload - - -def request_checks(req: Request_ProxmoxFirewall_DeleteIptablesAlias) -> dict[Any, Any]: - """ request checks """ - - extravars = {} - extravars["proxmox_vm_action"] = "firewall_vm_delete_iptables_alias" - - if req.proxmox_node: - extravars["proxmox_node"] = req.proxmox_node - - #### - - if req.vm_id is not None: - extravars["vm_id"] = req.vm_id - - if req.vm_fw_alias_name is not None: - extravars["vm_fw_alias_name"] = req.vm_fw_alias_name - - - - # nothing : - if not extravars: - extravars = None - - if debug == 1: - print(f", extra vars : {extravars}") - - extravars["hosts"] = "proxmox" - return extravars - diff --git a/app/routes/v0/proxmox/firewall/delete_iptables_rule.py b/app/routes/v0/proxmox/firewall/delete_iptables_rule.py deleted file mode 100644 index 99bbc6f..0000000 --- a/app/routes/v0/proxmox/firewall/delete_iptables_rule.py +++ /dev/null @@ -1,154 +0,0 @@ -from typing import Any -import logging -from fastapi import APIRouter, HTTPException -from fastapi.responses import JSONResponse - -from app.schemas.proxmox.firewall.delete_iptables_rule import Request_ProxmoxFirewall_DeleteIptablesRule -from app.schemas.proxmox.firewall.delete_iptables_rule import Reply_ProxmoxFirewallWithStorageName_DeleteIptablesRule - -from app.runner import run_playbook_core # , extract_action_results -from app.extract_actions import extract_action_results -from app import utils -from pathlib import Path -import os - -# -# ISSUE - #12 -# - -debug = 0 - -router = APIRouter() -logger = logging.getLogger(__name__) - -# PROJECT_ROOT = Path(__file__).resolve().parents[6] -PROJECT_ROOT = Path(os.getenv("PROJECT_ROOT_DIR")).resolve() -INVENTORY_NAME = "hosts" -PLAYBOOK_SRC = PROJECT_ROOT / "playbooks" / "generic.yml" -# INVENTORY_SRC = PROJECT_ROOT / "inventory" / "hosts.yml" - -# -# => /v0/admin/proxmox/firewall/vm/rules/delete -# -@router.delete( - path="/vm/rules/delete", - summary="Delete a firewall rule", - description="Remove an existing rule from the proxmox firewall configuration", - tags=["proxmox - firewall"], - response_model=Request_ProxmoxFirewall_DeleteIptablesRule, - response_description="Details of the deleted firewall rule.", -) -def proxmox_firewall_vm_rules_delete(req: Request_ProxmoxFirewall_DeleteIptablesRule): - - if not PLAYBOOK_SRC.exists(): - err = f":: err - MISSING PLAYBOOK : {PLAYBOOK_SRC}" - logging.error(err) - raise HTTPException(status_code=400, detail=err) - - checked_playbook_filepath = PLAYBOOK_SRC - checked_inventory_filepath = utils.resolve_inventory(INVENTORY_NAME) - - - if debug ==1: - print("") - print(":: REQUEST ::", req.dict()) - print(f":: checked_inventory_filepath :: {checked_inventory_filepath} ") - print(f":: checked_playbook_filepath :: {checked_playbook_filepath} ") - print("") - - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - extravars = request_checks(req) - - #### - - rc, events, log_plain, log_ansi = run_playbook_core( - checked_playbook_filepath, - checked_inventory_filepath, - # limit=req.hosts, - limit=extravars["hosts"], - extravars=extravars, - ) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - payload = reply_processing(events, extravars, log_plain, rc, req) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - if rc == 0: - status = 200 - else: - status = 500 - - return JSONResponse(payload, status_code=status) - - -def reply_processing(events: list[dict] | list[Any], - extravars: dict[Any, Any], - log_plain: str, - rc, - req: Request_ProxmoxFirewall_DeleteIptablesRule) -> dict[str, list | Any]: - - """ reply post-processing - json or ansible raw output """ - - if req.as_json: - - ##### OUTPUT TYPE - as_json=True - ##### - - action = extravars["proxmox_vm_action"] - result = extract_action_results(events, action) - - payload = { - "rc": rc, - "result": result - # "action": action, - } # raw - - # payload = {"rc": rc, "action": action, "result": events} - else: - #### - #### OUTPUT AS TEXT - as_json=False - #### - - # payload = {"rc": rc, "log_plain": log_plain, "log_multiline": log_plain.splitlines()} - payload = {"rc": rc, "log_multiline": log_plain.splitlines()} - return payload - - -def request_checks(req: Request_ProxmoxFirewall_DeleteIptablesRule) -> dict[Any, Any]: - """ request checks """ - - extravars = {} - extravars["proxmox_vm_action"] = "firewall_vm_delete_iptables_rule" - - - if req.proxmox_node: - extravars["proxmox_node"] = req.proxmox_node - - if req.vm_id is not None: - extravars["vm_id"] = req.vm_id - - #### - - if req.vm_fw_pos is not None: - extravars["vm_fw_pos"] = req.vm_fw_pos - - # nothing : - if not extravars: - extravars = None - - if debug == 1: - print(f", extra vars : {extravars}") - - extravars["hosts"] = "proxmox" - return extravars - diff --git a/app/routes/v0/proxmox/firewall/disable_firewall_dc.py b/app/routes/v0/proxmox/firewall/disable_firewall_dc.py deleted file mode 100644 index 6a8a7f6..0000000 --- a/app/routes/v0/proxmox/firewall/disable_firewall_dc.py +++ /dev/null @@ -1,150 +0,0 @@ -from typing import Any -import logging -from fastapi import APIRouter, HTTPException -from fastapi.responses import JSONResponse - -from app.schemas.proxmox.firewall.disable_firewall_dc import Request_ProxmoxFirewall_DisableFirewallDc -from app.schemas.proxmox.firewall.disable_firewall_dc import Reply_ProxmoxFirewallWithStorageName_DisableFirewallDc - -from app.runner import run_playbook_core # , extract_action_results -from app.extract_actions import extract_action_results -from app import utils -from pathlib import Path -import os - -# -# ISSUE - #12 -# - -debug = 0 - -router = APIRouter() -logger = logging.getLogger(__name__) - -# PROJECT_ROOT = Path(__file__).resolve().parents[6] -PROJECT_ROOT = Path(os.getenv("PROJECT_ROOT_DIR")).resolve() -INVENTORY_NAME = "hosts" -PLAYBOOK_SRC = PROJECT_ROOT / "playbooks" / "generic.yml" -# INVENTORY_SRC = PROJECT_ROOT / "inventory" / "hosts.yml" - -# -# => /v0/admin/proxmox/firewall/datacenter/disable -# -@router.post( - path="/datacenter/disable", - summary="Disable datacenter firewall", - description="Disable the proxmox firewall at the datacenter level", - tags=["proxmox - firewall"], - response_model=Reply_ProxmoxFirewallWithStorageName_DisableFirewallDc, - response_description="Details of the disabled datacenter firewall", -) -def proxmox_firewall_dc_disable(req: Request_ProxmoxFirewall_DisableFirewallDc): - - if not PLAYBOOK_SRC.exists(): - err = f":: err - MISSING PLAYBOOK : {PLAYBOOK_SRC}" - logging.error(err) - raise HTTPException(status_code=400, detail=err) - - checked_playbook_filepath = PLAYBOOK_SRC - checked_inventory_filepath = utils.resolve_inventory(INVENTORY_NAME) - - - if debug ==1: - print("") - print(":: REQUEST ::", req.dict()) - print(f":: checked_inventory_filepath :: {checked_inventory_filepath} ") - print(f":: checked_playbook_filepath :: {checked_playbook_filepath} ") - print("") - - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - extravars = request_checks(req) - - #### - - rc, events, log_plain, log_ansi = run_playbook_core( - checked_playbook_filepath, - checked_inventory_filepath, - # limit=req.hosts, - limit=extravars["hosts"], - extravars=extravars, - ) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - payload = reply_processing(events, extravars, log_plain, rc, req) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - if rc == 0: - status = 200 - else: - status = 500 - - return JSONResponse(payload, status_code=status) - - -def reply_processing(events: list[dict] | list[Any], - extravars: dict[Any, Any], - log_plain: str, - rc, - req: Request_ProxmoxFirewall_DisableFirewallDc) -> dict[str, list | Any]: - - """ reply post-processing - json or ansible raw output """ - - if req.as_json: - - ##### OUTPUT TYPE - as_json=True - ##### - - action = extravars["proxmox_vm_action"] - result = extract_action_results(events, action) - - payload = { - "rc": rc, - "result": result - # "action": action, - } # raw - - # payload = {"rc": rc, "action": action, "result": events} - else: - #### - #### OUTPUT AS TEXT - as_json=False - #### - - # payload = {"rc": rc, "log_plain": log_plain, "log_multiline": log_plain.splitlines()} - payload = {"rc": rc, "log_multiline": log_plain.splitlines()} - return payload - - -def request_checks(req: Request_ProxmoxFirewall_DisableFirewallDc) -> dict[Any, Any]: - """ request checks """ - - extravars = {} - extravars["proxmox_vm_action"] = "firewall_dc_disable" - - # if req.proxmox_node: - # extravars["proxmox_node"] = req.proxmox_node - - #### - - if req.proxmox_api_host: - extravars["proxmox_api_host"] = req.proxmox_api_host - - # nothing : - if not extravars: - extravars = None - - if debug == 1: - print(f", extra vars : {extravars}") - - extravars["hosts"] = "proxmox" - return extravars - diff --git a/app/routes/v0/proxmox/firewall/disable_firewall_node.py b/app/routes/v0/proxmox/firewall/disable_firewall_node.py deleted file mode 100644 index 161b12d..0000000 --- a/app/routes/v0/proxmox/firewall/disable_firewall_node.py +++ /dev/null @@ -1,147 +0,0 @@ -from typing import Any -import logging -from fastapi import APIRouter, HTTPException -from fastapi.responses import JSONResponse - -from app.schemas.proxmox.firewall.disable_firewall_node import Request_ProxmoxFirewall_DistableFirewallNode -from app.schemas.proxmox.firewall.disable_firewall_node import Reply_ProxmoxFirewallWithStorageName_DisableFirewallNode - -from app.runner import run_playbook_core # , extract_action_results -from app.extract_actions import extract_action_results -from app import utils -from pathlib import Path -import os - -# -# ISSUE - #12 -# - -debug = 0 - -router = APIRouter() -logger = logging.getLogger(__name__) - -# PROJECT_ROOT = Path(__file__).resolve().parents[6] -PROJECT_ROOT = Path(os.getenv("PROJECT_ROOT_DIR")).resolve() -INVENTORY_NAME = "hosts" -PLAYBOOK_SRC = PROJECT_ROOT / "playbooks" / "generic.yml" -# INVENTORY_SRC = PROJECT_ROOT / "inventory" / "hosts.yml" - -# -# => /v0/admin/proxmox/firewall/node/disable -# -@router.post( - path="/node/disable", - summary="Disable node firewall", - description="Disable the proxmox firewall on a specific node", - tags=["proxmox - firewall"], - response_model=Reply_ProxmoxFirewallWithStorageName_DisableFirewallNode, - response_description="Details of the disabled node firewall", -) -def proxmox_firewall_node_disable(req: Request_ProxmoxFirewall_DistableFirewallNode): - - if not PLAYBOOK_SRC.exists(): - err = f":: err - MISSING PLAYBOOK : {PLAYBOOK_SRC}" - logging.error(err) - raise HTTPException(status_code=400, detail=err) - - checked_playbook_filepath = PLAYBOOK_SRC - checked_inventory_filepath = utils.resolve_inventory(INVENTORY_NAME) - - - if debug ==1: - print("") - print(":: REQUEST ::", req.dict()) - print(f":: checked_inventory_filepath :: {checked_inventory_filepath} ") - print(f":: checked_playbook_filepath :: {checked_playbook_filepath} ") - print("") - - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - extravars = request_checks(req) - - #### - - rc, events, log_plain, log_ansi = run_playbook_core( - checked_playbook_filepath, - checked_inventory_filepath, - # limit=req.hosts, - limit=extravars["hosts"], - extravars=extravars, - ) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - payload = reply_processing(events, extravars, log_plain, rc, req) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - if rc == 0: - status = 200 - else: - status = 500 - - return JSONResponse(payload, status_code=status) - - -def reply_processing(events: list[dict] | list[Any], - extravars: dict[Any, Any], - log_plain: str, - rc, - req: Request_ProxmoxFirewall_DistableFirewallNode) -> dict[str, list | Any]: - - """ reply post-processing - json or ansible raw output """ - - if req.as_json: - - ##### OUTPUT TYPE - as_json=True - ##### - - action = extravars["proxmox_vm_action"] - result = extract_action_results(events, action) - - payload = { - "rc": rc, - "result": result - # "action": action, - } # raw - - # payload = {"rc": rc, "action": action, "result": events} - else: - #### - #### OUTPUT AS TEXT - as_json=False - #### - - # payload = {"rc": rc, "log_plain": log_plain, "log_multiline": log_plain.splitlines()} - payload = {"rc": rc, "log_multiline": log_plain.splitlines()} - return payload - - -def request_checks(req: Request_ProxmoxFirewall_DistableFirewallNode) -> dict[Any, Any]: - """ request checks """ - - extravars = {} - extravars["proxmox_vm_action"] = "firewall_node_disable" - - if req.proxmox_node: - extravars["proxmox_node"] = req.proxmox_node - - #### - - # nothing : - if not extravars: - extravars = None - - if debug == 1: - print(f", extra vars : {extravars}") - - extravars["hosts"] = "proxmox" - return extravars - diff --git a/app/routes/v0/proxmox/firewall/disable_firewall_vm.py b/app/routes/v0/proxmox/firewall/disable_firewall_vm.py deleted file mode 100644 index 06d285e..0000000 --- a/app/routes/v0/proxmox/firewall/disable_firewall_vm.py +++ /dev/null @@ -1,154 +0,0 @@ -from typing import Any -import logging -from fastapi import APIRouter, HTTPException -from fastapi.responses import JSONResponse - -from app.schemas.proxmox.firewall.disable_firewall_vm import Request_ProxmoxFirewall_DistableFirewallVm -from app.schemas.proxmox.firewall.disable_firewall_vm import Reply_ProxmoxFirewallWithStorageName_DisableFirewallVm - -from app.runner import run_playbook_core # , extract_action_results -from app.extract_actions import extract_action_results -from app.utils.vm_id_name_resolver import resolv_id_to_vm_name -from app import utils -from pathlib import Path -import os - -# -# ISSUE - #12 -# - -debug = 0 - -router = APIRouter() -logger = logging.getLogger(__name__) - -# PROJECT_ROOT = Path(__file__).resolve().parents[6] -PROJECT_ROOT = Path(os.getenv("PROJECT_ROOT_DIR")).resolve() -INVENTORY_NAME = "hosts" -PLAYBOOK_SRC = PROJECT_ROOT / "playbooks" / "generic.yml" -# INVENTORY_SRC = PROJECT_ROOT / "inventory" / "hosts.yml" - -# -# => /v0/admin/proxmox/firewall/vm/disable -# -@router.post( - path="/vm/disable", - summary="Disable VM firewall", - description="Disable the proxmox firewall for a specific virtual machine", - tags=["proxmox - firewall"], - response_model=Reply_ProxmoxFirewallWithStorageName_DisableFirewallVm, - response_description="Details of the disabled VM firewall", -) -def proxmox_firewall_vm_disable(req: Request_ProxmoxFirewall_DistableFirewallVm): - - if not PLAYBOOK_SRC.exists(): - err = f":: err - MISSING PLAYBOOK : {PLAYBOOK_SRC}" - logging.error(err) - raise HTTPException(status_code=400, detail=err) - - checked_playbook_filepath = PLAYBOOK_SRC - checked_inventory_filepath = utils.resolve_inventory(INVENTORY_NAME) - - - if debug ==1: - print("") - print(":: REQUEST ::", req.dict()) - print(f":: checked_inventory_filepath :: {checked_inventory_filepath} ") - print(f":: checked_playbook_filepath :: {checked_playbook_filepath} ") - print("") - - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - extravars = request_checks(req) - - #### - - rc, events, log_plain, log_ansi = run_playbook_core( - checked_playbook_filepath, - checked_inventory_filepath, - # limit=req.hosts, - limit=extravars["hosts"], - extravars=extravars, - ) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - payload = reply_processing(events, extravars, log_plain, rc, req) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - if rc == 0: - status = 200 - else: - status = 500 - - return JSONResponse(payload, status_code=status) - - -def reply_processing(events: list[dict] | list[Any], - extravars: dict[Any, Any], - log_plain: str, - rc, - req: Request_ProxmoxFirewall_DistableFirewallVm) -> dict[str, list | Any]: - - """ reply post-processing - json or ansible raw output """ - - if req.as_json: - - ##### OUTPUT TYPE - as_json=True - ##### - - action = extravars["proxmox_vm_action"] - result = extract_action_results(events, action) - - payload = { - "rc": rc, - "result": result - # "action": action, - } # raw - - # payload = {"rc": rc, "action": action, "result": events} - else: - #### - #### OUTPUT AS TEXT - as_json=False - #### - - # payload = {"rc": rc, "log_plain": log_plain, "log_multiline": log_plain.splitlines()} - payload = {"rc": rc, "log_multiline": log_plain.splitlines()} - return payload - - -def request_checks(req: Request_ProxmoxFirewall_DistableFirewallVm) -> dict[Any, Any]: - """ request checks """ - - extravars = {} - extravars["proxmox_vm_action"] = "firewall_vm_disable" - - if req.proxmox_node: - extravars["proxmox_node"] = req.proxmox_node - - #### - - if req.vm_id: - extravars["vm_id"] = req.vm_id - - # extravars["vm_name"] = "admin-wazuh" # TODO - add vm_id <> vm_name resolver func ! - extravars["vm_name"] = resolv_id_to_vm_name(extravars["proxmox_node"], extravars["vm_id"]) - - # nothing : - if not extravars: - extravars = None - - if debug == 1: - print(f", extra vars : {extravars}") - - extravars["hosts"] = "proxmox" - return extravars - diff --git a/app/routes/v0/proxmox/firewall/enable_firewall_dc.py b/app/routes/v0/proxmox/firewall/enable_firewall_dc.py deleted file mode 100644 index da0d7c3..0000000 --- a/app/routes/v0/proxmox/firewall/enable_firewall_dc.py +++ /dev/null @@ -1,150 +0,0 @@ -from typing import Any -import logging -from fastapi import APIRouter, HTTPException -from fastapi.responses import JSONResponse - -from app.schemas.proxmox.firewall.enable_firewall_dc import Request_ProxmoxFirewall_EnableFirewallDc -from app.schemas.proxmox.firewall.enable_firewall_dc import Reply_ProxmoxFirewallWithStorageName_EnableFirewallDc - -from app.runner import run_playbook_core # , extract_action_results -from app.extract_actions import extract_action_results -from app import utils -from pathlib import Path -import os - -# -# ISSUE - #12 -# - -debug = 0 - -router = APIRouter() -logger = logging.getLogger(__name__) - -# PROJECT_ROOT = Path(__file__).resolve().parents[6] -PROJECT_ROOT = Path(os.getenv("PROJECT_ROOT_DIR")).resolve() -INVENTORY_NAME = "hosts" -PLAYBOOK_SRC = PROJECT_ROOT / "playbooks" / "generic.yml" -# INVENTORY_SRC = PROJECT_ROOT / "inventory" / "hosts.yml" - -# -# => /v0/admin/proxmox/firewall/datacenter/enable -# -@router.post( - path="/datacenter/enable", - summary="Enable datacenter firewall", - description="Enable the proxmox firewall at the datacenter level", - tags=["proxmox - firewall"], - response_model=Reply_ProxmoxFirewallWithStorageName_EnableFirewallDc, - response_description="Details of the enabled datacenter firewall", -) -def proxmox_firewall_dc_enable(req: Request_ProxmoxFirewall_EnableFirewallDc): - - if not PLAYBOOK_SRC.exists(): - err = f":: err - MISSING PLAYBOOK : {PLAYBOOK_SRC}" - logging.error(err) - raise HTTPException(status_code=400, detail=err) - - checked_playbook_filepath = PLAYBOOK_SRC - checked_inventory_filepath = utils.resolve_inventory(INVENTORY_NAME) - - - if debug ==1: - print("") - print(":: REQUEST ::", req.dict()) - print(f":: checked_inventory_filepath :: {checked_inventory_filepath} ") - print(f":: checked_playbook_filepath :: {checked_playbook_filepath} ") - print("") - - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - extravars = request_checks(req) - - #### - - rc, events, log_plain, log_ansi = run_playbook_core( - checked_playbook_filepath, - checked_inventory_filepath, - # limit=req.hosts, - limit=extravars["hosts"], - extravars=extravars, - ) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - payload = reply_processing(events, extravars, log_plain, rc, req) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - if rc == 0: - status = 200 - else: - status = 500 - - return JSONResponse(payload, status_code=status) - - -def reply_processing(events: list[dict] | list[Any], - extravars: dict[Any, Any], - log_plain: str, - rc, - req: Request_ProxmoxFirewall_EnableFirewallDc) -> dict[str, list | Any]: - - """ reply post-processing - json or ansible raw output """ - - if req.as_json: - - ##### OUTPUT TYPE - as_json=True - ##### - - action = extravars["proxmox_vm_action"] - result = extract_action_results(events, action) - - payload = { - "rc": rc, - "result": result - # "action": action, - } # raw - - # payload = {"rc": rc, "action": action, "result": events} - else: - #### - #### OUTPUT AS TEXT - as_json=False - #### - - # payload = {"rc": rc, "log_plain": log_plain, "log_multiline": log_plain.splitlines()} - payload = {"rc": rc, "log_multiline": log_plain.splitlines()} - return payload - - -def request_checks(req: Request_ProxmoxFirewall_EnableFirewallDc) -> dict[Any, Any]: - """ request checks """ - - extravars = {} - extravars["proxmox_vm_action"] = "firewall_dc_enable" - - # if req.proxmox_node: - # extravars["proxmox_node"] = req.proxmox_node - - #### - - if req.proxmox_api_host: - extravars["proxmox_api_host"] = req.proxmox_api_host - - # nothing : - if not extravars: - extravars = None - - if debug == 1: - print(f", extra vars : {extravars}") - - extravars["hosts"] = "proxmox" - return extravars - diff --git a/app/routes/v0/proxmox/firewall/enable_firewall_node.py b/app/routes/v0/proxmox/firewall/enable_firewall_node.py deleted file mode 100644 index fce5437..0000000 --- a/app/routes/v0/proxmox/firewall/enable_firewall_node.py +++ /dev/null @@ -1,147 +0,0 @@ -from typing import Any -import logging -from fastapi import APIRouter, HTTPException -from fastapi.responses import JSONResponse - -from app.schemas.proxmox.firewall.enable_firewall_node import Request_ProxmoxFirewall_EnableFirewallNode -from app.schemas.proxmox.firewall.enable_firewall_node import Reply_ProxmoxFirewallWithStorageName_EnableFirewallNode - -from app.runner import run_playbook_core # , extract_action_results -from app.extract_actions import extract_action_results -from app import utils -from pathlib import Path -import os - -# -# ISSUE - #12 -# - -debug = 0 - -router = APIRouter() -logger = logging.getLogger(__name__) - -# PROJECT_ROOT = Path(__file__).resolve().parents[6] -PROJECT_ROOT = Path(os.getenv("PROJECT_ROOT_DIR")).resolve() -INVENTORY_NAME = "hosts" -PLAYBOOK_SRC = PROJECT_ROOT / "playbooks" / "generic.yml" -# INVENTORY_SRC = PROJECT_ROOT / "inventory" / "hosts.yml" - -# -# => /v0/admin/proxmox/firewall/node/enable -# -@router.post( - path="/node/enable", - summary="Enable node firewall", - description="Enable the proxmox firewall on a specific node", - tags=["proxmox - firewall"], - response_model=Reply_ProxmoxFirewallWithStorageName_EnableFirewallNode, - response_description="Details of the enabled node firewall", -) -def proxmox_firewall_node_enable(req: Request_ProxmoxFirewall_EnableFirewallNode): - - if not PLAYBOOK_SRC.exists(): - err = f":: err - MISSING PLAYBOOK : {PLAYBOOK_SRC}" - logging.error(err) - raise HTTPException(status_code=400, detail=err) - - checked_playbook_filepath = PLAYBOOK_SRC - checked_inventory_filepath = utils.resolve_inventory(INVENTORY_NAME) - - - if debug ==1: - print("") - print(":: REQUEST ::", req.dict()) - print(f":: checked_inventory_filepath :: {checked_inventory_filepath} ") - print(f":: checked_playbook_filepath :: {checked_playbook_filepath} ") - print("") - - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - extravars = request_checks(req) - - #### - - rc, events, log_plain, log_ansi = run_playbook_core( - checked_playbook_filepath, - checked_inventory_filepath, - # limit=req.hosts, - limit=extravars["hosts"], - extravars=extravars, - ) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - payload = reply_processing(events, extravars, log_plain, rc, req) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - if rc == 0: - status = 200 - else: - status = 500 - - return JSONResponse(payload, status_code=status) - - -def reply_processing(events: list[dict] | list[Any], - extravars: dict[Any, Any], - log_plain: str, - rc, - req: Request_ProxmoxFirewall_EnableFirewallNode) -> dict[str, list | Any]: - - """ reply post-processing - json or ansible raw output """ - - if req.as_json: - - ##### OUTPUT TYPE - as_json=True - ##### - - action = extravars["proxmox_vm_action"] - result = extract_action_results(events, action) - - payload = { - "rc": rc, - "result": result - # "action": action, - } # raw - - # payload = {"rc": rc, "action": action, "result": events} - else: - #### - #### OUTPUT AS TEXT - as_json=False - #### - - # payload = {"rc": rc, "log_plain": log_plain, "log_multiline": log_plain.splitlines()} - payload = {"rc": rc, "log_multiline": log_plain.splitlines()} - return payload - - -def request_checks(req: Request_ProxmoxFirewall_EnableFirewallNode) -> dict[Any, Any]: - """ request checks """ - - extravars = {} - extravars["proxmox_vm_action"] = "firewall_node_enable" - - if req.proxmox_node: - extravars["proxmox_node"] = req.proxmox_node - - #### - - # nothing : - if not extravars: - extravars = None - - if debug == 1: - print(f", extra vars : {extravars}") - - extravars["hosts"] = "proxmox" - return extravars - diff --git a/app/routes/v0/proxmox/firewall/enable_firewall_vm.py b/app/routes/v0/proxmox/firewall/enable_firewall_vm.py deleted file mode 100644 index ba92533..0000000 --- a/app/routes/v0/proxmox/firewall/enable_firewall_vm.py +++ /dev/null @@ -1,157 +0,0 @@ -from typing import Any -import logging -from fastapi import APIRouter, HTTPException -from fastapi.responses import JSONResponse - -from app.schemas.proxmox.firewall.enable_firewall_vm import Request_ProxmoxFirewall_EnableFirewallVm -from app.schemas.proxmox.firewall.enable_firewall_vm import Reply_ProxmoxFirewallWithStorageName_EnableFirewallVm - -from app.runner import run_playbook_core # , extract_action_results -from app.extract_actions import extract_action_results -from app.utils.vm_id_name_resolver import resolv_id_to_vm_name -from app import utils -from pathlib import Path -import os - -# -# ISSUE - #12 -# - -debug = 0 - -router = APIRouter() -logger = logging.getLogger(__name__) - -# PROJECT_ROOT = Path(__file__).resolve().parents[6] -PROJECT_ROOT = Path(os.getenv("PROJECT_ROOT_DIR")).resolve() -INVENTORY_NAME = "hosts" -PLAYBOOK_SRC = PROJECT_ROOT / "playbooks" / "generic.yml" -# INVENTORY_SRC = PROJECT_ROOT / "inventory" / "hosts.yml" - -# -# => /v0/admin/proxmox/firewall/vm/enable -# -@router.post( - path="/vm/enable", - summary="Enable VM firewall", - description="Enable the proxmox firewall for a specific virtual machine", - tags=["proxmox - firewall"], - response_model=Reply_ProxmoxFirewallWithStorageName_EnableFirewallVm, - response_description="Details of the enabled VM firewall", -) -def proxmox_firewall_vm_enable(req: Request_ProxmoxFirewall_EnableFirewallVm): - - if not PLAYBOOK_SRC.exists(): - err = f":: err - MISSING PLAYBOOK : {PLAYBOOK_SRC}" - logging.error(err) - raise HTTPException(status_code=400, detail=err) - - checked_playbook_filepath = PLAYBOOK_SRC - checked_inventory_filepath = utils.resolve_inventory(INVENTORY_NAME) - - - if debug ==1: - print("") - print(":: REQUEST ::", req.dict()) - print(f":: checked_inventory_filepath :: {checked_inventory_filepath} ") - print(f":: checked_playbook_filepath :: {checked_playbook_filepath} ") - print("") - - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - extravars = request_checks(req) - - #### - - rc, events, log_plain, log_ansi = run_playbook_core( - checked_playbook_filepath, - checked_inventory_filepath, - # limit=req.hosts, - limit=extravars["hosts"], - extravars=extravars, - ) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - payload = reply_processing(events, extravars, log_plain, rc, req) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - if rc == 0: - status = 200 - else: - status = 500 - - return JSONResponse(payload, status_code=status) - - -def reply_processing(events: list[dict] | list[Any], - extravars: dict[Any, Any], - log_plain: str, - rc, - req: Request_ProxmoxFirewall_EnableFirewallVm) -> dict[str, list | Any]: - - """ reply post-processing - json or ansible raw output """ - - if req.as_json: - - ##### OUTPUT TYPE - as_json=True - ##### - - action = extravars["proxmox_vm_action"] - result = extract_action_results(events, action) - - payload = { - "rc": rc, - "result": result - # "action": action, - } # raw - - # payload = {"rc": rc, "action": action, "result": events} - else: - #### - #### OUTPUT AS TEXT - as_json=False - #### - - # payload = {"rc": rc, "log_plain": log_plain, "log_multiline": log_plain.splitlines()} - payload = {"rc": rc, "log_multiline": log_plain.splitlines()} - return payload - - -def request_checks(req: Request_ProxmoxFirewall_EnableFirewallVm) -> dict[Any, Any]: - """ request checks """ - - extravars = {} - extravars["proxmox_vm_action"] = "firewall_vm_enable" - - if req.proxmox_node: - extravars["proxmox_node"] = req.proxmox_node - - #### - - if req.vm_id: - extravars["vm_id"] = req.vm_id - - # if req.vm_name: - # extravars["vm_name"] = req.vm_name - - # extravars["vm_name"] = "admin-wazuh" # TODO - add vm_id <> vm_name resolver func ! - extravars["vm_name"] = resolv_id_to_vm_name(extravars["proxmox_node"], extravars["vm_id"] ) - - # nothing : - if not extravars: - extravars = None - - if debug == 1: - print(f", extra vars : {extravars}") - - extravars["hosts"] = "proxmox" - return extravars - diff --git a/app/routes/v0/proxmox/firewall/list_iptables_alias.py b/app/routes/v0/proxmox/firewall/list_iptables_alias.py deleted file mode 100644 index e8d74ea..0000000 --- a/app/routes/v0/proxmox/firewall/list_iptables_alias.py +++ /dev/null @@ -1,150 +0,0 @@ -from typing import Any -import logging -from fastapi import APIRouter, HTTPException -from fastapi.responses import JSONResponse - -from app.schemas.proxmox.firewall.list_iptables_alias import Request_ProxmoxFirewall_ListIptablesAlias -from app.schemas.proxmox.firewall.list_iptables_alias import Reply_ProxmoxFirewallWithStorageName_ListIptablesAlias - -from app.runner import run_playbook_core # , extract_action_results -from app.extract_actions import extract_action_results -from app import utils -from pathlib import Path -import os - -# -# ISSUE - #12 -# - -debug = 0 - -router = APIRouter() -logger = logging.getLogger(__name__) - -# PROJECT_ROOT = Path(__file__).resolve().parents[6] -PROJECT_ROOT = Path(os.getenv("PROJECT_ROOT_DIR")).resolve() -INVENTORY_NAME = "hosts" -PLAYBOOK_SRC = PROJECT_ROOT / "playbooks" / "generic.yml" -# INVENTORY_SRC = PROJECT_ROOT / "inventory" / "hosts.yml" - -# -# => /v0/admin/proxmox/firewall/vm/alias/list -# -@router.post( - path="/vm/alias/list", - summary="List VM firewall aliases", - description="List firewall aliases for a specific virtual machine", - tags=["proxmox - firewall"], - response_model=Reply_ProxmoxFirewallWithStorageName_ListIptablesAlias, - response_description="Details of the VM firewall aliases", -) -def proxmox_vm_alias_list(req: Request_ProxmoxFirewall_ListIptablesAlias): - - if not PLAYBOOK_SRC.exists(): - err = f":: err - MISSING PLAYBOOK : {PLAYBOOK_SRC}" - logging.error(err) - raise HTTPException(status_code=400, detail=err) - - checked_playbook_filepath = PLAYBOOK_SRC - checked_inventory_filepath = utils.resolve_inventory(INVENTORY_NAME) - - - if debug ==1: - print("") - print(":: REQUEST ::", req.dict()) - print(f":: checked_inventory_filepath :: {checked_inventory_filepath} ") - print(f":: checked_playbook_filepath :: {checked_playbook_filepath} ") - print("") - - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - extravars = request_checks(req) - - #### - - rc, events, log_plain, log_ansi = run_playbook_core( - checked_playbook_filepath, - checked_inventory_filepath, - # limit=req.hosts, - limit=extravars["hosts"], - extravars=extravars, - ) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - payload = reply_processing(events, extravars, log_plain, rc, req) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - if rc == 0: - status = 200 - else: - status = 500 - - return JSONResponse(payload, status_code=status) - - -def reply_processing(events: list[dict] | list[Any], - extravars: dict[Any, Any], - log_plain: str, - rc, - req: Request_ProxmoxFirewall_ListIptablesAlias) -> dict[str, list | Any]: - - """ reply post-processing - json or ansible raw output """ - - if req.as_json: - - ##### OUTPUT TYPE - as_json=True - ##### - - action = extravars["proxmox_vm_action"] - result = extract_action_results(events, action) - - payload = { - "rc": rc, - "result": result - # "action": action, - } # raw - - # payload = {"rc": rc, "action": action, "result": events} - else: - #### - #### OUTPUT AS TEXT - as_json=False - #### - - # payload = {"rc": rc, "log_plain": log_plain, "log_multiline": log_plain.splitlines()} - payload = {"rc": rc, "log_multiline": log_plain.splitlines()} - return payload - - -def request_checks(req: Request_ProxmoxFirewall_ListIptablesAlias) -> dict[Any, Any]: - """ request checks """ - - extravars = {} - extravars["proxmox_vm_action"] = "firewall_vm_list_iptables_alias" - - if req.proxmox_node: - extravars["proxmox_node"] = req.proxmox_node - - #### - - if req.vm_id: - extravars["vm_id"] = req.vm_id - - # nothing : - if not extravars: - extravars = None - - if debug == 1: - print(f", extra vars : {extravars}") - - extravars["hosts"] = "proxmox" - return extravars - diff --git a/app/routes/v0/proxmox/firewall/list_iptables_rules.py b/app/routes/v0/proxmox/firewall/list_iptables_rules.py deleted file mode 100644 index 4549219..0000000 --- a/app/routes/v0/proxmox/firewall/list_iptables_rules.py +++ /dev/null @@ -1,150 +0,0 @@ -from typing import Any -import logging -from fastapi import APIRouter, HTTPException -from fastapi.responses import JSONResponse - -from app.schemas.proxmox.firewall.list_iptables_rules import Request_ProxmoxFirewall_ListIptablesRules -from app.schemas.proxmox.firewall.list_iptables_rules import Reply_ProxmoxFirewallWithStorageName_ListIptablesRules - -from app.runner import run_playbook_core # , extract_action_results -from app.extract_actions import extract_action_results -from app import utils -from pathlib import Path -import os - -# -# ISSUE - #12 -# - -debug = 0 - -router = APIRouter() -logger = logging.getLogger(__name__) - -# PROJECT_ROOT = Path(__file__).resolve().parents[6] -PROJECT_ROOT = Path(os.getenv("PROJECT_ROOT_DIR")).resolve() -INVENTORY_NAME = "hosts" -PLAYBOOK_SRC = PROJECT_ROOT / "playbooks" / "generic.yml" -# INVENTORY_SRC = PROJECT_ROOT / "inventory" / "hosts.yml" - -# -# => /v0/admin/proxmox/firewall/vm/rules/list -# -@router.post( - path="/vm/rules/list", - summary="List VM firewall rules", - description="List firewall rules for a specific virtual machine", - tags=["proxmox - firewall"], - response_model=Reply_ProxmoxFirewallWithStorageName_ListIptablesRules, - response_description="Details of the VM firewall rules", -) -def proxmox_vm_rules_list(req: Request_ProxmoxFirewall_ListIptablesRules): - - if not PLAYBOOK_SRC.exists(): - err = f":: err - MISSING PLAYBOOK : {PLAYBOOK_SRC}" - logging.error(err) - raise HTTPException(status_code=400, detail=err) - - checked_playbook_filepath = PLAYBOOK_SRC - checked_inventory_filepath = utils.resolve_inventory(INVENTORY_NAME) - - - if debug ==1: - print("") - print(":: REQUEST ::", req.dict()) - print(f":: checked_inventory_filepath :: {checked_inventory_filepath} ") - print(f":: checked_playbook_filepath :: {checked_playbook_filepath} ") - print("") - - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - extravars = request_checks(req) - - #### - - rc, events, log_plain, log_ansi = run_playbook_core( - checked_playbook_filepath, - checked_inventory_filepath, - # limit=req.hosts, - limit=extravars["hosts"], - extravars=extravars, - ) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - payload = reply_processing(events, extravars, log_plain, rc, req) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - if rc == 0: - status = 200 - else: - status = 500 - - return JSONResponse(payload, status_code=status) - - -def reply_processing(events: list[dict] | list[Any], - extravars: dict[Any, Any], - log_plain: str, - rc, - req: Request_ProxmoxFirewall_ListIptablesRules) -> dict[str, list | Any]: - - """ reply post-processing - json or ansible raw output """ - - if req.as_json: - - ##### OUTPUT TYPE - as_json=True - ##### - - action = extravars["proxmox_vm_action"] - result = extract_action_results(events, action) - - payload = { - "rc": rc, - "result": result - # "action": action, - } # raw - - # payload = {"rc": rc, "action": action, "result": events} - else: - #### - #### OUTPUT AS TEXT - as_json=False - #### - - # payload = {"rc": rc, "log_plain": log_plain, "log_multiline": log_plain.splitlines()} - payload = {"rc": rc, "log_multiline": log_plain.splitlines()} - return payload - - -def request_checks(req: Request_ProxmoxFirewall_ListIptablesRules) -> dict[Any, Any]: - """ request checks """ - - extravars = {} - extravars["proxmox_vm_action"] = "firewall_vm_list_iptables_rule" - - if req.proxmox_node: - extravars["proxmox_node"] = req.proxmox_node - - #### - - if req.vm_id: - extravars["vm_id"] = req.vm_id - - # nothing : - if not extravars: - extravars = None - - if debug == 1: - print(f", extra vars : {extravars}") - - extravars["hosts"] = "proxmox" - return extravars - diff --git a/app/routes/v0/proxmox/network/node/add_network.py b/app/routes/v0/proxmox/network/node/add_network.py deleted file mode 100644 index bd684b0..0000000 --- a/app/routes/v0/proxmox/network/node/add_network.py +++ /dev/null @@ -1,174 +0,0 @@ -from typing import Any -import logging -from fastapi import APIRouter, HTTPException -from fastapi.responses import JSONResponse - -from app.schemas.proxmox.network.node_name.add_network import Request_ProxmoxNetwork_WithNodeName_AddNetworkInterface -from app.schemas.proxmox.network.node_name.add_network import Reply_ProxmoxNetwork_WithNodeName_AddNetworkInterface - -from app.runner import run_playbook_core # , extract_action_results -from app.extract_actions import extract_action_results -from app import utils -from pathlib import Path -import os - -# -# ISSUE - #11 -# - -debug = 0 - -router = APIRouter() -logger = logging.getLogger(__name__) - -# PROJECT_ROOT = Path(__file__).resolve().parents[6] -PROJECT_ROOT = Path(os.getenv("PROJECT_ROOT_DIR")).resolve() -INVENTORY_NAME = "hosts" -PLAYBOOK_SRC = PROJECT_ROOT / "playbooks" / "generic.yml" -# INVENTORY_SRC = PROJECT_ROOT / "inventory" / "hosts.yml" - -# -# => /api/v0/admin/proxmox/network/node/addy -# -@router.post( - path="/node/add", - summary="Add node network interface", - description="Create and attach a new network interface to a Proxmox node.", - tags=["proxmox - network - node"], - response_model=Reply_ProxmoxNetwork_WithNodeName_AddNetworkInterface, - response_description="Information about the added network interface.", -) -def proxmox_network_node_add_interface(req: Request_ProxmoxNetwork_WithNodeName_AddNetworkInterface): - - if not PLAYBOOK_SRC.exists(): - err = f":: err - MISSING PLAYBOOK : {PLAYBOOK_SRC}" - logging.error(err) - raise HTTPException(status_code=400, detail=err) - - checked_playbook_filepath = PLAYBOOK_SRC - checked_inventory_filepath = utils.resolve_inventory(INVENTORY_NAME) - - - if debug ==1: - print("") - print(":: REQUEST ::", req.dict()) - print(f":: checked_inventory_filepath :: {checked_inventory_filepath} ") - print(f":: checked_playbook_filepath :: {checked_playbook_filepath} ") - print("") - - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - extravars = request_checks(req) - - #### - - rc, events, log_plain, log_ansi = run_playbook_core( - checked_playbook_filepath, - checked_inventory_filepath, - # limit=req.hosts, - limit=extravars["hosts"], - extravars=extravars, - ) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - payload = reply_processing(events, extravars, log_plain, rc, req) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - if rc == 0: - status = 200 - else: - status = 500 - - return JSONResponse(payload, status_code=status) - - -def reply_processing(events: list[dict] | list[Any], - extravars: dict[Any, Any], - log_plain: str, - rc, - req: Request_ProxmoxNetwork_WithNodeName_AddNetworkInterface) -> dict[str, list | Any]: - - """ reply post-processing - json or ansible raw output """ - - if req.as_json: - - ##### OUTPUT TYPE - as_json=True - ##### - - action = extravars["proxmox_vm_action"] - result = extract_action_results(events, action) - - payload = { - "rc": rc, - "result": result - # "action": action, - } # raw - - # payload = {"rc": rc, "action": action, "result": events} - else: - #### - #### OUTPUT AS TEXT - as_json=False - #### - - # payload = {"rc": rc, "log_plain": log_plain, "log_multiline": log_plain.splitlines()} - payload = {"rc": rc, "log_multiline": log_plain.splitlines()} - return payload - - -def request_checks(req: Request_ProxmoxNetwork_WithNodeName_AddNetworkInterface) -> dict[Any, Any]: - """ request checks """ - - extravars = {} - extravars["proxmox_vm_action"] = "network_add_interfaces_node" - - if req.proxmox_node: - extravars["proxmox_node"] = req.proxmox_node - - # if req.vm_id is not None: - # extravars["vm_id"] = req.vm_id - # - #### - - if req.bridge_ports is not None: - extravars["bridge_ports"] = req.bridge_ports - - if req.iface_name is not None: - extravars["iface_name"] = req.iface_name - - if req.iface_type is not None: - extravars["iface_type"] = req.iface_type - - if req.iface_autostart is not None: - extravars["iface_autostart"] = req.iface_autostart - - if req.ip_address is not None: - extravars["ip_address"] = req.ip_address - - if req.ip_netmask is not None: - extravars["ip_netmask"] = req.ip_netmask - - if req.ip_gateway is not None: - extravars["ip_gateway"] = req.ip_gateway - - if req.ovs_bridge is not None: - extravars["ovs_bridge"] = req.ovs_bridge - - # nothing : - if not extravars: - extravars = None - - if debug == 1: - print(f", extra vars : {extravars}") - - extravars["hosts"] = "proxmox" - return extravars - diff --git a/app/routes/v0/proxmox/network/node/delete_network.py b/app/routes/v0/proxmox/network/node/delete_network.py deleted file mode 100644 index 32b818a..0000000 --- a/app/routes/v0/proxmox/network/node/delete_network.py +++ /dev/null @@ -1,153 +0,0 @@ -from typing import Any -import logging -from fastapi import APIRouter, HTTPException -from fastapi.responses import JSONResponse - -from app.schemas.proxmox.network.node_name.delete_network import Request_ProxmoxNetwork_WithNodeName_DeleteInterface -from app.schemas.proxmox.network.node_name.delete_network import Reply_ProxmoxNetwork_WithNodeName_DeleteInterface - -from app.runner import run_playbook_core # , extract_action_results -from app.extract_actions import extract_action_results -from app import utils -from pathlib import Path -import os - -# -# ISSUE - #11 -# - -debug = 0 - -router = APIRouter() -logger = logging.getLogger(__name__) - -# PROJECT_ROOT = Path(__file__).resolve().parents[6] -PROJECT_ROOT = Path(os.getenv("PROJECT_ROOT_DIR")).resolve() -INVENTORY_NAME = "hosts" -PLAYBOOK_SRC = PROJECT_ROOT / "playbooks" / "generic.yml" -# INVENTORY_SRC = PROJECT_ROOT / "inventory" / "hosts.yml" - -# -# => /api/v0/admin/proxmox/network/node/delete -# -@router.post( - path="/node/delete", - summary="Delete node network interface", - description="Remove a network interface from a Proxmox node.", - tags=["proxmox - network - node"], - response_model=Reply_ProxmoxNetwork_WithNodeName_DeleteInterface, - response_description="Information about the deleted network interface.", -) -def proxmox_network_node_delete_interface(req: Request_ProxmoxNetwork_WithNodeName_DeleteInterface): - - if not PLAYBOOK_SRC.exists(): - err = f":: err - MISSING PLAYBOOK : {PLAYBOOK_SRC}" - logging.error(err) - raise HTTPException(status_code=400, detail=err) - - checked_playbook_filepath = PLAYBOOK_SRC - checked_inventory_filepath = utils.resolve_inventory(INVENTORY_NAME) - - - if debug ==1: - print("") - print(":: REQUEST ::", req.dict()) - print(f":: checked_inventory_filepath :: {checked_inventory_filepath} ") - print(f":: checked_playbook_filepath :: {checked_playbook_filepath} ") - print("") - - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - extravars = request_checks(req) - - #### - - rc, events, log_plain, log_ansi = run_playbook_core( - checked_playbook_filepath, - checked_inventory_filepath, - # limit=req.hosts, - limit=extravars["hosts"], - extravars=extravars, - ) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - payload = reply_processing(events, extravars, log_plain, rc, req) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - if rc == 0: - status = 200 - else: - status = 500 - - return JSONResponse(payload, status_code=status) - - -def reply_processing(events: list[dict] | list[Any], - extravars: dict[Any, Any], - log_plain: str, - rc, - req: Request_ProxmoxNetwork_WithNodeName_DeleteInterface) -> dict[str, list | Any]: - - """ reply post-processing - json or ansible raw output """ - - if req.as_json: - - ##### OUTPUT TYPE - as_json=True - ##### - - action = extravars["proxmox_vm_action"] - result = extract_action_results(events, action) - - payload = { - "rc": rc, - "result": result - # "action": action, - } # raw - - # payload = {"rc": rc, "action": action, "result": events} - else: - #### - #### OUTPUT AS TEXT - as_json=False - #### - - # payload = {"rc": rc, "log_plain": log_plain, "log_multiline": log_plain.splitlines()} - payload = {"rc": rc, "log_multiline": log_plain.splitlines()} - return payload - - -def request_checks(req: Request_ProxmoxNetwork_WithNodeName_DeleteInterface) -> dict[Any, Any]: - """ request checks """ - - extravars = {} - extravars["proxmox_vm_action"] = "network_delete_interfaces_node" - - if req.proxmox_node: - extravars["proxmox_node"] = req.proxmox_node - - # if req.vm_id is not None: - # extravars["vm_id"] = req.vm_id - - #### - - if req.iface_name is not None: - extravars["iface_name"] = req.iface_name - - # nothing : - if not extravars: - extravars = None - - if debug == 1: - print(f", extra vars : {extravars}") - - extravars["hosts"] = "proxmox" - return extravars - diff --git a/app/routes/v0/proxmox/network/node/list_network.py b/app/routes/v0/proxmox/network/node/list_network.py deleted file mode 100644 index 4751a55..0000000 --- a/app/routes/v0/proxmox/network/node/list_network.py +++ /dev/null @@ -1,150 +0,0 @@ -from typing import Any -import logging -from fastapi import APIRouter, HTTPException -from fastapi.responses import JSONResponse - -from app.schemas.proxmox.network.node_name.list_network import Request_ProxmoxNetwork_WithNodeName_ListInterface -from app.schemas.proxmox.network.node_name.list_network import Reply_ProxmoxNetwork_WithNodeName_ListInterface - -from app.runner import run_playbook_core # , extract_action_results -from app.extract_actions import extract_action_results -from app import utils -from pathlib import Path -import os - -# -# ISSUE - #11 -# - -debug = 0 - -router = APIRouter() -logger = logging.getLogger(__name__) - -# PROJECT_ROOT = Path(__file__).resolve().parents[6] -PROJECT_ROOT = Path(os.getenv("PROJECT_ROOT_DIR")).resolve() -INVENTORY_NAME = "hosts" -PLAYBOOK_SRC = PROJECT_ROOT / "playbooks" / "generic.yml" -# INVENTORY_SRC = PROJECT_ROOT / "inventory" / "hosts.yml" - -# -# => /api/v0/admin/proxmox/network/node/list -# -@router.post( - path="//node/list", - summary="List node network interfaces", - description="Retrieve all network interfaces configured on a Proxmox node.", - tags=["proxmox - network - node"], - response_model=Reply_ProxmoxNetwork_WithNodeName_ListInterface, - response_description="List of node network interfaces.", -) -def proxmox_network_node_list_interface(req: Request_ProxmoxNetwork_WithNodeName_ListInterface): - - if not PLAYBOOK_SRC.exists(): - err = f":: err - MISSING PLAYBOOK : {PLAYBOOK_SRC}" - logging.error(err) - raise HTTPException(status_code=400, detail=err) - - checked_playbook_filepath = PLAYBOOK_SRC - checked_inventory_filepath = utils.resolve_inventory(INVENTORY_NAME) - - - if debug ==1: - print("") - print(":: REQUEST ::", req.dict()) - print(f":: checked_inventory_filepath :: {checked_inventory_filepath} ") - print(f":: checked_playbook_filepath :: {checked_playbook_filepath} ") - print("") - - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - extravars = request_checks(req) - - #### - - rc, events, log_plain, log_ansi = run_playbook_core( - checked_playbook_filepath, - checked_inventory_filepath, - # limit=req.hosts, - limit=extravars["hosts"], - extravars=extravars, - ) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - payload = reply_processing(events, extravars, log_plain, rc, req) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - if rc == 0: - status = 200 - else: - status = 500 - - return JSONResponse(payload, status_code=status) - - -def reply_processing(events: list[dict] | list[Any], - extravars: dict[Any, Any], - log_plain: str, - rc, - req: Request_ProxmoxNetwork_WithNodeName_ListInterface) -> dict[str, list | Any]: - - """ reply post-processing - json or ansible raw output """ - - if req.as_json: - - ##### OUTPUT TYPE - as_json=True - ##### - - action = extravars["proxmox_vm_action"] - result = extract_action_results(events, action) - - payload = { - "rc": rc, - "result": result - # "action": action, - } # raw - - # payload = {"rc": rc, "action": action, "result": events} - else: - #### - #### OUTPUT AS TEXT - as_json=False - #### - - # payload = {"rc": rc, "log_plain": log_plain, "log_multiline": log_plain.splitlines()} - payload = {"rc": rc, "log_multiline": log_plain.splitlines()} - return payload - - -def request_checks(req: Request_ProxmoxNetwork_WithNodeName_ListInterface) -> dict[Any, Any]: - """ request checks """ - - extravars = {} - extravars["proxmox_vm_action"] = "network_list_interfaces_node" - - if req.proxmox_node: - extravars["proxmox_node"] = req.proxmox_node - - # if req.vm_id is not None: - # extravars["vm_id"] = req.vm_id - - #### - - # nothing : - if not extravars: - extravars = None - - if debug == 1: - print(f", extra vars : {extravars}") - - extravars["hosts"] = "proxmox" - return extravars - diff --git a/app/routes/v0/proxmox/network/vm/add_network.py b/app/routes/v0/proxmox/network/vm/add_network.py deleted file mode 100644 index 5f3a9a8..0000000 --- a/app/routes/v0/proxmox/network/vm/add_network.py +++ /dev/null @@ -1,189 +0,0 @@ -from typing import Any -import logging -from fastapi import APIRouter, HTTPException -from fastapi.responses import JSONResponse - -from app.schemas.proxmox.network.vm_id.add_network import Request_ProxmoxNetwork_WithVmId_AddNetwork -from app.schemas.proxmox.network.vm_id.add_network import Reply_ProxmoxNetwork_WithVmId_AddNetworkInterface - -from app.runner import run_playbook_core # , extract_action_results -from app.extract_actions import extract_action_results -# from app.utils.vm_id_name_resolver import resolv_id_to_vm_name -from app import utils -from pathlib import Path -import os - -# -# ISSUE - #11 -# - -debug = 0 - -router = APIRouter() -logger = logging.getLogger(__name__) - -# PROJECT_ROOT = Path(__file__).resolve().parents[6] -PROJECT_ROOT = Path(os.getenv("PROJECT_ROOT_DIR")).resolve() -INVENTORY_NAME = "hosts" -PLAYBOOK_SRC = PROJECT_ROOT / "playbooks" / "generic.yml" -# INVENTORY_SRC = PROJECT_ROOT / "inventory" / "hosts.yml" - -# -# => /api/v0/admin/proxmox/network/node/add -# -@router.post( - path="/vm/add", - summary="Add VM network interface", - description="Create and attach a new network interface to a Proxmox VM.", - tags=["proxmox - network - vm"], - response_model=Reply_ProxmoxNetwork_WithVmId_AddNetworkInterface, - response_description="Information about the added network interface.", -) -def proxmox_network_vm_add_interface(req: Request_ProxmoxNetwork_WithVmId_AddNetwork): - - if not PLAYBOOK_SRC.exists(): - err = f":: err - MISSING PLAYBOOK : {PLAYBOOK_SRC}" - logging.error(err) - raise HTTPException(status_code=400, detail=err) - - checked_playbook_filepath = PLAYBOOK_SRC - checked_inventory_filepath = utils.resolve_inventory(INVENTORY_NAME) - - - if debug ==1: - print("") - print(":: REQUEST ::", req.dict()) - print(f":: checked_inventory_filepath :: {checked_inventory_filepath} ") - print(f":: checked_playbook_filepath :: {checked_playbook_filepath} ") - print("") - - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - extravars = request_checks(req) - - #### - - rc, events, log_plain, log_ansi = run_playbook_core( - checked_playbook_filepath, - checked_inventory_filepath, - # limit=req.hosts, - limit=extravars["hosts"], - extravars=extravars, - ) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - payload = reply_processing(events, extravars, log_plain, rc, req) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - if rc == 0: - status = 200 - else: - status = 500 - - return JSONResponse(payload, status_code=status) - - -def reply_processing(events: list[dict] | list[Any], - extravars: dict[Any, Any], - log_plain: str, - rc, - req: Request_ProxmoxNetwork_WithVmId_AddNetwork) -> dict[str, list | Any]: - - """ reply post-processing - json or ansible raw output """ - - if req.as_json: - - ##### OUTPUT TYPE - as_json=True - ##### - - action = extravars["proxmox_vm_action"] - result = extract_action_results(events, action) - - payload = { - "rc": rc, - "result": result - # "action": action, - } # raw - - # payload = {"rc": rc, "action": action, "result": events} - else: - #### - #### OUTPUT AS TEXT - as_json=False - #### - - # payload = {"rc": rc, "log_plain": log_plain, "log_multiline": log_plain.splitlines()} - payload = {"rc": rc, "log_multiline": log_plain.splitlines()} - return payload - - -def request_checks(req: Request_ProxmoxNetwork_WithVmId_AddNetwork) -> dict[Any, Any]: - """ request checks """ - - extravars = {} - extravars["proxmox_vm_action"] = "network_add_interfaces_vm" - - if req.proxmox_node: - extravars["proxmox_node"] = req.proxmox_node - - if req.vm_id is not None: - extravars["vm_id"] = req.vm_id - - #### - - if req.iface_model is not None: - extravars["iface_model"] = req.iface_model - - if req.iface_bridge is not None: - extravars["iface_bridge"] = req.iface_bridge - # - # if req.net_index is not None: - # extravars["net_index"] = req.net_index - - if req.vm_vmnet_id is not None: - extravars["vm_vmnet_id"] = req.vm_vmnet_id - - #### fields to test later. - - if req.iface_trunks is not None: - extravars["iface_trunks"] = req.iface_trunks - - if req.iface_tag is not None: - extravars["iface_tag"] = req.iface_tag - - if req.iface_rate is not None: - extravars["iface_rate"] = req.iface_rate - - if req.iface_queues is not None: - extravars["iface_queues"] = req.iface_queues - - if req.iface_mtu is not None: - extravars["iface_mtu"] = req.iface_mtu - - if req.iface_macaddr is not None: - extravars["iface_macaddr"] = req.iface_macaddr - - if req.iface_link_down is not None: - extravars["iface_link_down"] = req.iface_link_down - - if req.iface_firewall is not None: - extravars["iface_firewall"] = req.iface_firewall - - # nothing : - if not extravars: - extravars = None - - if debug == 1: - print(f", extra vars : {extravars}") - - extravars["hosts"] = "proxmox" - return extravars - diff --git a/app/routes/v0/proxmox/network/vm/delete_network.py b/app/routes/v0/proxmox/network/vm/delete_network.py deleted file mode 100644 index 61f9aed..0000000 --- a/app/routes/v0/proxmox/network/vm/delete_network.py +++ /dev/null @@ -1,153 +0,0 @@ -from typing import Any -import logging -from fastapi import APIRouter, HTTPException -from fastapi.responses import JSONResponse - -from app.schemas.proxmox.network.vm_id.delete_network import Request_ProxmoxNetwork_WithVmId_DeleteNetwork -from app.schemas.proxmox.network.vm_id.delete_network import Reply_ProxmoxNetwork_WithVmId_DeleteNetworkInterface - -from app.runner import run_playbook_core # , extract_action_results -from app.extract_actions import extract_action_results -from app import utils -from pathlib import Path -import os - -# -# ISSUE - #11 -# - -debug = 0 - -router = APIRouter() -logger = logging.getLogger(__name__) - -# PROJECT_ROOT = Path(__file__).resolve().parents[6] -PROJECT_ROOT = Path(os.getenv("PROJECT_ROOT_DIR")).resolve() -INVENTORY_NAME = "hosts" -PLAYBOOK_SRC = PROJECT_ROOT / "playbooks" / "generic.yml" -# INVENTORY_SRC = PROJECT_ROOT / "inventory" / "hosts.yml" - -# -# => /api/v0/admin/proxmox/network/node/delete -# -@router.post( - path="/vm/delete", - summary="Delete VM network interface", - description="Remove a network interface from a Proxmox VM.", - tags=["proxmox - network - vm"], - response_model=Reply_ProxmoxNetwork_WithVmId_DeleteNetworkInterface, - response_description="Information about the deleted network interface.", -) -def proxmox_network_vm_delete_interface(req: Request_ProxmoxNetwork_WithVmId_DeleteNetwork): - - if not PLAYBOOK_SRC.exists(): - err = f":: err - MISSING PLAYBOOK : {PLAYBOOK_SRC}" - logging.error(err) - raise HTTPException(status_code=400, detail=err) - - checked_playbook_filepath = PLAYBOOK_SRC - checked_inventory_filepath = utils.resolve_inventory(INVENTORY_NAME) - - - if debug ==1: - print("") - print(":: REQUEST ::", req.dict()) - print(f":: checked_inventory_filepath :: {checked_inventory_filepath} ") - print(f":: checked_playbook_filepath :: {checked_playbook_filepath} ") - print("") - - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - extravars = request_checks(req) - - #### - - rc, events, log_plain, log_ansi = run_playbook_core( - checked_playbook_filepath, - checked_inventory_filepath, - # limit=req.hosts, - limit=extravars["hosts"], - extravars=extravars, - ) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - payload = reply_processing(events, extravars, log_plain, rc, req) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - if rc == 0: - status = 200 - else: - status = 500 - - return JSONResponse(payload, status_code=status) - - -def reply_processing(events: list[dict] | list[Any], - extravars: dict[Any, Any], - log_plain: str, - rc, - req: Request_ProxmoxNetwork_WithVmId_DeleteNetwork) -> dict[str, list | Any]: - - """ reply post-processing - json or ansible raw output """ - - if req.as_json: - - ##### OUTPUT TYPE - as_json=True - ##### - - action = extravars["proxmox_vm_action"] - result = extract_action_results(events, action) - - payload = { - "rc": rc, - "result": result - # "action": action, - } # raw - - # payload = {"rc": rc, "action": action, "result": events} - else: - #### - #### OUTPUT AS TEXT - as_json=False - #### - - # payload = {"rc": rc, "log_plain": log_plain, "log_multiline": log_plain.splitlines()} - payload = {"rc": rc, "log_multiline": log_plain.splitlines()} - return payload - - -def request_checks(req: Request_ProxmoxNetwork_WithVmId_DeleteNetwork) -> dict[Any, Any]: - """ request checks """ - - extravars = {} - extravars["proxmox_vm_action"] = "network_delete_interfaces_vm" - - if req.proxmox_node: - extravars["proxmox_node"] = req.proxmox_node - - if req.vm_id is not None: - extravars["vm_id"] = req.vm_id - - #### - - if req.vm_vmnet_id is not None: - extravars["vm_vmnet_id"] = req.vm_vmnet_id - - # nothing : - if not extravars: - extravars = None - - if debug == 1: - print(f", extra vars : {extravars}") - - extravars["hosts"] = "proxmox" - return extravars - diff --git a/app/routes/v0/proxmox/network/vm/list_network.py b/app/routes/v0/proxmox/network/vm/list_network.py deleted file mode 100644 index 7dd2efa..0000000 --- a/app/routes/v0/proxmox/network/vm/list_network.py +++ /dev/null @@ -1,150 +0,0 @@ -from typing import Any -import logging -from fastapi import APIRouter, HTTPException -from fastapi.responses import JSONResponse - -from app.schemas.proxmox.network.vm_id.list_network import Request_ProxmoxNetwork_WithVmId_ListNetwork -from app.schemas.proxmox.network.vm_id.list_network import Reply_ProxmoxNetwork_WithVmId_ListNetworkInterface - -from app.runner import run_playbook_core # , extract_action_results -from app.extract_actions import extract_action_results -from app import utils -from pathlib import Path -import os - -# -# ISSUE - #11 -# - -debug = 0 - -router = APIRouter() -logger = logging.getLogger(__name__) - -# PROJECT_ROOT = Path(__file__).resolve().parents[6] -PROJECT_ROOT = Path(os.getenv("PROJECT_ROOT_DIR")).resolve() -INVENTORY_NAME = "hosts" -PLAYBOOK_SRC = PROJECT_ROOT / "playbooks" / "generic.yml" -# INVENTORY_SRC = PROJECT_ROOT / "inventory" / "hosts.yml" - -# -# => /api/v0/admin/proxmox/network/node/list -# -@router.post( - path="/vm/list", - summary="List VM network interfaces", - description="Retrieve all network interfaces attached to a Proxmox VM.", - tags=["proxmox - network - vm"], - response_model=Reply_ProxmoxNetwork_WithVmId_ListNetworkInterface, - response_description="List of VM network interfaces.", -) -def proxmox_network_vm_list_interface(req: Request_ProxmoxNetwork_WithVmId_ListNetwork): - - if not PLAYBOOK_SRC.exists(): - err = f":: err - MISSING PLAYBOOK : {PLAYBOOK_SRC}" - logging.error(err) - raise HTTPException(status_code=400, detail=err) - - checked_playbook_filepath = PLAYBOOK_SRC - checked_inventory_filepath = utils.resolve_inventory(INVENTORY_NAME) - - - if debug ==1: - print("") - print(":: REQUEST ::", req.dict()) - print(f":: checked_inventory_filepath :: {checked_inventory_filepath} ") - print(f":: checked_playbook_filepath :: {checked_playbook_filepath} ") - print("") - - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - extravars = request_checks(req) - - #### - - rc, events, log_plain, log_ansi = run_playbook_core( - checked_playbook_filepath, - checked_inventory_filepath, - # limit=req.hosts, - limit=extravars["hosts"], - extravars=extravars, - ) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - payload = reply_processing(events, extravars, log_plain, rc, req) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - if rc == 0: - status = 200 - else: - status = 500 - - return JSONResponse(payload, status_code=status) - - -def reply_processing(events: list[dict] | list[Any], - extravars: dict[Any, Any], - log_plain: str, - rc, - req: Request_ProxmoxNetwork_WithVmId_ListNetwork) -> dict[str, list | Any]: - - """ reply post-processing - json or ansible raw output """ - - if req.as_json: - - ##### OUTPUT TYPE - as_json=True - ##### - - action = extravars["proxmox_vm_action"] - result = extract_action_results(events, action) - - payload = { - "rc": rc, - "result": result - # "action": action, - } # raw - - # payload = {"rc": rc, "action": action, "result": events} - else: - #### - #### OUTPUT AS TEXT - as_json=False - #### - - # payload = {"rc": rc, "log_plain": log_plain, "log_multiline": log_plain.splitlines()} - payload = {"rc": rc, "log_multiline": log_plain.splitlines()} - return payload - - -def request_checks(req: Request_ProxmoxNetwork_WithVmId_ListNetwork) -> dict[Any, Any]: - """ request checks """ - - extravars = {} - extravars["proxmox_vm_action"] = "network_list_interfaces_vm" - - if req.proxmox_node: - extravars["proxmox_node"] = req.proxmox_node - - if req.vm_id is not None: - extravars["vm_id"] = req.vm_id - - #### - - # nothing : - if not extravars: - extravars = None - - if debug == 1: - print(f", extra vars : {extravars}") - - extravars["hosts"] = "proxmox" - return extravars - diff --git a/app/routes/v0/proxmox/storage/__init__.py b/app/routes/v0/proxmox/storage/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/routes/v0/proxmox/storage/download_iso.py b/app/routes/v0/proxmox/storage/download_iso.py deleted file mode 100644 index e4f3ebf..0000000 --- a/app/routes/v0/proxmox/storage/download_iso.py +++ /dev/null @@ -1,167 +0,0 @@ -from typing import Any -import logging -from fastapi import APIRouter, HTTPException -from fastapi.responses import JSONResponse - -from app.schemas.proxmox.vm_id.config.vm_get_config import Request_ProxmoxVmsVMID_VmGetConfig -from app.schemas.proxmox.vm_id.config.vm_get_config import Reply_ProxmoxVmsVMID_VmGetConfig - - -from app.schemas.proxmox.storage.download_iso import Request_ProxmoxStorage_DownloadIso -from app.schemas.proxmox.storage.download_iso import Reply_ProxmoxStorage_DownloadIsoItem - - - -from app.runner import run_playbook_core # , extract_action_results -from app.extract_actions import extract_action_results -from app import utils -from pathlib import Path -import os - -# -# ISSUE - #17 -# - -debug = 0 - -router = APIRouter() -logger = logging.getLogger(__name__) - -# PROJECT_ROOT = Path(__file__).resolve().parents[6] -PROJECT_ROOT = Path(os.getenv("PROJECT_ROOT_DIR")).resolve() -INVENTORY_NAME = "hosts" -PLAYBOOK_SRC = PROJECT_ROOT / "playbooks" / "generic.yml" - -# -# => /v0/admin/proxmox/vms/vmd_id/vm_get_config -# -@router.post( - path="/download_iso", - summary="Retrieve configuration of a VM", - description="Returns the configuration details of the specified virtual machine (VM).", - tags=["proxmox - storage"], - response_model=Reply_ProxmoxStorage_DownloadIsoItem, - response_description="VM configuration details", -) - -def proxmox_storage_download_iso(req: Request_ProxmoxStorage_DownloadIso): - - if not PLAYBOOK_SRC.exists(): - err = f":: err - MISSING PLAYBOOK : {PLAYBOOK_SRC}" - logging.error(err) - raise HTTPException(status_code=400, detail=err) - - checked_playbook_filepath = PLAYBOOK_SRC - checked_inventory_filepath = utils.resolve_inventory(INVENTORY_NAME) - - - if debug ==1: - print("") - print(":: REQUEST ::", req.dict()) - print(f":: checked_inventory_filepath :: {checked_inventory_filepath} ") - print(f":: checked_playbook_filepath :: {checked_playbook_filepath} ") - print("") - - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - extravars = request_checks(req) - - #### - - rc, events, log_plain, log_ansi = run_playbook_core( - checked_playbook_filepath, - checked_inventory_filepath, - # limit=req.hosts, - limit=extravars["hosts"], - extravars=extravars, - ) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - payload = reply_processing(events, extravars, log_plain, rc, req) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - if rc == 0: - status = 200 - else: - status = 500 - - return JSONResponse(payload, status_code=status) - - -def reply_processing(events: list[dict] | list[Any], - extravars: dict[Any, Any], - log_plain: str, - rc, - req: Request_ProxmoxStorage_DownloadIso) -> dict[str, list | Any]: - - """ reply post-processing - json or ansible raw output """ - - if req.as_json: - - ##### OUTPUT TYPE - as_json=True - ##### - - action = extravars["proxmox_vm_action"] - result = extract_action_results(events, action) - - payload = { - "rc": rc, - "result": result - # "action": action, - } # raw - - # payload = {"rc": rc, "action": action, "result": events} - else: - #### - #### OUTPUT AS TEXT - as_json=False - #### - - # payload = {"rc": rc, "log_plain": log_plain, "log_multiline": log_plain.splitlines()} - payload = {"rc": rc, "log_multiline": log_plain.splitlines()} - return payload - - -def request_checks(req: Request_ProxmoxStorage_DownloadIso) -> dict[Any, Any]: - """ request checks """ - - extravars = {} - extravars["proxmox_vm_action"] = "storage_download_iso" - - if req.proxmox_node: - extravars["proxmox_node"] = req.proxmox_node - - if req.proxmox_storage is not None: - extravars["proxmox_storage"] = req.proxmox_storage - - # if req.storage_name is not None: - # extravars["storage_name"] = req.storage_name - - if req.iso_file_content_type is not None: - extravars["iso_file_content_type"] = req.iso_file_content_type - - if req.iso_file_name is not None: - extravars["iso_file_name"] = req.iso_file_name - - if req.iso_url is not None: - extravars["iso_url"] = req.iso_url - - - # nothing : - if not extravars: - extravars = None - - if debug == 1: - print(f", extra vars : {extravars}") - - extravars["hosts"] = "proxmox" - return extravars - diff --git a/app/routes/v0/proxmox/storage/list.py b/app/routes/v0/proxmox/storage/list.py deleted file mode 100644 index 2b182d7..0000000 --- a/app/routes/v0/proxmox/storage/list.py +++ /dev/null @@ -1,150 +0,0 @@ -from typing import Any -import logging -from fastapi import APIRouter, HTTPException -from fastapi.responses import JSONResponse - -from app.schemas.proxmox.storage.list import Request_ProxmoxStorage_List -from app.schemas.proxmox.storage.list import Reply_ProxmoxStorage_ListItem - - -from app.runner import run_playbook_core # , extract_action_results -from app.extract_actions import extract_action_results -from app import utils -from pathlib import Path -import os - -# -# ISSUE - #17 -# - -debug = 0 - -router = APIRouter() -logger = logging.getLogger(__name__) - -# PROJECT_ROOT = Path(__file__).resolve().parents[6] -PROJECT_ROOT = Path(os.getenv("PROJECT_ROOT_DIR")).resolve() -INVENTORY_NAME = "hosts" -PLAYBOOK_SRC = PROJECT_ROOT / "playbooks" / "generic.yml" - -# INVENTORY_SRC = PROJECT_ROOT / "inventory" / "hosts.yml" -# -# => /v0/admin/proxmox/vms/vmd_id/vm_get_config -# -@router.post( - path="/list", - summary="Retrieve configuration of a VM", - description="Returns the configuration details of the specified virtual machine (VM).", - tags=["proxmox - storage"], - response_model=Reply_ProxmoxStorage_ListItem, - response_description="VM configuration details", -) - -def proxmox_storage_list(req: Request_ProxmoxStorage_List): - - if not PLAYBOOK_SRC.exists(): - err = f":: err - MISSING PLAYBOOK : {PLAYBOOK_SRC}" - logging.error(err) - raise HTTPException(status_code=400, detail=err) - - checked_playbook_filepath = PLAYBOOK_SRC - checked_inventory_filepath = utils.resolve_inventory(INVENTORY_NAME) - - - if debug ==1: - print("") - print(":: REQUEST ::", req.dict()) - print(f":: checked_inventory_filepath :: {checked_inventory_filepath} ") - print(f":: checked_playbook_filepath :: {checked_playbook_filepath} ") - print("") - - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - extravars = request_checks(req) - - #### - - rc, events, log_plain, log_ansi = run_playbook_core( - checked_playbook_filepath, - checked_inventory_filepath, - # limit=req.hosts, - limit=extravars["hosts"], - extravars=extravars, - ) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - payload = reply_processing(events, extravars, log_plain, rc, req) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - if rc == 0: - status = 200 - else: - status = 500 - - return JSONResponse(payload, status_code=status) - - -def reply_processing(events: list[dict] | list[Any], - extravars: dict[Any, Any], - log_plain: str, - rc, - req: Request_ProxmoxStorage_List) -> dict[str, list | Any]: - - """ reply post-processing - json or ansible raw output """ - - if req.as_json: - - ##### OUTPUT TYPE - as_json=True - ##### - - action = extravars["proxmox_vm_action"] - result = extract_action_results(events, action) - - payload = { - "rc": rc, - "result": result - # "action": action, - } # raw - - # payload = {"rc": rc, "action": action, "result": events} - else: - #### - #### OUTPUT AS TEXT - as_json=False - #### - - # payload = {"rc": rc, "log_plain": log_plain, "log_multiline": log_plain.splitlines()} - payload = {"rc": rc, "log_multiline": log_plain.splitlines()} - return payload - - -def request_checks(req: Request_ProxmoxStorage_List) -> dict[Any, Any]: - """ request checks """ - - extravars = {} - extravars["proxmox_vm_action"] = "storage_list" - - if req.storage_name is not None: - extravars["storage_name"] = req.storage_name - - if req.proxmox_node: - extravars["proxmox_node"] = req.proxmox_node - - # nothing : - if not extravars: - extravars = None - - if debug == 1: - print(f", extra vars : {extravars}") - - extravars["hosts"] = "proxmox" - return extravars - diff --git a/app/routes/v0/proxmox/storage/storage_name/__init__.py b/app/routes/v0/proxmox/storage/storage_name/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/routes/v0/proxmox/storage/storage_name/list_iso.py b/app/routes/v0/proxmox/storage/storage_name/list_iso.py deleted file mode 100644 index c54a9f2..0000000 --- a/app/routes/v0/proxmox/storage/storage_name/list_iso.py +++ /dev/null @@ -1,166 +0,0 @@ -from typing import Any -import logging -from fastapi import APIRouter, HTTPException -from fastapi.responses import JSONResponse - -# from app.schemas.proxmox.vm_id.config.vm_get_config import Request_ProxmoxVmsVMID_VmGetConfig -# from app.schemas.proxmox.vm_id.config.vm_get_config import Reply_ProxmoxVmsVMID_VmGetConfig - -from app.schemas.proxmox.storage.storage_name.list_iso import Request_ProxmoxStorage_ListIso -from app.schemas.proxmox.storage.storage_name.list_iso import Reply_ProxmoxStorageWithStorageName_ListIsoItem - -from app.runner import run_playbook_core # , extract_action_results -from app.extract_actions import extract_action_results -from app import utils -from pathlib import Path -import os - -# -# ISSUE - #17 -# - -debug = 0 - -router = APIRouter() -logger = logging.getLogger(__name__) - -# PROJECT_ROOT = Path(__file__).resolve().parents[6] -PROJECT_ROOT = Path(os.getenv("PROJECT_ROOT_DIR")).resolve() -INVENTORY_NAME = "hosts" -PLAYBOOK_SRC = PROJECT_ROOT / "playbooks" / "generic.yml" - -# INVENTORY_SRC = PROJECT_ROOT / "inventory" / "hosts.yml" - -# @router.post("/{vm_id}/start") -# def proxmox_vms_vm_id_start( -# vm_id: int, -# req: Request_ProxmoxVmsVMID_StartStopPauseResume, -# ): - -# -# => /v0/admin/proxmox/vms/vmd_id/vm_get_config -# -# todo : -## should be move into api/proxmox/vms/vm_id/config/vm -## should be move into api/proxmox/vms/vm_id/config/cdrom -## should be move into api/proxmox/vms/vm_id/config/cpu -## should be move into api/proxmox/vms/vm_id/config/ram -# -@router.post( - path="/list_iso", - summary="Retrieve configuration of a VM", - description="Returns the configuration details of the specified virtual machine (VM).", - tags=["proxmox - storage"], - response_model=Reply_ProxmoxStorageWithStorageName_ListIsoItem, - response_description="VM configuration details", -) - - -def proxmox_storage_with_storage_name_list_iso(req: Request_ProxmoxStorage_ListIso): - - if not PLAYBOOK_SRC.exists(): - err = f":: err - MISSING PLAYBOOK : {PLAYBOOK_SRC}" - logging.error(err) - raise HTTPException(status_code=400, detail=err) - - checked_playbook_filepath = PLAYBOOK_SRC - checked_inventory_filepath = utils.resolve_inventory(INVENTORY_NAME) - - - if debug ==1: - print("") - print(":: REQUEST ::", req.dict()) - print(f":: checked_inventory_filepath :: {checked_inventory_filepath} ") - print(f":: checked_playbook_filepath :: {checked_playbook_filepath} ") - print("") - - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - extravars = request_checks(req) - - #### - - rc, events, log_plain, log_ansi = run_playbook_core( - checked_playbook_filepath, - checked_inventory_filepath, - # limit=req.hosts, - limit=extravars["hosts"], - extravars=extravars, - ) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - payload = reply_processing(events, extravars, log_plain, rc, req) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - if rc == 0: - status = 200 - else: - status = 500 - - return JSONResponse(payload, status_code=status) - - -def reply_processing(events: list[dict] | list[Any], - extravars: dict[Any, Any], - log_plain: str, - rc, - req: Request_ProxmoxStorage_ListIso) -> dict[str, list | Any]: - - """ reply post-processing - json or ansible raw output """ - - if req.as_json: - - ##### OUTPUT TYPE - as_json=True - ##### - - action = extravars["proxmox_vm_action"] - result = extract_action_results(events, action) - - payload = { - "rc": rc, - "result": result - # "action": action, - } # raw - - # payload = {"rc": rc, "action": action, "result": events} - else: - #### - #### OUTPUT AS TEXT - as_json=False - #### - - # payload = {"rc": rc, "log_plain": log_plain, "log_multiline": log_plain.splitlines()} - payload = {"rc": rc, "log_multiline": log_plain.splitlines()} - return payload - - -def request_checks(req: Request_ProxmoxStorage_ListIso) -> dict[Any, Any]: - """ request checks """ - - extravars = {} - extravars["proxmox_vm_action"] = "storage_list_iso" - - if req.storage_name is not None: - extravars["storage_name"] = req.storage_name - - if req.proxmox_node: - extravars["proxmox_node"] = req.proxmox_node - - # nothing : - if not extravars: - extravars = None - - if debug == 1: - print(f", extra vars : {extravars}") - - extravars["hosts"] = "proxmox" - return extravars - diff --git a/app/routes/v0/proxmox/storage/storage_name/list_template.py b/app/routes/v0/proxmox/storage/storage_name/list_template.py deleted file mode 100644 index ec384be..0000000 --- a/app/routes/v0/proxmox/storage/storage_name/list_template.py +++ /dev/null @@ -1,163 +0,0 @@ -from typing import Any -import logging -from fastapi import APIRouter, HTTPException -from fastapi.responses import JSONResponse - -from app.schemas.proxmox.storage.storage_name.list_template import Request_ProxmoxStorage_ListTemplate -from app.schemas.proxmox.storage.storage_name.list_template import Reply_ProxmoxStorageWithStorageName_ListTemplate - - -from app.runner import run_playbook_core # , extract_action_results -from app.extract_actions import extract_action_results -from app import utils -from pathlib import Path -import os - -# -# ISSUE - #17 -# - -debug = 0 - -router = APIRouter() -logger = logging.getLogger(__name__) - -# PROJECT_ROOT = Path(__file__).resolve().parents[6] -PROJECT_ROOT = Path(os.getenv("PROJECT_ROOT_DIR")).resolve() -INVENTORY_NAME = "hosts" -PLAYBOOK_SRC = PROJECT_ROOT / "playbooks" / "generic.yml" - -# INVENTORY_SRC = PROJECT_ROOT / "inventory" / "hosts.yml" - -# @router.post("/{vm_id}/start") -# def proxmox_vms_vm_id_start( -# vm_id: int, -# req: Request_ProxmoxVmsVMID_StartStopPauseResume, -# ): - -# -# => /v0/admin/proxmox/vms/vmd_id/vm_get_config -# -# todo : -## should be move into api/proxmox/vms/vm_id/config/vm -## should be move into api/proxmox/vms/vm_id/config/cdrom -## should be move into api/proxmox/vms/vm_id/config/cpu -## should be move into api/proxmox/vms/vm_id/config/ram -# -@router.post( - path="/list_template", - summary="Retrieve configuration of a VM", - description="Returns the configuration details of the specified virtual machine (VM).", - tags=["proxmox - storage"], - response_model=Reply_ProxmoxStorageWithStorageName_ListTemplate, - response_description="VM configuration details", -) - -def proxmox_storage_with_storage_name_list_template(req: Request_ProxmoxStorage_ListTemplate): - - if not PLAYBOOK_SRC.exists(): - err = f":: err - MISSING PLAYBOOK : {PLAYBOOK_SRC}" - logging.error(err) - raise HTTPException(status_code=400, detail=err) - - checked_playbook_filepath = PLAYBOOK_SRC - checked_inventory_filepath = utils.resolve_inventory(INVENTORY_NAME) - - - if debug ==1: - print("") - print(":: REQUEST ::", req.dict()) - print(f":: checked_inventory_filepath :: {checked_inventory_filepath} ") - print(f":: checked_playbook_filepath :: {checked_playbook_filepath} ") - print("") - - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - extravars = request_checks(req) - - #### - - rc, events, log_plain, log_ansi = run_playbook_core( - checked_playbook_filepath, - checked_inventory_filepath, - # limit=req.hosts, - limit=extravars["hosts"], - extravars=extravars, - ) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - payload = reply_processing(events, extravars, log_plain, rc, req) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - if rc == 0: - status = 200 - else: - status = 500 - - return JSONResponse(payload, status_code=status) - - -def reply_processing(events: list[dict] | list[Any], - extravars: dict[Any, Any], - log_plain: str, - rc, - req: Request_ProxmoxStorage_ListTemplate) -> dict[str, list | Any]: - - """ reply post-processing - json or ansible raw output """ - - if req.as_json: - - ##### OUTPUT TYPE - as_json=True - ##### - - action = extravars["proxmox_vm_action"] - result = extract_action_results(events, action) - - payload = { - "rc": rc, - "result": result - # "action": action, - } # raw - - # payload = {"rc": rc, "action": action, "result": events} - else: - #### - #### OUTPUT AS TEXT - as_json=False - #### - - # payload = {"rc": rc, "log_plain": log_plain, "log_multiline": log_plain.splitlines()} - payload = {"rc": rc, "log_multiline": log_plain.splitlines()} - return payload - - -def request_checks(req: Request_ProxmoxStorage_ListTemplate) -> dict[Any, Any]: - """ request checks """ - - extravars = {} - extravars["proxmox_vm_action"] = "storage_list_template" - - if req.storage_name is not None: - extravars["storage_name"] = req.storage_name - - if req.proxmox_node: - extravars["proxmox_node"] = req.proxmox_node - - # nothing : - if not extravars: - extravars = None - - if debug == 1: - print(f", extra vars : {extravars}") - - extravars["hosts"] = "proxmox" - return extravars - diff --git a/app/routes/v0/proxmox/vms/__init__.py b/app/routes/v0/proxmox/vms/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/routes/v0/proxmox/vms/list.py b/app/routes/v0/proxmox/vms/list.py deleted file mode 100644 index c2933e4..0000000 --- a/app/routes/v0/proxmox/vms/list.py +++ /dev/null @@ -1,154 +0,0 @@ -from typing import Any -import logging - -from fastapi import APIRouter, HTTPException -from fastapi.responses import JSONResponse - -from app.runner import run_playbook_core # , extract_action_results -from app.extract_actions import extract_action_results - -from app.schemas.proxmox.vm_list import Request_ProxmoxVms_VmList, Reply_ProxmoxVmList - -from pathlib import Path -import os - -# -# ISSUE - #2 -# - -debug = 0 - -router = APIRouter() -logger = logging.getLogger(__name__) - -# PROJECT_ROOT = Path(__file__).resolve().parents[5] -PROJECT_ROOT = Path(os.getenv("PROJECT_ROOT_DIR")).resolve() -PLAYBOOK_SRC = PROJECT_ROOT / "playbooks" / "generic.yml" -INVENTORY_SRC = PROJECT_ROOT / "inventory" / "hosts.yml" - -# @router.post("/{vm_id}/start") -# def proxmox_vms_vm_id_start( -# vm_id: int, -# req: Request_ProxmoxVmsVMID_StartStopPauseResume, -# ): - -#### -# => /v0/admin/proxmox/vms/list -@router.post( - path="/list", - summary="List VMs and LXC containers", - description="This endpoint retrieves all virtual machines (VMs) and LXC containers from Proxmox.", - tags=["proxmox - usage"], - # - response_model=Reply_ProxmoxVmList, - response_description="List VM result", -) - -def proxmox_vms_list(req: Request_ProxmoxVms_VmList): - - if debug ==1: - print(":: REQUEST ::", req.dict()) - print(f":: PROJECT_ROOT :: {PROJECT_ROOT} ") - print(f":: PLAYBOOK_SRC :: {PLAYBOOK_SRC} ") - print(f":: INVENTORY_SRC :: {INVENTORY_SRC} ") - - if not PLAYBOOK_SRC.exists(): - err = f":: err - MISSING PLAYBOOK : {PLAYBOOK_SRC}" - logging.error(err) - raise HTTPException(status_code=400, detail=err) - - if not INVENTORY_SRC.exists(): - err = f":: err - MISSING INVENTORY : {INVENTORY_SRC}" - logging.error(err) - raise HTTPException(status_code=400, detail=err) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - extravars = request_checks(req) - - #### - - rc, events, log_plain, log_ansi = run_playbook_core( - PLAYBOOK_SRC, - INVENTORY_SRC, - extravars=extravars, - limit=extravars["hosts"], - # limit=req.hosts, - ) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - payload = reply_processing(events, extravars, log_plain, rc, req) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - if rc == 0: - status = 200 - else: - status = 500 - - return JSONResponse(payload, status_code=status) - - -def reply_processing(events: list[dict] | list[Any], - extravars: dict[Any, Any], - log_plain: str, - rc, - req: Request_ProxmoxVms_VmList) -> dict[str, list | Any]: - - """ reply post-processing - json or ansible raw output """ - - if req.as_json: - - #### - ##### OUTPUT TYPE - as_json=True - ##### - - action = extravars["proxmox_vm_action"] - result = extract_action_results(events, action) - - payload = { - "rc": rc, - "result": result - # "action": action, - } # raw - - # payload = {"rc": rc, "action": action, "result": events} - else: - #### - #### OUTPUT AS TEXT - as_json=False - #### - - # payload = {"rc": rc, "log_plain": log_plain, "log_multiline": log_plain.splitlines()} - payload = {"rc": rc, "log_multiline": log_plain.splitlines()} - return payload - - -def request_checks(req: Request_ProxmoxVms_VmList) -> dict[Any, Any]: - """ request checks """ - - extravars = {} - extravars["proxmox_vm_action"] = "vm_list" - - # if req.vm_id is not None: - # extravars["vm_id"] = req.vm_id - - if req.proxmox_node: - extravars["proxmox_node"] = req.proxmox_node - - # nothing : - if not extravars: - extravars = None - - extravars["hosts"] = "proxmox" - - if debug == 1: - print(f", extra vars : {extravars}") - return extravars - diff --git a/app/routes/v0/proxmox/vms/list_usage.py b/app/routes/v0/proxmox/vms/list_usage.py deleted file mode 100644 index c4a89e5..0000000 --- a/app/routes/v0/proxmox/vms/list_usage.py +++ /dev/null @@ -1,156 +0,0 @@ -from typing import Any -import logging - -from fastapi import APIRouter, HTTPException -from fastapi.responses import JSONResponse - -from app.runner import run_playbook_core # , extract_action_results -from app.extract_actions import extract_action_results - -from app.schemas.proxmox.vm_list_usage import Request_ProxmoxVms_VmListUsage -from app.schemas.proxmox.vm_list_usage import Reply_ProxmoxVms_VmListUsage - -from pathlib import Path -import os - -# -# ISSUE - #5 -# - -debug = 0 - -router = APIRouter() -logger = logging.getLogger(__name__) - -# PROJECT_ROOT = Path(__file__).resolve().parents[5] -PROJECT_ROOT = Path(os.getenv("PROJECT_ROOT_DIR")).resolve() -PLAYBOOK_SRC = PROJECT_ROOT / "playbooks" / "generic.yml" -INVENTORY_SRC = PROJECT_ROOT / "inventory" / "hosts.yml" - - -# @router.post("/{vm_id}/start") -# def proxmox_vms_vm_id_start( -# vm_id: int, -# req: Request_ProxmoxVmsVMID_StartStopPauseResume, -# ): - -#### -# => /v0/admin/proxmox/vms/usage -@router.post( - path="/list_usage", - summary="Retrieve current resource usage of VMs and LXC containers", - description="Returns the current RAM, disk, and CPU usage for all virtual machines (VMs) and LXC containers.", - tags=["proxmox - usage"], - response_model=Reply_ProxmoxVms_VmListUsage, - response_description="Resource usage details", -) - - -def proxmox_vms_list_usage(req: Request_ProxmoxVms_VmListUsage): - - if debug ==1: - print(":: REQUEST ::", req.dict()) - print(f":: PROJECT_ROOT :: {PROJECT_ROOT} ") - print(f":: PLAYBOOK_SRC :: {PLAYBOOK_SRC} ") - print(f":: INVENTORY_SRC :: {INVENTORY_SRC} ") - - if not PLAYBOOK_SRC.exists(): - err = f":: err - MISSING PLAYBOOK : {PLAYBOOK_SRC}" - logging.error(err) - raise HTTPException(status_code=400, detail=err) - - if not INVENTORY_SRC.exists(): - err = f":: err - MISSING INVENTORY : {INVENTORY_SRC}" - logging.error(err) - raise HTTPException(status_code=400, detail=err) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - extravars = request_checks(req) - - #### - - rc, events, log_plain, log_ansi = run_playbook_core( - PLAYBOOK_SRC, - INVENTORY_SRC, - extravars=extravars, - limit=extravars["hosts"], - # limit=req.hosts, - ) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - payload = reply_processing(events, extravars, log_plain, rc, req) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - if rc == 0: - status = 200 - else: - status = 500 - - return JSONResponse(payload, status_code=status) - - -def reply_processing(events: list[dict] | list[Any], - extravars: dict[Any, Any], - log_plain: str, - rc, - req: Request_ProxmoxVms_VmListUsage) -> dict[str, list | Any]: - - """ reply post-processing - json or ansible raw output """ - - if req.as_json: - - #### - ##### OUTPUT TYPE - as_json=True - ##### - - action = extravars["proxmox_vm_action"] - result = extract_action_results(events, action) - - payload = { - "rc": rc, - "result": result - # "action": action, - } # raw - - # payload = {"rc": rc, "action": action, "result": events} - else: - #### - #### OUTPUT AS TEXT - as_json=False - #### - - # payload = {"rc": rc, "log_plain": log_plain, "log_multiline": log_plain.splitlines()} - payload = {"rc": rc, "log_multiline": log_plain.splitlines()} - return payload - - -def request_checks(req: Request_ProxmoxVms_VmListUsage) -> dict[Any, Any]: - """ request checks """ - - extravars = {} - extravars["proxmox_vm_action"] = "vm_list_usage" - - # if req.vm_id is not None: - # extravars["vm_id"] = req.vm_id - - if req.proxmox_node: - extravars["proxmox_node"] = req.proxmox_node - - # nothing : - if not extravars: - extravars = None - - extravars["hosts"] = "proxmox" - - if debug == 1: - print(f", extra vars : {extravars}") - return extravars - diff --git a/app/routes/v0/proxmox/vms/vm_id/__init__.py b/app/routes/v0/proxmox/vms/vm_id/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/routes/v0/proxmox/vms/vm_id/clone.py b/app/routes/v0/proxmox/vms/vm_id/clone.py deleted file mode 100644 index f0b6ab2..0000000 --- a/app/routes/v0/proxmox/vms/vm_id/clone.py +++ /dev/null @@ -1,167 +0,0 @@ -from typing import Any -import logging -from fastapi import APIRouter, HTTPException -from fastapi.responses import JSONResponse - -from app.schemas.proxmox.vm_id.clone import Request_ProxmoxVmsVMID_Clone -from app.schemas.proxmox.vm_id.clone import Reply_ProxmoxVmsVMID_Clone - -from app.runner import run_playbook_core # , extract_action_results -from app.utils.vm_id_name_resolver import resolv_id_to_vm_name - -from app.extract_actions import extract_action_results -from app import utils -from pathlib import Path -import os - -# -# ISSUE - #3 -# - -debug = 0 - -router = APIRouter() -logger = logging.getLogger(__name__) - -PROJECT_ROOT = Path(os.getenv("PROJECT_ROOT_DIR")).resolve() -INVENTORY_NAME = "hosts" -PLAYBOOK_SRC = PROJECT_ROOT / "playbooks" / "generic.yml" - -# -# => /v0/admin/proxmox/vms/vmd_id/clone - POST -# -@router.post( - path="/clone", - summary="Clone a specific VM", - description="This endpoint clone the target virtual machine (VM).", - tags=["proxmox - vm management"], - # - response_model=Reply_ProxmoxVmsVMID_Clone, - response_description="Delete result", -) - -def proxmox_vms_vm_id_clone(req: Request_ProxmoxVmsVMID_Clone): - """ This endpoint clone the target virtual machine (VM).""" - - if not PLAYBOOK_SRC.exists(): - err = f":: err - MISSING PLAYBOOK : {PLAYBOOK_SRC}" - logging.error(err) - raise HTTPException(status_code=400, detail=err) - - checked_playbook_filepath = PLAYBOOK_SRC - checked_inventory_filepath = utils.resolve_inventory(INVENTORY_NAME) - - if debug ==1: - print("") - print(":: REQUEST ::", req.model_dump()) - print(f":: checked_inventory_filepath :: {checked_inventory_filepath} ") - print(f":: checked_playbook_filepath :: {checked_playbook_filepath} ") - print("") - - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - extravars = request_checks(req) - - #### - - rc, events, log_plain, log_ansi = run_playbook_core( - checked_playbook_filepath, - checked_inventory_filepath, - # limit=req.hosts, - limit=extravars["hosts"], - extravars=extravars, - ) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - payload = reply_processing(events, extravars, log_plain, rc, req) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - if rc == 0: - status = 200 - else: - status = 500 - - return JSONResponse(payload, status_code=status) - - -def reply_processing(events: list[dict] | list[Any], - extravars: dict[Any, Any], - log_plain: str, - rc, - req: Request_ProxmoxVmsVMID_Clone) -> dict[str, list | Any]: - - """ reply post-processing - json or ansible raw output """ - - if req.as_json: - - ##### OUTPUT TYPE - as_json=True - ##### - - action = extravars["proxmox_vm_action"] - result = extract_action_results(events, action) - - payload = { - "rc": rc, - "result": result - # "action": action, - } # raw - - # payload = {"rc": rc, "action": action, "result": events} - else: - #### - #### OUTPUT AS TEXT - as_json=False - #### - - # payload = {"rc": rc, "log_plain": log_plain, "log_multiline": log_plain.splitlines()} - payload = {"rc": rc, "log_multiline": log_plain.splitlines()} - return payload - - -def request_checks(req: Request_ProxmoxVmsVMID_Clone) -> dict[Any, Any]: - """ request checks """ - - extravars = {} - - extravars["proxmox_vm_action"] = "vm_clone" - # - - - if req.proxmox_node: - extravars["proxmox_node"] = req.proxmox_node - - if req.vm_id is not None: - extravars["vm_id"] = req.vm_id - - extravars["vm_name"] = resolv_id_to_vm_name(extravars["proxmox_node"], extravars["vm_id"] ) - - if req.vm_new_id is not None: - extravars["vm_new_id"] = req.vm_new_id - - if req.vm_name is not None: - extravars["vm_name"] = req.vm_name - - if req.vm_description is not None: - extravars["vm_description"] = req.vm_description - - if req.proxmox_node: - extravars["proxmox_node"] = req.proxmox_node - - # nothing : - if not extravars: - extravars = None - - if debug == 1: - print(f", extra vars : {extravars}") - - extravars["hosts"] = "proxmox" - return extravars - diff --git a/app/routes/v0/proxmox/vms/vm_id/config/vm_get_config.py b/app/routes/v0/proxmox/vms/vm_id/config/vm_get_config.py deleted file mode 100644 index c1db2fd..0000000 --- a/app/routes/v0/proxmox/vms/vm_id/config/vm_get_config.py +++ /dev/null @@ -1,156 +0,0 @@ -from typing import Any -import logging -from fastapi import APIRouter, HTTPException -from fastapi.responses import JSONResponse - -from app.schemas.proxmox.vm_id.config.vm_get_config import Request_ProxmoxVmsVMID_VmGetConfig -from app.schemas.proxmox.vm_id.config.vm_get_config import Reply_ProxmoxVmsVMID_VmGetConfig - -from app.runner import run_playbook_core # , extract_action_results -from app.extract_actions import extract_action_results -from app import utils -from pathlib import Path -import os - -# -# ISSUE - #5 -# - -debug = 0 - -router = APIRouter() -logger = logging.getLogger(__name__) - -# PROJECT_ROOT = Path(__file__).resolve().parents[6] -PROJECT_ROOT = Path(os.getenv("PROJECT_ROOT_DIR")).resolve() -INVENTORY_NAME = "hosts" -PLAYBOOK_SRC = PROJECT_ROOT / "playbooks" / "generic.yml" - -# INVENTORY_SRC = PROJECT_ROOT / "inventory" / "hosts.yml" - -# @router.post("/{vm_id}/start") -# def proxmox_vms_vm_id_start( -# vm_id: int, -# req: Request_ProxmoxVmsVMID_StartStopPauseResume, -# ) - -# -# => /v0/admin/proxmox/vms/vmd_id/vm_get_config -# -@router.post( - path="/vm_get_config", - summary="Retrieve configuration of a VM", - description="Returns the configuration details of the specified virtual machine (VM).", - tags=["proxmox - vm configuration"], - response_model=Reply_ProxmoxVmsVMID_VmGetConfig, - response_description="VM configuration details", -) - -def proxmox_vms_vm_id_vm_get_config(req: Request_ProxmoxVmsVMID_VmGetConfig): - - if not PLAYBOOK_SRC.exists(): - err = f":: err - MISSING PLAYBOOK : {PLAYBOOK_SRC}" - logging.error(err) - raise HTTPException(status_code=400, detail=err) - - checked_playbook_filepath = PLAYBOOK_SRC - checked_inventory_filepath = utils.resolve_inventory(INVENTORY_NAME) - - - if debug ==1: - print("") - print(":: REQUEST ::", req.model_dump()) - print(f":: checked_inventory_filepath :: {checked_inventory_filepath} ") - print(f":: checked_playbook_filepath :: {checked_playbook_filepath} ") - print("") - - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - extravars = request_checks(req) - - #### - - rc, events, log_plain, log_ansi = run_playbook_core( - checked_playbook_filepath, - checked_inventory_filepath, - # limit=req.hosts, - limit=extravars["hosts"], - extravars=extravars, - ) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - payload = reply_processing(events, extravars, log_plain, rc, req) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - if rc == 0: - status = 200 - else: - status = 500 - - return JSONResponse(payload, status_code=status) - - -def reply_processing(events: list[dict] | list[Any], - extravars: dict[Any, Any], - log_plain: str, - rc, - req: Request_ProxmoxVmsVMID_VmGetConfig) -> dict[str, list | Any]: - - """ reply post-processing - json or ansible raw output """ - - if req.as_json: - - ##### OUTPUT TYPE - as_json=True - ##### - - action = extravars["proxmox_vm_action"] - result = extract_action_results(events, action) - - payload = { - "rc": rc, - "result": result - # "action": action, - } # raw - - # payload = {"rc": rc, "action": action, "result": events} - else: - #### - #### OUTPUT AS TEXT - as_json=False - #### - - # payload = {"rc": rc, "log_plain": log_plain, "log_multiline": log_plain.splitlines()} - payload = {"rc": rc, "log_multiline": log_plain.splitlines()} - return payload - - -def request_checks(req: Request_ProxmoxVmsVMID_VmGetConfig) -> dict[Any, Any]: - """ request checks """ - - extravars = {} - extravars["proxmox_vm_action"] = "vm_get_config" - - if req.vm_id is not None: - extravars["vm_id"] = req.vm_id - - if req.proxmox_node: - extravars["proxmox_node"] = req.proxmox_node - - # nothing : - if not extravars: - extravars = None - - if debug == 1: - print(f", extra vars : {extravars}") - - extravars["hosts"] = "proxmox" - return extravars - diff --git a/app/routes/v0/proxmox/vms/vm_id/config/vm_get_config_cdrom.py b/app/routes/v0/proxmox/vms/vm_id/config/vm_get_config_cdrom.py deleted file mode 100644 index ba128e1..0000000 --- a/app/routes/v0/proxmox/vms/vm_id/config/vm_get_config_cdrom.py +++ /dev/null @@ -1,155 +0,0 @@ -from typing import Any -import logging -from fastapi import APIRouter, HTTPException -from fastapi.responses import JSONResponse - -from app.schemas.proxmox.vm_id.config.vm_get_config_cdrom import Request_ProxmoxVmsVMID_VmGetConfigCdrom -from app.schemas.proxmox.vm_id.config.vm_get_config_cdrom import Reply_ProxmoxVmsVMID_VmGetConfigCdrom - -from app.runner import run_playbook_core # , extract_action_results -from app.extract_actions import extract_action_results -from app import utils -from pathlib import Path -import os - -# -# ISSUE - #5 -# - -debug = 0 - -router = APIRouter() -logger = logging.getLogger(__name__) - -# PROJECT_ROOT = Path(__file__).resolve().parents[6] -PROJECT_ROOT = Path(os.getenv("PROJECT_ROOT_DIR")).resolve() -INVENTORY_NAME = "hosts" -PLAYBOOK_SRC = PROJECT_ROOT / "playbooks" / "generic.yml" - -# INVENTORY_SRC = PROJECT_ROOT / "inventory" / "hosts.yml" - -# @router.post("/{vm_id}/start") -# def proxmox_vms_vm_id_start( -# vm_id: int, -# req: Request_ProxmoxVmsVMID_StartStopPauseResume, -# ): - -# -# => /v0/admin/proxmox/vms/vmd_id/vm_get_config -# -@router.post( - path="/vm_get_config_cdrom", - summary="Get cdrom configuration of a VM", - description="Returns the cdrom configuration details of the specified virtual machine (VM).", - tags=["proxmox - vm configuration"], - response_model=Reply_ProxmoxVmsVMID_VmGetConfigCdrom, - response_description="cdrom configuration details", -) -def proxmox_vms_vm_id_vm_get_config_cdrom(req: Request_ProxmoxVmsVMID_VmGetConfigCdrom): - - if not PLAYBOOK_SRC.exists(): - err = f":: err - MISSING PLAYBOOK : {PLAYBOOK_SRC}" - logging.error(err) - raise HTTPException(status_code=400, detail=err) - - checked_playbook_filepath = PLAYBOOK_SRC - checked_inventory_filepath = utils.resolve_inventory(INVENTORY_NAME) - - - if debug ==1: - print("") - print(":: REQUEST ::", req.model_dump()) - print(f":: checked_inventory_filepath :: {checked_inventory_filepath} ") - print(f":: checked_playbook_filepath :: {checked_playbook_filepath} ") - print("") - - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - extravars = request_checks(req) - - #### - - rc, events, log_plain, log_ansi = run_playbook_core( - checked_playbook_filepath, - checked_inventory_filepath, - # limit=req.hosts, - limit=extravars["hosts"], - extravars=extravars, - ) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - payload = reply_processing(events, extravars, log_plain, rc, req) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - if rc == 0: - status = 200 - else: - status = 500 - - return JSONResponse(payload, status_code=status) - - -def reply_processing(events: list[dict] | list[Any], - extravars: dict[Any, Any], - log_plain: str, - rc, - req: Request_ProxmoxVmsVMID_VmGetConfigCdrom) -> dict[str, list | Any]: - - """ reply post-processing - json or ansible raw output """ - - if req.as_json: - - ##### OUTPUT TYPE - as_json=True - ##### - - action = extravars["proxmox_vm_action"] - result = extract_action_results(events, action) - - payload = { - "rc": rc, - "result": result - # "action": action, - } # raw - - # payload = {"rc": rc, "action": action, "result": events} - else: - #### - #### OUTPUT AS TEXT - as_json=False - #### - - # payload = {"rc": rc, "log_plain": log_plain, "log_multiline": log_plain.splitlines()} - payload = {"rc": rc, "log_multiline": log_plain.splitlines()} - return payload - - -def request_checks(req: Request_ProxmoxVmsVMID_VmGetConfigCdrom) -> dict[Any, Any]: - """ request checks """ - - extravars = {} - extravars["proxmox_vm_action"] = "vm_get_config_cdrom" - - if req.vm_id is not None: - extravars["vm_id"] = req.vm_id - - if req.proxmox_node: - extravars["proxmox_node"] = req.proxmox_node - - # nothing : - if not extravars: - extravars = None - - if debug == 1: - print(f", extra vars : {extravars}") - - extravars["hosts"] = "proxmox" - return extravars - diff --git a/app/routes/v0/proxmox/vms/vm_id/config/vm_get_config_cpu.py b/app/routes/v0/proxmox/vms/vm_id/config/vm_get_config_cpu.py deleted file mode 100644 index 5c2d49c..0000000 --- a/app/routes/v0/proxmox/vms/vm_id/config/vm_get_config_cpu.py +++ /dev/null @@ -1,155 +0,0 @@ -from typing import Any -import logging -from fastapi import APIRouter, HTTPException -from fastapi.responses import JSONResponse - -from app.schemas.proxmox.vm_id.config.vm_get_config_cpu import Request_ProxmoxVmsVMID_VmGetConfigCpu -from app.schemas.proxmox.vm_id.config.vm_get_config_cpu import Reply_ProxmoxVmsVMID_VmGetConfigCpu - -from app.runner import run_playbook_core # , extract_action_results -from app.extract_actions import extract_action_results -from app import utils -from pathlib import Path -import os - -# -# ISSUE - #5 -# - -debug = 0 - -router = APIRouter() -logger = logging.getLogger(__name__) - -# PROJECT_ROOT = Path(__file__).resolve().parents[6] -PROJECT_ROOT = Path(os.getenv("PROJECT_ROOT_DIR")).resolve() -INVENTORY_NAME = "hosts" -PLAYBOOK_SRC = PROJECT_ROOT / "playbooks" / "generic.yml" - -# INVENTORY_SRC = PROJECT_ROOT / "inventory" / "hosts.yml" - -# @router.post("/{vm_id}/start") -# def proxmox_vms_vm_id_start( -# vm_id: int, -# req: Request_ProxmoxVmsVMID_StartStopPauseResume, -# ): - -# -# => /v0/admin/proxmox/vms/vmd_id/vm_get_config -# -@router.post( - path="/vm_get_config_cpu", - summary="Get cpu configuration of a VM", - description="Returns the cpu configuration details of the specified virtual machine (VM).", - tags=["proxmox - vm configuration"], - response_model=Reply_ProxmoxVmsVMID_VmGetConfigCpu, - response_description="cpu configuration details", -) -def proxmox_vms_vm_id_vm_get_config_cpu(req: Request_ProxmoxVmsVMID_VmGetConfigCpu): - - if not PLAYBOOK_SRC.exists(): - err = f":: err - MISSING PLAYBOOK : {PLAYBOOK_SRC}" - logging.error(err) - raise HTTPException(status_code=400, detail=err) - - checked_playbook_filepath = PLAYBOOK_SRC - checked_inventory_filepath = utils.resolve_inventory(INVENTORY_NAME) - - - if debug ==1: - print("") - print(":: REQUEST ::", req.model_dump()) - print(f":: checked_inventory_filepath :: {checked_inventory_filepath} ") - print(f":: checked_playbook_filepath :: {checked_playbook_filepath} ") - print("") - - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - extravars = request_checks(req) - - #### - - rc, events, log_plain, log_ansi = run_playbook_core( - checked_playbook_filepath, - checked_inventory_filepath, - # limit=req.hosts, - limit=extravars["hosts"], - extravars=extravars, - ) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - payload = reply_processing(events, extravars, log_plain, rc, req) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - if rc == 0: - status = 200 - else: - status = 500 - - return JSONResponse(payload, status_code=status) - - -def reply_processing(events: list[dict] | list[Any], - extravars: dict[Any, Any], - log_plain: str, - rc, - req: Request_ProxmoxVmsVMID_VmGetConfigCpu) -> dict[str, list | Any]: - - """ reply post-processing - json or ansible raw output """ - - if req.as_json: - - ##### OUTPUT TYPE - as_json=True - ##### - - action = extravars["proxmox_vm_action"] - result = extract_action_results(events, action) - - payload = { - "rc": rc, - "result": result - # "action": action, - } # raw - - # payload = {"rc": rc, "action": action, "result": events} - else: - #### - #### OUTPUT AS TEXT - as_json=False - #### - - # payload = {"rc": rc, "log_plain": log_plain, "log_multiline": log_plain.splitlines()} - payload = {"rc": rc, "log_multiline": log_plain.splitlines()} - return payload - - -def request_checks(req: Request_ProxmoxVmsVMID_VmGetConfigCpu) -> dict[Any, Any]: - """ request checks """ - - extravars = {} - extravars["proxmox_vm_action"] = "vm_get_config_cpu" - - if req.vm_id is not None: - extravars["vm_id"] = req.vm_id - - if req.proxmox_node: - extravars["proxmox_node"] = req.proxmox_node - - # nothing : - if not extravars: - extravars = None - - if debug == 1: - print(f", extra vars : {extravars}") - - extravars["hosts"] = "proxmox" - return extravars - diff --git a/app/routes/v0/proxmox/vms/vm_id/config/vm_get_config_ram.py b/app/routes/v0/proxmox/vms/vm_id/config/vm_get_config_ram.py deleted file mode 100644 index e5258f4..0000000 --- a/app/routes/v0/proxmox/vms/vm_id/config/vm_get_config_ram.py +++ /dev/null @@ -1,155 +0,0 @@ -from typing import Any -import logging -from fastapi import APIRouter, HTTPException -from fastapi.responses import JSONResponse - -from app.schemas.proxmox.vm_id.config.vm_get_config_ram import Request_ProxmoxVmsVMID_VmGetConfigRam -from app.schemas.proxmox.vm_id.config.vm_get_config_ram import Reply_ProxmoxVmsVMID_VmGetConfigRam - -from app.runner import run_playbook_core # , extract_action_results -from app.extract_actions import extract_action_results -from app import utils -from pathlib import Path -import os - -# -# ISSUE - #5 -# - -debug = 0 - -router = APIRouter() -logger = logging.getLogger(__name__) - -# PROJECT_ROOT = Path(__file__).resolve().parents[6] -PROJECT_ROOT = Path(os.getenv("PROJECT_ROOT_DIR")).resolve() -INVENTORY_NAME = "hosts" -PLAYBOOK_SRC = PROJECT_ROOT / "playbooks" / "generic.yml" - -# INVENTORY_SRC = PROJECT_ROOT / "inventory" / "hosts.yml" - -# @router.post("/{vm_id}/start") -# def proxmox_vms_vm_id_start( -# vm_id: int, -# req: Request_ProxmoxVmsVMID_StartStopPauseResume, -# ): - -# -# => /v0/admin/proxmox/vms/vmd_id/vm_get_config -# -@router.post( - path="/vm_get_config_ram", - summary="Get ram configuration of a VM", - description="Returns the ram configuration details of the specified virtual machine (VM).", - tags=["proxmox - vm configuration"], - response_model=Reply_ProxmoxVmsVMID_VmGetConfigRam, - response_description="ram configuration details", -) -def proxmox_vms_vm_id_vm_get_config_ram(req: Request_ProxmoxVmsVMID_VmGetConfigRam): - - if not PLAYBOOK_SRC.exists(): - err = f":: err - MISSING PLAYBOOK : {PLAYBOOK_SRC}" - logging.error(err) - raise HTTPException(status_code=400, detail=err) - - checked_playbook_filepath = PLAYBOOK_SRC - checked_inventory_filepath = utils.resolve_inventory(INVENTORY_NAME) - - - if debug ==1: - print("") - print(":: REQUEST ::", req.model_dump()) - print(f":: checked_inventory_filepath :: {checked_inventory_filepath} ") - print(f":: checked_playbook_filepath :: {checked_playbook_filepath} ") - print("") - - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - extravars = request_checks(req) - - #### - - rc, events, log_plain, log_ansi = run_playbook_core( - checked_playbook_filepath, - checked_inventory_filepath, - # limit=req.hosts, - limit=extravars["hosts"], - extravars=extravars, - ) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - payload = reply_processing(events, extravars, log_plain, rc, req) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - if rc == 0: - status = 200 - else: - status = 500 - - return JSONResponse(payload, status_code=status) - - -def reply_processing(events: list[dict] | list[Any], - extravars: dict[Any, Any], - log_plain: str, - rc, - req: Request_ProxmoxVmsVMID_VmGetConfigRam) -> dict[str, list | Any]: - - """ reply post-processing - json or ansible raw output """ - - if req.as_json: - - ##### OUTPUT TYPE - as_json=True - ##### - - action = extravars["proxmox_vm_action"] - result = extract_action_results(events, action) - - payload = { - "rc": rc, - "result": result - # "action": action, - } # raw - - # payload = {"rc": rc, "action": action, "result": events} - else: - #### - #### OUTPUT AS TEXT - as_json=False - #### - - # payload = {"rc": rc, "log_plain": log_plain, "log_multiline": log_plain.splitlines()} - payload = {"rc": rc, "log_multiline": log_plain.splitlines()} - return payload - - -def request_checks(req: Request_ProxmoxVmsVMID_VmGetConfigRam) -> dict[Any, Any]: - """ request checks """ - - extravars = {} - extravars["proxmox_vm_action"] = "vm_get_config_ram" - - if req.vm_id is not None: - extravars["vm_id"] = req.vm_id - - if req.proxmox_node: - extravars["proxmox_node"] = req.proxmox_node - - # nothing : - if not extravars: - extravars = None - - if debug == 1: - print(f", extra vars : {extravars}") - - extravars["hosts"] = "proxmox" - return extravars - diff --git a/app/routes/v0/proxmox/vms/vm_id/config/vm_set_tag.py b/app/routes/v0/proxmox/vms/vm_id/config/vm_set_tag.py deleted file mode 100644 index 87731bf..0000000 --- a/app/routes/v0/proxmox/vms/vm_id/config/vm_set_tag.py +++ /dev/null @@ -1,159 +0,0 @@ -from typing import Any -import logging -from fastapi import APIRouter, HTTPException -from fastapi.responses import JSONResponse - -from app.schemas.proxmox.vm_id.config.vm_set_tag import Request_ProxmoxVmsVMID_VmSetTag -from app.schemas.proxmox.vm_id.config.vm_set_tag import Reply_ProxmoxVmsVMID_VmSetTag - -from app.runner import run_playbook_core # , extract_action_results -from app.extract_actions import extract_action_results -from app import utils -from pathlib import Path -import os - -# -# ISSUE - #7 -# - -debug = 0 - -router = APIRouter() -logger = logging.getLogger(__name__) - -# PROJECT_ROOT = Path(__file__).resolve().parents[6] -PROJECT_ROOT = Path(os.getenv("PROJECT_ROOT_DIR")).resolve() -INVENTORY_NAME = "hosts" -PLAYBOOK_SRC = PROJECT_ROOT / "playbooks" / "generic.yml" - -# INVENTORY_SRC = PROJECT_ROOT / "inventory" / "hosts.yml" - -# @router.post("/{vm_id}/start") -# def proxmox_vms_vm_id_start( -# vm_id: int, -# req: Request_ProxmoxVmsVMID_StartStopPauseResume, -# ): - -# -# => /v0/admin/proxmox/vms/vmd_id/vm_get_config -# -@router.post( - path="/vm_set_tag", - summary="Retrieve configuration of a VM", - description="Returns the configuration details of the specified virtual machine (VM).", - tags=["proxmox - vm configuration"], - response_model=Reply_ProxmoxVmsVMID_VmSetTag, - response_description="VM configuration details", -) - -def proxmox_vms_vm_id_vm_set_tags(req: Request_ProxmoxVmsVMID_VmSetTag): - - if not PLAYBOOK_SRC.exists(): - err = f":: err - MISSING PLAYBOOK : {PLAYBOOK_SRC}" - logging.error(err) - raise HTTPException(status_code=400, detail=err) - - checked_playbook_filepath = PLAYBOOK_SRC - checked_inventory_filepath = utils.resolve_inventory(INVENTORY_NAME) - - - if debug ==1: - print("") - print(":: REQUEST ::", req.model_dump()) - print(f":: checked_inventory_filepath :: {checked_inventory_filepath} ") - print(f":: checked_playbook_filepath :: {checked_playbook_filepath} ") - print("") - - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - extravars = request_checks(req) - - #### - - rc, events, log_plain, log_ansi = run_playbook_core( - checked_playbook_filepath, - checked_inventory_filepath, - # limit=req.hosts, - limit=extravars["hosts"], - extravars=extravars, - ) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - payload = reply_processing(events, extravars, log_plain, rc, req) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - if rc == 0: - status = 200 - else: - status = 500 - - return JSONResponse(payload, status_code=status) - - -def reply_processing(events: list[dict] | list[Any], - extravars: dict[Any, Any], - log_plain: str, - rc, - req: Request_ProxmoxVmsVMID_VmSetTag) -> dict[str, list | Any]: - - """ reply post-processing - json or ansible raw output """ - - if req.as_json: - - ##### OUTPUT TYPE - as_json=True - ##### - - action = extravars["proxmox_vm_action"] - result = extract_action_results(events, action) - - payload = { - "rc": rc, - "result": result - # "action": action, - } # raw - - # payload = {"rc": rc, "action": action, "result": events} - else: - #### - #### OUTPUT AS TEXT - as_json=False - #### - - # payload = {"rc": rc, "log_plain": log_plain, "log_multiline": log_plain.splitlines()} - payload = {"rc": rc, "log_multiline": log_plain.splitlines()} - return payload - - -def request_checks(req: Request_ProxmoxVmsVMID_VmSetTag) -> dict[Any, Any]: - """ request checks """ - - extravars = {} - extravars["proxmox_vm_action"] = "vm_set_tag" - - if req.vm_id is not None: - extravars["vm_id"] = req.vm_id - - if req.proxmox_node: - extravars["proxmox_node"] = req.proxmox_node - - if req.proxmox_node: - extravars["vm_tag_name"] = req.vm_tag_name - - # nothing : - if not extravars: - extravars = None - - if debug == 1: - print(f", extra vars : {extravars}") - - extravars["hosts"] = "proxmox" - return extravars - diff --git a/app/routes/v0/proxmox/vms/vm_id/create.py b/app/routes/v0/proxmox/vms/vm_id/create.py deleted file mode 100644 index 59535e2..0000000 --- a/app/routes/v0/proxmox/vms/vm_id/create.py +++ /dev/null @@ -1,173 +0,0 @@ -from typing import Any -import logging -from fastapi import APIRouter, HTTPException, Body - -from fastapi.responses import JSONResponse - -from app.schemas.proxmox.vm_id.create import Request_ProxmoxVmsVMID_Create -from app.schemas.proxmox.vm_id.create import Reply_ProxmoxVmsVMID_Create - -from app.runner import run_playbook_core # , extract_action_results -from app.extract_actions import extract_action_results -from app import utils -from pathlib import Path - -import os - -# -# ISSUE - #3 -# - -debug = 0 - -router = APIRouter() -logger = logging.getLogger(__name__) - -PROJECT_ROOT = Path(os.getenv("PROJECT_ROOT_DIR")).resolve() -INVENTORY_NAME = "hosts" -PLAYBOOK_SRC = PROJECT_ROOT / "playbooks" / "generic.yml" - -# -# => /v0/admin/proxmox/vms/vmd_id/create - POST -# - -@router.post( - path="/create", - summary="Create a specific VM", - description="This endpoint create the target virtual machine (VM).", - tags=["proxmox - vm management"], - # - response_model=Reply_ProxmoxVmsVMID_Create, - response_description="Delete result", -) - -def proxmox_vms_vm_id_create(req: Request_ProxmoxVmsVMID_Create): - """ This endpoint clone the target virtual machine (VM).""" - - if not PLAYBOOK_SRC.exists(): - err = f":: err - MISSING PLAYBOOK : {PLAYBOOK_SRC}" - logger.error(err) - raise HTTPException(status_code=400, detail=err) - - checked_playbook_filepath = PLAYBOOK_SRC - checked_inventory_filepath = utils.resolve_inventory(INVENTORY_NAME) - - if debug ==1: - print("") - print(":: REQUEST ::", req.model_dump()) - print(f":: checked_inventory_filepath :: {checked_inventory_filepath} ") - print(f":: checked_playbook_filepath :: {checked_playbook_filepath} ") - print("") - - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - extravars = request_checks(req) - - #### - - rc, events, log_plain, log_ansi = run_playbook_core( - checked_playbook_filepath, - checked_inventory_filepath, - # limit=req.hosts, - limit=extravars["hosts"], - extravars=extravars, - ) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - payload = reply_processing(events, extravars, log_plain, rc, req) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - if rc == 0: - status = 200 - else: - status = 500 - - return JSONResponse(payload, status_code=status) - - -def reply_processing(events: list[dict] | list[Any], - extravars: dict[Any, Any], - log_plain: str, - rc, - req: Request_ProxmoxVmsVMID_Create) -> dict[str, list | Any]: - - """ reply post-processing - json or ansible raw output """ - - if req.as_json: - - ##### OUTPUT TYPE - as_json=True - ##### - - action = extravars["proxmox_vm_action"] - result = extract_action_results(events, action) - - payload = { - "rc": rc, - "result": result - # "action": action, - } # raw - - # payload = {"rc": rc, "action": action, "result": events} - else: - #### - #### OUTPUT AS TEXT - as_json=False - #### - - # payload = {"rc": rc, "log_plain": log_plain, "log_multiline": log_plain.splitlines()} - payload = {"rc": rc, "log_multiline": log_plain.splitlines()} - return payload - - -def request_checks(req: Request_ProxmoxVmsVMID_Create) -> dict[Any, Any]: - """ request checks """ - - extravars = {} - - extravars["proxmox_vm_action"] = "vm_create" - - if req.proxmox_node: - extravars["proxmox_node"] = req.proxmox_node - - if req.vm_id is not None: - extravars["vm_id"] = req.vm_id - - if req.vm_name is not None: - extravars["vm_name"] = req.vm_name - - if req.vm_cpu is not None: - extravars["vm_cpu"] = req.vm_cpu - - if req.vm_cores is not None: - extravars["vm_cores"] = req.vm_cores - - if req.vm_sockets is not None: - extravars["vm_sockets"] = req.vm_sockets - - if req.vm_memory is not None: - extravars["vm_memory"] = req.vm_memory - - if req.vm_disk_size is not None: - extravars["vm_disk_size"] = req.vm_disk_size - - if req.vm_iso is not None: - extravars["vm_iso"] = req.vm_iso - - # nothing : - if not extravars: - extravars = None - - if debug == 1: - print(f", extra vars : {extravars}") - - extravars["hosts"] = "proxmox" - return extravars - diff --git a/app/routes/v0/proxmox/vms/vm_id/delete.py b/app/routes/v0/proxmox/vms/vm_id/delete.py deleted file mode 100644 index 4dc0408..0000000 --- a/app/routes/v0/proxmox/vms/vm_id/delete.py +++ /dev/null @@ -1,153 +0,0 @@ -from typing import Any -import logging -from fastapi import APIRouter, HTTPException -from fastapi.responses import JSONResponse - -from app.schemas.proxmox.vm_id.delete import Request_ProxmoxVmsVMID_Delete -from app.schemas.proxmox.vm_id.delete import Reply_ProxmoxVmsVMID_Delete - -from app.runner import run_playbook_core # , extract_action_results -from app.utils.vm_id_name_resolver import resolv_id_to_vm_name - -from app.extract_actions import extract_action_results -from app import utils -from pathlib import Path -import os - -# -# ISSUE - #3 -# - -debug = 0 - -router = APIRouter() -logger = logging.getLogger(__name__) - -PROJECT_ROOT = Path(os.getenv("PROJECT_ROOT_DIR")).resolve() -INVENTORY_NAME = "hosts" -PLAYBOOK_SRC = PROJECT_ROOT / "playbooks" / "generic.yml" - -# -# => /v0/admin/proxmox/vms/vmd_id/delete - DELETE -# -@router.delete( - path="/delete", - summary="Delete a specific VM", - description="This endpoint delete the target virtual machine (VM).", - tags=["proxmox - vm management"], - # - response_model=Reply_ProxmoxVmsVMID_Delete, - response_description="Delete result", -) - -def proxmox_vms_vm_id_delete(req: Request_ProxmoxVmsVMID_Delete): - """ This endpoint pauses the target virtual machine (VM).""" - - if not PLAYBOOK_SRC.exists(): - err = f":: err - MISSING PLAYBOOK : {PLAYBOOK_SRC}" - logging.error(err) - raise HTTPException(status_code=400, detail=err) - - checked_playbook_filepath = PLAYBOOK_SRC - checked_inventory_filepath = utils.resolve_inventory(INVENTORY_NAME) - - if debug ==1: - print("") - print(":: REQUEST ::", req.model_dump()) - print(f":: checked_inventory_filepath :: {checked_inventory_filepath} ") - print(f":: checked_playbook_filepath :: {checked_playbook_filepath} ") - print("") - - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - extravars = request_checks(req) - - #### - - rc, events, log_plain, log_ansi = run_playbook_core( - checked_playbook_filepath, - checked_inventory_filepath, - # limit=req.hosts, - limit=extravars["hosts"], - extravars=extravars, - ) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - payload = reply_processing(events, extravars, log_plain, rc, req) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - if rc == 0: - status = 200 - else: - status = 500 - - return JSONResponse(payload, status_code=status) - - -def reply_processing(events: list[dict] | list[Any], - extravars: dict[Any, Any], - log_plain: str, - rc, - req: Request_ProxmoxVmsVMID_Delete) -> dict[str, list | Any]: - - """ reply post-processing - json or ansible raw output """ - - if req.as_json: - - ##### OUTPUT TYPE - as_json=True - ##### - - action = extravars["proxmox_vm_action"] - result = extract_action_results(events, action) - - payload = { - "rc": rc, - "result": result - # "action": action, - } # raw - - # payload = {"rc": rc, "action": action, "result": events} - else: - #### - #### OUTPUT AS TEXT - as_json=False - #### - - # payload = {"rc": rc, "log_plain": log_plain, "log_multiline": log_plain.splitlines()} - payload = {"rc": rc, "log_multiline": log_plain.splitlines()} - return payload - - -def request_checks(req: Request_ProxmoxVmsVMID_Delete) -> dict[Any, Any]: - """ request checks """ - - extravars = {} - extravars["proxmox_vm_action"] = "vm_delete" - extravars["vm_name"] = "todo" # add func to resolve vm_id to vm_name - - if req.proxmox_node: - extravars["proxmox_node"] = req.proxmox_node - - if req.vm_id is not None: - extravars["vm_id"] = req.vm_id - - extravars["vm_name"] = resolv_id_to_vm_name(extravars["proxmox_node"], extravars["vm_id"] ) - - # nothing : - if not extravars: - extravars = None - - if debug == 1: - print(f", extra vars : {extravars}") - - extravars["hosts"] = "proxmox" - return extravars - diff --git a/app/routes/v0/proxmox/vms/vm_id/pause.py b/app/routes/v0/proxmox/vms/vm_id/pause.py deleted file mode 100644 index 4a61deb..0000000 --- a/app/routes/v0/proxmox/vms/vm_id/pause.py +++ /dev/null @@ -1,158 +0,0 @@ -from typing import Any -import logging -from fastapi import APIRouter, HTTPException -from fastapi.responses import JSONResponse - -from app.schemas.proxmox.vm_id.start_stop_resume_pause import Request_ProxmoxVmsVMID_StartStopPauseResume -from app.schemas.proxmox.vm_id.start_stop_resume_pause import Reply_ProxmoxVmsVMID_StartStopPauseResume - -from app.runner import run_playbook_core # , extract_action_results -from app.extract_actions import extract_action_results -from app import utils -from pathlib import Path -import os - -# -# ISSUE - #3 -# - -debug = 0 - -router = APIRouter() -logger = logging.getLogger(__name__) - -# PROJECT_ROOT = Path(__file__).resolve().parents[6] -PROJECT_ROOT = Path(os.getenv("PROJECT_ROOT_DIR")).resolve() -INVENTORY_NAME = "hosts" -PLAYBOOK_SRC = PROJECT_ROOT / "playbooks" / "generic.yml" - -# INVENTORY_SRC = PROJECT_ROOT / "inventory" / "hosts.yml" - -# @router.post("/{vm_id}/start") -# def proxmox_vms_vm_id_start( -# vm_id: int, -# req: Request_ProxmoxVmsVMID_StartStopPauseResume, -# ): - -# -# => /v0/admin/proxmox/vms/vmd_id/pause -# -@router.post( - path="/pause", - summary="Pause a specific VM", - description="This endpoint pauses the target virtual machine (VM).", - tags=["proxmox - vm lifecycle"], - # - response_model=Reply_ProxmoxVmsVMID_StartStopPauseResume, - response_description="Start result", -) - -def proxmox_vms_vm_id_pause(req: Request_ProxmoxVmsVMID_StartStopPauseResume): - """ This endpoint pauses the target virtual machine (VM).""" - - if not PLAYBOOK_SRC.exists(): - err = f":: err - MISSING PLAYBOOK : {PLAYBOOK_SRC}" - logging.error(err) - raise HTTPException(status_code=400, detail=err) - - checked_playbook_filepath = PLAYBOOK_SRC - checked_inventory_filepath = utils.resolve_inventory(INVENTORY_NAME) - - - if debug ==1: - print("") - print(":: REQUEST ::", req.model_dump()) - print(f":: checked_inventory_filepath :: {checked_inventory_filepath} ") - print(f":: checked_playbook_filepath :: {checked_playbook_filepath} ") - print("") - - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - extravars = request_checks(req) - - #### - - rc, events, log_plain, log_ansi = run_playbook_core( - checked_playbook_filepath, - checked_inventory_filepath, - # limit=req.hosts, - limit=extravars["hosts"], - extravars=extravars, - ) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - payload = reply_processing(events, extravars, log_plain, rc, req) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - if rc == 0: - status = 200 - else: - status = 500 - - return JSONResponse(payload, status_code=status) - - -def reply_processing(events: list[dict] | list[Any], - extravars: dict[Any, Any], - log_plain: str, - rc, - req: Request_ProxmoxVmsVMID_StartStopPauseResume) -> dict[str, list | Any]: - - """ reply post-processing - json or ansible raw output """ - - if req.as_json: - - ##### OUTPUT TYPE - as_json=True - ##### - - action = extravars["proxmox_vm_action"] - result = extract_action_results(events, action) - - payload = { - "rc": rc, - "result": result - # "action": action, - } # raw - - # payload = {"rc": rc, "action": action, "result": events} - else: - #### - #### OUTPUT AS TEXT - as_json=False - #### - - # payload = {"rc": rc, "log_plain": log_plain, "log_multiline": log_plain.splitlines()} - payload = {"rc": rc, "log_multiline": log_plain.splitlines()} - return payload - - -def request_checks(req: Request_ProxmoxVmsVMID_StartStopPauseResume) -> dict[Any, Any]: - """ request checks """ - - extravars = {} - extravars["proxmox_vm_action"] = "vm_pause" - - if req.vm_id is not None: - extravars["vm_id"] = req.vm_id - - if req.proxmox_node: - extravars["proxmox_node"] = req.proxmox_node - - # nothing : - if not extravars: - extravars = None - - if debug == 1: - print(f", extra vars : {extravars}") - - extravars["hosts"] = "proxmox" - return extravars - diff --git a/app/routes/v0/proxmox/vms/vm_id/resume.py b/app/routes/v0/proxmox/vms/vm_id/resume.py deleted file mode 100644 index bad34ee..0000000 --- a/app/routes/v0/proxmox/vms/vm_id/resume.py +++ /dev/null @@ -1,156 +0,0 @@ -from typing import Any -import logging -from fastapi import APIRouter, HTTPException -from fastapi.responses import JSONResponse - -from app.runner import run_playbook_core # , extract_action_results -from app.extract_actions import extract_action_results - -from app.schemas.proxmox.vm_id.start_stop_resume_pause import Request_ProxmoxVmsVMID_StartStopPauseResume -from app.schemas.proxmox.vm_id.start_stop_resume_pause import Reply_ProxmoxVmsVMID_StartStopPauseResume - -from pathlib import Path -import os - -# -# ISSUE - #3 -# - -debug = 0 - -router = APIRouter() -logger = logging.getLogger(__name__) - -# PROJECT_ROOT = Path(__file__).resolve().parents[6] -PROJECT_ROOT = Path(os.getenv("PROJECT_ROOT_DIR")).resolve() - -PLAYBOOK_SRC = PROJECT_ROOT / "playbooks" / "generic.yml" -INVENTORY_SRC = PROJECT_ROOT / "inventory" / "hosts.yml" - -# @router.post("/{vm_id}/start") -# def proxmox_vms_vm_id_start( -# vm_id: int, -# req: Request_ProxmoxVmsVMID_StartStopPauseResume, -# ): - -# -# => /v0/admin/proxmox/vms/vmd_id/resume -# -@router.post( - path="/resume", - summary="Resume a specific VM", - description="This endpoint resume the target virtual machine (VM).", - tags=["proxmox - vm lifecycle"], - # - response_model=Reply_ProxmoxVmsVMID_StartStopPauseResume, - response_description="Start result", -) - -def proxmox_vms_vm_id_resume(req: Request_ProxmoxVmsVMID_StartStopPauseResume): - """ This endpoint resume the target virtual machine (VM""" - - if debug ==1: - print(":: REQUEST ::", req.model_dump()) - print(f":: PROJECT_ROOT :: {PROJECT_ROOT} ") - print(f":: PLAYBOOK_SRC :: {PLAYBOOK_SRC} ") - print(f":: INVENTORY_SRC :: {INVENTORY_SRC} ") - - if not PLAYBOOK_SRC.exists(): - err = f":: err - MISSING PLAYBOOK : {PLAYBOOK_SRC}" - logging.error(err) - raise HTTPException(status_code=400, detail=err) - - if not INVENTORY_SRC.exists(): - err = f":: err - MISSING INVENTORY : {INVENTORY_SRC}" - logging.error(err) - raise HTTPException(status_code=400, detail=err) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - extravars = request_checks(req) - - #### - - rc, events, log_plain, log_ansi = run_playbook_core( - PLAYBOOK_SRC, - INVENTORY_SRC, - # limit=req.hosts, - limit=extravars["hosts"], - extravars=extravars, - ) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - payload = reply_processing(events, extravars, log_plain, rc, req) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - if rc == 0: - status = 200 - else: - status = 500 - - return JSONResponse(payload, status_code=status) - - -def reply_processing(events: list[dict] | list[Any], - extravars: dict[Any, Any], - log_plain: str, - rc, - req: Request_ProxmoxVmsVMID_StartStopPauseResume) -> dict[str, list | Any]: - - """ reply post-processing - json or ansible raw output """ - - if req.as_json: - - ##### OUTPUT TYPE - as_json=True - ##### - - action = extravars["proxmox_vm_action"] - result = extract_action_results(events, action) - - payload = { - "rc": rc, - "result": result - # "action": action, - } # raw - - # payload = {"rc": rc, "action": action, "result": events} - else: - #### - #### OUTPUT AS TEXT - as_json=False - #### - - # payload = {"rc": rc, "log_plain": log_plain, "log_multiline": log_plain.splitlines()} - payload = {"rc": rc, "log_multiline": log_plain.splitlines()} - return payload - - -def request_checks(req: Request_ProxmoxVmsVMID_StartStopPauseResume) -> dict[Any, Any]: - """ request checks """ - - extravars = {} - extravars["proxmox_vm_action"] = "vm_resume" - - if req.vm_id is not None: - extravars["vm_id"] = req.vm_id - - if req.proxmox_node: - extravars["proxmox_node"] = req.proxmox_node - - # nothing : - if not extravars: - extravars = None - - if debug == 1: - print(f", extra vars : {extravars}") - - extravars["hosts"] = "proxmox" - return extravars - diff --git a/app/routes/v0/proxmox/vms/vm_id/snapshots/vm_create.py b/app/routes/v0/proxmox/vms/vm_id/snapshots/vm_create.py deleted file mode 100644 index c2ad2b3..0000000 --- a/app/routes/v0/proxmox/vms/vm_id/snapshots/vm_create.py +++ /dev/null @@ -1,160 +0,0 @@ -from typing import Any -import logging -from fastapi import APIRouter, HTTPException, Body - -from fastapi.responses import JSONResponse - -from app.schemas.proxmox.vm_id.snapshot.vm_create import Request_ProxmoxVmsVMID_CreateSnapshot -from app.schemas.proxmox.vm_id.snapshot.vm_create import Reply_ProxmoxVmsVMID_CreateSnapshot - -from app.runner import run_playbook_core # , extract_action_results -from app.extract_actions import extract_action_results -from app.utils.vm_id_name_resolver import resolv_id_to_vm_name - -from app import utils -from pathlib import Path - -import os - -# -# ISSUE - #9 -# - -debug = 0 - -router = APIRouter() -logger = logging.getLogger(__name__) - -PROJECT_ROOT = Path(os.getenv("PROJECT_ROOT_DIR")).resolve() -INVENTORY_NAME = "hosts" -PLAYBOOK_SRC = PROJECT_ROOT / "playbooks" / "generic.yml" - -# -# => /v0/admin/proxmox/vms/vmd_id/snapshot/create - POST -# -@router.post( - path="/create", - summary="Create a snapshot for a VM", - description="Creates a snapshot of the specified virtual machine (VM).", - tags=["proxmox - vm snapshots"], - response_model=Reply_ProxmoxVmsVMID_CreateSnapshot, - response_description="Snapshot creation result", -) - -def proxmox_vms_vm_id_create_snapshot(req: Request_ProxmoxVmsVMID_CreateSnapshot): - """ This endpoint clone the target virtual machine (VM).""" - - if not PLAYBOOK_SRC.exists(): - err = f":: err - MISSING PLAYBOOK : {PLAYBOOK_SRC}" - logger.error(err) - raise HTTPException(status_code=400, detail=err) - - checked_playbook_filepath = PLAYBOOK_SRC - checked_inventory_filepath = utils.resolve_inventory(INVENTORY_NAME) - - if debug ==1: - print("") - print(":: REQUEST ::", req.model_dump()) - print(f":: checked_inventory_filepath :: {checked_inventory_filepath} ") - print(f":: checked_playbook_filepath :: {checked_playbook_filepath} ") - print("") - - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - extravars = request_checks(req) - - #### - - rc, events, log_plain, log_ansi = run_playbook_core( - checked_playbook_filepath, - checked_inventory_filepath, - # limit=req.hosts, - limit=extravars["hosts"], - extravars=extravars, - ) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - payload = reply_processing(events, extravars, log_plain, rc, req) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - if rc == 0: - status = 200 - else: - status = 500 - - return JSONResponse(payload, status_code=status) - - -def reply_processing(events: list[dict] | list[Any], - extravars: dict[Any, Any], - log_plain: str, - rc, - req: Request_ProxmoxVmsVMID_CreateSnapshot) -> dict[str, list | Any]: - - """ reply post-processing - json or ansible raw output """ - - if req.as_json: - - ##### OUTPUT TYPE - as_json=True - ##### - - action = extravars["proxmox_vm_action"] - result = extract_action_results(events, action) - - payload = { - "rc": rc, - "result": result - # "action": action, - } # raw - - # payload = {"rc": rc, "action": action, "result": events} - else: - #### - #### OUTPUT AS TEXT - as_json=False - #### - - # payload = {"rc": rc, "log_plain": log_plain, "log_multiline": log_plain.splitlines()} - payload = {"rc": rc, "log_multiline": log_plain.splitlines()} - return payload - - -def request_checks(req: Request_ProxmoxVmsVMID_CreateSnapshot) -> dict[Any, Any]: - """ request checks """ - - extravars = {} - - extravars["proxmox_vm_action"] = "snapshot_vm_create" - - if req.proxmox_node: - extravars["proxmox_node"] = req.proxmox_node - - if req.vm_id is not None: - extravars["vm_id"] = req.vm_id - - extravars["vm_name"] = resolv_id_to_vm_name(extravars["proxmox_node"], extravars["vm_id"] ) - - if req.vm_snapshot_name is not None: - extravars["vm_snapshot_name"] = req.vm_snapshot_name - - if req.vm_snapshot_description is not None: - extravars["vm_snapshot_description"] = req.vm_snapshot_description - - # nothing : - if not extravars: - extravars = None - - if debug == 1: - print(f", extra vars : {extravars}") - - extravars["hosts"] = "proxmox" - return extravars - diff --git a/app/routes/v0/proxmox/vms/vm_id/snapshots/vm_delete.py b/app/routes/v0/proxmox/vms/vm_id/snapshots/vm_delete.py deleted file mode 100644 index f2922cd..0000000 --- a/app/routes/v0/proxmox/vms/vm_id/snapshots/vm_delete.py +++ /dev/null @@ -1,166 +0,0 @@ -from typing import Any -import logging - -from fastapi import APIRouter, HTTPException -from fastapi.responses import JSONResponse - -from app.runner import run_playbook_core # , extract_action_resu# lts -from app.extract_actions import extract_action_results -from app.utils.vm_id_name_resolver import resolv_id_to_vm_name - -from app.schemas.proxmox.vm_id.snapshot.vm_delete import Request_ProxmoxVmsVMID_DeleteSnapshot -from app.schemas.proxmox.vm_id.snapshot.vm_delete import Reply_ProxmoxVmsVMID_DeleteSnapshot - -from pathlib import Path -import os - -# -# ISSUE - #9 -# - -debug = 0 - -router = APIRouter() -logger = logging.getLogger(__name__) - -# PROJECT_ROOT = Path(__file__).resolve().parents[5] -PROJECT_ROOT = Path(os.getenv("PROJECT_ROOT_DIR")).resolve() -PLAYBOOK_SRC = PROJECT_ROOT / "playbooks" / "generic.yml" -INVENTORY_SRC = PROJECT_ROOT / "inventory" / "hosts.yml" - -# @router.post("/{vm_id}/start") -# def proxmox_vms_vm_id_start( -# vm_id: int, -# req: Request_ProxmoxVmsVMID_StartStopPauseResume, -# ): - -#### -# => /v0/admin/proxmox/vms/vmd_id/snapshot/delete - DELETE -# -@router.delete( - path="/delete", - summary="Delete a snapshot for a VM", - description="Delete a snapshot of the specified virtual machine (VM).", - tags=["proxmox - vm snapshots"], - response_model=Reply_ProxmoxVmsVMID_DeleteSnapshot, - response_description="Snapshot delete result", -) - - -def proxmox_vms_vm_id_delete_snapshot(req: Request_ProxmoxVmsVMID_DeleteSnapshot): - - if debug ==1: - print(":: REQUEST ::", req.model_dump()) - print(f":: PROJECT_ROOT :: {PROJECT_ROOT} ") - print(f":: PLAYBOOK_SRC :: {PLAYBOOK_SRC} ") - print(f":: INVENTORY_SRC :: {INVENTORY_SRC} ") - - if not PLAYBOOK_SRC.exists(): - err = f":: err - MISSING PLAYBOOK : {PLAYBOOK_SRC}" - logging.error(err) - raise HTTPException(status_code=400, detail=err) - - if not INVENTORY_SRC.exists(): - err = f":: err - MISSING INVENTORY : {INVENTORY_SRC}" - logging.error(err) - raise HTTPException(status_code=400, detail=err) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - extravars = request_checks(req) - - #### - - rc, events, log_plain, log_ansi = run_playbook_core( - PLAYBOOK_SRC, - INVENTORY_SRC, - extravars=extravars, - limit=extravars["hosts"], - # limit=req.hosts, - ) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - payload = reply_processing(events, extravars, log_plain, rc, req) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - if rc == 0: - status = 200 - else: - status = 500 - - return JSONResponse(payload, status_code=status) - - -def reply_processing(events: list[dict] | list[Any], - extravars: dict[Any, Any], - log_plain: str, - rc, - req: Request_ProxmoxVmsVMID_DeleteSnapshot) -> dict[str, list | Any]: - - """ reply post-processing - json or ansible raw output """ - - if req.as_json: - - #### - ##### OUTPUT TYPE - as_json=True - ##### - - action = extravars["proxmox_vm_action"] - result = extract_action_results(events, action) - - payload = { - "rc": rc, - "result": result - # "action": action, - } # raw - - # payload = {"rc": rc, "action": action, "result": events} - else: - #### - #### OUTPUT AS TEXT - as_json=False - #### - - # payload = {"rc": rc, "log_plain": log_plain, "log_multiline": log_plain.splitlines()} - payload = {"rc": rc, "log_multiline": log_plain.splitlines()} - return payload - - -def request_checks(req: Request_ProxmoxVmsVMID_DeleteSnapshot) -> dict[Any, Any]: - """ request checks """ - - extravars = {} - extravars["proxmox_vm_action"] = "snapshot_vm_delete" - - # if req.vm_id is not None: - # extravars["vm_id"] = req.vm_id - - if req.proxmox_node: - extravars["proxmox_node"] = req.proxmox_node - - if req.vm_id is not None: - extravars["vm_id"] = req.vm_id - - extravars["vm_name"] = resolv_id_to_vm_name(extravars["proxmox_node"], extravars["vm_id"] ) - - if req.vm_snapshot_name: - extravars["vm_snapshot_name"] = req.vm_snapshot_name - - - # nothing : - if not extravars: - extravars = None - - extravars["hosts"] = "proxmox" - - if debug == 1: - print(f", extra vars : {extravars}") - return extravars - diff --git a/app/routes/v0/proxmox/vms/vm_id/snapshots/vm_list.py b/app/routes/v0/proxmox/vms/vm_id/snapshots/vm_list.py deleted file mode 100644 index 8ee4626..0000000 --- a/app/routes/v0/proxmox/vms/vm_id/snapshots/vm_list.py +++ /dev/null @@ -1,155 +0,0 @@ -from typing import Any -import logging - -from fastapi import APIRouter, HTTPException -from fastapi.responses import JSONResponse - -from app.runner import run_playbook_core # , extract_action_results -from app.extract_actions import extract_action_results - -from app.schemas.proxmox.vm_id.snapshot.vm_list import Request_ProxmoxVmsVMID_ListSnapshot -from app.schemas.proxmox.vm_id.snapshot.vm_list import Reply_ProxmoxVmsVMID_ListSnapshot - -from pathlib import Path -import os - -# -# ISSUE - #9 -# - -debug = 0 - -router = APIRouter() -logger = logging.getLogger(__name__) - -# PROJECT_ROOT = Path(__file__).resolve().parents[5] -PROJECT_ROOT = Path(os.getenv("PROJECT_ROOT_DIR")).resolve() -PLAYBOOK_SRC = PROJECT_ROOT / "playbooks" / "generic.yml" -INVENTORY_SRC = PROJECT_ROOT / "inventory" / "hosts.yml" - -# @router.post("/{vm_id}/start") -# def proxmox_vms_vm_id_start( -# vm_id: int, -# req: Request_ProxmoxVmsVMID_StartStopPauseResume, -# ): - -#### -# => /v0/admin/proxmox/vms/vmd_id/snapshot/list - POST -# -@router.post( - path="/list", - summary="List a snapshot for a VM", - description="List snapshot of the specified virtual machine (VM).", - tags=["proxmox - vm snapshots"], - response_model=Reply_ProxmoxVmsVMID_ListSnapshot, - response_description="Snapshot list result", -) - -def proxmox_vms_vm_id_list_snapshot(req: Request_ProxmoxVmsVMID_ListSnapshot): - - if debug ==1: - print(":: REQUEST ::", req.model_dump()) - print(f":: PROJECT_ROOT :: {PROJECT_ROOT} ") - print(f":: PLAYBOOK_SRC :: {PLAYBOOK_SRC} ") - print(f":: INVENTORY_SRC :: {INVENTORY_SRC} ") - - if not PLAYBOOK_SRC.exists(): - err = f":: err - MISSING PLAYBOOK : {PLAYBOOK_SRC}" - logging.error(err) - raise HTTPException(status_code=400, detail=err) - - if not INVENTORY_SRC.exists(): - err = f":: err - MISSING INVENTORY : {INVENTORY_SRC}" - logging.error(err) - raise HTTPException(status_code=400, detail=err) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - extravars = request_checks(req) - - #### - - rc, events, log_plain, log_ansi = run_playbook_core( - PLAYBOOK_SRC, - INVENTORY_SRC, - extravars=extravars, - limit=extravars["hosts"], - # limit=req.hosts, - ) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - payload = reply_processing(events, extravars, log_plain, rc, req) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - if rc == 0: - status = 200 - else: - status = 500 - - return JSONResponse(payload, status_code=status) - - -def reply_processing(events: list[dict] | list[Any], - extravars: dict[Any, Any], - log_plain: str, - rc, - req: Request_ProxmoxVmsVMID_ListSnapshot) -> dict[str, list | Any]: - - """ reply post-processing - json or ansible raw output """ - - if req.as_json: - - #### - ##### OUTPUT TYPE - as_json=True - ##### - - action = extravars["proxmox_vm_action"] - result = extract_action_results(events, action) - - payload = { - "rc": rc, - "result": result - # "action": action, - } # raw - - # payload = {"rc": rc, "action": action, "result": events} - else: - #### - #### OUTPUT AS TEXT - as_json=False - #### - - # payload = {"rc": rc, "log_plain": log_plain, "log_multiline": log_plain.splitlines()} - payload = {"rc": rc, "log_multiline": log_plain.splitlines()} - return payload - - -def request_checks(req: Request_ProxmoxVmsVMID_ListSnapshot) -> dict[Any, Any]: - """ request checks """ - - extravars = {} - extravars["proxmox_vm_action"] = "snapshot_vm_list" - - if req.vm_id is not None: - extravars["vm_id"] = req.vm_id - - if req.proxmox_node: - extravars["proxmox_node"] = req.proxmox_node - - # nothing : - if not extravars: - extravars = None - - extravars["hosts"] = "proxmox" - - if debug == 1: - print(f", extra vars : {extravars}") - return extravars - diff --git a/app/routes/v0/proxmox/vms/vm_id/snapshots/vm_revert.py b/app/routes/v0/proxmox/vms/vm_id/snapshots/vm_revert.py deleted file mode 100644 index c58bcfc..0000000 --- a/app/routes/v0/proxmox/vms/vm_id/snapshots/vm_revert.py +++ /dev/null @@ -1,162 +0,0 @@ -from typing import Any -import logging - -from fastapi import APIRouter, HTTPException -from fastapi.responses import JSONResponse - -from app.runner import run_playbook_core # , extract_action_results -from app.extract_actions import extract_action_results -from app.utils.vm_id_name_resolver import resolv_id_to_vm_name - -from app.schemas.proxmox.vm_id.snapshot.vm_revert import Request_ProxmoxVmsVMID_RevertSnapshot -from app.schemas.proxmox.vm_id.snapshot.vm_revert import Reply_ProxmoxVmsVMID_RevertSnapshot - -from pathlib import Path -import os - -# -# ISSUE - #9 -# - -debug = 0 - -router = APIRouter() -logger = logging.getLogger(__name__) - -# PROJECT_ROOT = Path(__file__).resolve().parents[5] -PROJECT_ROOT = Path(os.getenv("PROJECT_ROOT_DIR")).resolve() -PLAYBOOK_SRC = PROJECT_ROOT / "playbooks" / "generic.yml" -INVENTORY_SRC = PROJECT_ROOT / "inventory" / "hosts.yml" - -# @router.post("/{vm_id}/start") -# def proxmox_vms_vm_id_start( -# vm_id: int, -# req: Request_ProxmoxVmsVMID_StartStopPauseResume, -# ): - -#### -# => /v0/admin/proxmox/vms/vmd_id/snapshot/revert - POST -# -@router.post( - path="/revert", - summary="Revert a VM to a snapshot", - description="Reverts the specified virtual machine (VM) to the given snapshot.", - tags=["proxmox - vm snapshots"], - response_model=Reply_ProxmoxVmsVMID_RevertSnapshot, - response_description="Snapshot revert result", -) - - -def proxmox_vms_vm_id_revert_snapshot(req: Request_ProxmoxVmsVMID_RevertSnapshot): - - if debug ==1: - print(":: REQUEST ::", req.model_dump()) - print(f":: PROJECT_ROOT :: {PROJECT_ROOT} ") - print(f":: PLAYBOOK_SRC :: {PLAYBOOK_SRC} ") - print(f":: INVENTORY_SRC :: {INVENTORY_SRC} ") - - if not PLAYBOOK_SRC.exists(): - err = f":: err - MISSING PLAYBOOK : {PLAYBOOK_SRC}" - logging.error(err) - raise HTTPException(status_code=400, detail=err) - - if not INVENTORY_SRC.exists(): - err = f":: err - MISSING INVENTORY : {INVENTORY_SRC}" - logging.error(err) - raise HTTPException(status_code=400, detail=err) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - extravars = request_checks(req) - - #### - - rc, events, log_plain, log_ansi = run_playbook_core( - PLAYBOOK_SRC, - INVENTORY_SRC, - extravars=extravars, - limit=extravars["hosts"], - # limit=req.hosts, - ) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - payload = reply_processing(events, extravars, log_plain, rc, req) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - if rc == 0: - status = 200 - else: - status = 500 - - return JSONResponse(payload, status_code=status) - - -def reply_processing(events: list[dict] | list[Any], - extravars: dict[Any, Any], - log_plain: str, - rc, - req: Request_ProxmoxVmsVMID_RevertSnapshot) -> dict[str, list | Any]: - - """ reply post-processing - json or ansible raw output """ - - if req.as_json: - - #### - ##### OUTPUT TYPE - as_json=True - ##### - - action = extravars["proxmox_vm_action"] - result = extract_action_results(events, action) - - payload = { - "rc": rc, - "result": result - # "action": action, - } # raw - - # payload = {"rc": rc, "action": action, "result": events} - else: - #### - #### OUTPUT AS TEXT - as_json=False - #### - - # payload = {"rc": rc, "log_plain": log_plain, "log_multiline": log_plain.splitlines()} - payload = {"rc": rc, "log_multiline": log_plain.splitlines()} - return payload - - -def request_checks(req: Request_ProxmoxVmsVMID_RevertSnapshot) -> dict[Any, Any]: - """ request checks """ - - extravars = {} - extravars["proxmox_vm_action"] = "snapshot_vm_revert" - - if req.proxmox_node: - extravars["proxmox_node"] = req.proxmox_node - - if req.vm_id is not None: - extravars["vm_id"] = req.vm_id - - extravars["vm_name"] = resolv_id_to_vm_name(extravars["proxmox_node"], extravars["vm_id"] ) - - if req.vm_snapshot_name is not None: - extravars["vm_snapshot_name"] = req.vm_snapshot_name - - # nothing : - if not extravars: - extravars = None - - extravars["hosts"] = "proxmox" - - if debug == 1: - print(f", extra vars : {extravars}") - return extravars - diff --git a/app/routes/v0/proxmox/vms/vm_id/start.py b/app/routes/v0/proxmox/vms/vm_id/start.py deleted file mode 100644 index 9cef21a..0000000 --- a/app/routes/v0/proxmox/vms/vm_id/start.py +++ /dev/null @@ -1,151 +0,0 @@ -from typing import Any -import logging -from fastapi import APIRouter, HTTPException -from fastapi.responses import JSONResponse - -from app.runner import run_playbook_core # , extract_action_results -from app.extract_actions import extract_action_results - -from app.schemas.proxmox.vm_id.start_stop_resume_pause import Request_ProxmoxVmsVMID_StartStopPauseResume -from app.schemas.proxmox.vm_id.start_stop_resume_pause import Reply_ProxmoxVmsVMID_StartStopPauseResume - -from pathlib import Path -import os - -# -# ISSUE - #3 -# - -debug = 0 - -router = APIRouter() -logger = logging.getLogger(__name__) - -# PROJECT_ROOT = Path(__file__).resolve().parents[6] -PROJECT_ROOT = Path(os.getenv("PROJECT_ROOT_DIR")).resolve() -PLAYBOOK_SRC = PROJECT_ROOT / "playbooks" / "generic.yml" -INVENTORY_SRC = PROJECT_ROOT / "inventory" / "hosts.yml" - - -# @router.post("/{vm_id}/start") -# def proxmox_vms_vm_id_start( -# vm_id: int, -# req: Request_ProxmoxVmsVMID_StartStopPauseResume, -# ): - -# -# => /v0/admin/proxmox/vms/vmd_id/start -# -@router.post( - path="/start", - summary="Start a specific VM", - description="This endpoint start the target virtual machine (VM).", - tags=["proxmox - vm lifecycle"], - # - response_model=Reply_ProxmoxVmsVMID_StartStopPauseResume, - response_description="Start result", -) - -def proxmox_vms_vm_id_start(req: Request_ProxmoxVmsVMID_StartStopPauseResume): - """ This endpoint start the target virtual machine (VM""" - - if debug ==1: - print(":: REQUEST ::", req.model_dump()) - print(f":: PROJECT_ROOT :: {PROJECT_ROOT} ") - print(f":: PLAYBOOK_SRC :: {PLAYBOOK_SRC} ") - print(f":: INVENTORY_SRC :: {INVENTORY_SRC} ") - - if not PLAYBOOK_SRC.exists(): - raise HTTPException(status_code=400, detail=f":: MISSING PLAYBOOK : {PLAYBOOK_SRC}") - if not INVENTORY_SRC.exists(): - raise HTTPException(status_code=400, detail=f":: MISSING INVENTORY : {INVENTORY_SRC}") - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - extravars = request_checks(req) - - #### - - rc, events, log_plain, log_ansi = run_playbook_core( - PLAYBOOK_SRC, - INVENTORY_SRC, - # limit=req.hosts, - limit=extravars["hosts"], - extravars=extravars, - ) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - payload = reply_processing(events, extravars, log_plain, rc, req) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - if rc == 0: - status = 200 - else: - status = 500 - - return JSONResponse(payload, status_code=status) - - -def reply_processing(events: list[dict] | list[Any], - extravars: dict[Any, Any], - log_plain: str, - rc, - req: Request_ProxmoxVmsVMID_StartStopPauseResume) -> dict[str, list | Any]: - - """ reply post-processing - json or ansible raw output """ - - if req.as_json: - - ##### OUTPUT TYPE - as_json=True - ##### - - action = extravars["proxmox_vm_action"] - result = extract_action_results(events, action) - - payload = { - "rc": rc, - "result": result - # "action": action, - } # raw - - # payload = {"rc": rc, "action": action, "result": events} - else: - #### - #### OUTPUT AS TEXT - as_json=False - #### - - # payload = {"rc": rc, "log_plain": log_plain, "log_multiline": log_plain.splitlines()} - payload = {"rc": rc, "log_multiline": log_plain.splitlines()} - return payload - - -def request_checks(req: Request_ProxmoxVmsVMID_StartStopPauseResume) -> dict[Any, Any]: - """ request checks """ - - extravars = {} - extravars["proxmox_vm_action"] = "vm_start" - - if req.vm_id is not None: - extravars["vm_id"] = req.vm_id - - if req.proxmox_node: - extravars["proxmox_node"] = req.proxmox_node - - # nothing : - if not extravars: - extravars = None - - if debug == 1: - print(f", extra vars : {extravars}") - - extravars["hosts"] = "proxmox" - return extravars - diff --git a/app/routes/v0/proxmox/vms/vm_id/stop.py b/app/routes/v0/proxmox/vms/vm_id/stop.py deleted file mode 100644 index 1630338..0000000 --- a/app/routes/v0/proxmox/vms/vm_id/stop.py +++ /dev/null @@ -1,154 +0,0 @@ -from typing import Any -import logging -from fastapi import APIRouter, HTTPException -from fastapi.responses import JSONResponse - -from app.runner import run_playbook_core # , extract_action_results -from app.extract_actions import extract_action_results - -from app.schemas.proxmox.vm_id.start_stop_resume_pause import Request_ProxmoxVmsVMID_StartStopPauseResume -from app.schemas.proxmox.vm_id.start_stop_resume_pause import Reply_ProxmoxVmsVMID_StartStopPauseResume - -from pathlib import Path -import os - -# -# ISSUE - #3 -# - -debug = 0 - -router = APIRouter() -logger = logging.getLogger(__name__) - -# PROJECT_ROOT = Path(__file__).resolve().parents[6] -PROJECT_ROOT = Path(os.getenv("PROJECT_ROOT_DIR")).resolve() -PLAYBOOK_SRC = PROJECT_ROOT / "playbooks" / "generic.yml" -INVENTORY_SRC = PROJECT_ROOT / "inventory" / "hosts.yml" - -# @router.post("/{vm_id}/start") -# def proxmox_vms_vm_id_start( -# vm_id: int, -# req: Request_ProxmoxVmsVMID_StartStopPauseResume, -# ): - -# -# => /v0/admin/proxmox/vms/vmd_id/stop -# -@router.post( - path="/stop", - summary="Stop a specific VM", - description="This endpoint stop the target virtual machine (VM).", - tags=["proxmox - vm lifecycle"], - # - response_model=Reply_ProxmoxVmsVMID_StartStopPauseResume, - response_description="Start result", -) - -def proxmox_vms_vm_id_stop(req: Request_ProxmoxVmsVMID_StartStopPauseResume): - """ This endpoint stop the target virtual machine VM """ - if debug ==1: - print(":: REQUEST ::", req.model_dump()) - print(f":: PROJECT_ROOT :: {PROJECT_ROOT} ") - print(f":: PLAYBOOK_SRC :: {PLAYBOOK_SRC} ") - print(f":: INVENTORY_SRC :: {INVENTORY_SRC} ") - - if not PLAYBOOK_SRC.exists(): - err = f":: err - MISSING PLAYBOOK : {PLAYBOOK_SRC}" - logging.error(err) - raise HTTPException(status_code=400, detail=err) - - if not INVENTORY_SRC.exists(): - err = f":: err - MISSING INVENTORY : {INVENTORY_SRC}" - logging.error(err) - raise HTTPException(status_code=400, detail=err) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - extravars = request_checks(req) - - #### - - rc, events, log_plain, log_ansi = run_playbook_core( - PLAYBOOK_SRC, - INVENTORY_SRC, - # limit=req.hosts, - limit=extravars["hosts"], - extravars=extravars, - ) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - payload = reply_processing(events, extravars, log_plain, rc, req) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - if rc == 0: - status = 200 - else: - status = 500 - - return JSONResponse(payload, status_code=status) - - -def reply_processing(events: list[dict] | list[Any], - extravars: dict[Any, Any], - log_plain: str, - rc, - req: Request_ProxmoxVmsVMID_StartStopPauseResume) -> dict[str, list | Any]: - """ reply post-processing - json or ansible raw output """ - - if req.as_json: - - ##### OUTPUT TYPE - as_json=True - ##### - - action = extravars["proxmox_vm_action"] - result = extract_action_results(events, action) - - payload = { - "rc": rc, - "result": result - # "action": action, - } # raw - - # payload = {"rc": rc, "action": action, "result": events} - else: - #### - #### OUTPUT AS TEXT - as_json=False - #### - - # payload = {"rc": rc, "log_plain": log_plain, "log_multiline": log_plain.splitlines()} - payload = {"rc": rc, "log_multiline": log_plain.splitlines()} - return payload - - -def request_checks(req: Request_ProxmoxVmsVMID_StartStopPauseResume) -> dict[Any, Any]: - """ request checks """ - - extravars = {} - extravars["proxmox_vm_action"] = "vm_stop" - - - if req.vm_id is not None: - extravars["vm_id"] = req.vm_id - - if req.proxmox_node: - extravars["proxmox_node"] = req.proxmox_node - - # nothing : - if not extravars: - extravars = None - - if debug == 1: - print(f", extra vars : {extravars}") - - extravars["hosts"] = "proxmox" - return extravars - diff --git a/app/routes/v0/proxmox/vms/vm_id/stop_force.py b/app/routes/v0/proxmox/vms/vm_id/stop_force.py deleted file mode 100644 index 4a8aec8..0000000 --- a/app/routes/v0/proxmox/vms/vm_id/stop_force.py +++ /dev/null @@ -1,154 +0,0 @@ -from typing import Any -import logging -from fastapi import APIRouter, HTTPException -from fastapi.responses import JSONResponse - -from app.runner import run_playbook_core # , extract_action_results -from app.extract_actions import extract_action_results - -from app.schemas.proxmox.vm_id.start_stop_resume_pause import Request_ProxmoxVmsVMID_StartStopPauseResume -from app.schemas.proxmox.vm_id.start_stop_resume_pause import Reply_ProxmoxVmsVMID_StartStopPauseResume - -from pathlib import Path -import os - -# -# ISSUE - #3 -# - -debug = 0 - -router = APIRouter() -logger = logging.getLogger(__name__) - -# PROJECT_ROOT = Path(__file__).resolve().parents[6] -PROJECT_ROOT = Path(os.getenv("PROJECT_ROOT_DIR")).resolve() -PLAYBOOK_SRC = PROJECT_ROOT / "playbooks" / "generic.yml" -INVENTORY_SRC = PROJECT_ROOT / "inventory" / "hosts.yml" - -# @router.post("/{vm_id}/start") -# def proxmox_vms_vm_id_start( -# vm_id: int, -# req: Request_ProxmoxVmsVMID_StartStopPauseResume, -# ): - -# -# => /v0/admin/proxmox/vms/vmd_id/stop_force -# -@router.post( - path="/stop_force", - summary="Force stop a specific VM", - description="This endpoint force stop the target virtual machine (VM).", - tags=["proxmox - vm lifecycle"], - # - response_model=Reply_ProxmoxVmsVMID_StartStopPauseResume, - response_description="Start result", -) -def proxmox_vms_vm_id_stop_force(req: Request_ProxmoxVmsVMID_StartStopPauseResume): - """ This endpoint force stop the target virtual machine (VM) """ - - if debug ==1: - print(":: REQUEST ::", req.model_dump()) - print(f":: PROJECT_ROOT :: {PROJECT_ROOT} ") - print(f":: PLAYBOOK_SRC :: {PLAYBOOK_SRC} ") - print(f":: INVENTORY_SRC :: {INVENTORY_SRC} ") - - if not PLAYBOOK_SRC.exists(): - err = f":: err - MISSING PLAYBOOK : {PLAYBOOK_SRC}" - logging.error(err) - raise HTTPException(status_code=400, detail=err) - - if not INVENTORY_SRC.exists(): - err = f":: err - MISSING INVENTORY : {INVENTORY_SRC}" - logging.error(err) - raise HTTPException(status_code=400, detail=err) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - extravars = request_checks(req) - - #### - - rc, events, log_plain, log_ansi = run_playbook_core( - PLAYBOOK_SRC, - INVENTORY_SRC, - # limit=req.hosts, - limit=extravars["hosts"], - extravars=extravars, - ) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - payload = reply_processing(events, extravars, log_plain, rc, req) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - if rc == 0: - status = 200 - else: - status = 500 - - return JSONResponse(payload, status_code=status) - - -def reply_processing(events: list[dict] | list[Any], - extravars: dict[Any, Any], - log_plain: str, - rc, - req: Request_ProxmoxVmsVMID_StartStopPauseResume) -> dict[str, list | Any]: - - """ reply post-processing - json or ansible raw output """ - - if req.as_json: - - ##### OUTPUT TYPE - as_json=True - ##### - - action = extravars["proxmox_vm_action"] - result = extract_action_results(events, action) - - payload = { - "rc": rc, - "result": result - # "action": action, - } # raw - - # payload = {"rc": rc, "action": action, "result": events} - else: - #### - #### OUTPUT AS TEXT - as_json=False - #### - - # payload = {"rc": rc, "log_plain": log_plain, "log_multiline": log_plain.splitlines()} - payload = {"rc": rc, "log_multiline": log_plain.splitlines()} - return payload - - -def request_checks(req: Request_ProxmoxVmsVMID_StartStopPauseResume) -> dict[Any, Any]: - """ request checks """ - - extravars = {} - extravars["proxmox_vm_action"] = "vm_stop_force" - - if req.vm_id is not None: - extravars["vm_id"] = req.vm_id - - if req.proxmox_node: - extravars["proxmox_node"] = req.proxmox_node - - # nothing : - if not extravars: - extravars = None - - if debug == 1: - print(f", extra vars : {extravars}") - - extravars["hosts"] = "proxmox" - return extravars - diff --git a/app/routes/v0/proxmox/vms/vm_ids/__init__.py b/app/routes/v0/proxmox/vms/vm_ids/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/routes/v0/proxmox/vms/vm_ids/mass_delete.py b/app/routes/v0/proxmox/vms/vm_ids/mass_delete.py deleted file mode 100644 index 6c13b5e..0000000 --- a/app/routes/v0/proxmox/vms/vm_ids/mass_delete.py +++ /dev/null @@ -1,142 +0,0 @@ -from typing import Any -import logging -from fastapi import APIRouter, HTTPException -from fastapi.responses import JSONResponse - -from app.schemas.proxmox.vm_ids.mass_delete import Request_ProxmoxVmsVmIds_MassDelete -from app.schemas.proxmox.vm_ids.mass_delete import Reply_ProxmoxVmsVmIds_MassDelete -# from app.schemas.proxmox.vm_id.start_stop_resume_pause import Reply_ProxmoxVmsVMID_StartStopPauseResume - -from app.runner import run_playbook_core # , extract_action_results -from app.extract_actions import extract_action_results -from app import utils -from pathlib import Path -import os - - -debug = 1 - -router = APIRouter() -logger = logging.getLogger(__name__) - -# PROJECT_ROOT = Path(__file__).resolve().parents[5] -PROJECT_ROOT = Path(os.getenv("PROJECT_ROOT_DIR")).resolve() -INVENTORY_NAME = "hosts" - -# -# => /v0/admin/proxmox/vms/vmd_ids/delete -# - -@router.delete( - path="/delete", - summary="Mass delete vms ", - description="Delete all specified virtual machines", - tags=["proxmox - vm lifecycle"], - response_model=Reply_ProxmoxVmsVmIds_MassDelete, -) -def proxmox_vms_vm_ids_mass_delete(req: Request_ProxmoxVmsVmIds_MassDelete): - - action_name = "core/proxmox/configure/default/vms/delete-vms-vuln" - # - return proxmox_vms_vm_ids_mass_delete_run(req, action_name) - - ######################################################################################################################## - -def proxmox_vms_vm_ids_mass_delete_run( - req: Request_ProxmoxVmsVmIds_MassDelete, - action_name: str) -> JSONResponse: - - checked_inventory_filepath = utils.resolve_inventory(INVENTORY_NAME) - checked_playbook_filepath = utils.resolve_bundles_playbook(action_name, "public_github") - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - if debug == 1: - print(":: ACTION_NAME", action_name) - print(":: REQUEST ::", req.model_dump()) - print(f":: PROJECT_ROOT :: {PROJECT_ROOT} ") - print(f":: checked_inventory_filepath :: {checked_inventory_filepath} ") - print(f":: checked_playbook_filepath :: {checked_playbook_filepath} ") - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - - extravars = request_checks_mass_delete(req) - - #### - - rc, events, log_plain, log_ansi = run_playbook_core( - checked_playbook_filepath, - checked_inventory_filepath, - limit=req.proxmox_node, - extravars=extravars, - ) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - payload = reply_processing_mass_delete(events, extravars, log_plain, rc, req) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - if rc == 0: - status = 200 - else: - status = 500 - - return JSONResponse(payload, status_code=status) - - - -def reply_processing_mass_delete(events: list[dict] | list[Any], - extravars: dict[Any, Any], - log_plain: str, - rc, - req: Request_ProxmoxVmsVmIds_MassDelete) -> dict[str, list | Any]: - - """ reply post-processing - json or ansible raw output """ - - if req.as_json: ##### OUTPUT TYPE - as_json=True - - - # do not remove this. We want only vm_delete json line from the role (not the vm_stop_force !) - extravars["proxmox_vm_action"] = "vm_delete" - # - - action = extravars["proxmox_vm_action"] - result = extract_action_results(events, action) - - payload = { - "rc": rc, - "result": result - # "action": action, - } # raw - - else: #### OUTPUT AS TEXT - as_json=False - payload = {"rc": rc, "log_multiline": log_plain.splitlines()} - return payload - - -def request_checks_mass_delete(req: Request_ProxmoxVmsVmIds_MassDelete) -> dict[Any, Any]: - - extravars = {} - - # extravars["proxmox_vm_action"] = "vm_delete" # dont do this in this case ! - - if req.proxmox_node: - extravars["PROXMOX_NODE"] = req.proxmox_node - # - - if getattr(req, "vms", None): - extravars["VMS"] = [{"ID": vm.id, "NAME": vm.name} for vm in req.vms] # we need a vm_name to vm_delete - - # nothing : - if not extravars: - extravars = None - - if debug == 1: - print(f", extra vars : {extravars}") - - # extravars["hosts"] = "proxmox" - - return extravars - diff --git a/app/routes/v0/proxmox/vms/vm_ids/mass_start_stop_pause_resume.py b/app/routes/v0/proxmox/vms/vm_ids/mass_start_stop_pause_resume.py deleted file mode 100644 index 024617f..0000000 --- a/app/routes/v0/proxmox/vms/vm_ids/mass_start_stop_pause_resume.py +++ /dev/null @@ -1,221 +0,0 @@ -from typing import Any -import logging -from fastapi import APIRouter, HTTPException -from fastapi.responses import JSONResponse - -from app.schemas.proxmox.vm_ids.mass_start_stop_resume_pause import Request_ProxmoxVmsVmIds_MassStartStopPauseResume -from app.schemas.proxmox.vm_id.start_stop_resume_pause import Reply_ProxmoxVmsVMID_StartStopPauseResume - -from app.runner import run_playbook_core # , extract_action_results -from app.extract_actions import extract_action_results -from app import utils -from pathlib import Path -import os - - -debug = 0 - -router = APIRouter() -logger = logging.getLogger(__name__) - -# PROJECT_ROOT = Path(__file__).resolve().parents[5] -PROJECT_ROOT = Path(os.getenv("PROJECT_ROOT_DIR")).resolve() -INVENTORY_NAME = "hosts" - -# -# => /v0/admin/proxmox/vms/vmd_ids/stop -# - -@router.post( - path="/stop", - summary="Mass stop vms ", - description="Stop all specified virtual machines", - tags=["proxmox - vm lifecycle"], - response_model=Reply_ProxmoxVmsVMID_StartStopPauseResume, -) -def proxmox_vms_vm_ids_mass_stop(req: Request_ProxmoxVmsVmIds_MassStartStopPauseResume): - - action_name = "core/proxmox/configure/default/vms/start-stop-pause-resume-vms-vuln" - # - return proxmox_vms_vm_ids_mass_run(req, action_name, "vm_stop" ) - - -######################################################################################################################## - -@router.post( - path="/stop_force", - summary="Mass force stop vms ", - description="Force stop all specified virtual machines", - tags=["proxmox - vm lifecycle"], - response_model=Reply_ProxmoxVmsVMID_StartStopPauseResume, -) -def proxmox_vms_vm_ids_mass_stop(req: Request_ProxmoxVmsVmIds_MassStartStopPauseResume): - - action_name = "core/proxmox/configure/default/vms/start-stop-pause-resume-vms-vuln" - # - return proxmox_vms_vm_ids_mass_run(req, action_name, "vm_stop_force") - -######################################################################################################################## - -@router.post( - path="/start", - summary="Mass start vms ", - description="Start all specified virtual machines", - tags=["proxmox - vm lifecycle"], - response_model=Reply_ProxmoxVmsVMID_StartStopPauseResume, -) -def proxmox_vms_vm_ids_mass_start(req: Request_ProxmoxVmsVmIds_MassStartStopPauseResume): - - action_name = "core/proxmox/configure/default/vms/start-stop-pause-resume-vms-vuln" - # - # - return proxmox_vms_vm_ids_mass_run(req, action_name, "vm_start") - - -######################################################################################################################## - -@router.post( - path="/pause", - summary="Mass pause vms ", - description="Pause all specified virtual machines", - tags=["proxmox - vm lifecycle"], - response_model=Reply_ProxmoxVmsVMID_StartStopPauseResume, -) -def proxmox_vms_vm_ids_mass_pause(req: Request_ProxmoxVmsVmIds_MassStartStopPauseResume): - - action_name = "core/proxmox/configure/default/vms/start-stop-pause-resume-vms-vuln" - # - - return proxmox_vms_vm_ids_mass_run(req, action_name, "vm_pause") - - -######################################################################################################################## - -@router.post( - path="/resume", - summary="Mass resume vms ", - description="Resume all specified virtual machines", - tags=["proxmox - vm lifecycle"], - response_model=Reply_ProxmoxVmsVMID_StartStopPauseResume, -) -def proxmox_vms_vm_ids_mass_resume(req: Request_ProxmoxVmsVmIds_MassStartStopPauseResume): - - action_name = "core/proxmox/configure/default/vms/start-stop-pause-resume-vms-vuln" - # - # - return proxmox_vms_vm_ids_mass_run(req, action_name, "vm_resume") - -######################################################################################################################## -######################################################################################################################## -######################################################################################################################## - - -def proxmox_vms_vm_ids_mass_run( - req: Request_ProxmoxVmsVmIds_MassStartStopPauseResume, - action_name: str, - proxmox_vm_action: str) -> JSONResponse: - - - - checked_inventory_filepath = utils.resolve_inventory(INVENTORY_NAME) - # checked_playbook_filepath = utils.resolve_actions_playbook(action_name, "public_github") - checked_playbook_filepath = utils.resolve_bundles_playbook(action_name, "public_github") - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - if debug == 1: - print(":: ACTION_NAME", action_name) - print(":: REQUEST ::", req.model_dump()) - print(f":: PROJECT_ROOT :: {PROJECT_ROOT} ") - print(f":: checked_inventory_filepath :: {checked_inventory_filepath} ") - print(f":: checked_playbook_filepath :: {checked_playbook_filepath} ") - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - extravars = request_checks(req, proxmox_vm_action) - - #### - - rc, events, log_plain, log_ansi = run_playbook_core( - checked_playbook_filepath, - checked_inventory_filepath, - limit=req.proxmox_node, - extravars=extravars, - ) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - payload = reply_processing(events, extravars, log_plain, rc, req) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - if rc == 0: - status = 200 - else: - status = 500 - - return JSONResponse(payload, status_code=status) - - - -def reply_processing(events: list[dict] | list[Any], - extravars: dict[Any, Any], - log_plain: str, - rc, - req: Request_ProxmoxVmsVmIds_MassStartStopPauseResume) -> dict[str, list | Any]: - - """ reply post-processing - json or ansible raw output """ - - if req.as_json: - - ##### OUTPUT TYPE - as_json=True - ##### - - action = extravars["PROXMOX_VM_ACTION"] - result = extract_action_results(events, action) - - payload = { - "rc": rc, - "result": result - # "action": action, - } # raw - - # payload = {"rc": rc, "action": action, "result": events} - else: - #### - #### OUTPUT AS TEXT - as_json=False - #### - - # payload = {"rc": rc, "log_plain": log_plain, "log_multiline": log_plain.splitlines()} - payload = {"rc": rc, "log_multiline": log_plain.splitlines()} - return payload - - -def request_checks(req: Request_ProxmoxVmsVmIds_MassStartStopPauseResume, - proxmox_vm_action: str) -> dict[Any, Any]: - - extravars = {} - - extravars["PROXMOX_VM_ACTION"] = proxmox_vm_action # "vm_stop_force" - - if req.proxmox_node: - extravars["PROXMOX_NODE"] = req.proxmox_node - # - if req.vm_ids: - extravars["VM_IDS"] = req.vm_ids - - # nothing : - if not extravars: - extravars = None - - if debug == 1: - print(f", extra vars : {extravars}") - - # extravars["hosts"] = "proxmox" - - return extravars - diff --git a/app/routes/v0/run/__init__.py b/app/routes/v0/run/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/routes/v0/run/bundles/__init__.py b/app/routes/v0/run/bundles/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/routes/v0/run/bundles/actions_run.py b/app/routes/v0/run/bundles/actions_run.py deleted file mode 100644 index 7aa3c9d..0000000 --- a/app/routes/v0/run/bundles/actions_run.py +++ /dev/null @@ -1,112 +0,0 @@ - -from pathlib import Path -import os - -from typing import Any -from fastapi import APIRouter -from fastapi.responses import JSONResponse - -from app.runner import run_playbook_core -from app.schemas.debug.ping import Request_DebugPing - -from app import utils - -# -# ISSUE - #15 -# - -router = APIRouter() - -# PROJECT_ROOT = Path(__file__).resolve().parents[5] -PROJECT_ROOT = Path(os.getenv("PROJECT_ROOT_DIR")).resolve() -INVENTORY_NAME = "hosts" - -# PLAYBOOK_SRC = PROJECT_ROOT / "playbooks" / "ping.yml" -# PROJECT_ROOT = Path(os.getenv("PROJECT_ROOT_DIR")).resolve() -# INVENTORY_NAME = "hosts" - -debug = 1 - -@router.post( - path="/{bundles_name}/run", - summary="Run bundles", - description="Run generic bundles with default (and static) extras_vars ", - tags=["runner"], -) - -def debug_ping(bundles_name: str, req: Request_DebugPing): - - checked_inventory_filepath = utils.resolve_inventory(INVENTORY_NAME) - checked_playbook_filepath = utils.resolve_bundles_playbook(bundles_name, "public_github") - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - if debug ==1: - print(":: bundles_NAME", bundles_name) - print(":: REQUEST ::", req.model_dump()) - print(f":: PROJECT_ROOT :: {PROJECT_ROOT} ") - print(f":: checked_inventory_filepath :: {checked_inventory_filepath} ") - print(f":: checked_playbook_filepath :: {checked_playbook_filepath} ") - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - extravars = request_checks(req) - - #### - - rc, events, log_plain, log_ansi = run_playbook_core( - checked_playbook_filepath, - checked_inventory_filepath, - limit=req.hosts, - extravars=extravars, - ) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - payload = reply_processing(log_plain, rc) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - if rc == 0: - status = 200 - else: - status = 500 - - return JSONResponse(payload, status_code=status) - - -def reply_processing(log_plain: str, - rc - ) -> dict[str, list[str] | Any]: - - """ reply post-processing - for ansible raw output """ - - payload = {"rc": rc, "log_multiline": log_plain.splitlines()} - - return payload - - -def request_checks(req: Request_DebugPing) -> dict[Any, Any]: - """ request checks """ - - extravars = {} - - if req.proxmox_node: - extravars["proxmox_node"] = req.proxmox_node - - # nothing : - if not extravars: - extravars = None - - if debug == 1: - print(f", extra vars : {extravars}") - - - # extravars["hosts"] = "proxmox" - - return extravars - diff --git a/app/routes/v0/run/scenarios/__init__.py b/app/routes/v0/run/scenarios/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/routes/v0/run/scenarios/scenarios_run.py b/app/routes/v0/run/scenarios/scenarios_run.py deleted file mode 100644 index 241f986..0000000 --- a/app/routes/v0/run/scenarios/scenarios_run.py +++ /dev/null @@ -1,106 +0,0 @@ - -from pathlib import Path -import os - -from typing import Any -from fastapi import APIRouter -from fastapi.responses import JSONResponse - -from app.runner import run_playbook_core -from app.schemas.debug.ping import Request_DebugPing - -from app import utils - -# -# ISSUE - #15 -# - -router = APIRouter() - -PROJECT_ROOT = Path(os.getenv("PROJECT_ROOT_DIR")).resolve() -INVENTORY_NAME = "hosts" - -debug = 1 - -@router.post( - path="/{scenario_name}/run", - summary="Run scenario", - description="Run generic scenario with default (and static) extras_vars ", - tags=["runner"], -) - -def debug_ping(scenario_name: str, req: Request_DebugPing): - - checked_inventory_filepath = utils.resolve_inventory(INVENTORY_NAME) - checked_playbook_filepath = utils.resolve_scenarios_playbook(scenario_name, "public_github") - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - if debug ==1: - print(":: SCENARIO_NAME", scenario_name) - print(":: REQUEST ::", req.model_dump()) - print(f":: PROJECT_ROOT :: {PROJECT_ROOT} ") - print(f":: checked_inventory_filepath :: {checked_inventory_filepath} ") - print(f":: checked_playbook_filepath :: {checked_playbook_filepath} ") - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - extravars = request_checks(req) - - #### - - rc, events, log_plain, log_ansi = run_playbook_core( - checked_playbook_filepath, - checked_inventory_filepath, - limit=req.hosts, - extravars=extravars, - ) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - payload = reply_processing(log_plain, rc) - - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - - if rc == 0: - status = 200 - else: - status = 500 - - return JSONResponse(payload, status_code=status) - - -def reply_processing(log_plain: str, - rc - ) -> dict[str, list[str] | Any]: - - """ reply post-processing - for ansible raw output """ - - payload = {"rc": rc, "log_multiline": log_plain.splitlines()} - - return payload - - -def request_checks(req: Request_DebugPing) -> dict[Any, Any]: - """ request checks """ - - extravars = {} - - if req.proxmox_node: - extravars["proxmox_node"] = req.proxmox_node - - # nothing : - if not extravars: - extravars = None - - if debug == 1: - print(f", extra vars : {extravars}") - - - # extravars["hosts"] = "proxmox" - return extravars - diff --git a/app/routes/vm_config.py b/app/routes/vm_config.py index 3fc1a1b..9293b73 100644 --- a/app/routes/vm_config.py +++ b/app/routes/vm_config.py @@ -10,15 +10,17 @@ from fastapi import APIRouter, HTTPException from fastapi.responses import JSONResponse -from app.runner import run_playbook_core -from app.extract_actions import extract_action_results +from app.core.runner import run_playbook_core +from app.core.extractor import extract_action_results from app import utils -from app.schemas.proxmox.vm_id.config.vm_get_config import Request_ProxmoxVmsVMID_VmGetConfig, Reply_ProxmoxVmsVMID_VmGetConfig -from app.schemas.proxmox.vm_id.config.vm_get_config_cdrom import Request_ProxmoxVmsVMID_VmGetConfigCdrom, Reply_ProxmoxVmsVMID_VmGetConfigCdrom -from app.schemas.proxmox.vm_id.config.vm_get_config_cpu import Request_ProxmoxVmsVMID_VmGetConfigCpu, Reply_ProxmoxVmsVMID_VmGetConfigCpu -from app.schemas.proxmox.vm_id.config.vm_get_config_ram import Request_ProxmoxVmsVMID_VmGetConfigRam, Reply_ProxmoxVmsVMID_VmGetConfigRam -from app.schemas.proxmox.vm_id.config.vm_set_tag import Request_ProxmoxVmsVMID_VmSetTag, Reply_ProxmoxVmsVMID_VmSetTag +from app.schemas.vm_config import ( + Request_ProxmoxVmsVMID_VmGetConfig, Reply_ProxmoxVmsVMID_VmGetConfig, + Request_ProxmoxVmsVMID_VmGetConfigCdrom, Reply_ProxmoxVmsVMID_VmGetConfigCdrom, + Request_ProxmoxVmsVMID_VmGetConfigCpu, Reply_ProxmoxVmsVMID_VmGetConfigCpu, + Request_ProxmoxVmsVMID_VmGetConfigRam, Reply_ProxmoxVmsVMID_VmGetConfigRam, + Request_ProxmoxVmsVMID_VmSetTag, Reply_ProxmoxVmsVMID_VmSetTag, +) logger = logging.getLogger(__name__) diff --git a/app/routes/vms.py b/app/routes/vms.py index ba3aceb..f64cb68 100644 --- a/app/routes/vms.py +++ b/app/routes/vms.py @@ -13,22 +13,21 @@ from fastapi import APIRouter, HTTPException from fastapi.responses import JSONResponse -from app.runner import run_playbook_core -from app.extract_actions import extract_action_results +from app.core.runner import run_playbook_core +from app.core.extractor import extract_action_results from app.utils.vm_id_name_resolver import resolv_id_to_vm_name from app import utils -from app.schemas.proxmox.vm_list import Request_ProxmoxVms_VmList, Reply_ProxmoxVmList -from app.schemas.proxmox.vm_list_usage import Request_ProxmoxVms_VmListUsage, Reply_ProxmoxVms_VmListUsage -from app.schemas.proxmox.vm_id.start_stop_resume_pause import ( - Request_ProxmoxVmsVMID_StartStopPauseResume, - Reply_ProxmoxVmsVMID_StartStopPauseResume, +from app.schemas.vms import ( + Request_ProxmoxVms_VmList, Reply_ProxmoxVmList, + Request_ProxmoxVms_VmListUsage, Reply_ProxmoxVms_VmListUsage, + Request_ProxmoxVmsVMID_StartStopPauseResume, Reply_ProxmoxVmsVMID_StartStopPauseResume, + Request_ProxmoxVmsVMID_Create, Reply_ProxmoxVmsVMID_Create, + Request_ProxmoxVmsVMID_Delete, Reply_ProxmoxVmsVMID_Delete, + Request_ProxmoxVmsVMID_Clone, Reply_ProxmoxVmsVMID_Clone, + Request_ProxmoxVmsVmIds_MassStartStopPauseResume, + Request_ProxmoxVmsVmIds_MassDelete, Reply_ProxmoxVmsVmIds_MassDelete, ) -from app.schemas.proxmox.vm_id.create import Request_ProxmoxVmsVMID_Create, Reply_ProxmoxVmsVMID_Create -from app.schemas.proxmox.vm_id.delete import Request_ProxmoxVmsVMID_Delete, Reply_ProxmoxVmsVMID_Delete -from app.schemas.proxmox.vm_id.clone import Request_ProxmoxVmsVMID_Clone, Reply_ProxmoxVmsVMID_Clone -from app.schemas.proxmox.vm_ids.mass_start_stop_resume_pause import Request_ProxmoxVmsVmIds_MassStartStopPauseResume -from app.schemas.proxmox.vm_ids.mass_delete import Request_ProxmoxVmsVmIds_MassDelete, Reply_ProxmoxVmsVmIds_MassDelete logger = logging.getLogger(__name__) diff --git a/app/runner.py b/app/runner.py deleted file mode 100644 index 4b1a59d..0000000 --- a/app/runner.py +++ /dev/null @@ -1,246 +0,0 @@ - -import os, shutil, tempfile -from pathlib import Path -from fastapi.responses import JSONResponse -from ansible_runner import run - -from . import utils -from . import vault - - -def print_env_vars(envvars: dict): - print(f"ANSIBLE_ROLES_PATH : {envvars.get('ANSIBLE_ROLES_PATH', '')}") - print(f"ANSIBLE_COLLECTIONS_PATH : {envvars.get('ANSIBLE_COLLECTIONS_PATH','')}") - print(f"ANSIBLE_COLLECTIONS_PATHS : {envvars.get('ANSIBLE_COLLECTIONS_PATHS','')}") - print(f"ANSIBLE_LIBRARY : {envvars.get('ANSIBLE_LIBRARY', '')}") - print(f"ANSIBLE_FILTER_PLUGINS : {envvars.get('ANSIBLE_FILTER_PLUGINS', '')}") - print(f"ANSIBLE_HOST_KEY_CHECKING : {envvars.get('ANSIBLE_HOST_KEY_CHECKING', 'False')}") - print(f"ANSIBLE_DEPRECATION_WARNINGS : {envvars.get('ANSIBLE_DEPRECATION_WARNINGS', 'False')}") - print(f"PYTHONWARNINGS : {envvars.get('PYTHONWARNINGS', 'ignore::DeprecationWarning')}") - print(f"ANSIBLE_INVENTORY_ENABLED : {envvars.get('ANSIBLE_INVENTORY_ENABLED', 'yaml,ini')}") - - -def set_env_vars(tmp_dir: Path): - """ write env file in tmp_dir """ - # envvars = {} - # - # if os.getenv("ANSIBLE_ROLES_PATH"): envvars["ANSIBLE_ROLES_PATH"] = os.environ["ANSIBLE_ROLES_PATH"] - # if os.getenv("ANSIBLE_COLLECTIONS_PATHS"): envvars["ANSIBLE_COLLECTIONS_PATHS"] = os.environ["ANSIBLE_COLLECTIONS_PATHS"] - # if os.getenv("ANSIBLE_LIBRARY"): envvars["ANSIBLE_LIBRARY"] = os.environ["ANSIBLE_LIBRARY"] - # if os.getenv("ANSIBLE_FILTER_PLUGINS"): envvars["ANSIBLE_FILTER_PLUGINS"] = os.environ["ANSIBLE_FILTER_PLUGINS"] - # if os.getenv("ANSIBLE_HOST_KEY_CHECKING"): envvars["ANSIBLE_HOST_KEY_CHECKING"] = os.environ["ANSIBLE_HOST_KEY_CHECKING"] - # if os.getenv("ANSIBLE_DEPRECATION_WARNINGS"): envvars["ANSIBLE_DEPRECATION_WARNINGS"] = os.environ["ANSIBLE_DEPRECATION_WARNINGS"] - # if os.getenv("ANSIBLE_INVENTORY_ENABLED"): envvars["ANSIBLE_INVENTORY_ENABLED"] = os.environ["ANSIBLE_INVENTORY_ENABLED"] - # if os.getenv("PYTHONWARNINGS"): envvars["PYTHONWARNINGS"] = os.environ["PYTHONWARNINGS"] - - # maybe useful later. - # - # if os.getenv("ANSIBLE_CONFIG"): envvars["ANSIBLE_CONFIG"] = os.environ["ANSIBLE_CONFIG"] - # if os.getenv("ANSIBLE_CALLBACK_PLUGINS"): envvars["ANSIBLE_CALLBACK_PLUGINS"] = os.environ["ANSIBLE_CALLBACK_PLUGINS"] - # #if os.getenv("ANSIBLE_STDOUT_CALLBACK"): envvars["ANSIBLE_STDOUT_CALLBACK"] = os.environ["ANSIBLE_STDOUT_CALLBACK"] - # - - home_collections = os.path.expanduser("~/.ansible/collections") - sys_collections = "/usr/share/ansible/collections" - coll_paths = f"{home_collections}:{sys_collections}" - - envvars = { - "ANSIBLE_HOST_KEY_CHECKING": "True", - "ANSIBLE_DEPRECATION_WARNINGS": "False", - "ANSIBLE_INVENTORY_ENABLED": "yaml,ini", - "PYTHONWARNINGS": "ignore::DeprecationWarning", - "ANSIBLE_ROLES_PATH": os.environ.get("ANSIBLE_ROLES_PATH", ""), - # "ANSIBLE_COLLECTIONS_PATHS": os.environ.get("ANSIBLE_COLLECTIONS_PATHS", "/home/grml/_products.git-hyde-repo/range42-backend-api/collections/"), - # "ANSIBLE_LIBRARY": os.environ.get("ANSIBLE_LIBRARY", ""), - "ANSIBLE_FILTER_PLUGINS": os.environ.get("ANSIBLE_FILTER_PLUGINS", ""), - - # ⬇️ corrections - "ANSIBLE_COLLECTIONS_PATH": os.environ.get("ANSIBLE_COLLECTIONS_PATH", coll_paths), - "ANSIBLE_COLLECTIONS_PATHS": os.environ.get("ANSIBLE_COLLECTIONS_PATHS", coll_paths), - # ⬆️ corrections - "ANSIBLE_LIBRARY": os.environ.get("ANSIBLE_LIBRARY", ""), - } - - # VAULT ENV VARS - SET PRIORITY ENV VAR FIRST - if os.getenv("VAULT_PASSWORD_FILE"): - envvars["ANSIBLE_VAULT_PASSWORD_FILE"] = os.environ["VAULT_PASSWORD_FILE"] - - elif vault.get_vault_path(): - envvars["ANSIBLE_VAULT_PASSWORD_FILE"] = str(vault.get_vault_path()) - - - if os.getenv("ANSIBLE_CONFIG"): - envvars["ANSIBLE_CONFIG"] = os.environ["ANSIBLE_CONFIG"] - - - # WRITE ENV IN tmp_dir/env/envvars - env_dir = tmp_dir / "env" - env_dir.mkdir(parents=True, exist_ok=True) - env_file = env_dir / "envvars" - env_file.write_text( - "\n".join(f"{k}={v}" for k, v in envvars.items()) + "\n" - ) - - print_env_vars(envvars) - - -def create_temp_dir(inventory: Path, playbook: Path, tmp_dir: Path) -> tuple[Path, Path]: - """ - copy playbook files ___TREE___ to /tmp_dir - return : inventory_path_in_tmp, playbook_path - """ - project_dir = tmp_dir / "project" - inventory_dir = tmp_dir / "inventory" - project_dir.mkdir(parents=True, exist_ok=True) - inventory_dir.mkdir(parents=True, exist_ok=True) - - src_dir = playbook.parent - dst_dir = project_dir / src_dir.name - shutil.copytree(src_dir, dst_dir, dirs_exist_ok=True) - - play_rel = (dst_dir / playbook.name).relative_to(project_dir) - inv_dest = inventory_dir / inventory.name - shutil.copy(inventory, inv_dest) - - set_env_vars(tmp_dir) - - return inv_dest, play_rel - -def build_logs(events) -> tuple[str, str]: - """ build ansible plain logs with/without ansi""" - - # lines = [ - # ev.get("stdout") - # for ev in events if ev.get("stdout") - # ] - - lines = [] - for ev in events: - stdout = ev.get("stdout") - if stdout: - lines.append(stdout) - - text_ansi = "\n".join(lines).strip() - return text_ansi, utils.strip_ansi(text_ansi) - - -#### -#### -#### - -def run_playbook_core( - playbook: Path, inventory: Path, - limit: str | None = None, - tags: str | None = None, - cmdline: str | None = None, - extravars: dict | None = None, - quiet: bool = False, -): - - tmp_dir = Path(tempfile.mkdtemp(prefix="runner-")) - try: - inv_dest, play_rel = create_temp_dir(inventory, playbook, tmp_dir) - - # Auto vault - # if not cmdline: - # vf = os.getenv("VAULT_PASSWORD_FILE") or (str(vault.get_vault_path()) if vault.get_vault_path() else None) - # if vf: - # cmdline = f'--vault-password-file "{vf}"' - - if not cmdline: - vf = os.getenv("VAULT_PASSWORD_FILE") - if not vf: - vp = vault.get_vault_path() - vf = str(vp) if vp else None - - - if vf: - cmdline = f'--vault-password-file "{vf}"' - - vars_file = os.getenv("API_BACKEND_VAULT_FILE") # - if vars_file: - - # will add the vault file as arg like : ansible-playbook ping.yml -e @/etc/ansible/my_vars.yml - # note : following the ansible doc => no quote with @file - cmdline = f'{(cmdline or "").strip()} -e "@{vars_file}"'.strip() - - if tags: - cmdline = f'{cmdline} --tags {tags}'.strip() - - - # print (cmdline) - - # print (" ---------------------------------------------") - r = run( - private_data_dir=str(tmp_dir), - playbook=str(play_rel), - inventory=str(inv_dest), - streamer="json", - limit=limit, - cmdline=cmdline, - extravars=extravars or {}, - quiet=quiet, - ) - # print (" ---------------------------------------------") - - # events = list(r.events) if hasattr(r, "events") else [] - - if hasattr(r, "events"): - events = list(r.events) - else: - events = [] - - - log_ansi, log_plain = build_logs(events) - return r.rc, events, log_plain, log_ansi - finally: - print (f" :: work done :: {tmp_dir}") - # shutil.rmtree(tmp_dir, ignore_errors=True) - -def run_playbook( - playbook: Path, inventory: Path, - limit: str | None = None, - cmdline: str | None = None, - extravars: dict | None = None, - as_json: bool = False, -) -> JSONResponse: - - rc, events, log_plain, log_ansi = run_playbook_core(playbook, inventory, limit, cmdline, extravars) - # payload = {"rc": rc, "events": events} if as_json else {"rc": rc, "log_plain": log_plain, "log_ansi": log_ansi} - - # if as_json: - # payload = { - # "rc": rc, - # "events": events, - # } - # else: - # payload = { - # "rc": rc, - # "log_plain": log_plain, - # "log_ansi": log_ansi, - # } - - if as_json: - payload = { - "rc": rc, - "events": events, # brut - # "result": extract_action_results(events, "vm_list"), - } - else: - payload = { - "rc": rc, - "log_plain": log_plain, - "log_ansi": log_ansi, - } - - #### STATUS - - if rc == 0: - status = 200 - else: - status = 500 - - return JSONResponse(payload, status_code=status) - return JSONResponse(payload, status_code=200 if rc == 0 else 500) - return JSONResponse(payload, status_code=200 if rc == 0 else 500) - diff --git a/app/schemas/bundles/core/linux/ubuntu/configure/add_user.py b/app/schemas/bundles/core/linux/ubuntu/configure/add_user.py deleted file mode 100644 index e47a673..0000000 --- a/app/schemas/bundles/core/linux/ubuntu/configure/add_user.py +++ /dev/null @@ -1,113 +0,0 @@ - -from typing import Literal -from pydantic import BaseModel, Field - -# -# ISSUE - #19 -# - -class Request_BundlesCoreLinuxUbuntuConfigure_AddUser(BaseModel): - - proxmox_node: str = Field( - ..., - # default= "px-testing", - description = "Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" - ) - - hosts: str = Field( - ..., - description= "Hosts or groups", - pattern = r"^[a-zA-Z0-9._:-]+$" - ) - - # as_json: bool = Field( - # default=True, - # description="If true : JSON output else : raw output" - # ) - # - - #### - - user: str = Field( - ..., - description = "New user", - pattern = r"^[a-z_][a-z0-9_-]*$", - ) - - - password: str = Field( - ..., - description = "New password", - pattern = r"^[A-Za-z0-9@._-]*$" # dangerous chars removed. - ) - - - change_pwd_at_logon : bool = Field( - ..., - description = "Force user to change password on first login" - ) - - shell_path: str = Field( - ..., - description = "Default user shell ", - pattern = r"^/[a-z/]*$" - ) - - model_config = { - "json_schema_extra": { - "example": { - "proxmox_node": "px-testing", - "hosts": "r42.vuln-box-00", - # "as_json": True, - # - "user": "elliot", - "password": "r0b0t_aLd3rs0n", - "change_pwd_at_logon": False, - "shell_path": "/bin/sh", - } - } - } - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### -#### #### #### #### #### #### #### #### #### #### #### #### #### #### - -class Reply_BundlesCoreLinuxUbuntuConfigure_AddUserItem(BaseModel): - - # action: Literal["vm_get_config"] - # source: Literal["proxmox"] - proxmox_node: str - # vm_id: int = Field(..., ge=1) - # vm_name: str - raw_data: str = Field(..., description="Raw string returned by proxmox") - - -class Reply_BundlesCoreLinuxUbuntuConfigure_AddUser(BaseModel): - - rc: int = Field(0, description="RETURN code (0 = OK)") - result: list[Reply_BundlesCoreLinuxUbuntuConfigure_AddUserItem] - - model_config = { - "json_schema_extra": { - "example": { - "rc": 0, - "result": [ - { - # "action": "vm_get_config", - # "proxmox_node": "px-testing", - # "raw_data": { - # "data": { - # ... - # } - # }, - # "source": "proxmox", - # "vm_id": "1000" - } - ] - } - } - } - - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### -#### #### #### #### #### #### #### #### #### #### #### #### #### #### diff --git a/app/schemas/bundles/core/linux/ubuntu/install/basic_packages.py b/app/schemas/bundles/core/linux/ubuntu/install/basic_packages.py deleted file mode 100644 index 6e1fb79..0000000 --- a/app/schemas/bundles/core/linux/ubuntu/install/basic_packages.py +++ /dev/null @@ -1,131 +0,0 @@ - -from typing import Literal -from pydantic import BaseModel, Field - -# -# ISSUE - #20 -# - -class Request_BundlesCoreLinuxUbuntuInstall_BasicPackages(BaseModel): - - proxmox_node: str = Field( - ..., - # default= "px-testing", - description = "Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" - ) - - hosts: str = Field( - ..., - description= "Hosts or groups", - pattern = r"^[a-zA-Z0-9._:-]+$" - ) - - # as_json: bool = Field( - # default=True, - # description="If true : JSON output else : raw output" - # ) - # - - #### - - install_package_basics : bool = Field( - ..., - description="", - ) - - install_package_firewalls : bool = Field( - ..., - description="", - ) - - install_package_docker : bool = Field( - ..., - description="", - ) - - install_package_docker_compose: bool = Field( - ..., - description="", - ) - - install_package_utils_json : bool = Field( - ..., - description="", - ) - - install_package_utils_network : bool = Field( - ..., - description="", - ) - - #### - - install_ntpclient_and_update_time: bool = Field( - ..., - description="", - ) - - packages_cleaning: bool = Field( - ..., - description="", - ) - - model_config = { - "json_schema_extra": { - "example": { - "proxmox_node": "px-testing", - "hosts": "r42.vuln-box-00", - # "as_json": True, - # - "install_package_basics" : True, - "install_package_firewalls" : False, - "install_package_docker" : False, - "install_package_docker_compose" : False, - "install_package_utils_json" : False, - "install_package_utils_network" : False, - "install_ntpclient_and_update_time" : True, - "packages_cleaning" : True, - - } - } - } - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### -#### #### #### #### #### #### #### #### #### #### #### #### #### #### - -class Reply_BundlesCoreLinuxUbuntuInstall_BasicPackagesItem(BaseModel): - - # action: Literal["vm_get_config"] - # source: Literal["proxmox"] - proxmox_node: str - # vm_id: int = Field(..., ge=1) - # vm_name: str - raw_data: str = Field(..., description="Raw string returned by proxmox") - - -class Reply_BundlesCoreLinuxUbuntuInstall_BasicPackages(BaseModel): - - rc: int = Field(0, description="RETURN code (0 = OK)") - result: list[Reply_BundlesCoreLinuxUbuntuInstall_BasicPackagesItem] - - model_config = { - "json_schema_extra": { - "example": { - "rc": 0, - "result": [ - { - # "action": "vm_get_config", - # "proxmox_node": "px-testing", - # ... - # "source": "proxmox", - # "vm_id": "1000" - } - ] - } - } - } - - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### -#### #### #### #### #### #### #### #### #### #### #### #### #### #### diff --git a/app/schemas/bundles/core/linux/ubuntu/install/docker.py b/app/schemas/bundles/core/linux/ubuntu/install/docker.py deleted file mode 100644 index f27acde..0000000 --- a/app/schemas/bundles/core/linux/ubuntu/install/docker.py +++ /dev/null @@ -1,131 +0,0 @@ - -from typing import Literal -from pydantic import BaseModel, Field - -# -# ISSUE - #21 -# - -class Request_BundlesCoreLinuxUbuntuInstall_Docker(BaseModel): - - proxmox_node: str = Field( - ..., - # default= "px-testing", - description = "Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" - ) - - hosts: str = Field( - ..., - description= "Hosts or groups", - pattern = r"^[a-zA-Z0-9._:-]+$" - ) - - # as_json: bool = Field( - # default=True, - # description="If true : JSON output else : raw output" - # ) - # - - #### - # - # install_package_basics : bool = Field( - # ..., - # description="", - # ) - # - # install_package_firewalls : bool = Field( - # ..., - # description="", - # ) - - install_package_docker : bool = Field( - ..., - description="", - ) - - # install_package_docker_compose: bool = Field( - # ..., - # description="", - # ) - # - # install_package_utils_json : bool = Field( - # ..., - # description="", - # ) - # - # install_package_utils_network : bool = Field( - # ..., - # description="", - # ) - - #### - - install_ntpclient_and_update_time: bool = Field( - ..., - description="", - ) - - packages_cleaning: bool = Field( - ..., - description="", - ) - - model_config = { - "json_schema_extra": { - "example": { - "proxmox_node": "px-testing", - "hosts": "r42.vuln-box-00", - # "as_json": True, - # - # "install_package_basics" : True, - # "install_package_firewalls" : False, - "install_package_docker" : True, - # "install_package_docker_compose" : False, - # "install_package_utils_json" : False, - # "install_package_utils_network" : False, - "install_ntpclient_and_update_time" : True, - "packages_cleaning" : True, - - } - } - } - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### -#### #### #### #### #### #### #### #### #### #### #### #### #### #### - -class Reply_BundlesCoreLinuxUbuntuInstall_DockerItem(BaseModel): - - # action: Literal["vm_get_config"] - # source: Literal["proxmox"] - proxmox_node: str - # vm_id: int = Field(..., ge=1) - # vm_name: str - raw_data: str = Field(..., description="Raw string returned by proxmox") - - -class Reply_BundlesCoreLinuxUbuntuInstall_Docker(BaseModel): - - rc: int = Field(0, description="RETURN code (0 = OK)") - result: list[Reply_BundlesCoreLinuxUbuntuInstall_DockerItem] - - model_config = { - "json_schema_extra": { - "example": { - "rc": 0, - "result": [ - { - # "action": "vm_get_config", - # "proxmox_node": "px-testing", - # ... - # "source": "proxmox", - # "vm_id": "1000" - } - ] - } - } - } - - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### -#### #### #### #### #### #### #### #### #### #### #### #### #### #### diff --git a/app/schemas/bundles/core/linux/ubuntu/install/docker_compose.py b/app/schemas/bundles/core/linux/ubuntu/install/docker_compose.py deleted file mode 100644 index 928d01c..0000000 --- a/app/schemas/bundles/core/linux/ubuntu/install/docker_compose.py +++ /dev/null @@ -1,132 +0,0 @@ - -from typing import Literal -from pydantic import BaseModel, Field - -# -# ISSUE - #22 -# - -class Request_BundlesCoreLinuxUbuntuInstall_DockerCompose(BaseModel): - - proxmox_node: str = Field( - ..., - # default= "px-testing", - description = "Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" - ) - - hosts: str = Field( - ..., - description= "Hosts or groups", - pattern = r"^[a-zA-Z0-9._:-]+$" - - ) - - # as_json: bool = Field( - # default=True, - # description="If true : JSON output else : raw output" - # ) - # - - #### - - # install_package_basics : bool = Field( - # ..., - # description="", - # ) - # - # install_package_firewalls : bool = Field( - # ..., - # description="", - # ) - - install_package_docker : bool = Field( - ..., - description="", - ) - - install_package_docker_compose: bool = Field( - ..., - description="", - ) - - # install_package_utils_json : bool = Field( - # ..., - # description="", - # ) - # - # install_package_utils_network : bool = Field( - # ..., - # description="", - # ) - - #### - - install_ntpclient_and_update_time: bool = Field( - ..., - description="", - ) - - packages_cleaning: bool = Field( - ..., - description="", - ) - - model_config = { - "json_schema_extra": { - "example": { - "proxmox_node": "px-testing", - "hosts": "r42.vuln-box-00", - # "as_json": True, - # - # "install_package_basics" : True, - # "install_package_firewalls" : False, - "install_package_docker" : True, - "install_package_docker_compose" : True, - # "install_package_utils_json" : False, - # "install_package_utils_network" : False, - "install_ntpclient_and_update_time" : True, - "packages_cleaning" : True, - - } - } - } - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### -#### #### #### #### #### #### #### #### #### #### #### #### #### #### - -class Reply_BundlesCoreLinuxUbuntuInstall_DockerComposeItem(BaseModel): - - # action: Literal["vm_get_config"] - # source: Literal["proxmox"] - proxmox_node: str - # vm_id: int = Field(..., ge=1) - # vm_name: str - raw_data: str = Field(..., description="Raw string returned by proxmox") - - -class Reply_BundlesCoreLinuxUbuntuInstall_DockerCompose(BaseModel): - - rc: int = Field(0, description="RETURN code (0 = OK)") - result: list[Reply_BundlesCoreLinuxUbuntuInstall_DockerComposeItem] - - model_config = { - "json_schema_extra": { - "example": { - "rc": 0, - "result": [ - { - # "action": "vm_get_config", - # "proxmox_node": "px-testing", - # ... - # "source": "proxmox", - # "vm_id": "1000" - } - ] - } - } - } - - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### -#### #### #### #### #### #### #### #### #### #### #### #### #### #### diff --git a/app/schemas/bundles/core/linux/ubuntu/install/dot_files.py b/app/schemas/bundles/core/linux/ubuntu/install/dot_files.py deleted file mode 100644 index c42385e..0000000 --- a/app/schemas/bundles/core/linux/ubuntu/install/dot_files.py +++ /dev/null @@ -1,105 +0,0 @@ - -from typing import Literal -from pydantic import BaseModel, Field - -# -# ISSUE - #28 -# - -class Request_BundlesCoreLinuxUbuntuInstall_DotFiles(BaseModel): - - proxmox_node: str = Field( - ..., - # default= "px-testing", - description = "Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" - ) - - hosts: str = Field( - ..., - description= "Hosts or groups", - pattern = r"^[a-zA-Z0-9._:-]+$" - ) - - # as_json: bool = Field( - # default=True, - # description="If true : JSON output else : raw output" - # ) - # - - #### - - user: str = Field( - ..., - description="targeted username", - - ) - - install_vim_dot_files: bool = Field( - ..., - description= "Install vim dot file in user directory" - ) - - install_zsh_dot_files: bool = Field( - ..., - description= "Install zsh dot file in user directory" - ) - - apply_for_root: bool = Field( - ..., - description= "Install dot files in /root" - ) - - model_config = { - "json_schema_extra": { - "example": { - "proxmox_node": "px-testing", - "hosts": "r42.vuln-box-00", - # "as_json": True, - # - "user": "jane", - "install_vim_dot_files": True, - "install_zsh_dot_files": True, - "apply_for_root": False, - } - } - } - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### -#### #### #### #### #### #### #### #### #### #### #### #### #### #### - -class Reply_BundlesCoreLinuxUbuntuInstall_DotFilesItem(BaseModel): - - # action: Literal["vm_get_config"] - # source: Literal["proxmox"] - proxmox_node: str - # vm_id: int = Field(..., ge=1) - # vm_name: str - raw_data: str = Field(..., description="Raw string returned by proxmox") - - -class Reply_BundlesCoreLinuxUbuntuInstall_DotFilesItem(BaseModel): - - rc: int = Field(0, description="RETURN code (0 = OK)") - result: list[Reply_BundlesCoreLinuxUbuntuInstall_DotFilesItem] - - model_config = { - "json_schema_extra": { - "example": { - "rc": 0, - "result": [ - { - # "action": "vm_get_config", - # "proxmox_node": "px-testing", - # ... - # "source": "proxmox", - # "vm_id": "1000" - } - ] - } - } - } - - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### -#### #### #### #### #### #### #### #### #### #### #### #### #### #### diff --git a/app/schemas/bundles/core/proxmox/configure/default/vms/create_vms_admin_default.py b/app/schemas/bundles/core/proxmox/configure/default/vms/create_vms_admin_default.py deleted file mode 100644 index 14bc55e..0000000 --- a/app/schemas/bundles/core/proxmox/configure/default/vms/create_vms_admin_default.py +++ /dev/null @@ -1,140 +0,0 @@ - -# -# ISSUE - #30 -# - -from typing import Dict, Optional -from pydantic import BaseModel, Field - - -class Request_BundlesCoreProxmoxConfigureDefaultVms_CreateAdminVmsItem(BaseModel): - - vm_id: int = Field( - ..., - ge=1, - description="Virtual machine id", - ) - - # vm_id: str = Field( - # ..., - # # default="4000", - # description="Virtual machine id", - # pattern=r"^[0-9]+$" - # ) - - vm_ip: str = Field( - ..., - description="vm ipv4", - pattern=r"^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$" - ) - - vm_description: str = Field( - ..., - strip_whitespace=True, - max_length=200, - # pattern=VM_DESCRIPTION_RE, - description="Description" - ) - -class Request_BundlesCoreProxmoxConfigureDefaultVms_CreateAdminVms(BaseModel): - - proxmox_node: str = Field( - ..., - # default= "px-testing", - description = "Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" - ) - - vms: Dict[str, Request_BundlesCoreProxmoxConfigureDefaultVms_CreateAdminVmsItem] = Field( - ..., - description="Map - vm override vm_id vm_ip vm_description, ... " - ) - - model_config = { - "json_schema_extra": { - "example": { - "proxmox_node": "px-testing", - "vms": { - "admin-wazuh": { - "vm_id": 1000, - "vm_description": "Wazuh - dashboard", - "vm_ip": "192.168.42.100", - }, - "admin-web-api-kong": { - "vm_id": 1020, - "vm_description": "API gateway", - "vm_ip": "192.168.42.120", - }, - - "admin-web-builder-api": { - "vm_id": 1021, - "vm_description": "www - backend API ", - "vm_ip": "192.168.42.121", - }, - "admin-web-emp": { - "vm_id": 1022, - "vm_description": "www - front end - r42 - EMP ", - "vm_ip": "192.168.42.122", - }, - "admin-web-deployer-ui": { - "vm_id": 1023, - "vm_description": "www - front end - r42 - deployer -ui ", - "vm_ip": "192.168.42.123", - } - # "admin-builder-docker-registry": { - # "vm_id": 1001, - # "vm_description": "docker registry", - # "vm_ip": "192.168.42.101", - # }, - # - # "admin-builder-api-devkit": { - # "vm_id": 1002, - # "vm_description": "devkit tooling", - # "vm_ip": "192.168.42.102", - # }, - } - } - } - } - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### -#### #### #### #### #### #### #### #### #### #### #### #### #### #### - -class Reply_BundlesCoreProxmoxConfigureDefaultVms_CreateAdminVmsItem(BaseModel): - - # action: Literal["vm_get_config"] - # source: Literal["proxmox"] - proxmox_node: str - # vm_id: int = Field(..., ge=1) - # vm_name: str - raw_data: str = Field(..., description="Raw string returned by proxmox") - - -class Reply_BundlesCoreProxmoxConfigureDefaultVms_CreateAdminVms(BaseModel): - - rc: int = Field(0, description="RETURN code (0 = OK)") - result: list[Reply_BundlesCoreProxmoxConfigureDefaultVms_CreateAdminVmsItem] - - model_config = { - "json_schema_extra": { - "example": { - "rc": 0, - "result": [ - { - # "action": "vm_get_config", - # "proxmox_node": "px-testing", - # "raw_data": { - # "data": { - # ... - # } - # }, - # "source": "proxmox", - # "vm_id": "1000" - } - ] - } - } - } - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### -#### #### #### #### #### #### #### #### #### #### #### #### #### #### diff --git a/app/schemas/bundles/core/proxmox/configure/default/vms/create_vms_student_default.py b/app/schemas/bundles/core/proxmox/configure/default/vms/create_vms_student_default.py deleted file mode 100644 index 377265a..0000000 --- a/app/schemas/bundles/core/proxmox/configure/default/vms/create_vms_student_default.py +++ /dev/null @@ -1,109 +0,0 @@ - -# -# ISSUE - #30 -# - -from typing import Dict, Optional -from pydantic import BaseModel, Field - - -class Request_BundlesCoreProxmoxConfigureDefaultVms_CreateStudentVmsItem(BaseModel): - - vm_id: int = Field( - ..., - ge=1, - description="Virtual machine id", - ) - - # vm_id: str = Field( - # ..., - # # default="4000", - # description="Virtual machine id", - # pattern=r"^[0-9]+$" - # ) - - vm_ip: str = Field( - ..., - description="vm ipv4", - pattern=r"^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$" - ) - - vm_description: str = Field( - ..., - strip_whitespace=True, - max_length=200, - # pattern=VM_DESCRIPTION_RE, - description="Description" - ) - -class Request_BundlesCoreProxmoxConfigureDefaultVms_CreateStudentVms(BaseModel): - - proxmox_node: str = Field( - ..., - # default= "px-testing", - description = "Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" - ) - - vms: Dict[str, Request_BundlesCoreProxmoxConfigureDefaultVms_CreateStudentVmsItem] = Field( - ..., - description="Map - vm override vm_id vm_ip vm_description, ... " - ) - - - model_config = { - "json_schema_extra": { - "example": { - "proxmox_node": "px-testing", - "vms": { - "student-box-01": { - "vm_id": 3001, - "vm_description": "student R42 student vm", - "vm_ip": "192.168.42.160" , - } - } - } - } - } - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### -#### #### #### #### #### #### #### #### #### #### #### #### #### #### - -class Reply_BundlesCoreProxmoxConfigureDefaultVms_CreateStudentVmsItem(BaseModel): - - # action: Literal["vm_get_config"] - # source: Literal["proxmox"] - proxmox_node: str - # vm_id: int = Field(..., ge=1) - # vm_name: str - raw_data: str = Field(..., description="Raw string returned by proxmox") - - -class Reply_BundlesCoreProxmoxConfigureDefaultVms_CreateStudentVms(BaseModel): - - rc: int = Field(0, description="RETURN code (0 = OK)") - result: list[Reply_BundlesCoreProxmoxConfigureDefaultVms_CreateStudentVmsItem] - - model_config = { - "json_schema_extra": { - "example": { - "rc": 0, - "result": [ - { - # "action": "vm_get_config", - # "proxmox_node": "px-testing", - # "raw_data": { - # "data": { - # ... - # } - # }, - # "source": "proxmox", - # "vm_id": "1000" - } - ] - } - } - } - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### -#### #### #### #### #### #### #### #### #### #### #### #### #### #### diff --git a/app/schemas/bundles/core/proxmox/configure/default/vms/create_vms_vuln_default.py b/app/schemas/bundles/core/proxmox/configure/default/vms/create_vms_vuln_default.py deleted file mode 100644 index d7e3483..0000000 --- a/app/schemas/bundles/core/proxmox/configure/default/vms/create_vms_vuln_default.py +++ /dev/null @@ -1,128 +0,0 @@ - -# -# ISSUE - #30 -# - -from typing import Dict, Optional -from pydantic import BaseModel, Field - - -class Request_BundlesCoreProxmoxConfigureDefaultVms_CreateVulnVmsItem(BaseModel): - - vm_id: int = Field( - ..., - ge=1, - description="Virtual machine id", - ) - - # vm_id: str = Field( - # ..., - # # default="4000", - # description="Virtual machine id", - # pattern=r"^[0-9]+$" - # ) - - vm_ip: str = Field( - ..., - description="vm ipv4", - pattern=r"^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$" - ) - - vm_description: str = Field( - ..., - strip_whitespace=True, - max_length=200, - # pattern=VM_DESCRIPTION_RE, - description="Description" - ) - -class Request_BundlesCoreProxmoxConfigureDefaultVms_CreateVulnVms(BaseModel): - - proxmox_node: str = Field( - ..., - # default= "px-testing", - description = "Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" - ) - - vms: Dict[str, Request_BundlesCoreProxmoxConfigureDefaultVms_CreateVulnVmsItem] = Field( - ..., - description="Map - vm override vm_id vm_ip vm_description, ... " - ) - - model_config = { - "json_schema_extra": { - "example": { - "proxmox_node": "px-testing", - "vms": { - "vuln-box-00": { - "vm_id": 4000, - "vm_description": "vulnerable vm 00", - "vm_ip": "192.168.42.170", - }, - "vuln-box-01": { - "vm_id": 4001, - "vm_description": "vulnerable vm 01", - "vm_ip": "192.168.42.171", - }, - "vuln-box-02": { - "vm_id": 4002, - "vm_description": "vulnerable vm 02", - "vm_ip": "192.168.42.172", - }, - "vuln-box-03": { - "vm_id": 4003, - "vm_description": "vulnerable vm 03", - "vm_ip": "192.168.42.173", - }, - "vuln-box-04": { - "vm_id": 4004, - "vm_description": "vulnerable vm 04", - "vm_ip": "192.168.42.174", - }, - } - } - } - } - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### -#### #### #### #### #### #### #### #### #### #### #### #### #### #### - -class Reply_BundlesCoreProxmoxConfigureDefaultVms_CreateVulnVmsItem(BaseModel): - - # action: Literal["vm_get_config"] - # source: Literal["proxmox"] - proxmox_node: str - # vm_id: int = Field(..., ge=1) - # vm_name: str - raw_data: str = Field(..., description="Raw string returned by proxmox") - - -class Reply_BundlesCoreProxmoxConfigureDefaultVms_CreateVulnVms(BaseModel): - - rc: int = Field(0, description="RETURN code (0 = OK)") - result: list[Reply_BundlesCoreProxmoxConfigureDefaultVms_CreateVulnVmsItem] - - model_config = { - "json_schema_extra": { - "example": { - "rc": 0, - "result": [ - { - # "action": "vm_get_config", - # "proxmox_node": "px-testing", - # "raw_data": { - # "data": { - # ... - # } - # }, - # "source": "proxmox", - # "vm_id": "1000" - } - ] - } - } - } - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### -#### #### #### #### #### #### #### #### #### #### #### #### #### #### diff --git a/app/schemas/bundles/core/proxmox/configure/default/vms/revert_snapshot_default.py b/app/schemas/bundles/core/proxmox/configure/default/vms/revert_snapshot_default.py deleted file mode 100644 index 330a944..0000000 --- a/app/schemas/bundles/core/proxmox/configure/default/vms/revert_snapshot_default.py +++ /dev/null @@ -1,52 +0,0 @@ - -from typing import Literal, List -from pydantic import BaseModel, Field - - -class Request_BundlesCoreProxmoxConfigureDefaultVms_RevertSnapshotAdminVulnStudentVms(BaseModel): - - proxmox_node: str = Field( - ..., - # default= "px-testing", - description = "Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" - ) - - as_json: bool = Field( - default=True, - description="If true : JSON output else : raw output" - ) - - vm_snapshot_name: str | None = Field( - default=None, - description="Name of the snapshot to create", - pattern=r"^[A-Za-z0-9_-]+$" - ) - # - - # vm_id: str = Field( - # ..., - # # default="1000", - # description="Virtual machine id", - # pattern=r"^[0-9]+$" - # ) - - # vm_ids: List[str] = Field( - # ..., - # # default="1000", - # description="Virtual machine id", - # min_items=1, - # # pattern=r"^[0-9]+$" - # ) - - - model_config = { - "json_schema_extra": { - "example": { - "proxmox_node": "px-testing", - "vm_snapshot_name": "default-snapshot-from-API-220925-1734", - "as_json": True, - - } - } - } diff --git a/app/schemas/bundles/core/proxmox/configure/default/vms/start_stop_resume_pause_default.py b/app/schemas/bundles/core/proxmox/configure/default/vms/start_stop_resume_pause_default.py deleted file mode 100644 index d854694..0000000 --- a/app/schemas/bundles/core/proxmox/configure/default/vms/start_stop_resume_pause_default.py +++ /dev/null @@ -1,45 +0,0 @@ - -from typing import Literal, List -from pydantic import BaseModel, Field - - -class Request_BundlesCoreProxmoxConfigureDefaultVms_StartStopPauseResumeAdminVulnStudentVms(BaseModel): - - proxmox_node: str = Field( - ..., - # default= "px-testing", - description = "Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" - ) - - as_json: bool = Field( - default=True, - description="If true : JSON output else : raw output" - ) - # - - # vm_id: str = Field( - # ..., - # # default="1000", - # description="Virtual machine id", - # pattern=r"^[0-9]+$" - # ) - - # vm_ids: List[str] = Field( - # ..., - # # default="1000", - # description="Virtual machine id", - # min_items=1, - # # pattern=r"^[0-9]+$" - # ) - - - model_config = { - "json_schema_extra": { - "example": { - "proxmox_node": "px-testing", - "as_json": True, - - } - } - } diff --git a/app/schemas/debug/ping.py b/app/schemas/debug/ping.py deleted file mode 100644 index 6bd7e55..0000000 --- a/app/schemas/debug/ping.py +++ /dev/null @@ -1,78 +0,0 @@ - -from typing import List -from pydantic import BaseModel, Field - -# -# ISSUE - #1 -# - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### -#### #### #### #### #### #### #### #### #### #### #### #### #### #### - - -class Request_DebugPing(BaseModel): - - proxmox_node: str = Field( - ..., - # default="px-testing", - description="Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" - ) - - as_json: bool | None = Field( - default=False, - description="If true : JSON output else : raw output" - ) - # - - hosts: str | None = Field( - ..., - # default="all", - description="Targeted ansible hosts", - pattern=r"^[A-Za-z0-9\._-]+$" - ) - - - - model_config = { - "json_schema_extra": { - "example": { - "proxmox_node": "px-testing", - "hosts": "all", - "as_json": False - } - } - } - - -# #### #### #### #### #### #### #### #### #### #### #### #### #### #### -# #### #### #### #### #### #### #### #### #### #### #### #### #### #### - -class Reply_DebugPing(BaseModel): - - rc: int = Field( - ..., # mandatory field. - description="Return code of the job (0 = success, >0 = error/warning)" - ) - log_multiline: List[str] = Field( - ..., # mandatory field. - description="Execution log as a list of lines (chronological order)" - ) - - model_config = { - "json_schema_extra": { - "something": { - "rc": 0, - "log_multiline": [ - "PLAY [debug - ping targeted host/group] ****************************************", - "", - "TASK [ansible.builtin.ping] ****************************************************", - "ok: [something-1]", - "", - "PLAY RECAP *********************************************************************", - "something-1 : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0" - ] - } - } - } - diff --git a/app/schemas/proxmox/firewall/add_iptable_alias.py b/app/schemas/proxmox/firewall/add_iptable_alias.py deleted file mode 100644 index 8ee1bf8..0000000 --- a/app/schemas/proxmox/firewall/add_iptable_alias.py +++ /dev/null @@ -1,103 +0,0 @@ - - -from enum import Enum -from typing import List, Literal -from pydantic import BaseModel, Field - -# -# ISSUE - #12 -# - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### - -class Request_ProxmoxFirewall_AddIptablesAlias(BaseModel): - - proxmox_node: str = Field( - ..., - # default= "px-testing", - description = "Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" - ) - - as_json: bool = Field( - default=True, - description="If true : JSON output else : raw output" - ) - # - - vm_id: str = Field( - ..., - # default="4000", - description="Virtual machine id", - pattern=r"^[0-9]+$" - ) - - vm_fw_alias_name: str = Field( - ..., - description="Firewall alias name", - pattern=r"^[A-Za-z0-9-_]+$", - ) - - vm_fw_alias_cidr: str = Field( - ..., - description="CIDR notation for the alias - eg 192.168.123.0/24", - pattern=r"^[0-9./]+$", - ) - - vm_fw_alias_comment: str | None = Field( - ..., - description="Optional comment for the firewall alias", - pattern=r"^[A-Za-z0-9 _-]*$", - ) - - model_config = { - "json_schema_extra": { - "example": { - "proxmox_node": "px-testing", - "as_json": True, - # - "vm_id":"1000", - # - "vm_fw_alias_name":"test", - "vm_fw_alias_cidr":"192.168.123.0/24", - "vm_fw_alias_comment":"this_comment" - } - } - } - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### - -class Reply_ProxmoxFirewallWithStorageName_AddIptablesAliasItem(BaseModel): - - action: Literal["vm_ListIso_usage"] - source: Literal["proxmox"] - proxmox_node: str - ## - # vm_id: int = Field(..., ge=1) - vm_fw_alias_cidr: str - vm_fw_alias_name: str - vm_id: str - -class Reply_ProxmoxFirewallWithStorageName_AddIptablesAlias(BaseModel): - - rc: int = Field(0, description="RETURN code (0 = OK)") - result: list[Reply_ProxmoxFirewallWithStorageName_AddIptablesAliasItem] - - model_config = { - "json_schema_extra": { - "example": { - "rc": 0, - "result": [ - { - "action": "firewall_vm_add_iptables_alias", - "source": "proxmox", - "proxmox_node": "px-testing", - ## - "vm_fw_alias_cidr": "192.168.123.0/24", - "vm_fw_alias_name": "test", - "vm_id": "1000" - } - ] - } - } - } diff --git a/app/schemas/proxmox/firewall/apply_iptables_rules.py b/app/schemas/proxmox/firewall/apply_iptables_rules.py deleted file mode 100644 index ffed7bb..0000000 --- a/app/schemas/proxmox/firewall/apply_iptables_rules.py +++ /dev/null @@ -1,253 +0,0 @@ - - -from enum import Enum -from typing import List, Literal -from pydantic import BaseModel, Field, ConfigDict - -# -# ISSUE - #12 -# - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### - -class Request_ProxmoxFirewall_ApplyIptablesRules(BaseModel): - - proxmox_node: str = Field( - ..., - description="Target Proxmox node name.", - pattern=r"^[A-Za-z0-9-]+$", - ) - - as_json: bool = Field( - default=True, - description="If true: return JSON output, otherwise raw output.", - ) - # - - vm_id: str = Field( - ..., - # default="4000", - description="Virtual machine id", - pattern=r"^[0-9]+$" - ) - - vm_fw_action: str = Field( - ..., - description="Firewall action - ACCEPT, DROP, REJECT", - pattern=r"^(ACCEPT|DROP|REJECT)$", - ) - - vm_fw_dport: str = Field( - ..., - description="Destination port or port range", - pattern=r"^[0-9:-]+$", - ) - - vm_fw_enable: int = Field( - ..., - description="Enable flag - 1 = enabled, 0 = disabled", - ) - - vm_fw_proto: str = Field( - ..., - description="Protocol - tcp, udp, icmp", - pattern=r"^[a-zA-Z0-9]+$", - ) - - vm_fw_type: str = Field( - ..., - description="Rule type - in or out", - pattern=r"^(in|out)$", - ) - - vm_fw_log: str | None = Field( - default=None, - description="Optional logging level - info, debug,...", - pattern=r"^[A-Za-z0-9_-]+$", - ) - - vm_fw_iface: str | None = Field( - default=None, - description="Optional network interface name", - pattern=r"^[A-Za-z0-9_-]+$", - ) - - vm_fw_source: str | None = Field( - default=None, - description="Optional source address or CIDR", - pattern=r"^[0-9./]+$", - ) - - vm_fw_dest: str | None = Field( - default=None, - description="Optional destination address or CIDR", - pattern=r"^[0-9./]+$", - ) - - vm_fw_sport: str | None = Field( - default=None, - description="Optional source port or port range", - pattern=r"^[0-9:-]+$", - ) - - vm_fw_comment: str | None = Field( - default=None, - description="Optional comment for the rule", - pattern=r"^[A-Za-z0-9 _-]*$", - ) - - vm_fw_pos: int | None = Field( - default=None, - description="Optional position index rule in the chain.", - ) - - model_config = ConfigDict( - json_schema_extra={ - "example":[ - { - "proxmox_node": "px-node-01", - "as_json": True, - # - "vm_id": "1000", - "vm_fw_action": "ACCEPT", - "vm_fw_type": "in", - "vm_fw_proto": "tcp", - "vm_fw_dport": "22", - "vm_fw_enable": 1, - "vm_fw_iface": "net0", - "vm_fw_source": "192.168.1.0/24", - "vm_fw_dest": "0.0.0.0/0", - "vm_fw_sport": "1024", - "vm_fw_comment": "Test comment", - "vm_fw_pos": 5, - "vm_fw_log": "debug", - }, - {"vm_id": 100, "vm_fw_action": "ACCEPT", "vm_fw_type": "in", "vm_fw_proto": "tcp", "vm_fw_dport": "22", "vm_fw_enable": 1}, - {"vm_id": 100, "vm_fw_action": "ACCEPT", "vm_fw_type": "in", "vm_fw_proto": "tcp", "vm_fw_dport": "22", "vm_fw_log": "alert", "vm_fw_enable": 1}, - {"vm_id": 100, "vm_fw_action": "ACCEPT", "vm_fw_type": "in", "vm_fw_proto": "tcp", "vm_fw_dport": "22", "vm_fw_log": "warning", "vm_fw_enable": 1}, - {"vm_id": 100, "vm_fw_action": "ACCEPT", "vm_fw_type": "in", "vm_fw_proto": "tcp", "vm_fw_dport": "22", "vm_fw_log": "info", "vm_fw_enable": 1}, - {"vm_id": 100, "vm_fw_action": "ACCEPT", "vm_fw_type": "in", "vm_fw_iface": "net0", "vm_fw_proto": "tcp", "vm_fw_dport": "22", "vm_fw_enable": 1}, - {"vm_id": 100, "vm_fw_action": "ACCEPT", "vm_fw_type": "in", "vm_fw_iface": "net0", "vm_fw_proto": "tcp", "vm_fw_dport": "22", "vm_fw_log": "alert", "vm_fw_enable": 1}, - {"vm_id": 100, "vm_fw_action": "ACCEPT", "vm_fw_type": "in", "vm_fw_iface": "net0", "vm_fw_proto": "tcp", "vm_fw_dport": "22", "vm_fw_log": "warning", "vm_fw_enable": 1}, - {"vm_id": 100, "vm_fw_action": "ACCEPT", "vm_fw_type": "in", "vm_fw_iface": "net0", "vm_fw_proto": "tcp", "vm_fw_dport": "22", "vm_fw_log": "info", "vm_fw_enable": 1}, - {"vm_id": 100, "vm_fw_action": "ACCEPT", "vm_fw_type": "out", "vm_fw_proto": "tcp", "vm_fw_dport": "80", "vm_fw_enable": 1}, - {"vm_id": 100, "vm_fw_action": "ACCEPT", "vm_fw_type": "out", "vm_fw_proto": "tcp", "vm_fw_dport": "80", "vm_fw_log": "alert", "vm_fw_enable": 1}, - {"vm_id": 100, "vm_fw_action": "ACCEPT", "vm_fw_type": "out", "vm_fw_proto": "tcp", "vm_fw_dport": "80", "vm_fw_log": "warning", "vm_fw_enable": 1}, - {"vm_id": 100, "vm_fw_action": "ACCEPT", "vm_fw_type": "out", "vm_fw_proto": "tcp", "vm_fw_dport": "80", "vm_fw_log": "info", "vm_fw_enable": 1}, - {"vm_id": 100, "vm_fw_action": "ACCEPT", "vm_fw_type": "out", "vm_fw_iface": "net0", "vm_fw_proto": "tcp", "vm_fw_dport": "80", "vm_fw_enable": 1}, - {"vm_id": 100, "vm_fw_action": "ACCEPT", "vm_fw_type": "out", "vm_fw_iface": "net0", "vm_fw_proto": "tcp", "vm_fw_dport": "80", "vm_fw_log": "alert", "vm_fw_enable": 1}, - {"vm_id": 100, "vm_fw_action": "ACCEPT", "vm_fw_type": "out", "vm_fw_iface": "net0", "vm_fw_proto": "tcp", "vm_fw_dport": "80", "vm_fw_log": "warning", "vm_fw_enable": 1}, - {"vm_id": 100, "vm_fw_action": "ACCEPT", "vm_fw_type": "out", "vm_fw_iface": "net0", "vm_fw_proto": "tcp", "vm_fw_dport": "80", "vm_fw_log": "info", "vm_fw_enable": 1}, - {"vm_id": 100, "vm_fw_action": "ACCEPT", "vm_fw_type": "out", "vm_fw_proto": "tcp", "vm_fw_dport": "443", "vm_fw_enable": 1}, - {"vm_id": 100, "vm_fw_action": "ACCEPT", "vm_fw_type": "out", "vm_fw_proto": "tcp", "vm_fw_dport": "443", "vm_fw_log": "alert", "vm_fw_enable": 1}, - {"vm_id": 100, "vm_fw_action": "ACCEPT", "vm_fw_type": "out", "vm_fw_proto": "tcp", "vm_fw_dport": "443", "vm_fw_log": "warning", "vm_fw_enable": 1}, - {"vm_id": 100, "vm_fw_action": "ACCEPT", "vm_fw_type": "out", "vm_fw_proto": "tcp", "vm_fw_dport": "443", "vm_fw_log": "info", "vm_fw_enable": 1}, - {"vm_id": 100, "vm_fw_action": "ACCEPT", "vm_fw_type": "out", "vm_fw_iface": "net0", "vm_fw_proto": "tcp", "vm_fw_dport": "443", "vm_fw_enable": 1}, - {"vm_id": 100, "vm_fw_action": "ACCEPT", "vm_fw_type": "out", "vm_fw_iface": "net0", "vm_fw_proto": "tcp", "vm_fw_dport": "443", "vm_fw_log": "alert", "vm_fw_enable": 1}, - {"vm_id": 100, "vm_fw_action": "ACCEPT", "vm_fw_type": "out", "vm_fw_iface": "net0", "vm_fw_proto": "tcp", "vm_fw_dport": "443", "vm_fw_log": "warning", "vm_fw_enable": 1}, - {"vm_id": 100, "vm_fw_action": "ACCEPT", "vm_fw_type": "out", "vm_fw_iface": "net0", "vm_fw_proto": "tcp", "vm_fw_dport": "443", "vm_fw_log": "info", "vm_fw_enable": 1}, - {"vm_id": 100, "vm_fw_action": "ACCEPT", "vm_fw_type": "out", "vm_fw_proto": "udp", "vm_fw_dport": "53", "vm_fw_enable": 1}, - {"vm_id": 100, "vm_fw_action": "ACCEPT", "vm_fw_type": "out", "vm_fw_proto": "udp", "vm_fw_dport": "53", "vm_fw_log": "alert", "vm_fw_enable": 1}, - {"vm_id": 100, "vm_fw_action": "ACCEPT", "vm_fw_type": "out", "vm_fw_proto": "udp", "vm_fw_dport": "53", "vm_fw_log": "warning", "vm_fw_enable": 1}, - {"vm_id": 100, "vm_fw_action": "ACCEPT", "vm_fw_type": "out", "vm_fw_proto": "udp", "vm_fw_dport": "53", "vm_fw_log": "info", "vm_fw_enable": 1}, - {"vm_id": 100, "vm_fw_action": "ACCEPT", "vm_fw_type": "out", "vm_fw_iface": "net0", "vm_fw_proto": "udp", "vm_fw_dport": "53", "vm_fw_enable": 1}, - {"vm_id": 100, "vm_fw_action": "ACCEPT", "vm_fw_type": "out", "vm_fw_iface": "net0", "vm_fw_proto": "udp", "vm_fw_dport": "53", "vm_fw_log": "alert", "vm_fw_enable": 1}, - {"vm_id": 100, "vm_fw_action": "ACCEPT", "vm_fw_type": "out", "vm_fw_iface": "net0", "vm_fw_proto": "udp", "vm_fw_dport": "53", "vm_fw_log": "warning", "vm_fw_enable": 1}, - {"vm_id": 100, "vm_fw_action": "ACCEPT", "vm_fw_type": "out", "vm_fw_iface": "net0", "vm_fw_proto": "udp", "vm_fw_dport": "53", "vm_fw_log": "info", "vm_fw_enable": 1}, - {"vm_id": 100, "vm_fw_action": "DROP", "vm_fw_type": "in", "vm_fw_enable": 1}, - {"vm_id": 100, "vm_fw_action": "DROP", "vm_fw_type": "in", "vm_fw_log": "alert", "vm_fw_enable": 1}, - {"vm_id": 100, "vm_fw_action": "DROP", "vm_fw_type": "in", "vm_fw_log": "warning", "vm_fw_enable": 1}, - {"vm_id": 100, "vm_fw_action": "DROP", "vm_fw_type": "in", "vm_fw_log": "info", "vm_fw_enable": 1}, - {"vm_id": 100, "vm_fw_action": "DROP", "vm_fw_type": "in", "vm_fw_iface": "net0", "vm_fw_enable": 1}, - {"vm_id": 100, "vm_fw_action": "DROP", "vm_fw_type": "in", "vm_fw_iface": "net0", "vm_fw_log": "alert", "vm_fw_enable": 1}, - {"vm_id": 100, "vm_fw_action": "DROP", "vm_fw_type": "in", "vm_fw_iface": "net0", "vm_fw_log": "warning", "vm_fw_enable": 1}, - {"vm_id": 100, "vm_fw_action": "DROP", "vm_fw_type": "in", "vm_fw_iface": "net0", "vm_fw_log": "info", "vm_fw_enable": 1}, - {"vm_id": 100, "vm_fw_action": "ACCEPT", "vm_fw_type": "in", "vm_fw_proto": "tcp", "vm_fw_dport": "22", "vm_fw_enable": 1}, - {"vm_id": 100, "vm_fw_action": "ACCEPT", "vm_fw_type": "in", "vm_fw_proto": "tcp", "vm_fw_dport": "22", "vm_fw_enable": 1, "vm_fw_source": "192.168.1.0/24"}, - {"vm_id": 100, "vm_fw_action": "ACCEPT", "vm_fw_type": "in", "vm_fw_proto": "tcp", "vm_fw_dport": "22", "vm_fw_enable": 1, "vm_fw_dest": "0.0.0.0/0"}, - {"vm_id": 100, "vm_fw_action": "ACCEPT", "vm_fw_type": "in", "vm_fw_proto": "tcp", "vm_fw_dport": "22", "vm_fw_enable": 1, "vm_fw_sport": "1024"}, - {"vm_id": 100, "vm_fw_action": "ACCEPT", "vm_fw_type": "in", "vm_fw_proto": "tcp", "vm_fw_dport": "22", "vm_fw_enable": 1, "vm_fw_comment": "TESTCOMMENT"}, - {"vm_id": 100, "vm_fw_action": "ACCEPT", "vm_fw_type": "in", "vm_fw_proto": "tcp", "vm_fw_dport": "22", "vm_fw_enable": 1, "vm_fw_comment": "ABCD1234-123123"}, - {"vm_id": 100, "vm_fw_action": "ACCEPT", "vm_fw_type": "in", "vm_fw_proto": "tcp", "vm_fw_dport": "22", "vm_fw_enable": 1, "vm_fw_pos": 4242}, - {"vm_id": 100, "vm_fw_action": "ACCEPT", "vm_fw_type": "in", "vm_fw_proto": "tcp", "vm_fw_dport": "22", "vm_fw_enable": 1, "vm_fw_log": "info"}, - {"vm_id": 100, "vm_fw_action": "ACCEPT", "vm_fw_type": "in", "vm_fw_proto": "tcp", "vm_fw_dport": "22", "vm_fw_enable": 1, "vm_fw_iface": "net0", "vm_fw_source": "192.168.1.0/24", "vm_fw_dest": "0.0.0.0/0", "vm_fw_sport": "1024", "vm_fw_comment": "Testcomment", "vm_fw_pos": 5, "vm_fw_log": "DEBUG"}, - {"vm_id": 100, "vm_fw_action": "ACCEPT", "vm_fw_type": "in", "vm_fw_proto": "tcp", "vm_fw_dport": "22", "vm_fw_enable": 1, "vm_fw_iface": "net0", "vm_fw_source": "192.168.1.0/24", "vm_fw_dest": "0.0.0.0/0", "vm_fw_sport": "1024", "vm_fw_comment": "Testcomment", "vm_fw_pos": 5, "vm_fw_log": "DEBUG"}, - {"vm_id": 100, "vm_fw_action": "ACCEPT", "vm_fw_type": "in", "vm_fw_proto": "tcp", "vm_fw_dport": "22", "vm_fw_enable": 1}, - {"vm_id": 100, "vm_fw_action": "DROP", "vm_fw_type": "in", "vm_fw_enable": 1}, - {"vm_id": 100, "vm_fw_action": "ACCEPT", "vm_fw_type": "out", "vm_fw_proto": "tcp", "vm_fw_dport": "80", "vm_fw_enable": 1}, - {"vm_id": 100, "vm_fw_action": "ACCEPT", "vm_fw_type": "out", "vm_fw_proto": "tcp", "vm_fw_dport": "443", "vm_fw_enable": 1}, - {"vm_id": 100, "vm_fw_action": "ACCEPT", "vm_fw_type": "out", "vm_fw_proto": "tcp", "vm_fw_dport": "53", "vm_fw_enable": 1}, - {"vm_id": 100, "vm_fw_action": "ACCEPT", "vm_fw_type": "out", "vm_fw_proto": "tcp", "vm_fw_dport": "22", "vm_fw_enable": 1}, - ] - } - ) - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### - -class Reply_ProxmoxFirewallWithStorageName_ApplyIptablesRulesItem(BaseModel): - - action: Literal["vm_ApplyIptablesRules_usage"] - source: Literal["proxmox"] - proxmox_node: str - ## - # vm_id: int = Field(..., ge=1) - vm_fw_action : str - vm_fw_comment : str - vm_fw_dest : str - vm_fw_dport : str - vm_fw_enable : int - vm_fw_iface : str - vm_fw_log : str - vm_fw_pos : int - vm_fw_proto : str - vm_fw_source : str - vm_fw_sport : str - vm_fw_type : str - vm_id : str - -class Reply_ProxmoxFirewallWithStorageName_ApplyIptablesRules(BaseModel): - - rc: int = Field(0, description="RETURN code (0 = OK)") - result: list[Reply_ProxmoxFirewallWithStorageName_ApplyIptablesRulesItem] - - model_config = { - "json_schema_extra": { - "example": { - "rc": 0, - "result": [ - { - "action": "firewall_vm_apply_iptables_rule", - "proxmox_node": "px-testing", - "source": "proxmox", - # - "vm_fw_action": "ACCEPT", - "vm_fw_dport": "80", - "vm_fw_enable": "1", - "vm_fw_proto": "tcp", - "vm_fw_type": "out", - "vm_id": "100" - }, - { - "action": "firewall_vm_apply_iptables_rule", - "proxmox_node": "px-testing", - "source": "proxmox", - # - "vm_fw_action": "ACCEPT", - "vm_fw_comment": "Test comment", - "vm_fw_dest": "0.0.0.0/0", - "vm_fw_dport": "22", - "vm_fw_enable": "1", - "vm_fw_iface": "net0", - "vm_fw_log": "warning", - "vm_fw_pos": "5", - "vm_fw_proto": "tcp", - "vm_fw_source": "192.168.1.0/24", - "vm_fw_sport": "1024", - "vm_fw_type": "in", - "vm_id": "100" - - } - ] - } - } - } diff --git a/app/schemas/proxmox/firewall/delete_iptables_alias.py b/app/schemas/proxmox/firewall/delete_iptables_alias.py deleted file mode 100644 index e1ea372..0000000 --- a/app/schemas/proxmox/firewall/delete_iptables_alias.py +++ /dev/null @@ -1,87 +0,0 @@ - - -from enum import Enum -from typing import List, Literal -from pydantic import BaseModel, Field - -# -# ISSUE - #12 -# - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### - -class Request_ProxmoxFirewall_DeleteIptablesAlias(BaseModel): - - proxmox_node: str = Field( - ..., - # default= "px-testing", - description = "Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" - ) - - as_json: bool = Field( - default=True, - description="If true : JSON output else : raw output" - ) - # - - vm_id: str = Field( - ..., - # default="4000", - description="Virtual machine id", - pattern=r"^[0-9]+$" - ) - - vm_fw_alias_name: str = Field( - ..., - description="Firewall alias name", - pattern=r"^[A-Za-z0-9-_]+$", - ) - - model_config = { - "json_schema_extra": { - "example": { - "proxmox_node": "px-testing", - "as_json": True, - # - "vm_id": "1000", - "vm_fw_alias_name": "test", - } - } - } - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### - -class Reply_ProxmoxFirewallWithStorageName_DeleteIptablesAliasItem(BaseModel): - - action: Literal["firewall_vm_delete_iptables_alias"] - source: Literal["proxmox"] - proxmox_node: str - # ## - # vm_id: int = Field(..., ge=1) - vm_fw_alias_name: str - vm_id: int - - -class Reply_ProxmoxFirewallWithStorageName_DeleteIptablesAlias(BaseModel): - - rc: int = Field(0, description="RETURN code (0 = OK)") - result: list[Reply_ProxmoxFirewallWithStorageName_DeleteIptablesAliasItem] - - model_config = { - "json_schema_extra": { - "example": { - "rc": 0, - "result": [ - { - "action": "firewall_vm_delete_iptables_alias", - "source": "proxmox", - "proxmox_node": "px-testing", - ## - "vm_fw_alias_name": "test", - "vm_id": "1000", - } - ] - } - } - } diff --git a/app/schemas/proxmox/firewall/delete_iptables_rule.py b/app/schemas/proxmox/firewall/delete_iptables_rule.py deleted file mode 100644 index cbcffd2..0000000 --- a/app/schemas/proxmox/firewall/delete_iptables_rule.py +++ /dev/null @@ -1,86 +0,0 @@ - - -from enum import Enum -from typing import List, Literal -from pydantic import BaseModel, Field - -# -# ISSUE - #12 -# - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### - -class Request_ProxmoxFirewall_DeleteIptablesRule(BaseModel): - - proxmox_node: str = Field( - ..., - # default= "px-testing", - description = "Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" - ) - - as_json: bool = Field( - default=True, - description="If true : JSON output else : raw output" - ) - # - - vm_id: str = Field( - ..., - # default="4000", - description="Virtual machine id", - pattern=r"^[0-9]+$" - ) - - vm_fw_pos: int | None = Field( - ..., - description="Optional position index rule in the chain.", - ) - - model_config = { - "json_schema_extra": { - "example": { - "proxmox_node": "px-testing", - "as_json": True, - # - "vm_id":"1000", - "vm_fw_pos":1, - - } - } - } - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### - -class Reply_ProxmoxFirewallWithStorageName_DeleteIptablesRuleItem(BaseModel): - - action: Literal["vm_DeleteIptablesRule_usage"] - source: Literal["proxmox"] - proxmox_node: str - ## - # vm_id: int = Field(..., ge=1) - vm_id: str - vm_fw_pos: int - -class Reply_ProxmoxFirewallWithStorageName_DeleteIptablesRule(BaseModel): - - rc: int = Field(0, description="RETURN code (0 = OK)") - result: list[Reply_ProxmoxFirewallWithStorageName_DeleteIptablesRuleItem] - - model_config = { - "json_schema_extra": { - "example": { - "rc": 0, - "result": [ - { - "action": "firewall_vm_delete_iptables_rule", - "source": "proxmox", - "proxmox_node": "px-testing", - ## - "vm_fw_pos": "0", - "vm_id": "1000" - } - ] - } - } - } diff --git a/app/schemas/proxmox/firewall/disable_firewall_dc.py b/app/schemas/proxmox/firewall/disable_firewall_dc.py deleted file mode 100644 index 7adeaf7..0000000 --- a/app/schemas/proxmox/firewall/disable_firewall_dc.py +++ /dev/null @@ -1,82 +0,0 @@ - - -from enum import Enum -from typing import List, Literal -from pydantic import BaseModel, Field - -# -# ISSUE - #12 -# - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### -class Request_ProxmoxFirewall_DisableFirewallDc(BaseModel): - - proxmox_node: str = Field( - ..., - # default= "px-testing", - description = "Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" - ) - - as_json: bool = Field( - default=True, - description="If true : JSON output else : raw output" - ) - # - - proxmox_api_host: str = Field( - ..., - # default= "px-testing", - description = "Proxmox api - ip:port", - pattern=r"^[A-Za-z0-9\.:-]*$" - ) - - model_config = { - "json_schema_extra": { - "example": { - "proxmox_node": "px-testing", - "as_json": True, - # - "proxmox_api_host": "127.0.0.1:1234", - - } - } - } - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### - -class Reply_ProxmoxFirewallWithStorageName_DisableFirewallDcItem(BaseModel): - - action: Literal["vm_DisableFirewallDc_usage"] - source: Literal["proxmox"] - proxmox_node: str - ## - # vm_id: int = Field(..., ge=1) - # iso_content: str - # iso_ctime: int - # iso_format: str - # iso_size: int - # iso_vol_id: str - # local: str - # storage_name: str - -class Reply_ProxmoxFirewallWithStorageName_DisableFirewallDc(BaseModel): - - rc: int = Field(0, description="RETURN code (0 = OK)") - result: list[Reply_ProxmoxFirewallWithStorageName_DisableFirewallDcItem] - - model_config = { - "json_schema_extra": { - "example": { - "rc": 0, - "result": [ - { - "action": "storage_list_iso", - "source": "proxmox", - "proxmox_node": "px-testing", - ## - } - ] - } - } - } diff --git a/app/schemas/proxmox/firewall/disable_firewall_node.py b/app/schemas/proxmox/firewall/disable_firewall_node.py deleted file mode 100644 index a53f337..0000000 --- a/app/schemas/proxmox/firewall/disable_firewall_node.py +++ /dev/null @@ -1,70 +0,0 @@ - - -from enum import Enum -from typing import List, Literal -from pydantic import BaseModel, Field - -# -# ISSUE - #12 -# - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### -class Request_ProxmoxFirewall_DistableFirewallNode(BaseModel): - - proxmox_node: str = Field( - ..., - # default= "px-testing", - description="Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" - ) - - as_json: bool = Field( - default=True, - description="If true : JSON output else : raw output" - ) - # - - model_config = { - "json_schema_extra": { - "example": { - "proxmox_node": "px-testing", - "as_json": True - } - } - } - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### - -class Reply_ProxmoxFirewallWithStorageName_DistableFirewallNodeItem(BaseModel): - - action: Literal["vm_EnableFirewallNode_usage"] - source: Literal["proxmox"] - proxmox_node: str - ## - # vm_id: int = Field(..., ge=1) - node_firewall: str - -class Reply_ProxmoxFirewallWithStorageName_DisableFirewallNode(BaseModel): - - rc: int = Field(0, description="RETURN code (0 = OK)") - result: list[Reply_ProxmoxFirewallWithStorageName_DistableFirewallNodeItem] - - # - # missing feat in role ? - # - model_config = { - "json_schema_extra": { - "example": { - "rc": 0, - "result": [ - { - "action": "firewall_node_enable", - "source": "proxmox", - "proxmox_node": "px-testing", - ## - "node_firewall": "disabled", - } - ] - } - } - } diff --git a/app/schemas/proxmox/firewall/disable_firewall_vm.py b/app/schemas/proxmox/firewall/disable_firewall_vm.py deleted file mode 100644 index 7330ba5..0000000 --- a/app/schemas/proxmox/firewall/disable_firewall_vm.py +++ /dev/null @@ -1,85 +0,0 @@ - - -from enum import Enum -from typing import List, Literal -from pydantic import BaseModel, Field - -# -# ISSUE - #12 -# - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### -class Request_ProxmoxFirewall_DistableFirewallVm(BaseModel): - - proxmox_node: str = Field( - ..., - # default= "px-testing", - description = "Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" - ) - - as_json: bool = Field( - default=True, - description="If true : JSON output else : raw output" - ) - # - - vm_id: str = Field( - ..., - # default="4000", - description="Virtual machine id", - pattern=r"^[0-9]+$" - ) - - # vm_name: str = Field( # todo - to_fix : add resolver vm_name - vm_id - # ..., - # description = "Proxmox storage name", - # pattern=r"^[A-Za-z0-9-]*$" - # ) - - model_config = { - "json_schema_extra": { - "example": { - "proxmox_node": "px-testing", - "as_json": True, - # - "vm_id": "1000", - # "vm_name": "test", - } - } - } - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### - -class Reply_ProxmoxFirewallWithStorageName_DistableFirewallVmItem(BaseModel): - - action: Literal["vm_EnableFirewallDc_usage"] - source: Literal["proxmox"] - proxmox_node: str - ## - # vm_id: int = Field(..., ge=1) - proxmox_api_host: str - -class Reply_ProxmoxFirewallWithStorageName_DisableFirewallVm(BaseModel): - - rc: int = Field(0, description="RETURN code (0 = OK)") - result: list[Reply_ProxmoxFirewallWithStorageName_DistableFirewallVmItem] - - model_config = { - "json_schema_extra": { - "example": { - "rc": 0, - "result": [ - { - "action": "firewall_vm_disable", - "source": "proxmox", - "proxmox_node": "px-testing", - ## - "vm_id": "1000", - "vm_firewall": "disable", - "vm_name": "test" - } - ] - } - } - } diff --git a/app/schemas/proxmox/firewall/enable_firewall_dc.py b/app/schemas/proxmox/firewall/enable_firewall_dc.py deleted file mode 100644 index d4136f5..0000000 --- a/app/schemas/proxmox/firewall/enable_firewall_dc.py +++ /dev/null @@ -1,71 +0,0 @@ - - -from enum import Enum -from typing import List, Literal -from pydantic import BaseModel, Field - -# -# ISSUE - #12 -# - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### -class Request_ProxmoxFirewall_EnableFirewallDc(BaseModel): - - proxmox_api_host: str = Field( - ..., - # default= "px-testing", - description = "Proxmox api - ip:port", - pattern=r"^[A-Za-z0-9\.:-]*$" - ) - - as_json: bool = Field( - default=True, - description="If true : JSON output else : raw output" - ) - # - - model_config = { - "json_schema_extra": { - "example": { - "proxmox_node": "px-testing", - "as_json": True, - # - "proxmox_api_host": "127.0.0.1:18007", - } - } - } - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### - -class Reply_ProxmoxFirewallWithStorageName_EnableFirewallDcItem(BaseModel): - - action: Literal["vm_EnableFirewallDc_usage"] - source: Literal["proxmox"] - proxmox_node: str - ## - # vm_id: int = Field(..., ge=1) - proxmox_api_host: str - -class Reply_ProxmoxFirewallWithStorageName_EnableFirewallDc(BaseModel): - - rc: int = Field(0, description="RETURN code (0 = OK)") - result: list[Reply_ProxmoxFirewallWithStorageName_EnableFirewallDcItem] - - model_config = { - "json_schema_extra": { - "example": { - "rc": 0, - "result": [ - { - "action": "firewall_vm_disable", - "source": "proxmox", - "proxmox_node": "px-testing", - ## - "vm_id": "100", - "vm_firewall": "disable", - "vm_name": "test" - } - ] - } - } - } diff --git a/app/schemas/proxmox/firewall/enable_firewall_node.py b/app/schemas/proxmox/firewall/enable_firewall_node.py deleted file mode 100644 index 701460e..0000000 --- a/app/schemas/proxmox/firewall/enable_firewall_node.py +++ /dev/null @@ -1,72 +0,0 @@ - - -from enum import Enum -from typing import List, Literal -from pydantic import BaseModel, Field - -# -# ISSUE - #12 -# - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### -class Request_ProxmoxFirewall_EnableFirewallNode(BaseModel): - - proxmox_node: str = Field( - ..., - # default= "px-testing", - description="Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" - ) - - as_json: bool = Field( - default=True, - description="If true : JSON output else : raw output" - ) - # - - model_config = { - "json_schema_extra": { - "example": { - "proxmox_node": "px-testing", - "as_json": True - } - } - } - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### - -class Reply_ProxmoxFirewallWithStorageName_EnableFirewallNodeItem(BaseModel): - - action: Literal["vm_EnableFirewallNode_usage"] - source: Literal["proxmox"] - proxmox_node: str - ## - # vm_id: int = Field(..., ge=1) - node_firewall: str - -class Reply_ProxmoxFirewallWithStorageName_EnableFirewallNode(BaseModel): - - rc: int = Field(0, description="RETURN code (0 = OK)") - result: list[Reply_ProxmoxFirewallWithStorageName_EnableFirewallNodeItem] - - # - # missing feat in role ? - # - - model_config = { - "json_schema_extra": { - "example": { - "rc": 0, - "result": [ - { - "action": "firewall_node_enable", - "source": "proxmox", - "proxmox_node": "px-testing", - ## - "node_firewall": "enabled", - - } - ] - } - } - } diff --git a/app/schemas/proxmox/firewall/enable_firewall_vm.py b/app/schemas/proxmox/firewall/enable_firewall_vm.py deleted file mode 100644 index fe65dea..0000000 --- a/app/schemas/proxmox/firewall/enable_firewall_vm.py +++ /dev/null @@ -1,100 +0,0 @@ - - -from enum import Enum -from typing import List, Literal -from pydantic import BaseModel, Field - -# -# ISSUE - #12 -# - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### - -class Request_ProxmoxFirewall_EnableFirewallVm(BaseModel): - - proxmox_node: str = Field( - ..., - # default= "px-testing", - description = "Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" - ) - - as_json: bool = Field( - default=True, - description="If true : JSON output else : raw output" - ) - # - - vm_id: str = Field( - ..., - # default="4000", - description="Virtual machine id", - pattern=r"^[0-9]+$" - ) - - # vm_name: str = Field( # todo - to_fix : add resolver vm_name - vm_id - # ..., - # # default= "px-testing", - # description = "Proxmox storage name", - # pattern=r"^[A-Za-z0-9-]*$" - # ) - - # vm_firewall: str = Field( - # ..., - # # default= "px-testing", - # description = "Proxmox storage name", - # pattern=r"^[A-Za-z0-9-]*$" - # ) - - model_config = { - "json_schema_extra": { - "example": { - "proxmox_node": "px-testing", - "as_json": True, - # - "vm_name": "test", - "vm_id": "1000", - } - } - } - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### - -class Reply_ProxmoxFirewallWithStorageName_EnableFirewallVmItem(BaseModel): - - action: Literal["vm_EnableFirewallVm_usage"] - source: Literal["proxmox"] - proxmox_node: str - ## - # vm_id: int = Field(..., ge=1) - vm_id: str - vm_name: str - vm_firewall: str - -class Reply_ProxmoxFirewallWithStorageName_EnableFirewallVm(BaseModel): - - rc: int = Field(0, description="RETURN code (0 = OK)") - result: list[Reply_ProxmoxFirewallWithStorageName_EnableFirewallVmItem] - - - # - # output missing - missing feat in role ? - # - model_config = { - "json_schema_extra": { - "example": { - "rc": 0, - "result": [ - { - "action": "firewall_vm_enable", - "source": "proxmox", - "proxmox_node": "px-testing", - ## - "vm_id": "100", - "vm_firewall": "enabled", - "vm_name": "test" - } - ] - } - } - } diff --git a/app/schemas/proxmox/firewall/list_iptables_alias.py b/app/schemas/proxmox/firewall/list_iptables_alias.py deleted file mode 100644 index e8787e8..0000000 --- a/app/schemas/proxmox/firewall/list_iptables_alias.py +++ /dev/null @@ -1,81 +0,0 @@ - - -from enum import Enum -from typing import List, Literal -from pydantic import BaseModel, Field - -# -# ISSUE - #12 -# - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### - -class Request_ProxmoxFirewall_ListIptablesAlias(BaseModel): - - proxmox_node: str = Field( - ..., - # default= "px-testing", - description = "Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" - ) - - as_json: bool = Field( - default=True, - description="If true : JSON output else : raw output" - ) - # - - vm_id: str = Field( - ..., - # default="4000", - description="Virtual machine id", - pattern=r"^[0-9]+$" - ) - - model_config = { - "json_schema_extra": { - "example": { - "proxmox_node": "px-testing", - "as_json": True, - # - "vm_id": "1000", - } - } - } - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### - -class Reply_ProxmoxFirewallWithStorageName_ListIptablesAliasItem(BaseModel): - - action: Literal["vm_ListIptablesAlias_usage"] - source: Literal["proxmox"] - proxmox_node: str - ## - # vm_id: int = Field(..., ge=1) - vm_fw_alias_cidr: str - vm_fw_alias_name: int - vm_id: str - -class Reply_ProxmoxFirewallWithStorageName_ListIptablesAlias(BaseModel): - - rc: int = Field(0, description="RETURN code (0 = OK)") - result: list[Reply_ProxmoxFirewallWithStorageName_ListIptablesAliasItem] - - model_config = { - "json_schema_extra": { - "example": { - "rc": 0, - "result": [ - { - "action": "firewall_vm_list_iptables_alias", - "source": "proxmox", - "proxmox_node": "px-testing", - ## - "vm_fw_alias_cidr": "192.168.123.0/24", - "vm_fw_alias_name": "test", - "vm_id": "1000" - } - ] - } - } - } diff --git a/app/schemas/proxmox/firewall/list_iptables_rules.py b/app/schemas/proxmox/firewall/list_iptables_rules.py deleted file mode 100644 index e2537a1..0000000 --- a/app/schemas/proxmox/firewall/list_iptables_rules.py +++ /dev/null @@ -1,125 +0,0 @@ - - -from enum import Enum -from typing import List, Literal -from pydantic import BaseModel, Field - -# -# ISSUE - #12 -# - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### - -class Request_ProxmoxFirewall_ListIptablesRules(BaseModel): - - proxmox_node: str = Field( - ..., - # default= "px-testing", - description = "Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" - ) - - as_json: bool = Field( - default=True, - description="If true : JSON output else : raw output" - ) - # - - vm_id: str = Field( - ..., - # default="4000", - description="Virtual machine id", - pattern=r"^[0-9]+$" - ) - - model_config = { - "json_schema_extra": { - "example": { - "proxmox_node": "px-testing", - "as_json": True, - # - "vm_id": "1000", - - } - } - } - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### - -class Reply_ProxmoxFirewallWithStorageName_ListIptablesRulesItem(BaseModel): - - action: Literal["vm_ListIptablesRules_usage"] - source: Literal["proxmox"] - proxmox_node: str - ## - # vm_id: int = Field(..., ge=1) - vm_fw_action: str - vm_fw_comment: str - vm_fw_dest: str - vm_fw_dport: str - vm_fw_enable: int - vm_fw_iface: str - vm_fw_log: str - vm_fw_pos: int - vm_fw_proto: str - vm_fw_source: str - vm_fw_sport: str - vm_fw_type: str - vm_id: str - -class Reply_ProxmoxFirewallWithStorageName_ListIptablesRules(BaseModel): - - rc: int = Field(0, description="RETURN code (0 = OK)") - result: list[Reply_ProxmoxFirewallWithStorageName_ListIptablesRulesItem] - - model_config = { - "json_schema_extra": { - "example": { - "rc": 0, - "result": [ - { - "action": "firewall_vm_list_iptables_rule", - "proxmox_node": "px-testing", - "source": "proxmox", - "vm_fw_action": "ACCEPT", - "vm_fw_enable": 0, - "vm_fw_log": "nolog", - "vm_fw_pos": 0, - "vm_fw_type": "in", - "vm_id": "100" - }, - { - "action": "firewall_vm_list_iptables_rule", - "proxmox_node": "px-testing", - "source": "proxmox", - "vm_fw_action": "ACCEPT", - "vm_fw_comment": "Test comment", - "vm_fw_dest": "0.0.0.0/0", - "vm_fw_dport": "22", - "vm_fw_enable": 1, - "vm_fw_iface": "net0", - "vm_fw_log": "warning", - "vm_fw_pos": 1, - "vm_fw_proto": "tcp", - "vm_fw_source": "192.168.1.0/24", - "vm_fw_sport": "1024", - "vm_fw_type": "in", - "vm_id": "100" - }, - { - "action": "firewall_vm_list_iptables_rule", - "proxmox_node": "px-testing", - "source": "proxmox", - "vm_fw_action": "ACCEPT", - "vm_fw_dport": "80", - "vm_fw_enable": 1, - "vm_fw_pos": 2, - "vm_fw_proto": "tcp", - "vm_fw_type": "out", - "vm_id": "100" - } - - ] - } - } - } diff --git a/app/schemas/proxmox/network/node_name/add_network.py b/app/schemas/proxmox/network/node_name/add_network.py deleted file mode 100644 index a87f9d8..0000000 --- a/app/schemas/proxmox/network/node_name/add_network.py +++ /dev/null @@ -1,155 +0,0 @@ - - -from enum import Enum -from typing import List, Literal -from pydantic import BaseModel, Field - -# -# ISSUE - #11 -# - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### - -class Request_ProxmoxNetwork_WithNodeName_AddNetworkInterface(BaseModel): - - proxmox_node: str = Field( - ..., - # default= "px-testing", - description = "Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" - ) - - as_json: bool = Field( - default=True, - description="If true : JSON output else : raw output" - ) - - # - - bridge_ports: str | None = Field( - default=None, - description="Bridge ports", - pattern=r"^[a-zA-Z0-9._-]+$" - ) - - iface_name: str | None = Field( - ..., - description="Interface name", - pattern=r"^[a-zA-Z0-9._-]+$" - ) - - iface_type: str | None = Field( - ..., - description="Interface type - ethernet, ovs, bridge", - pattern=r"^[a-zA-Z]+$" - ) - - iface_autostart: int | None = Field( - ..., - description="Autostart flag - 0 = no, 1 = yes" - ) - - ip_address: str | None = Field( - default=None, - description="ipv4 address", - pattern=r"^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$" - ) - - ip_netmask: str | None = Field( - default=None, - description="ipv4 netmask", - pattern=r"^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$|^\/[0-9]{1,2}$" - ) - - ip_gateway: str | None = Field( - default=None, - description="ipv4 gateway", - pattern=r"^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$" - ) - - ovs_bridge: str | None = Field( - default=None, - description="OVS bridge name", - pattern=r"^[a-zA-Z0-9._-]+$" - ) - - model_config = { - "json_schema_extra": { - "example":[ { - "proxmox_node": "px-testing", - "as_json": "true", - # - "iface_name": "vmbr142", - "iface_type": "bridge", - "bridge_ports": "enp87s0", - "iface_autostart": 1, - "ip_address": "192.168.99.2", - "ip_netmask": "255.255.255.0" - }, - # to test. - {"proxmox_node": "px-testing", "iface_name": "vmbr42", "iface_type": "bridge", "bridge_ports": "enp87s0"}, - {"proxmox_node": "px-testing", "iface_name": "vmbr42", "iface_type": "bridge", "bridge_ports": "enp87s0", "iface_autostart": 1, "ip_address": "192.168.88.2", "ip_netmask": "255.255.255.0"}, - {"proxmox_node": "px-testing", "iface_name": "vmbr42", "iface_type": "bridge", "bridge_ports": "enp87s0", "iface_autostart": 1, "ip_address": "192.168.88.2", "ip_netmask": "255.255.255.0", "ip_gateway": "192.168.88.1"}, - {"proxmox_node": "px-testing", "iface_name": "vmbr42", "iface_type": "OVSBridge", "ovs_bridge": "enp87s0", "iface_autostart": 1, "ip_address": "192.168.88.2", "ip_netmask": "255.255.255.0"}, - {"proxmox_node": "px-testing", "iface_name": "bridge42", "iface_type": "bridge", "iface_autostart": 1}, - {"proxmox_node": "px-testing", "iface_name": "bond", "iface_type": "bond", "iface_autostart": 1}, - {"proxmox_node": "px-testing", "iface_name": "alias", "iface_type": "alias","iface_autostart": 1}, - {"proxmox_node": "px-testing", "iface_name": "vlan", "iface_type": "vlan", "iface_autostart": 1}, - {"proxmox_node": "px-testing", "iface_name": "ovsbridge42", "iface_type": "OVSBridge", "iface_autostart": 1}, - {"proxmox_node": "px-testing", "iface_name": "ovsbond42", "iface_type": "OVSBond", "iface_autostart": 1}, - {"proxmox_node": "px-testing", "iface_name": "ovsport42", "iface_type": "OVSPort", "iface_autostart": 1}, - {"proxmox_node": "px-testing", "iface_name": "ovintport42", "iface_type": "OVSIntPort", "iface_autostart": 1}, - {"proxmox_node": "px-testing", "iface_name": "vmnet42", "iface_type": "vnet", "iface_autostart": 1}, - ] - } - } - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### - -class Reply_ProxmoxNetwork_WithNodeName_AddNetworkInterfaceItem(BaseModel): - - action: Literal["vm_DeleteIptablesRule_usage"] - source: Literal["proxmox"] - proxmox_node: str - as_json: bool - ## - # vm_id: int = Field(..., ge=1) - # vm_id: str - # vm_fw_pos: int - - bridge_ports: str - iface_name: str - iface_type: str - iface_autostart: int - ip_address: str - ip_netmask: str - ip_gateway: str - ovs_bridge: str - - -class Reply_ProxmoxNetwork_WithNodeName_AddNetworkInterface(BaseModel): - - rc: int = Field(0, description="RETURN code (0 = OK)") - result: list[Reply_ProxmoxNetwork_WithNodeName_AddNetworkInterfaceItem] - - model_config = { - "json_schema_extra": { - "example": { - "rc": 0, - "result": [ - { - - "action": "network_add_interfaces_node", - "proxmox_node": "px-testing", - "source": "proxmox", - # - "bridge_ports": "enp87s0", - "iface_autostart": "1", - "iface_name": "vmbr142", - "ip_address": "192.168.99.2", - "ip_netmask": "255.255.255.0", - } - ] - } - } - } diff --git a/app/schemas/proxmox/network/node_name/delete_network.py b/app/schemas/proxmox/network/node_name/delete_network.py deleted file mode 100644 index e745acf..0000000 --- a/app/schemas/proxmox/network/node_name/delete_network.py +++ /dev/null @@ -1,78 +0,0 @@ - - -from enum import Enum -from typing import List, Literal -from pydantic import BaseModel, Field - -# -# ISSUE - #11 -# - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### - -class Request_ProxmoxNetwork_WithNodeName_DeleteInterface(BaseModel): - - proxmox_node: str = Field( - ..., - # default= "px-testing", - description = "Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" - ) - # - - iface_name: str | None = Field( - description="Interface name", - pattern=r"^[a-zA-Z0-9._-]+$" - ) - - as_json: bool = Field( - default=True, - description="If true : JSON output else : raw output" - ) - - model_config = { - "json_schema_extra": { - "example": { - "proxmox_node": "px-testing", - "storage_name": "local", - "as_json": True, - # - "iface_name":"vmbr42", - } - } - } - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### - -class Reply_ProxmoxNetwork_WithNodeName_DeleteInterfaceItem(BaseModel): - - action: Literal["vm_DeleteIptablesRule_usage"] - source: Literal["proxmox"] - proxmox_node: str - ## - iface_name: str - # vm_id: int = Field(..., ge=1) - # vm_id: str - # vm_fw_pos: int - -class Reply_ProxmoxNetwork_WithNodeName_DeleteInterface(BaseModel): - - rc: int = Field(0, description="RETURN code (0 = OK)") - result: list[Reply_ProxmoxNetwork_WithNodeName_DeleteInterfaceItem] - - model_config = { - "json_schema_extra": { - "example": { - "rc": 0, - "result": [ - { - "action": "network_delete_interfaces_node", - "source": "proxmox", - "proxmox_node": "px-testing", - ## - "iface_name": "vmbr42", - } - ] - } - } - } diff --git a/app/schemas/proxmox/network/node_name/list_network.py b/app/schemas/proxmox/network/node_name/list_network.py deleted file mode 100644 index aaf82c5..0000000 --- a/app/schemas/proxmox/network/node_name/list_network.py +++ /dev/null @@ -1,148 +0,0 @@ - - -from enum import Enum -from typing import List, Literal -from pydantic import BaseModel, Field - -# -# ISSUE - #11 -# - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### - -class Request_ProxmoxNetwork_WithNodeName_ListInterface(BaseModel): - - proxmox_node: str = Field( - ..., - # default= "px-testing", - description = "Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" - ) - - as_json: bool = Field( - default=True, - description="If true : JSON output else : raw output" - ) - # - - model_config = { - "json_schema_extra": { - "example": { - "proxmox_node": "px-testing", - "as_json": True, - } - } - } - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### - -class Reply_ProxmoxNetwork_WithNodeName_ListInterfaceItem(BaseModel): - - action: Literal["vm_DeleteIptablesRule_usage"] - source: Literal["proxmox"] - proxmox_node: str - ## - # vm_id: int = Field(..., ge=1) - vm_id: str - vm_fw_pos: int - -class Reply_ProxmoxNetwork_WithNodeName_ListInterface(BaseModel): - - rc: int = Field(0, description="RETURN code (0 = OK)") - result: list[Reply_ProxmoxNetwork_WithNodeName_ListInterfaceItem] - - model_config = { - "json_schema_extra": { - "example": { - "rc": 0, - "result": [ - { - "action": "network_list_interfaces_node", - "iface": "wlp89s0", - "iface_priority": 8, - "ip_settings_method": "manual", - "ip_settings_method6": "manual", - "proxmox_node": "px-testing", - "source": "proxmox" - }, - { - "action": "network_list_interfaces_node", - "iface": "enp2s0f0np0", - "iface_exists": 1, - "iface_priority": 5, - "ip_settings_method": "manual", - "ip_settings_method6": "manual", - "proxmox_node": "px-testing", - "source": "proxmox" - }, - { - "action": "network_list_interfaces_node", - "iface": "vmbr0", - "iface_active": 1, - "iface_autostart": 1, - "iface_bridge_fd": "0", - "iface_bridge_ports": "enp88s0", - "iface_bridge_stp": "off", - "iface_priority": 7, - "ip_settings_address": "192.168.42.201", - "ip_settings_cidr": "192.168.42.201/24", - "ip_settings_gateway": "192.168.42.1", - "ip_settings_method": "static", - "ip_settings_method6": "manual", - "ip_settings_netmask": "24", - "proxmox_node": "px-testing", - "source": "proxmox" - }, - { - "action": "network_list_interfaces_node", - "iface": "enp87s0", - "iface_active": 1, - "iface_exists": 1, - "iface_priority": 4, - "ip_settings_method": "manual", - "ip_settings_method6": "manual", - "proxmox_node": "px-testing", - "source": "proxmox" - }, - { - "action": "network_list_interfaces_node", - "iface": "enp2s0f1np1", - "iface_exists": 1, - "iface_priority": 6, - "ip_settings_method": "manual", - "ip_settings_method6": "manual", - "proxmox_node": "px-testing", - "source": "proxmox" - }, - { - "action": "network_list_interfaces_node", - "iface": "vmbr142", - "iface_autostart": 1, - "iface_bridge_fd": "0", - "iface_bridge_ports": "enp87s0", - "iface_bridge_stp": "off", - "iface_priority": 9, - "ip_settings_address": "192.168.99.2", - "ip_settings_cidr": "192.168.99.2/24", - "ip_settings_method": "static", - "ip_settings_method6": "manual", - "ip_settings_netmask": "24", - "proxmox_node": "px-testing", - "source": "proxmox" - }, - { - "action": "network_list_interfaces_node", - "iface": "enp88s0", - "iface_active": 1, - "iface_exists": 1, - "iface_priority": 3, - "ip_settings_method": "manual", - "ip_settings_method6": "manual", - "proxmox_node": "px-testing", - "source": "proxmox" - } - - ] - } - } - } diff --git a/app/schemas/proxmox/network/vm_id/add_network.py b/app/schemas/proxmox/network/vm_id/add_network.py deleted file mode 100644 index dd18d34..0000000 --- a/app/schemas/proxmox/network/vm_id/add_network.py +++ /dev/null @@ -1,139 +0,0 @@ - - -from enum import Enum -from typing import List, Literal -from pydantic import BaseModel, Field - -# -# ISSUE - #11 -# - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### - -class Request_ProxmoxNetwork_WithVmId_AddNetwork(BaseModel): - - proxmox_node: str = Field( - ..., - # default= "px-testing", - description = "Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" - ) - - as_json: bool = Field( - default=True, - description="If true : JSON output else : raw output" - ) - # - - vm_id: str = Field( - ..., - # default="4000", - description="Virtual machine id", - pattern=r"^[0-9]+$" - ) - - # quick classic fields - - iface_model: str | None = Field( - description="Interface model- virtio, e1000, rtl8139", - pattern=r"^[A-Za-z0-9._-]+$" - ) - - # net_index: int | None = Field( - # description="Bridge name for interface - vmbr0, vmbr142", - # # pattern=r"^[A-Za-z0-9._-]+$" - # ) - - iface_bridge: str | None = Field( - description="Bridge name for interface - vmbr0, vmbr142", - pattern=r"^[A-Za-z0-9._-]+$" - ) - - vm_vmnet_id: int | None = Field( - description="Network device index - 0, 1, 2, ..." - ) - - #### below fields to test : - - iface_trunks: bool | None = Field( - description="Enable trunk - allow multiple vlan on interface" - ) - - iface_tag: int | None = Field( - description="VLAN tag id" - ) - - iface_rate: float | None = Field( - description="Limit bandwith - Mbps - 0 to x" - ) - - iface_queues: int | None = Field( - description="Allocated amount allocated tx/rx on interface" - ) - - iface_mtu: int | None = Field( - description="MTU" - ) - - iface_macaddr: str | None = Field( - description="MAC address - hexa format", - pattern = r'^(?:[0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$' - ) - - iface_link_down: bool | None = Field( - description="Force to set down the interface" - ) - - iface_firewall: bool | None = Field( - description="Apply firewall rules on this interface" - ) - - model_config = { - "json_schema_extra": { - "example": { - "proxmox_node": "px-testing", - "storage_name": "local", - "as_json": True, - # - "vm_id": "1000", - "vm_vmnet_id": "1", - "iface_model": "virtio", - "iface_bridge": "vmbr142", - } - } - } - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### - -class Reply_ProxmoxNetwork_WithVmId_AddNetworkInterfaceItem(BaseModel): - - action: Literal["vm_DeleteIptablesRule_usage"] - source: Literal["proxmox"] - proxmox_node: str - ## - # vm_id: int = Field(..., ge=1) - vm_id: str - vm_fw_pos: int - -class Reply_ProxmoxNetwork_WithVmId_AddNetworkInterface(BaseModel): - - rc: int = Field(0, description="RETURN code (0 = OK)") - result: list[Reply_ProxmoxNetwork_WithVmId_AddNetworkInterfaceItem] - - model_config = { - "json_schema_extra": { - "example": { - "rc": 0, - "result": [ - { - "action": "network_add_interfaces_vm", - "source": "proxmox", - "proxmox_node": "px-testing", - ## - "vm_id": "1000", - "iface_model": "virtio", - } - ] - } - } - } diff --git a/app/schemas/proxmox/network/vm_id/delete_network.py b/app/schemas/proxmox/network/vm_id/delete_network.py deleted file mode 100644 index daac6ce..0000000 --- a/app/schemas/proxmox/network/vm_id/delete_network.py +++ /dev/null @@ -1,87 +0,0 @@ - - -from enum import Enum -from typing import List, Literal -from pydantic import BaseModel, Field - -# -# ISSUE - #11 -# - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### - -class Request_ProxmoxNetwork_WithVmId_DeleteNetwork(BaseModel): - - proxmox_node: str = Field( - ..., - # default= "px-testing", - description = "Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" - ) - - as_json: bool = Field( - default=True, - description="If true : JSON output else : raw output" - ) - # - - vm_id: str = Field( - ..., - # default="4000", - description="Virtual machine id", - pattern=r"^[0-9]+$" - ) - - vm_vmnet_id: int | None = Field( - description="Network device index - 0, 1, 2, ..." - ) - - model_config = { - "json_schema_extra": { - "example": { - "proxmox_node": "px-testing", - "storage_name": "local", - "as_json": True, -# - "vm_id":"1000", - "vm_vmnet_id":1, - - } - } - } - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### - -class Reply_ProxmoxNetwork_WithVmId_DeleteNetworkInterfaceItem(BaseModel): - - action: Literal["vm_DeleteIptablesRule_usage"] - source: Literal["proxmox"] - proxmox_node: str - ## - # vm_id: int = Field(..., ge=1) - vm_id: str - vm_fw_pos: int - -class Reply_ProxmoxNetwork_WithVmId_DeleteNetworkInterface(BaseModel): - - rc: int = Field(0, description="RETURN code (0 = OK)") - result: list[Reply_ProxmoxNetwork_WithVmId_DeleteNetworkInterfaceItem] - - model_config = { - "json_schema_extra": { - "example": { - "rc": 0, - "result": [ - { - "action": "network_delete_interfaces_vm", - "source": "proxmox", - "proxmox_node": "px-testing", - ## - "vm_id": "1000", - "iface_model": "virtio" - - } - ] - } - } - } diff --git a/app/schemas/proxmox/network/vm_id/list_network.py b/app/schemas/proxmox/network/vm_id/list_network.py deleted file mode 100644 index bc978e2..0000000 --- a/app/schemas/proxmox/network/vm_id/list_network.py +++ /dev/null @@ -1,81 +0,0 @@ - - -from enum import Enum -from typing import List, Literal -from pydantic import BaseModel, Field - -# -# ISSUE - #11 -# - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### - -class Request_ProxmoxNetwork_WithVmId_ListNetwork(BaseModel): - - proxmox_node: str = Field( - ..., - # default= "px-testing", - description = "Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" - ) - - as_json: bool = Field( - default=True, - description="If true : JSON output else : raw output" - ) - # - - vm_id: str = Field( - ..., - # default="4000", - description="Virtual machine id", - pattern=r"^[0-9]+$" - ) - - model_config = { - "json_schema_extra": { - "example": { - "proxmox_node": "px-testing", - "as_json": True, - "vm_id": "1001", - } - } - } - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### - -class Reply_ProxmoxNetwork_WithVmId_ListNetworkInterfaceItem(BaseModel): - - action: Literal["vm_DeleteIptablesRule_usage"] - source: Literal["proxmox"] - proxmox_node: str - ## - # vm_id: int = Field(..., ge=1) - vm_id: str - -class Reply_ProxmoxNetwork_WithVmId_ListNetworkInterface(BaseModel): - - rc: int = Field(0, description="RETURN code (0 = OK)") - result: list[Reply_ProxmoxNetwork_WithVmId_ListNetworkInterfaceItem] - - model_config = { - "json_schema_extra": { - "example": { - "rc": 0, - "result": [ - { - "action": "network_list_interfaces_vm", - "source": "proxmox", - "proxmox_node": "px-testing", - ## - "vm_id": "1000", - "vm_network_bridge": "vmbr0", - "vm_network_device": "net0", - "vm_network_mac": "AA:BB:CC:DD:EE:FF", - "vm_network_type": "virtio" - - } - ] - } - } - } diff --git a/app/schemas/proxmox/storage/__init__.py b/app/schemas/proxmox/storage/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/schemas/proxmox/storage/download_iso.py b/app/schemas/proxmox/storage/download_iso.py deleted file mode 100644 index 2371493..0000000 --- a/app/schemas/proxmox/storage/download_iso.py +++ /dev/null @@ -1,119 +0,0 @@ - - -from enum import Enum -from typing import List, Literal -from pydantic import BaseModel, Field - -# -# ISSUE - #17 -# - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### - -class Request_ProxmoxStorage_DownloadIso(BaseModel): - - proxmox_node: str = Field( - ..., - # default= "px-testing", - description = "Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" - ) - - as_json: bool = Field( - default=True, - description="If true : JSON output else : raw output" - ) - # - - proxmox_storage: str = Field( - ..., - description="Target Proxmox storage name", - pattern=r"^[A-Za-z0-9-]+$", - ) - - iso_file_content_type: str = Field( - ..., - description="MIME type of the ISO file", - pattern=r"^[A-Za-z0-9-]*$" - # pattern = r"^application/(?:x-)?iso9660-image$", - ) - - iso_file_name: str = Field( - ..., - description="ISO file name - must end with .iso", - pattern=r"^[A-Za-z0-9._-]+\.iso$", - ) - - iso_url: str = Field( - ..., - description="http|https URL where the ISO will be downloaded from.", - pattern=r"^https?://[^\s]+$", - ) - - model_config = { - "json_schema_extra": { - "example": { - "proxmox_node": "px-testing", - "storage_name": "local", - # - "iso_file_content_type": "iso", - "iso_file_name": "ubuntu-24.04-live-server-amd64.iso", - "iso_url": "https://releases.ubuntu.com/24.04/ubuntu-24.04-live-server-amd64.iso", - "as_json": True, - } - } - } - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### - -class Reply_ProxmoxStorage_DownloadIsoItem(BaseModel): - - action: Literal["storage_download_iso"] - source: Literal["proxmox"] - proxmox_node: str - vm_id: int = Field(..., ge=1) - vm_name: str - # raw_data: str = Field(..., description="Raw string returned by proxmox") - cpu_allocated: int - cpu_current_usage: int - disk_current_usage: int - disk_max: int - disk_read: int - disk_write: int - net_in: int - net_out: int - ram_current_usage: int - ram_max: int - vm_status: str - vm_uptime: int - -class Reply_ProxmoxStorage_DownloadIso(BaseModel): - - rc: int = Field(0, description="RETURN code (0 = OK)") - result: list[Reply_ProxmoxStorage_DownloadIsoItem] - - model_config = { - "json_schema_extra": { - "example": { - "rc": 0, - "result": [ - { - "cpu_allocated":1, - "cpu_current_usage":0, - "disk_current_usage":0, - "disk_max":34359738368, - "disk_read":0, - "disk_write":0, - "net_in":280531583, - "net_out":6330590, - "ram_current_usage":1910544625, - "ram_max":4294967296, - "vm_id":1020, - "vm_name":"admin-web-api-kong", - "vm_status":"running", - "vm_uptime":79940 - } - ] - } - } - } diff --git a/app/schemas/proxmox/storage/list.py b/app/schemas/proxmox/storage/list.py deleted file mode 100644 index 966bbf4..0000000 --- a/app/schemas/proxmox/storage/list.py +++ /dev/null @@ -1,93 +0,0 @@ - - -from enum import Enum -from typing import List, Literal -from pydantic import BaseModel, Field - -# -# ISSUE - #17 -# - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### - -class Request_ProxmoxStorage_List(BaseModel): - - proxmox_node: str = Field( - ..., - # default= "px-testing", - description="Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" - ) - - as_json: bool = Field( - default=True, - description="If true : JSON output else : raw output" - ) - # - - storage_name: str = Field( - ..., - # default= "px-testing", - description="Proxmox storage name", - pattern=r"^[A-Za-z0-9-]*$" - ) - - model_config = { - "json_schema_extra": { - "example": { - "proxmox_node": "px-testing", - "storage_name": "local", - "as_json": True - } - } - } - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### - -class Reply_ProxmoxStorage_ListItem(BaseModel): - - action: Literal["storage_list"] - source: Literal["proxmox"] - proxmox_node: str - # - storage_active: int - storage_content_types: str - storage_is_enable: int - storage_is_share: int - storage_name : str - storage_space_available: int - storage_space_total: int - storage_space_used: int - storage_space_used_fraction: float - storage_type: str - -class Reply_ProxmoxStorage_List(BaseModel): - - rc: int = Field(0, description="RETURN code (0 = OK)") - result: list[Reply_ProxmoxStorage_ListItem] - - model_config = { - "json_schema_extra": { - "example": { - "rc": 0, - "result": [ - { - "action": "storage_list", - "proxmox_node": "px-testing", - "source": "proxmox", - # - "storage_active": 1, - "storage_content_types": "images,rootdir", - "storage_is_enable": 1, - "storage_is_share": 0, - "storage_name": "local-lvm", - "storage_space_available": 3600935440666, - "storage_space_total": 3836496314368, - "storage_space_used": 235560873702, - "storage_space_used_fraction": 0.0613999999999491, - "storage_type": "lvmthin" - } - ] - } - } - } diff --git a/app/schemas/proxmox/storage/storage_name/list_iso.py b/app/schemas/proxmox/storage/storage_name/list_iso.py deleted file mode 100644 index 2ad6079..0000000 --- a/app/schemas/proxmox/storage/storage_name/list_iso.py +++ /dev/null @@ -1,89 +0,0 @@ - - -from enum import Enum -from typing import List, Literal -from pydantic import BaseModel, Field - -# -# ISSUE - #17 -# - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### - -class Request_ProxmoxStorage_ListIso(BaseModel): - - proxmox_node: str = Field( - ..., - # default= "px-testing", - description = "Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" - ) - - as_json: bool = Field( - default=True, - description="If true : JSON output else : raw output" - ) - # - - storage_name: str = Field( - ..., - # default= "px-testing", - description = "Proxmox storage name", - pattern=r"^[A-Za-z0-9-]*$" - ) - - model_config = { - "json_schema_extra": { - "example": { - "proxmox_node": "px-testing", - "storage_name": "local", - "as_json": True - - } - } - } - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### - -class Reply_ProxmoxStorageWithStorageName_ListIsoItem(BaseModel): - - action: Literal["storage_list_iso"] - source: Literal["proxmox"] - proxmox_node: str - ## - # vm_id: int = Field(..., ge=1) - iso_content: str - iso_ctime: int - iso_format: str - iso_size: int - iso_vol_id: str - local: str - storage_name: str - -class Reply_ProxmoxStorageWithStorageName_ListIso(BaseModel): - - rc: int = Field(0, description="RETURN code (0 = OK)") - result: list[Reply_ProxmoxStorageWithStorageName_ListIsoItem] - - model_config = { - "json_schema_extra": { - "example": { - "rc": 0, - "result": [ - { - "action": "storage_list_iso", - "source": "proxmox", - "proxmox_node": "px-testing", - ## - "iso_content": "iso", - "iso_ctime": 1753343734, - "iso_format": "iso", - "iso_size": 614746112, - "iso_vol_id": - "local:iso/noble-server-cloudimg-amd64.img", - "storage_name": "local", - } - ] - } - } - } diff --git a/app/schemas/proxmox/storage/storage_name/list_template.py b/app/schemas/proxmox/storage/storage_name/list_template.py deleted file mode 100644 index 0abf125..0000000 --- a/app/schemas/proxmox/storage/storage_name/list_template.py +++ /dev/null @@ -1,87 +0,0 @@ - - -from enum import Enum -from typing import List, Literal -from pydantic import BaseModel, Field - -# -# ISSUE - #17 -# - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### - -class Request_ProxmoxStorage_ListTemplate(BaseModel): - - proxmox_node: str = Field( - ..., - # default= "px-testing", - description = "Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" - ) - - as_json: bool = Field( - default=True, - description="If true : JSON output else : raw output" - ) - # - - storage_name: str = Field( - ..., - # default= "px-testing", - description = "Proxmox storage name", - pattern=r"^[A-Za-z0-9-]*$" - ) - - model_config = { - "json_schema_extra": { - "example": { - "proxmox_node": "px-testing", - "storage_name": "local", - "as_json": True - - } - } - } - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### - -class Reply_ProxmoxStorageWithStorageName_ListTemplateItem(BaseModel): - - action: Literal["storage_list_template"] - source: Literal["proxmox"] - proxmox_node: str - - # vm_id: int = Field(..., ge=1) - storage_name: str - template_content: str - template_ctime: int - template_format: str - template_size: int - template_vol_id: str - -class Reply_ProxmoxStorageWithStorageName_ListTemplate(BaseModel): - - rc: int = Field(0, description="RETURN code (0 = OK)") - result: list[Reply_ProxmoxStorageWithStorageName_ListTemplateItem] - - model_config = { - "json_schema_extra": { - "example": { - "rc": 0, - "result": [ - { - "action": "storage_list_template", - "proxmox_node": "px-testing", - "source": "proxmox", - # - "storage_name": "local", - "template_content": "vztmpl", - "template_ctime": 1749734175, - "template_format": "tzst", - "template_size": 126515062, - "template_vol_id": "local:vztmpl/debian-12-standard_12.7-1_amd64.tar.zst" - } - ] - } - } - } diff --git a/app/schemas/proxmox/vm_id/clone.py b/app/schemas/proxmox/vm_id/clone.py deleted file mode 100644 index 14cb87d..0000000 --- a/app/schemas/proxmox/vm_id/clone.py +++ /dev/null @@ -1,111 +0,0 @@ - -from typing import Literal -from pydantic import BaseModel, Field - -# -# ISSUE - #3 -# - - -class Request_ProxmoxVmsVMID_Clone(BaseModel): - - # '{"vm_id":100, - # "vm_new_id":1001, - # "vm_name":"test-cloned", - # "vm_description":"my description"}' - - proxmox_node: str = Field( - ..., - # default= "px-testing", - description = "Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" - ) - - as_json: bool = Field( - default=True, - description="If true : JSON output else : raw output" - ) - # - - vm_id: str = Field( - ..., - # default="4000", - description="Virtual machine id", - pattern=r"^[0-9]+$" - ) - - vm_new_id: str = Field( - ..., - # default="5005", - description="New virtual machine id", - pattern=r"^[0-9]+$" - ) - - vm_description: str | None = Field( - default="cloned-vm", - description="Virtual machine meta description field", - pattern = r"^[A-Za-z0-9\s.,_\-]*$" - ) - - vm_name: str = Field( - ..., - # default="new-vm", - description="Virtual machine meta name", - pattern = r"^[A-Za-z0-9-]*$" - ) - - model_config = { - "json_schema_extra": { - "example": { - "proxmox_node": "px-testing", - "vm_id": "2000", - "vm_new_id": "3000", - "vm_name":"test-cloned", - "vm_description":"my description" - } - } - } - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### -#### #### #### #### #### #### #### #### #### #### #### #### #### #### - -class Reply_ProxmoxVmsVMID_CloneItem(BaseModel): - - action: Literal["vm_clone"] - source: Literal["proxmox"] - proxmox_node: str - vm_id: int = Field(..., ge=1) - vm_id_clone_from: int = Field(..., ge=1) - vm_name: str - vm_description: str - raw_info: str = Field(..., description="Raw string returned by proxmox") - - -class Reply_ProxmoxVmsVMID_Clone(BaseModel): - - rc: int = Field(0, description="RETURN code (0 = OK)") - result: list[Reply_ProxmoxVmsVMID_CloneItem] - - model_config = { - "json_schema_extra": { - "example": { - "rc": 0, - "result": [ - { - "action": "vm_clone", - "proxmox_node": "px-testing", - "raw_info": { - "data": "UPID:px-testing:0027CE9B:167F1A2C:68C03C17:qmclone:4004:API_master@pam!API_master:"}, - "source": "proxmox", - "vm_description": "my description", - "vm_id": "5004", - "vm_id_clone_from": "4004", - "vm_name": "test-cloned" - } - ] - } - } - } - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### -#### #### #### #### #### #### #### #### #### #### #### #### #### #### diff --git a/app/schemas/proxmox/vm_id/config/vm_get_config.py b/app/schemas/proxmox/vm_id/config/vm_get_config.py deleted file mode 100644 index 0601d91..0000000 --- a/app/schemas/proxmox/vm_id/config/vm_get_config.py +++ /dev/null @@ -1,104 +0,0 @@ - -from typing import Literal -from pydantic import BaseModel, Field - -# -# ISSUE - #5 -# - -class Request_ProxmoxVmsVMID_VmGetConfig(BaseModel): - - proxmox_node: str = Field( - ..., - # default= "px-testing", - description = "Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" - ) - - as_json: bool = Field( - default=True, - description="If true : JSON output else : raw output" - ) - # - - vm_id: str = Field( - ..., - # default="4000", - description="Virtual machine id", - pattern=r"^[0-9]+$" - ) - - model_config = { - "json_schema_extra": { - "example": { - "proxmox_node": "px-testing", - "vm_id": "1111", - "as_json": True, - } - } - } - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### -#### #### #### #### #### #### #### #### #### #### #### #### #### #### - -class Reply_ProxmoxVmsVMID_VmGetConfigItem(BaseModel): - - action: Literal["vm_get_config"] - source: Literal["proxmox"] - proxmox_node: str - vm_id: int = Field(..., ge=1) - vm_name: str - raw_data: str = Field(..., description="Raw string returned by proxmox") - - -class Reply_ProxmoxVmsVMID_VmGetConfig(BaseModel): - - rc: int = Field(0, description="RETURN code (0 = OK)") - result: list[Reply_ProxmoxVmsVMID_VmGetConfigItem] - - model_config = { - "json_schema_extra": { - "example": { - "rc": 0, - "result": [ - { - "action": "vm_get_config", - "proxmox_node": "px-testing", - "raw_data": { - "data": { - "balloon": 0, - "boot": "c", - "bootdisk": "scsi0", - "cipassword": "**********", - "ciuser": "alice", - "cores": 2, - "cpu": "host", - "digest": "29bec92_redacted", - "ide2": "local:1000/vm-1000-cloudinit.qcow2,media=cdrom,size=4M", - "ipconfig0": "ip=192.168.42.100/24,gw=192.168.42.1", - "memory": "8192", - "meta": "creation-qemu=9.0.2,ctime=1757418890", - "name": "admin-wazuh", - "net0": "virtio=BC:24:11:CB:B3:C7,bridge=vmbr0", - "scsi0": "local-lvm:vm-1000-disk-0,size=64G", - "scsihw": "virtio-scsi-pci", - "serial0": "socket", - "smbios1": "uuid=82c50ddc-a24f-4cbc-a013-c0e846f230fc", - "sockets": 1, - "sshkeys": "ssh-ed25519%20AAAAC....redacted", - "tags": "admin", - "vga": "serial0", - "vmgenid": "c7426562-ad4b-4719-81a1-72328f7ec018" - } - }, - "source": "proxmox", - "vm_id": "1000" - } - ] - } - } - } - - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### -#### #### #### #### #### #### #### #### #### #### #### #### #### #### diff --git a/app/schemas/proxmox/vm_id/config/vm_get_config_cdrom.py b/app/schemas/proxmox/vm_id/config/vm_get_config_cdrom.py deleted file mode 100644 index b02072c..0000000 --- a/app/schemas/proxmox/vm_id/config/vm_get_config_cdrom.py +++ /dev/null @@ -1,86 +0,0 @@ - -from typing import Literal -from pydantic import BaseModel, Field - -# -# ISSUE - #5 -# - -class Request_ProxmoxVmsVMID_VmGetConfigCdrom(BaseModel): - - proxmox_node: str = Field( - ..., - # default= "px-testing", - description = "Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" - ) - - as_json: bool = Field( - default=True, - description="If true : JSON output else : raw output" - ) - # - - vm_id: str = Field( - ..., - # default="4000", - description="Virtual machine id", - pattern=r"^[0-9]+$" - ) - - model_config = { - "json_schema_extra": { - "example": { - "proxmox_node": "px-testing", - "vm_id": "1111", - "as_json": True, - } - } - } - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### -#### #### #### #### #### #### #### #### #### #### #### #### #### #### - -class Reply_ProxmoxVmsVMID_VmGetConfigCdromItem(BaseModel): - - action: Literal["vm_get_config_cdrom"] - source: Literal["proxmox"] - proxmox_node : str - vm_id : str # int = Field(..., ge=1) - vm_cdrom_device: str - vm_cdrom_iso : str - vm_cdrom_media : str - vm_cdrom_size : str - - # vm_name: str - # raw_data: str = Field(..., description="Raw string returned by proxmox") - - -class Reply_ProxmoxVmsVMID_VmGetConfigCdrom(BaseModel): - - rc: int = Field(0, description="RETURN code (0 = OK)") - result: list[Reply_ProxmoxVmsVMID_VmGetConfigCdromItem] - - model_config = { - "json_schema_extra": { - "example": { - "rc": 0, - "result": [ - { - "action": "vm_get_config_cdrom", - "proxmox_node": "px-testing", - "source": "proxmox", - "vm_cdrom_device": "ide2", - "vm_cdrom_iso": "local:1000/vm-1000-cloudinit.qcow2", - "vm_cdrom_media": "cdrom", - "vm_cdrom_size": "4M", - "vm_id": "1000" - } - ] - } - } - } - - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### -#### #### #### #### #### #### #### #### #### #### #### #### #### #### diff --git a/app/schemas/proxmox/vm_id/config/vm_get_config_cpu.py b/app/schemas/proxmox/vm_id/config/vm_get_config_cpu.py deleted file mode 100644 index d3b01de..0000000 --- a/app/schemas/proxmox/vm_id/config/vm_get_config_cpu.py +++ /dev/null @@ -1,83 +0,0 @@ - -from typing import Literal -from pydantic import BaseModel, Field - -# -# ISSUE - #5 -# - -class Request_ProxmoxVmsVMID_VmGetConfigCpu(BaseModel): - - proxmox_node: str = Field( - ..., - # default= "px-testing", - description = "Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" - ) - - as_json: bool = Field( - default=True, - description="If true : JSON output else : raw output" - ) - # - - vm_id: str = Field( - ..., - # default="4000", - description="Virtual machine id", - pattern=r"^[0-9]+$" - ) - - model_config = { - "json_schema_extra": { - "example": { - "proxmox_node": "px-testing", - "vm_id": "1111", - "as_json": True, - } - } - } - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### -#### #### #### #### #### #### #### #### #### #### #### #### #### #### - -class Reply_ProxmoxVmsVMID_VmGetConfigCpuItem(BaseModel): - - action: Literal["vm_get_config_cpu"] - source: Literal["proxmox"] - proxmox_node: str - vm_id : str # int = Field(..., ge=1) - vm_arch : str #to fix ? - vm_cores : str #to fix ? - vm_sockets : str #to fix ? - # vm_name: str - # raw_data: str = Field(..., description="Raw string returned by proxmox") - - -class Reply_ProxmoxVmsVMID_VmGetConfigCpu(BaseModel): - - rc: int = Field(0, description="RETURN code (0 = OK)") - result: list[Reply_ProxmoxVmsVMID_VmGetConfigCpuItem] - - model_config = { - "json_schema_extra": { - "example": { - "rc": 0, - "result": [ - { - "action":"vm_get_config_cpu", - "proxmox_node":"px-testing", - "source":"proxmox", - "vm_arch":"host", - "vm_cores":"2", - "vm_id":"1000", - "vm_sockets":"1" - } - ] - } - } - } - - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### -#### #### #### #### #### #### #### #### #### #### #### #### #### #### diff --git a/app/schemas/proxmox/vm_id/config/vm_get_config_ram.py b/app/schemas/proxmox/vm_id/config/vm_get_config_ram.py deleted file mode 100644 index a048f7b..0000000 --- a/app/schemas/proxmox/vm_id/config/vm_get_config_ram.py +++ /dev/null @@ -1,80 +0,0 @@ - -from typing import Literal -from pydantic import BaseModel, Field - -# -# ISSUE - #5 -# - -class Request_ProxmoxVmsVMID_VmGetConfigRam(BaseModel): - - proxmox_node: str = Field( - ..., - # default= "px-testing", - description = "Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" - ) - - as_json: bool = Field( - default=True, - description="If true : JSON output else : raw output" - ) - # - - vm_id: str = Field( - ..., - # default="4000", - description="Virtual machine id", - pattern=r"^[0-9]+$" - ) - - model_config = { - "json_schema_extra": { - "example": { - "proxmox_node": "px-testing", - "vm_id": "1111", - "as_json": True, - } - } - } - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### -#### #### #### #### #### #### #### #### #### #### #### #### #### #### - -class Reply_ProxmoxVmsVMID_VmGetConfigRamItem(BaseModel): - - action: Literal["vm_get_config_ram"] - source: Literal["proxmox"] - proxmox_node: str - vm_id : str # int = Field(..., ge=1) - vm_ram_allocated: str # wtf... - fix todo - - # vm_name: str - # raw_data: str = Field(..., description="Raw string returned by proxmox") - - -class Reply_ProxmoxVmsVMID_VmGetConfigRam(BaseModel): - - rc: int = Field(0, description="RETURN code (0 = OK)") - result: list[Reply_ProxmoxVmsVMID_VmGetConfigRamItem] - - model_config = { - "json_schema_extra": { - "example": { - "rc": 0, - "result": [ - { - "action": "vm_get_config_ram", - "proxmox_node": "px-testing", - "source": "proxmox", - "vm_id": "1000", - "vm_ram_allocated": "8192" - } - ] - } - } - } - - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### -#### #### #### #### #### #### #### #### #### #### #### #### #### #### diff --git a/app/schemas/proxmox/vm_id/config/vm_set_tag.py b/app/schemas/proxmox/vm_id/config/vm_set_tag.py deleted file mode 100644 index 71d072e..0000000 --- a/app/schemas/proxmox/vm_id/config/vm_set_tag.py +++ /dev/null @@ -1,89 +0,0 @@ - - -from typing import Literal -from pydantic import BaseModel, Field - -# -# ISSUE - #7 -# - -class Request_ProxmoxVmsVMID_VmSetTag(BaseModel): - - proxmox_node: str = Field( - ..., - # default= "px-testing", - description = "Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" - ) - as_json: bool = Field( - default=True, - description="If true : JSON output else : raw output" - ) - # - - vm_id: str = Field( - ..., - # default="4000", - description="Virtual machine id", - pattern=r"^[0-9]+$" - ) - - # vm_tag_name: list[str] = Field( - # ..., - # description="list of tags to assign to the virtual machine", - # pattern = r"^[A-Za-z0-9_, -]+$" - # ) - - vm_tag_name: str = Field( - ..., - description="Comma separated list of tags to assign to the virtual machine", - pattern=r"^[A-Za-z0-9_, -]+$" - ) - - model_config = { - "json_schema_extra": { - "example": { - "proxmox_node": "px-testing", - "vm_id": "1111", - "vm_tag_name":"group_01,group_02", - "as_json": True, - } - } - } - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### -#### #### #### #### #### #### #### #### #### #### #### #### #### #### - -class Reply_ProxmoxVmsVMID_VmSetTagItem(BaseModel): - - action: Literal["vm_get_config"] - source: Literal["proxmox"] - # proxmox_node: str - # vm_id: int = Field(..., ge=1) - # vm_name: str - # raw_data: str = Field(..., description="Raw string returned by proxmox") - - -class Reply_ProxmoxVmsVMID_VmSetTag(BaseModel): - - rc: int = Field(0, description="RETURN code (0 = OK)") - result: list[Reply_ProxmoxVmsVMID_VmSetTagItem] - - model_config = { - "json_schema_extra": { - "example": { - "rc": 0, - "result": [ - { - "action": "vm_set_tag", - "source": "proxmox", - "tags": "group_01,group_02", - } - ] - } - } - } - - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### -#### #### #### #### #### #### #### #### #### #### #### #### #### #### diff --git a/app/schemas/proxmox/vm_id/create.py b/app/schemas/proxmox/vm_id/create.py deleted file mode 100644 index 29e4765..0000000 --- a/app/schemas/proxmox/vm_id/create.py +++ /dev/null @@ -1,161 +0,0 @@ - -from typing import Literal -from pydantic import BaseModel, Field - -# -# ISSUE - #3 -# - -class Request_ProxmoxVmsVMID_Create(BaseModel): - - model_config = { - "json_schema_extra": { - "example": { - "proxmox_node": "px-testing", - "vm_id": 1111, - "vm_name": "vm_with_local_iso", - "vm_cpu": "host", - "vm_cores": 2, - "vm_sockets": 1, - "vm_memory": 2042, - "vm_disk_size": 42, - "vm_iso": "local:iso/ubuntu-24.04.2-live-server-amd64.iso" - } - } - } - -class Request_ProxmoxVmsVMID_Create(BaseModel): - - proxmox_node: str = Field( - ..., - # default= "px-testing", - description = "Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" - ) - - as_json: bool = Field( - default=True, - description="If true : JSON output else : raw output" - ) - # - - vm_id: str = Field( - ..., - # default="4000", - description="Virtual machine id", - pattern=r"^[0-9]+$" - ) - - vm_name: str = Field( - ..., - # default="new-vm", - description="Virtual machine meta name", - pattern = r"^[A-Za-z0-9-]*$" - ) - - vm_cpu: str = Field( - ..., - # default= "host", - description='CPU type/model - host)', - pattern=r"^[A-Za-z0-9._-]+$" - ) - - vm_cores: int = Field( - ..., - # default=1, - ge=1, - description="Number of cores per socket" - ) - - vm_sockets: int = Field( - ..., - # default=1, - ge=1, - description="Number of CPU sockets" - ) - - vm_memory: int = Field( - ..., - # default=1024, - ge=128, - description="Memory in MiB" - ) - - vm_disk_size: int | None = Field( - default=None, - ge=1, - description="Disk size in GiB - optional" - ) - - vm_iso: str | None = Field( - default=None, - description="ISO volume path like 'local:iso/xxx.iso' - optional", - pattern=r"^[A-Za-z0-9._-]+:iso/.+\.iso$" - ) - - model_config = { - "json_schema_extra": { - "example": { - "proxmox_node": "px-testing", - "vm_id": "1111", - "vm_name": "new-vm", - "vm_cpu": "host", - "vm_cores": 2, - "vm_sockets": 1, - "vm_memory": 2042, - "vm_disk_size": 42, - "vm_iso": "local:iso/ubuntu-24.04.2-live-server-amd64.iso" - } - } - } - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### -#### #### #### #### #### #### #### #### #### #### #### #### #### #### - -class Reply_ProxmoxVmsVMID_CreateItem(BaseModel): - - action: Literal["vm_create"] - source: Literal["proxmox"] - proxmox_node: str - vm_id : int = Field(..., ge=1) - vm_name : str - vm_cpu : str - vm_cores : int = Field(..., ge=1) - vm_sockets: int = Field(..., ge=1) - vm_memory : int = Field(..., ge=1) - vm_net0 : str - vm_scsi0 : str - raw_data : str = Field(..., description="Raw string returned by Proxmox") - - -class Reply_ProxmoxVmsVMID_Create(BaseModel): - - rc: int = Field(0, description="RETURN code (0 = OK)") - result: list[Reply_ProxmoxVmsVMID_CreateItem] - - model_config = { - "json_schema_extra": { - "example": { - "rc": 0, - "result": [ - { - "action": "vm_create", - "proxmox_node": "px-testing", - "raw_data": "UPID:px-testing:00281144:16855865:68C04C13:qmcreate:9998:API_master@pam!API_master:", - "source": "proxmox", - "vm_cores": 2, - "vm_cpu": "host", - "vm_id": 9998, - "vm_memory": 2042, - "vm_name": "vm-with-local-iso-2", - "vm_net0": "virtio,bridge=vmbr0", - "vm_scsi0": "local-lvm:42,format=raw", - "vm_sockets": 1 - } - ] - } - } - } - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### -#### #### #### #### #### #### #### #### #### #### #### #### #### #### diff --git a/app/schemas/proxmox/vm_id/delete.py b/app/schemas/proxmox/vm_id/delete.py deleted file mode 100644 index 9a6fde2..0000000 --- a/app/schemas/proxmox/vm_id/delete.py +++ /dev/null @@ -1,80 +0,0 @@ - -from typing import Literal -from pydantic import BaseModel, Field - -# -# ISSUE - #3 -# - - -class Request_ProxmoxVmsVMID_Delete(BaseModel): - - proxmox_node: str = Field( - ..., - # default= "px-testing", - description = "Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" - ) - - as_json: bool = Field( - default=True, - description="If true : JSON output else : raw output" - ) - # - - vm_id: str = Field( - ..., - # default="4000", - description="Virtual machine id", - pattern=r"^[0-9]+$" - ) - - model_config = { - "json_schema_extra": { - "example": { - "proxmox_node": "px-testing", - "vm_id": "1111", - "as_json": True, - } - } - } - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### -#### #### #### #### #### #### #### #### #### #### #### #### #### #### - -class Reply_ProxmoxVmsVMID_DeleteItem(BaseModel): - - action: Literal["vm_delete"] - source: Literal["proxmox"] - proxmox_node: str - - vm_id: int = Field(..., ge=1) - vm_name: str - raw_data: str = Field(..., description="Raw string returned by proxmox") - -class Reply_ProxmoxVmsVMID_Delete(BaseModel): - - rc: int = Field(0, description="RETURN code (0 = OK)") - result: list[Reply_ProxmoxVmsVMID_DeleteItem] - - model_config = { - "json_schema_extra": { - "example": { - "rc": 0, - "result": [ - { - "action": "vm_delete", - "source": "proxmox", - "proxmox_node": "px-testing", - "vm_id": 1023, - "vm_name": "admin-web-deployer-ui", - "raw_data": "UPID:px-testing:123123:1123D4:68BFF2C7:qmdestroy:1023:API_master@pam!API_master:" - } - ] - } - } - } - - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### -#### #### #### #### #### #### #### #### #### #### #### #### #### #### diff --git a/app/schemas/proxmox/vm_id/snapshot/vm_create.py b/app/schemas/proxmox/vm_id/snapshot/vm_create.py deleted file mode 100644 index 28e7025..0000000 --- a/app/schemas/proxmox/vm_id/snapshot/vm_create.py +++ /dev/null @@ -1,101 +0,0 @@ - - -from typing import Literal -from pydantic import BaseModel, Field - -# -# ISSUE - #9 -# - -class Request_ProxmoxVmsVMID_CreateSnapshot(BaseModel): - - proxmox_node: str = Field( - ..., - # default= "px-testing", - description = "Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" - ) - - as_json: bool = Field( - default=True, - description="If true : JSON output else : raw output" - ) - # - - vm_id: str = Field( - ..., - # default="4000", - description="Virtual machine id", - pattern=r"^[0-9]+$" - ) - - vm_snapshot_name: str | None = Field( - default=None, - description="Name of the snapshot to create", - pattern=r"^[A-Za-z0-9_-]+$" - ) - - vm_snapshot_description: str | None = Field( - default=None, - description="Optional description for the snapshot", - pattern=r"^[A-Za-z0-9_-]+$" - ) - - model_config = { - "json_schema_extra": { - "example": { - "proxmox_node": "px-testing", - "vm_id": "1111", - "vm_snapshot_name":"MY_VM_SNAPSHOT", - "vm_snapshot_description":"MY_DESCRIPTION", - "as_json": True - } - } - } - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### -#### #### #### #### #### #### #### #### #### #### #### #### #### #### - -class Reply_ProxmoxVmsVMID_CreateSnapshotItem(BaseModel): - - action: Literal["vm_get_config"] - proxmox_node: str - source: Literal["proxmox"] - vm_id: str # int = Field(..., ge=1) - - vm_name: str - vm_snapshot_description: str - vm_snapshot_name: str - - raw_data: str = Field(..., description="Raw string returned by proxmox") - - -class Reply_ProxmoxVmsVMID_CreateSnapshot(BaseModel): - - rc: int = Field(0, description="RETURN code (0 = OK)") - result: list[Reply_ProxmoxVmsVMID_CreateSnapshotItem] - - model_config = { - "json_schema_extra": { - "example": { - "rc": 0, - "result": [ - { - "action": "snapshot_vm_create", - "proxmox_node": "px-testing", - "source": "proxmox", - - "vm_id": "1000", - "vm_name": "admin-wazuh", - "vm_snapshot_description": "MY_DESCRIPTION", - "vm_snapshot_name": "MY_VM_SNAPSHOT", - "raw_data": { "data": "UPID:px-testing:002D5E30:1706941B:68C196E9:qmsnapshot:1000:API_master@pam!API_master:" } - } - ] - } - } - } - - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### -#### #### #### #### #### #### #### #### #### #### #### #### #### #### diff --git a/app/schemas/proxmox/vm_id/snapshot/vm_delete.py b/app/schemas/proxmox/vm_id/snapshot/vm_delete.py deleted file mode 100644 index 7f4a578..0000000 --- a/app/schemas/proxmox/vm_id/snapshot/vm_delete.py +++ /dev/null @@ -1,89 +0,0 @@ - - -from typing import Literal -from pydantic import BaseModel, Field - -# -# ISSUE - #9 -# - -class Request_ProxmoxVmsVMID_DeleteSnapshot(BaseModel): - - proxmox_node: str = Field( - ..., - # default= "px-testing", - description = "Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" - ) - - as_json: bool = Field( - default=True, - description="If true : JSON output else : raw output" - ) - # - - vm_id: str = Field( - ..., - # default="4000", - description="Virtual machine id", - pattern=r"^[0-9]+$" - ) - - vm_snapshot_name: str | None = Field( - default=None, - description="Name of the snapshot to delete", - pattern=r"^[A-Za-z0-9_-]+$" - ) - - model_config = { - "json_schema_extra": { - "example": { - "proxmox_node": "px-testing", - "vm_id": "1111", - "vm_snapshot_name": "MY_VM_SNAPSHOT", - "as_json": True - } - } - } - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### -#### #### #### #### #### #### #### #### #### #### #### #### #### #### - -class Reply_ProxmoxVmsVMID_DeleteSnapshotItem(BaseModel): - - action: Literal["vm_get_config"] - source: Literal["proxmox"] - # proxmox_node: str - # vm_id: int = Field(..., ge=1) - # vm_name: str - # raw_data: str = Field(..., description="Raw string returned by proxmox") - - -class Reply_ProxmoxVmsVMID_DeleteSnapshot(BaseModel): - - rc: int = Field(0, description="RETURN code (0 = OK)") - result: list[Reply_ProxmoxVmsVMID_DeleteSnapshotItem] - - model_config = { - "json_schema_extra": { - "example": { - "rc": 0, - "result": [ - { - "action": "snapshot_vm_delete", - "proxmox_node": "px-testing", - "source": "proxmox", - - "vm_id": "1000", - "vm_name": "admin-wazuh", - "vm_snapshot_name": "BBBB", - "raw_data": { "data": "UPID:px-testing:002D6878:17077370:68C19925:qmdelsnapshot:1000:API_master@pam!API_master:"}, - } - ] - } - } - } - - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### -#### #### #### #### #### #### #### #### #### #### #### #### #### #### diff --git a/app/schemas/proxmox/vm_id/snapshot/vm_list.py b/app/schemas/proxmox/vm_id/snapshot/vm_list.py deleted file mode 100644 index dc55f87..0000000 --- a/app/schemas/proxmox/vm_id/snapshot/vm_list.py +++ /dev/null @@ -1,94 +0,0 @@ - - -from typing import Literal -from pydantic import BaseModel, Field - -# -# ISSUE - #9 -# - -class Request_ProxmoxVmsVMID_ListSnapshot(BaseModel): - - proxmox_node: str = Field( - ..., - # default= "px-testing", - description = "Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" - ) - - as_json: bool = Field( - default=True, - description="If true : JSON output else : raw output" - ) - # - - vm_id: str = Field( - ..., - # default="4000", - description="Virtual machine id", - pattern=r"^[0-9]+$" - ) - - model_config = { - "json_schema_extra": { - "example": { - "proxmox_node": "px-testing", - "vm_id": "1111" - } - } - } - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### -#### #### #### #### #### #### #### #### #### #### #### #### #### #### - -class Reply_ProxmoxVmsVMID_ListSnapshotItem(BaseModel): - - action: Literal["vm_get_config"] - source: Literal["proxmox"] - # proxmox_node: str - # vm_id: int = Field(..., ge=1) - # vm_name: str - # raw_data: str = Field(..., description="Raw string returned by proxmox") - - -class Reply_ProxmoxVmsVMID_ListSnapshot(BaseModel): - - rc: int = Field(0, description="RETURN code (0 = OK)") - result: list[Reply_ProxmoxVmsVMID_ListSnapshotItem] - - model_config = { - "json_schema_extra": { - "example": { - "rc": 0, - - "result": [ - { - "action": "snapshot_vm_list", - "proxmox_node": "px-testing", - "source": "proxmox", - - "vm_id": "1000", - "vm_snapshot_description": "MY_DESCRIPTION", - "vm_snapshot_name": "MY_VM_SNAPSHOT", - "vm_snapshot_parent": "", - "vm_snapshot_time": 1757517545 - }, - { - "action": "snapshot_vm_list", - "proxmox_node": "px-testing", - "source": "proxmox", - - "vm_id": "1000", - "vm_snapshot_description": "You are here!", - "vm_snapshot_name": "current", - "vm_snapshot_parent": "MY_VM_SNAPSHOT", - "vm_snapshot_sha1": "7cc59c988bb8f18601fe076ad239f8b760667270" - } - ] - } - } - } - - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### -#### #### #### #### #### #### #### #### #### #### #### #### #### #### diff --git a/app/schemas/proxmox/vm_id/snapshot/vm_revert.py b/app/schemas/proxmox/vm_id/snapshot/vm_revert.py deleted file mode 100644 index a30eb7b..0000000 --- a/app/schemas/proxmox/vm_id/snapshot/vm_revert.py +++ /dev/null @@ -1,89 +0,0 @@ - - -from typing import Literal -from pydantic import BaseModel, Field - -# -# ISSUE - #9 -# - -class Request_ProxmoxVmsVMID_RevertSnapshot(BaseModel): - - proxmox_node: str = Field( - ..., - # default= "px-testing", - description = "Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" - ) - - as_json: bool = Field( - default=True, - description="If true : JSON output else : raw output" - ) - # - - vm_id: str = Field( - ..., - # default="4000", - description="Virtual machine id", - pattern=r"^[0-9]+$" - ) - - vm_snapshot_name: str | None = Field( - default=None, - description="Name of the snapshot to create", - pattern=r"^[A-Za-z0-9_-]+$" - ) - - model_config = { - "json_schema_extra": { - "example": { - "proxmox_node": "px-testing", - "vm_id": "1111", - "vm_snapshot_name":"CCCC", - "as_json": True, - } - } - } - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### -#### #### #### #### #### #### #### #### #### #### #### #### #### #### - -class Reply_ProxmoxVmsVMID_RevertSnapshotItem(BaseModel): - - action: Literal["vm_get_config"] - source: Literal["proxmox"] - # proxmox_node: str - # vm_id: int = Field(..., ge=1) - # vm_name: str - # raw_data: str = Field(..., description="Raw string returned by proxmox") - - -class Reply_ProxmoxVmsVMID_RevertSnapshot(BaseModel): - - rc: int = Field(0, description="RETURN code (0 = OK)") - result: list[Reply_ProxmoxVmsVMID_RevertSnapshotItem] - - model_config = { - "json_schema_extra": { - "example": { - "rc": 0, - "result": [ - { - "action": "snapshot_vm_revert", - "source": "proxmox", - "proxmox_node": "px-testing", - - "vm_id": "1000", - "vm_name": "admin-wazuh", - "vm_snapshot_name": "CCCC", - "raw_data": {"data": "UPID:px-testing:002D7C57:17096777:68C19E25:qmrollback:1000:API_master@pam!API_master:"}, - } - ] - } - } - } - - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### -#### #### #### #### #### #### #### #### #### #### #### #### #### #### diff --git a/app/schemas/proxmox/vm_id/start_stop_resume_pause.py b/app/schemas/proxmox/vm_id/start_stop_resume_pause.py deleted file mode 100644 index 081d29d..0000000 --- a/app/schemas/proxmox/vm_id/start_stop_resume_pause.py +++ /dev/null @@ -1,98 +0,0 @@ - -from typing import Literal -from pydantic import BaseModel, Field - -# -# ISSUE - #3 -# - - -class Request_ProxmoxVmsVMID_StartStopPauseResume(BaseModel): - - proxmox_node: str = Field( - ..., - # default= "px-testing", - description = "Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" - ) - - as_json: bool = Field( - default=True, - description="If true : JSON output else : raw output" - ) - # - - vm_id: str = Field( - ..., - # default="1000", - description="Virtual machine id", - pattern=r"^[0-9]+$" - ) - - - model_config = { - "json_schema_extra": { - "example": { - "proxmox_node": "px-testing", - "vm_id": "2000", - # "vm_new_id": "3000", - # "vm_name":"test-cloned", - # "vm_description":"my description" - } - } - } - - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### - -class Reply_ProxmoxVmsVMID_StartStopPauseResumeItem(BaseModel): - - action: Literal["vm_start", "vm_stop", "vm_resume", "vm_pause", "vm_stop_force" ] - source: Literal["proxmox"] - - proxmox_node: str - vm_id : str # int = Field(..., ge=1) - vm_name : str - - - - -class Reply_ProxmoxVmsVMID_StartStopPauseResume(BaseModel): - - rc: int = Field(0, description="RETURN code (0 = OK)") - result: list[Reply_ProxmoxVmsVMID_StartStopPauseResumeItem] - - model_config = { - "json_schema_extra": { - "example": { - "rc": 0, - "result": [ - - { - "action": "vm_delete", - "source": "proxmox", - "proxmox_node": "px-testing", - "vm_id": 4001, - "vm_name": "vuln-box-01", - "raw_data": { - "data": "UPID:px-testing:0033649C:1D2619CC:68D143C5:qmdestroy:4001:API_master@pam!API_master:" - } - }, - { - "action": "vm_delete", - "source": "proxmox", - "proxmox_node": "px-testing", - "vm_id": 4002, - "vm_name": "vuln-box-02", - "raw_data": { - "data": "UPID:px-testing:003364A6:1D261A84:68D143C6:qmdestroy:4002:API_master@pam!API_master:" - } - }, - ] - } - } - } - - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### -#### #### #### #### #### #### #### #### #### #### #### #### #### #### diff --git a/app/schemas/proxmox/vm_ids/mass_delete.py b/app/schemas/proxmox/vm_ids/mass_delete.py deleted file mode 100644 index f80eb71..0000000 --- a/app/schemas/proxmox/vm_ids/mass_delete.py +++ /dev/null @@ -1,79 +0,0 @@ - -from typing import Literal, List -from pydantic import BaseModel, Field - - - -class vm(BaseModel): - - id: str = Field( - ..., - description="Virtual machine id", - pattern=r"^[0-9]+$" - ) - - name: str = Field( - ..., - description="Virtual machine meta name", - pattern="^[A-Za-z0-9-]+$" # deny void name - # pattern=r"^[A-Za-z0-9-]*$", - ) - - -class Request_ProxmoxVmsVmIds_MassDelete(BaseModel): - - proxmox_node: str = Field( - ..., - # default= "px-testing", - description = "Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" - ) - - as_json: bool = Field( - default=True, - description="If true : JSON output else : raw output" - ) - - vms: List[vm] = Field( - ..., - description="List of virtual machine (vm_id + vm_name)", - min_length=1, - ) - - model_config = { - "json_schema_extra": { - "example": { - "proxmox_node": "px-testing", - "as_json": True, - "vms": [ - {"id":"4000", "name":"vuln-box-00"}, - {"id":"4001", "name":"vuln-box-01"}, - {"id":"4002", "name":"vuln-box-02"}, - ], - } - } - } - - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### -#### #### #### #### #### #### #### #### #### #### #### #### #### #### - -class Reply_ProxmoxVmsVMID_MasseDeleteItem(BaseModel): - - action: Literal["vm_start", "vm_stop", "vm_resume", "vm_pause", "vm_stop_force" ] - source: Literal["proxmox"] - - proxmox_node: str - vm_id : str # int = Field(..., ge=1) - # vm_new_id : str # int = Field(..., ge=1) - vm_name : str - vm_status: Literal["running", "stopped", "paused"] - -class Reply_ProxmoxVmsVmIds_MassDelete(BaseModel): - - rc: int = Field(0, description="RETURN code (0 = OK)") - result: list[Reply_ProxmoxVmsVMID_MasseDeleteItem] - - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### -#### #### #### #### #### #### #### #### #### #### #### #### #### #### diff --git a/app/schemas/proxmox/vm_ids/mass_start_stop_resume_pause.py b/app/schemas/proxmox/vm_ids/mass_start_stop_resume_pause.py deleted file mode 100644 index e7e3703..0000000 --- a/app/schemas/proxmox/vm_ids/mass_start_stop_resume_pause.py +++ /dev/null @@ -1,45 +0,0 @@ - -from typing import Literal, List -from pydantic import BaseModel, Field - - -class Request_ProxmoxVmsVmIds_MassStartStopPauseResume(BaseModel): - - proxmox_node: str = Field( - ..., - # default= "px-testing", - description = "Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" - ) - - as_json: bool = Field( - default=True, - description="If true : JSON output else : raw output" - ) - # - - # vm_id: str = Field( - # ..., - # # default="1000", - # description="Virtual machine id", - # pattern=r"^[0-9]+$" - # ) - - vm_ids: List[str] = Field( - ..., - # default="1000", - description="Virtual machine id", - min_items=1, - # pattern=r"^[0-9]+$" - ) - - - model_config = { - "json_schema_extra": { - "example": { - "proxmox_node": "px-testing", - "as_json": True, - "vm_ids": ["4000", "4001"], - } - } - } diff --git a/app/schemas/proxmox/vm_list.py b/app/schemas/proxmox/vm_list.py deleted file mode 100644 index 73eee06..0000000 --- a/app/schemas/proxmox/vm_list.py +++ /dev/null @@ -1,86 +0,0 @@ -from enum import Enum -from typing import List -from pydantic import BaseModel, Field - -# -# ISSUE - #2 -# - - -class Request_ProxmoxVms_VmList(BaseModel): - - proxmox_node: str = Field( - ..., - # default= "px-testing", - description = "Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" - ) - - as_json: bool = Field( - default=True, - description="If true : JSON output else : raw output" - ) - - model_config = { - "json_schema_extra": { - "example": { - "proxmox_node": "px-testing", - "as_json": True - } - } - } - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### -#### #### #### #### #### #### #### #### #### #### #### #### #### #### - -class Reply_ProxmoxVms_VmList(str, Enum): - - LIST = "vm_list" - START = "vm_start" - STOP = "vm_stop" - RESUME = "vm_resume" - PAUSE = "vm_pause" - STOP_FORCE = "vm_stop_force" - - -class Reply_ProxmoxVmList_VmStatus(str, Enum): - - RUNNING = "running" - STOPPED = "stopped" - PAUSED = "paused" - - -class Reply_ProxmoxVmList_VmMeta(BaseModel): - - cpu_current_usage: int - cpu_allocated: int - disk_current_usage: int - disk_read: int - disk_write: int - disk_max: int - ram_current_usage: int - ram_max: int - net_in: int - net_out: int - - -class Reply_ProxmoxVmList_VmInfo(BaseModel): - - action: Reply_ProxmoxVms_VmList - source: str = Field("proxmox", description="data source provider") - proxmox_node: str - vm_name: str - vm_status: Reply_ProxmoxVmList_VmStatus - vm_id: int - vm_uptime: int - vm_meta: Reply_ProxmoxVmList_VmMeta - - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### -#### #### #### #### #### #### #### #### #### #### #### #### #### #### - -class Reply_ProxmoxVmList(BaseModel): - - rc: int = Field(..., description="RETURN CODE (0 = OK) ") - result: List[List[ Reply_ProxmoxVmList_VmInfo]] - diff --git a/app/schemas/proxmox/vm_list_usage.py b/app/schemas/proxmox/vm_list_usage.py deleted file mode 100644 index 8a43752..0000000 --- a/app/schemas/proxmox/vm_list_usage.py +++ /dev/null @@ -1,90 +0,0 @@ - - -from enum import Enum -from typing import List, Literal -from pydantic import BaseModel, Field - -# -# ISSUE - #5 -# - - - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### -class Request_ProxmoxVms_VmListUsage(BaseModel): - - proxmox_node: str = Field( - ..., - # default= "px-testing", - description = "Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" - ) - - as_json: bool = Field( - default=True, - description="If true : JSON output else : raw output" - ) - - model_config = { - "json_schema_extra": { - "example": { - "proxmox_node": "px-testing", - "as_json": True - } - } - } -#### #### #### #### #### #### #### #### #### #### #### #### #### #### - - -class Reply_ProxmoxVms_VmListUsageItem(BaseModel): - - action: Literal["vm_list_usage"] - source: Literal["proxmox"] - proxmox_node: str - vm_id: int = Field(..., ge=1) - vm_name: str - # raw_data: str = Field(..., description="Raw string returned by proxmox") - cpu_allocated: int - cpu_current_usage: int - disk_current_usage: int - disk_max: int - disk_read: int - disk_write: int - net_in: int - net_out: int - ram_current_usage: int - ram_max: int - vm_status: str - vm_uptime: int - - -class Reply_ProxmoxVms_VmListUsage(BaseModel): - - rc: int = Field(0, description="RETURN code (0 = OK)") - result: list[Reply_ProxmoxVms_VmListUsageItem] - - model_config = { - "json_schema_extra": { - "example": { - "rc": 0, - "result": [ - { - "cpu_allocated":1, - "cpu_current_usage":0, - "disk_current_usage":0, - "disk_max":34359738368, - "disk_read":0, - "disk_write":0, - "net_in":280531583, - "net_out":6330590, - "ram_current_usage":1910544625, - "ram_max":4294967296, - "vm_id":1020, - "vm_name":"admin-web-api-kong", - "vm_status":"running", - "vm_uptime":79940 - } - ] - } - } - } diff --git a/app/utils/vm_id_name_resolver.py b/app/utils/vm_id_name_resolver.py index c451e32..90780ab 100644 --- a/app/utils/vm_id_name_resolver.py +++ b/app/utils/vm_id_name_resolver.py @@ -3,8 +3,8 @@ import os, json, logging from fastapi import HTTPException -from app.runner import run_playbook_core # , extract_action_results -from app.extract_actions import extract_action_results +from app.core.runner import run_playbook_core +from app.core.extractor import extract_action_results debug =1 diff --git a/app/vault/__init__.py b/app/vault/__init__.py deleted file mode 100644 index ac134ab..0000000 --- a/app/vault/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ - -# - -from .vault import * diff --git a/app/vault/vault.py b/app/vault/vault.py deleted file mode 100644 index 085ee7f..0000000 --- a/app/vault/vault.py +++ /dev/null @@ -1,12 +0,0 @@ - -from pathlib import Path - -_VAULT_PASS_PATH: Path | None = None - -def set_vault_path(p: Path | None) -> None: - global _VAULT_PASS_PATH - _VAULT_PASS_PATH = p - -def get_vault_path() -> Path | None: - return _VAULT_PASS_PATH - diff --git a/docs/plans/2026-03-19-backend-api-restructure-refactor.md b/docs/plans/2026-03-19-backend-api-restructure-refactor.md new file mode 100644 index 0000000..20ffd9d --- /dev/null +++ b/docs/plans/2026-03-19-backend-api-restructure-refactor.md @@ -0,0 +1,1979 @@ +# Range42 Backend API — Restructure, Refactor, Dockerize & Document + +**Goal:** Restructure the FastAPI backend for clarity (#53), refactor for maintainability and best practices (#54), add Docker support (#51), and improve documentation (#52) — all while preserving 100% API compatibility with the deployer-ui frontend. + +**Architecture:** The backend is a FastAPI app that orchestrates Proxmox infrastructure via Ansible playbooks. Every route follows the same pattern: validate request → build extravars → call `run_playbook_core()` → extract results → return JSON. The refactoring consolidates 86 near-identical route files into domain-grouped modules using shared base classes, replaces global state with dependency injection, and wraps everything in Docker for portable deployment. + +**Tech Stack:** Python 3.12+, FastAPI 0.115, Pydantic v2, ansible-runner 2.4, uvicorn, httpx, Docker, docker-compose + +**API Compatibility Contract:** The deployer-ui frontend expects these exact behaviors — DO NOT break them: +- All endpoints under `/v0/admin/proxmox/`, `/v0/admin/run/`, `/v0/admin/debug/` +- POST for all operations (including reads), DELETE for deletions +- Every request body accepts `proxmox_node`, `as_json` fields (plus `hosts`/`inventory` injected by UI) +- Response format: `{"rc": , "result": [[...items]]}` when `as_json=True` +- Response format: `{"rc": , "log_multiline": [...lines]}` when `as_json=False` +- HTTP 200 when `rc=0`, HTTP 500 when `rc!=0` +- 422 validation errors with `{"detail": [{"field":..., "msg":..., "type":...}]}` format +- WebSocket at `/ws/vm-status` with `full`/`diff` message types + +**Related Issues:** #51 (Docker), #52 (Documentation), #53 (Restructure), #54 (Refactor) + +--- + +## Phase 0: Preparation & Safety Net + +### Task 0.1: Create Feature Branch and Baseline Tests + +Before touching any code, establish a safety net. + +**Files:** +- Create: `tests/__init__.py` +- Create: `tests/conftest.py` +- Create: `tests/test_api_smoke.py` +- Create: `pytest.ini` + +- [ ] **Step 1: Add pytest to requirements** + +Add to `requirements.txt`: +``` +pytest==8.3.4 +pytest-asyncio==0.24.0 +httpx==0.27.2 +``` + +(`httpx` is already there — just ensure pytest + pytest-asyncio are added) + +- [ ] **Step 2: Create pytest.ini** + +```ini +[pytest] +testpaths = tests +asyncio_mode = auto +``` + +- [ ] **Step 3: Create test conftest with FastAPI test client** + +```python +# tests/conftest.py +import json +import os +import pytest +from pathlib import Path +from fastapi.testclient import TestClient + +_PROJECT_ROOT = str(Path(__file__).resolve().parents[1]) + +# Set ALL env vars that are read at module-import time (before importing app). +# Routes call Path(os.getenv("PROJECT_ROOT_DIR")).resolve() at import time — missing vars crash immediately. +os.environ.setdefault("PROJECT_ROOT_DIR", _PROJECT_ROOT) +os.environ.setdefault("API_BACKEND_WWWAPP_PLAYBOOKS_DIR", _PROJECT_ROOT) +os.environ.setdefault("API_BACKEND_PUBLIC_PLAYBOOKS_DIR", _PROJECT_ROOT) # needed by checks_playbooks.py +os.environ.setdefault("API_BACKEND_INVENTORY_DIR", str(Path(_PROJECT_ROOT) / "inventory")) +# Vault vars are only needed at runtime (lifespan), not at import time — safe to skip in tests. + +@pytest.fixture +def client(): + from app.main import app + return TestClient(app) + + +@pytest.fixture(scope="session") +def openapi_schema(): + """Load OpenAPI schema once for all tests. Used as golden reference for route verification.""" + from app.main import app + client = TestClient(app) + resp = client.get("/docs/openapi.json") + return resp.json() +``` + +- [ ] **Step 4: Save current OpenAPI schema as golden reference** + +Before any restructuring, export the current schema to use as regression baseline: + +```bash +cd /home/ppa/projects/range42-base/range42-backend-api +mkdir -p tests/fixtures +# Start the app briefly to export schema (or use TestClient) +python -c " +import json, os +from pathlib import Path +os.environ.setdefault('PROJECT_ROOT_DIR', str(Path('.').resolve())) +os.environ.setdefault('API_BACKEND_WWWAPP_PLAYBOOKS_DIR', str(Path('.').resolve())) +os.environ.setdefault('API_BACKEND_PUBLIC_PLAYBOOKS_DIR', str(Path('.').resolve())) +os.environ.setdefault('API_BACKEND_INVENTORY_DIR', str(Path('.').resolve() / 'inventory')) +from fastapi.testclient import TestClient +from app.main import app +client = TestClient(app) +schema = client.get('/docs/openapi.json').json() +# Save just the paths (method -> path pairs) as the golden reference +routes = {} +for path, methods in schema.get('paths', {}).items(): + for method in methods: + if method.upper() in ('GET', 'POST', 'PUT', 'DELETE', 'PATCH'): + routes.setdefault(path, []).append(method.upper()) +with open('tests/fixtures/routes_golden.json', 'w') as f: + json.dump(routes, f, indent=2, sort_keys=True) +print(f'Saved {sum(len(v) for v in routes.values())} routes across {len(routes)} paths') +" +``` + +- [ ] **Step 5: Write smoke tests that verify all current routes exist** + +```python +# tests/test_api_smoke.py +"""Smoke tests: verify all API routes are registered and respond to requests. +These tests do NOT call Ansible — they only verify the FastAPI route table. +Route verification uses the auto-generated golden reference (tests/fixtures/routes_golden.json), +NOT a hand-written list, to catch ALL endpoints including dynamic bundle/scenario routes.""" + +import json +from pathlib import Path + + +def test_app_starts(client): + """App boots without error.""" + assert client is not None + + +def test_openapi_schema_loads(client): + """OpenAPI schema is generated without error.""" + resp = client.get("/docs/openapi.json") + assert resp.status_code == 200 + schema = resp.json() + assert schema["info"]["title"] == "CR42 - API" + assert schema["info"]["version"] == "v0.1" + + +def test_swagger_docs_available(client): + resp = client.get("/docs/swagger") + assert resp.status_code == 200 + + +def test_redoc_docs_available(client): + resp = client.get("/docs/redoc") + assert resp.status_code == 200 + + +def test_all_routes_match_golden_reference(client): + """Every endpoint from the pre-restructure golden reference must still exist. + This catches routes that are silently dropped during file consolidation. + The golden reference is auto-generated (Task 0.1 Step 4), NOT hand-coded.""" + golden_path = Path(__file__).parent / "fixtures" / "routes_golden.json" + assert golden_path.exists(), f"Golden reference not found at {golden_path}. Run Step 4 first." + + with open(golden_path) as f: + golden_routes = json.load(f) + + resp = client.get("/docs/openapi.json") + schema = resp.json() + registered = {} + for path, methods in schema.get("paths", {}).items(): + for method in methods: + if method.upper() in ("GET", "POST", "PUT", "DELETE", "PATCH"): + registered.setdefault(path, []).append(method.upper()) + + missing = [] + for path, methods in golden_routes.items(): + for method in methods: + if method not in registered.get(path, []): + missing.append(f"{method} {path}") + + assert not missing, f"Missing {len(missing)} routes after restructure:\n" + "\n".join(sorted(missing)) + + +def test_no_routes_accidentally_added(client): + """Verify no new routes were accidentally introduced during restructuring.""" + golden_path = Path(__file__).parent / "fixtures" / "routes_golden.json" + if not golden_path.exists(): + return # Skip if golden reference not yet generated + + with open(golden_path) as f: + golden_routes = json.load(f) + + resp = client.get("/docs/openapi.json") + schema = resp.json() + + golden_set = set() + for path, methods in golden_routes.items(): + for method in methods: + golden_set.add((method, path)) + + current_set = set() + for path, methods in schema.get("paths", {}).items(): + for method in methods: + if method.upper() in ("GET", "POST", "PUT", "DELETE", "PATCH"): + current_set.add((method.upper(), path)) + + added = current_set - golden_set + if added: + # Not a hard failure — just informational + print(f"INFO: {len(added)} new routes added: {sorted(added)}") +``` + +- [ ] **Step 5: Run smoke tests to establish baseline** + +Run: `cd /home/ppa/projects/range42-base/range42-backend-api && python -m pytest tests/test_api_smoke.py -v` +Expected: All tests PASS (or document which fail due to missing env vars — those are acceptable at this stage) + +- [ ] **Step 6: Commit baseline tests** + +```bash +git add tests/ pytest.ini requirements.txt +git commit -m "test: add smoke tests as safety net before restructuring (#53, #54)" +``` + +--- + +## Phase 1: Project Restructure (#53) + +Goal: Flatten deep nesting, group by domain, separate core logic from scripts/config. + +### Current Structure (problematic) +``` +app/ + routes/v0/proxmox/vms/vm_id/config/vm_get_config.py # 7 levels deep + routes/v0/proxmox/firewall/enable_firewall_vm.py + schemas/proxmox/vm_id/start_stop_resume_pause.py # mirrors route nesting +``` + +### Target Structure +``` +app/ + core/ # Core logic (was scattered) + config.py # App configuration (env vars, settings) + runner.py # Ansible playbook runner (moved from app/) + extractor.py # Event extraction (moved from app/extract_actions.py) + vault.py # Vault management (moved from app/vault/) + exceptions.py # Custom exception handlers + + routes/ # Flattened, domain-grouped + __init__.py # Router assembly + vms.py # All VM lifecycle routes (was 20+ files) + vm_config.py # VM config routes (was 5 files) + snapshots.py # Snapshot routes (was 4 files) + firewall.py # Firewall routes (was 13 files) + network.py # Network routes (was 6 files) + storage.py # Storage routes (was 4 files) + bundles.py # Core bundle routes — Ubuntu + Proxmox (was split across files) + runner.py # Dynamic bundle/scenario runner (/{name}/run endpoints) + debug.py # Debug routes (was 2 files) + ws_status.py # WebSocket (stays as-is) + + schemas/ # Flattened, domain-grouped + base.py # Shared base models + vms.py # VM schemas (was 10+ files) + vm_config.py # VM config schemas + snapshots.py # Snapshot schemas + firewall.py # Firewall schemas + network.py # Network schemas + storage.py # Storage schemas + bundles.py # Bundle/scenario schemas + debug.py # Debug schemas + + utils/ # Stays mostly as-is + __init__.py + checks_playbooks.py + checks_inventory.py + text_cleaner.py + vm_id_name_resolver.py + +curl_utils/ # Stays as-is (testing scripts) +playbooks/ # Stays as-is +inventory/ # Stays as-is +``` + +### Task 1.1: Create `app/core/config.py` — Centralized Configuration + +**Files:** +- Create: `app/core/__init__.py` +- Create: `app/core/config.py` +- Test: `tests/test_config.py` + +- [ ] **Step 1: Write failing test for config module** + +```python +# tests/test_config.py +import os +from pathlib import Path + + +def test_settings_loads_from_env(monkeypatch): + monkeypatch.setenv("PROJECT_ROOT_DIR", "/tmp/test-project") + monkeypatch.setenv("CORS_ORIGIN_REGEX", r"^https?://example\.com$") + + # Force reimport + import importlib + import app.core.config as config_mod + importlib.reload(config_mod) + from app.core.config import settings + + assert settings.project_root == Path("/tmp/test-project") + assert settings.cors_origin_regex == r"^https?://example\.com$" + + +def test_settings_defaults(monkeypatch): + monkeypatch.setenv("PROJECT_ROOT_DIR", "/tmp/test-project") + monkeypatch.delenv("CORS_ORIGIN_REGEX", raising=False) + + import importlib + import app.core.config as config_mod + importlib.reload(config_mod) + from app.core.config import settings + + assert "localhost" in settings.cors_origin_regex + assert settings.debug is False + + +def test_settings_playbook_path(monkeypatch): + monkeypatch.setenv("PROJECT_ROOT_DIR", "/tmp/test-project") + + import importlib + import app.core.config as config_mod + importlib.reload(config_mod) + from app.core.config import settings + + assert settings.playbook_path == Path("/tmp/test-project/playbooks/generic.yml") + assert settings.inventory_name == "hosts" +``` + +- [ ] **Step 2: Run test, verify it fails** + +Run: `python -m pytest tests/test_config.py -v` +Expected: FAIL — `ModuleNotFoundError: No module named 'app.core'` + +- [ ] **Step 3: Implement config module** + +```python +# app/core/__init__.py +``` + +```python +# app/core/config.py +"""Centralized application configuration. All env vars read here, nowhere else.""" + +import os +from dataclasses import dataclass, field +from pathlib import Path + + +@dataclass(frozen=True) +class Settings: + """Immutable application settings loaded from environment variables.""" + + project_root: Path = field(default_factory=lambda: Path(os.getenv("PROJECT_ROOT_DIR", ".")).resolve()) + + # Playbook paths + wwwapp_playbooks_dir: str = field(default_factory=lambda: os.getenv("API_BACKEND_WWWAPP_PLAYBOOKS_DIR", "")) + public_playbooks_dir: str = field(default_factory=lambda: os.getenv("API_BACKEND_PUBLIC_PLAYBOOKS_DIR", "")) + inventory_dir: str = field(default_factory=lambda: os.getenv("API_BACKEND_INVENTORY_DIR", "")) + vault_file: str = field(default_factory=lambda: os.getenv("API_BACKEND_VAULT_FILE", "")) + + # Vault credentials + vault_password_file: str = field(default_factory=lambda: os.getenv("VAULT_PASSWORD_FILE", "")) + vault_password: str = field(default_factory=lambda: os.getenv("VAULT_PASSWORD", "")) + + # CORS + cors_origin_regex: str = field( + default_factory=lambda: os.getenv( + "CORS_ORIGIN_REGEX", + r"^https?://(localhost|127\.0\.0\.1|\[::1\])(:\d+)?$", + ) + ) + + # Server + host: str = field(default_factory=lambda: os.getenv("HOST", "0.0.0.0")) + port: int = field(default_factory=lambda: int(os.getenv("PORT", "8000"))) + debug: bool = field(default_factory=lambda: os.getenv("DEBUG", "").lower() in ("1", "true", "yes")) + + @property + def playbook_path(self) -> Path: + return self.project_root / "playbooks" / "generic.yml" + + @property + def inventory_name(self) -> str: + return "hosts" + + +settings = Settings() +``` + +- [ ] **Step 4: Run test, verify it passes** + +Run: `python -m pytest tests/test_config.py -v` +Expected: PASS + +- [ ] **Step 5: Commit** + +```bash +git add app/core/ tests/test_config.py +git commit -m "refactor: add centralized config module (#53, #54)" +``` + +### Task 1.2: Create `app/core/vault.py` — Vault with Dependency Injection + +**Files:** +- Create: `app/core/vault.py` +- Test: `tests/test_vault.py` + +- [ ] **Step 1: Write failing test** + +```python +# tests/test_vault.py +from pathlib import Path +from app.core.vault import VaultManager + + +def test_vault_manager_starts_empty(): + vm = VaultManager() + assert vm.get_vault_path() is None + + +def test_vault_manager_set_and_get(): + vm = VaultManager() + p = Path("/tmp/test-vault.txt") + vm.set_vault_path(p) + assert vm.get_vault_path() == p + + +def test_vault_manager_reset(): + vm = VaultManager() + vm.set_vault_path(Path("/tmp/test-vault.txt")) + vm.set_vault_path(None) + assert vm.get_vault_path() is None +``` + +- [ ] **Step 2: Run test, verify it fails** + +Run: `python -m pytest tests/test_vault.py -v` +Expected: FAIL — `ModuleNotFoundError` + +- [ ] **Step 3: Implement VaultManager class** + +```python +# app/core/vault.py +"""Vault password management. No global state — uses a class instance.""" + +from pathlib import Path + + +class VaultManager: + """Manages the Ansible vault password file path.""" + + def __init__(self) -> None: + self._vault_pass_path: Path | None = None + + def set_vault_path(self, p: Path | None) -> None: + self._vault_pass_path = p + + def get_vault_path(self) -> Path | None: + return self._vault_pass_path +``` + +- [ ] **Step 4: Run test, verify it passes** + +Run: `python -m pytest tests/test_vault.py -v` +Expected: PASS + +- [ ] **Step 5: Commit** + +```bash +git add app/core/vault.py tests/test_vault.py +git commit -m "refactor: add VaultManager class replacing global state (#54)" +``` + +### Task 1.3: Create `app/core/runner.py` — Clean Playbook Runner + +**Files:** +- Create: `app/core/runner.py` +- Create: `app/core/extractor.py` +- Test: `tests/test_runner.py` +- Test: `tests/test_extractor.py` + +- [ ] **Step 1: Write failing test for extractor** + +```python +# tests/test_extractor.py +from app.core.extractor import extract_action_results + + +def test_extract_finds_matching_action(): + events = [ + {"event": "runner_on_ok", "event_data": {"res": {"vm_list": [{"vmid": 100}]}}}, + {"event": "runner_on_ok", "event_data": {"res": {"other_action": "data"}}}, + ] + result = extract_action_results(events, "vm_list") + assert result == [[{"vmid": 100}]] + + +def test_extract_returns_empty_for_no_match(): + events = [ + {"event": "runner_on_ok", "event_data": {"res": {"other": "data"}}}, + ] + result = extract_action_results(events, "vm_list") + assert result == [] + + +def test_extract_skips_non_ok_events(): + events = [ + {"event": "runner_on_failed", "event_data": {"res": {"vm_list": [{"vmid": 100}]}}}, + ] + result = extract_action_results(events, "vm_list") + assert result == [] + + +def test_extract_handles_empty_events(): + assert extract_action_results([], "vm_list") == [] + + +def test_extract_handles_missing_event_data(): + events = [{"event": "runner_on_ok"}] + result = extract_action_results(events, "vm_list") + assert result == [] +``` + +- [ ] **Step 2: Run test, verify it fails** + +Run: `python -m pytest tests/test_extractor.py -v` +Expected: FAIL + +- [ ] **Step 3: Implement extractor** + +```python +# app/core/extractor.py +"""Extract structured results from Ansible runner events.""" + + +def extract_action_results(events: list[dict], action_to_search: str) -> list: + """Find all runner_on_ok events containing the specified action key.""" + out = [] + for ev in events: + if ev.get("event") != "runner_on_ok": + continue + res = (ev.get("event_data") or {}).get("res") + if isinstance(res, dict) and action_to_search in res: + out.append(res[action_to_search]) + return out +``` + +- [ ] **Step 4: Run extractor tests, verify they pass** + +Run: `python -m pytest tests/test_extractor.py -v` +Expected: PASS + +- [ ] **Step 5: Write failing test for runner build_logs** + +```python +# tests/test_runner.py +from app.core.runner import build_logs + + +def test_build_logs_extracts_stdout(): + events = [ + {"stdout": "PLAY [all] ***"}, + {"stdout": "TASK [ping] ***"}, + {"other": "data"}, + {"stdout": "ok: [host1]"}, + ] + log_ansi, log_plain = build_logs(events) + assert "PLAY [all]" in log_ansi + assert "ok: [host1]" in log_plain + + +def test_build_logs_empty_events(): + log_ansi, log_plain = build_logs([]) + assert log_ansi == "" + assert log_plain == "" + + +def test_build_logs_strips_ansi(): + events = [{"stdout": "\x1b[32mok\x1b[0m: [host1]"}] + log_ansi, log_plain = build_logs(events) + assert "\x1b[32m" in log_ansi + assert "\x1b[32m" not in log_plain + assert "ok" in log_plain +``` + +- [ ] **Step 6: Implement runner module** + +```python +# app/core/runner.py +"""Ansible playbook runner. Handles temp dir lifecycle, env vars, and execution.""" + +import logging +import os +import shutil +import tempfile +from pathlib import Path + +from ansible_runner import run as ansible_run + +from app.core.vault import VaultManager +from app.utils.text_cleaner import strip_ansi + +logger = logging.getLogger(__name__) + +# Module-level vault manager instance. Set by app.main during lifespan startup. +# Routes import run_playbook_core() and never need to know about vault internals. +vault_manager = VaultManager() + + +def build_logs(events: list[dict]) -> tuple[str, str]: + """Build ansible logs with and without ANSI escape codes.""" + lines = [ev["stdout"] for ev in events if ev.get("stdout")] + text_ansi = "\n".join(lines).strip() + return text_ansi, strip_ansi(text_ansi) + + +def _build_envvars(vault_manager: VaultManager) -> dict[str, str]: + """Build Ansible environment variables dict.""" + home_collections = os.path.expanduser("~/.ansible/collections") + sys_collections = "/usr/share/ansible/collections" + coll_paths = f"{home_collections}:{sys_collections}" + + envvars = { + "ANSIBLE_HOST_KEY_CHECKING": "True", + "ANSIBLE_DEPRECATION_WARNINGS": "False", + "ANSIBLE_INVENTORY_ENABLED": "yaml,ini", + "PYTHONWARNINGS": "ignore::DeprecationWarning", + "ANSIBLE_ROLES_PATH": os.environ.get("ANSIBLE_ROLES_PATH", ""), + "ANSIBLE_FILTER_PLUGINS": os.environ.get("ANSIBLE_FILTER_PLUGINS", ""), + "ANSIBLE_COLLECTIONS_PATH": os.environ.get("ANSIBLE_COLLECTIONS_PATH", coll_paths), + "ANSIBLE_COLLECTIONS_PATHS": os.environ.get("ANSIBLE_COLLECTIONS_PATHS", coll_paths), + "ANSIBLE_LIBRARY": os.environ.get("ANSIBLE_LIBRARY", ""), + } + + # Vault env vars + vault_pw_file = os.getenv("VAULT_PASSWORD_FILE") + if vault_pw_file: + envvars["ANSIBLE_VAULT_PASSWORD_FILE"] = vault_pw_file + elif vault_manager.get_vault_path(): + envvars["ANSIBLE_VAULT_PASSWORD_FILE"] = str(vault_manager.get_vault_path()) + + ansible_config = os.getenv("ANSIBLE_CONFIG") + if ansible_config: + envvars["ANSIBLE_CONFIG"] = ansible_config + + return envvars + + +def _setup_temp_dir( + inventory: Path, playbook: Path, vault_manager: VaultManager +) -> tuple[Path, Path, Path]: + """Create temp execution dir with playbook tree and inventory copy. + Returns: (tmp_dir, inventory_dest, playbook_relative_path) + """ + tmp_dir = Path(tempfile.mkdtemp(prefix="runner-")) + project_dir = tmp_dir / "project" + inventory_dir = tmp_dir / "inventory" + project_dir.mkdir(parents=True, exist_ok=True) + inventory_dir.mkdir(parents=True, exist_ok=True) + + # Copy playbook tree + src_dir = playbook.parent + dst_dir = project_dir / src_dir.name + shutil.copytree(src_dir, dst_dir, dirs_exist_ok=True) + + play_rel = (dst_dir / playbook.name).relative_to(project_dir) + inv_dest = inventory_dir / inventory.name + shutil.copy(inventory, inv_dest) + + # Write envvars file + envvars = _build_envvars(vault_manager) + env_dir = tmp_dir / "env" + env_dir.mkdir(parents=True, exist_ok=True) + (env_dir / "envvars").write_text( + "\n".join(f"{k}={v}" for k, v in envvars.items()) + "\n" + ) + + return tmp_dir, inv_dest, play_rel + + +def _build_cmdline(vault_manager: VaultManager, cmdline: str | None, tags: str | None) -> str | None: + """Build ansible-playbook command line arguments.""" + if not cmdline: + vf = os.getenv("VAULT_PASSWORD_FILE") + if not vf: + vp = vault_manager.get_vault_path() + vf = str(vp) if vp else None + if vf: + cmdline = f'--vault-password-file "{vf}"' + + vars_file = os.getenv("API_BACKEND_VAULT_FILE") + if vars_file: + cmdline = f'{(cmdline or "").strip()} -e "@{vars_file}"'.strip() + + if tags: + cmdline = f'{(cmdline or "").strip()} --tags {tags}'.strip() + + return cmdline or None + + +def run_playbook_core( + playbook: Path, + inventory: Path, + limit: str | None = None, + tags: str | None = None, + cmdline: str | None = None, + extravars: dict | None = None, + quiet: bool = False, +) -> tuple[int, list[dict], str, str]: + """Execute an Ansible playbook and return (rc, events, log_plain, log_ansi). + + Uses the module-level `vault_manager` instance, which is set during app lifespan. + Routes call this function directly — no need to pass vault_manager explicitly. + """ + tmp_dir, inv_dest, play_rel = _setup_temp_dir(inventory, playbook, vault_manager) + try: + final_cmdline = _build_cmdline(vault_manager, cmdline, tags) + + r = ansible_run( + private_data_dir=str(tmp_dir), + playbook=str(play_rel), + inventory=str(inv_dest), + streamer="json", + limit=limit, + cmdline=final_cmdline, + extravars=extravars or {}, + quiet=quiet, + ) + + events = list(r.events) if hasattr(r, "events") else [] + log_ansi, log_plain = build_logs(events) + return r.rc, events, log_plain, log_ansi + finally: + shutil.rmtree(tmp_dir, ignore_errors=True) +``` + +- [ ] **Step 7: Run runner tests, verify they pass** + +Run: `python -m pytest tests/test_runner.py tests/test_extractor.py -v` +Expected: PASS + +- [ ] **Step 8: Commit** + +```bash +git add app/core/runner.py app/core/extractor.py tests/test_runner.py tests/test_extractor.py +git commit -m "refactor: add clean runner and extractor in app/core/ (#53, #54)" +``` + +### Task 1.4: Create `app/core/exceptions.py` — Exception Handlers + +**Files:** +- Create: `app/core/exceptions.py` +- Test: `tests/test_exceptions.py` + +- [ ] **Step 1: Write failing test** + +```python +# tests/test_exceptions.py +import json +from unittest.mock import AsyncMock, MagicMock +from app.core.exceptions import make_validation_error_detail + + +def test_make_validation_error_detail(): + error = { + "loc": ("body", "vm_id"), + "msg": "field required", + "type": "missing", + "input": None, + "ctx": None, + } + detail = make_validation_error_detail(error) + assert detail["field"] == "body.vm_id" + assert detail["msg"] == "field required" + assert detail["type"] == "missing" +``` + +- [ ] **Step 2: Run test, verify it fails** + +Run: `python -m pytest tests/test_exceptions.py -v` +Expected: FAIL + +- [ ] **Step 3: Implement exceptions module** + +```python +# app/core/exceptions.py +"""Custom exception handlers for FastAPI.""" + +import json +import logging + +from fastapi import Request +from fastapi.exceptions import RequestValidationError +from fastapi.responses import JSONResponse + +logger = logging.getLogger(__name__) + + +def make_validation_error_detail(err: dict) -> dict: + """Convert a Pydantic validation error to the response format the UI expects.""" + return { + "field": ".".join(str(p) for p in err.get("loc", [])), + "msg": err.get("msg", ""), + "type": err.get("type", ""), + "input": err.get("input", None), + "ctx": err.get("ctx", None), + } + + +async def validation_exception_handler(request: Request, exc: RequestValidationError) -> JSONResponse: + """Verbose 422 handler with debug logging. Matches deployer-ui expected format.""" + logger.error("422 on %s %s %s", request.method, request.url.path, request.url.query) + + try: + raw = await request.body() + if raw: + body_text = raw.decode("utf-8", "ignore") + if body_text.strip(): + try: + parsed = json.loads(body_text) + logger.error("Request body:\n%s", json.dumps(parsed, indent=2, ensure_ascii=False)) + except json.JSONDecodeError: + logger.error("Request body (raw): %s", body_text) + else: + logger.error("Request body: ") + except Exception: + logger.exception("Failed to read request body.") + + details = [] + for err in exc.errors(): + detail = make_validation_error_detail(err) + logger.error("field=%s | msg=%s | type=%s", detail["field"], detail["msg"], detail["type"]) + details.append(detail) + + return JSONResponse(status_code=422, content={"detail": details}) +``` + +- [ ] **Step 4: Run test, verify it passes** + +Run: `python -m pytest tests/test_exceptions.py -v` +Expected: PASS + +- [ ] **Step 5: Commit** + +```bash +git add app/core/exceptions.py tests/test_exceptions.py +git commit -m "refactor: extract exception handlers to app/core/exceptions (#54)" +``` + +### Task 1.5: Consolidate Schemas — Flatten into Domain Files + +This task consolidates 52 schema files into 8 domain-grouped files. + +**Files:** +- Create: `app/schemas/vms.py` +- Create: `app/schemas/vm_config.py` +- Create: `app/schemas/snapshots.py` +- Create: `app/schemas/firewall.py` +- Create: `app/schemas/network.py` +- Create: `app/schemas/storage.py` +- Create: `app/schemas/bundles.py` +- Modify: `app/schemas/base.py` +- Modify: `app/schemas/debug/ping.py` (keep as-is, just update imports later) +- Test: `tests/test_schemas.py` + +**IMPORTANT:** Keep old schema files in place with re-exports during migration. Remove them only after all routes are updated. + +- [ ] **Step 1: Write failing test for consolidated schemas** + +```python +# tests/test_schemas.py +"""Verify consolidated schemas match the original field definitions.""" +from pydantic import ValidationError +import pytest + + +def test_vm_list_request(): + from app.schemas.vms import VmListRequest + req = VmListRequest(proxmox_node="px-testing") + assert req.proxmox_node == "px-testing" + assert req.as_json is True + + +def test_vm_action_request_validates_vm_id(): + from app.schemas.vms import VmActionRequest + req = VmActionRequest(proxmox_node="px-testing", vm_id="100") + assert req.vm_id == "100" + + +def test_vm_action_request_rejects_invalid_vm_id(): + from app.schemas.vms import VmActionRequest + with pytest.raises(ValidationError): + VmActionRequest(proxmox_node="px-testing", vm_id="abc") + + +def test_vm_create_request(): + from app.schemas.vms import VmCreateRequest + req = VmCreateRequest( + proxmox_node="px-testing", + vm_id="200", + vm_name="test-vm", + ) + assert req.vm_name == "test-vm" + assert req.vm_cpu is None # optional + + +def test_vm_clone_request(): + from app.schemas.vms import VmCloneRequest + req = VmCloneRequest( + proxmox_node="px-testing", + vm_id="100", + vm_new_id="200", + vm_name="cloned-vm", + ) + assert req.vm_new_id == "200" + + +def test_proxmox_node_rejects_special_chars(): + from app.schemas.vms import VmListRequest + with pytest.raises(ValidationError): + VmListRequest(proxmox_node="node; rm -rf /") + + +def test_firewall_rule_request(): + from app.schemas.firewall import FirewallRuleApplyRequest + req = FirewallRuleApplyRequest( + proxmox_node="px-testing", + vm_id="100", + vm_fw_action="ACCEPT", + vm_fw_type="in", + ) + assert req.vm_fw_action == "ACCEPT" + + +def test_network_vm_add_request(): + from app.schemas.network import VmNetworkAddRequest + req = VmNetworkAddRequest( + proxmox_node="px-testing", + vm_id="100", + iface_bridge="vmbr100", + ) + assert req.iface_bridge == "vmbr100" + + +# --- Bundle schemas (complex — have nested per-VM models) --- + +def test_bundle_ubuntu_docker_request(): + """Ubuntu install bundle schemas must be preserved.""" + from app.schemas.bundles import DockerInstallRequest + req = DockerInstallRequest(proxmox_node="px-testing") + assert req.proxmox_node == "px-testing" + + +def test_bundle_proxmox_create_vms_request(): + """Proxmox bundle schemas have nested VMs dict — must preserve structure.""" + from app.schemas.bundles import CreateVmsAdminRequest + # These schemas have a 'vms' field with per-VM nested models + # The exact field names must match what admin_run_bundles_core routes expect + assert hasattr(CreateVmsAdminRequest, "model_fields") +``` + +**NOTE on bundle schema files to read:** Before consolidating, read ALL of these: +- `app/schemas/bundles/core/linux/ubuntu/install/docker.py` +- `app/schemas/bundles/core/linux/ubuntu/install/docker_compose.py` +- `app/schemas/bundles/core/linux/ubuntu/install/basic_packages.py` +- `app/schemas/bundles/core/linux/ubuntu/install/dot_files.py` +- `app/schemas/bundles/core/linux/ubuntu/configure/add_user.py` +- `app/schemas/bundles/core/proxmox/configure/default/vms/create_vms_admin_default.py` +- `app/schemas/bundles/core/proxmox/configure/default/vms/create_vms_student_default.py` +- `app/schemas/bundles/core/proxmox/configure/default/vms/create_vms_vuln_default.py` +- `app/schemas/bundles/core/proxmox/configure/default/vms/revert_snapshot_default.py` +- `app/schemas/bundles/core/proxmox/configure/default/vms/start_stop_resume_pause_default.py` + +- [ ] **Step 2: Run tests, verify they fail** + +Run: `python -m pytest tests/test_schemas.py -v` +Expected: FAIL + +- [ ] **Step 3: Read every existing schema file to extract field definitions** + +Read all files under `app/schemas/` to understand the exact Pydantic field definitions, patterns, and defaults. You must preserve EVERY field exactly as defined — the deployer-ui depends on these. + +- [ ] **Step 4: Create consolidated `app/schemas/vms.py`** + +Consolidate from: +- `app/schemas/proxmox/vm_list.py` (Request_ProxmoxVms_VmList, Reply_ProxmoxVmList) +- `app/schemas/proxmox/vm_list_usage.py` +- `app/schemas/proxmox/vm_id/start_stop_resume_pause.py` +- `app/schemas/proxmox/vm_id/create.py` +- `app/schemas/proxmox/vm_id/delete.py` +- `app/schemas/proxmox/vm_id/clone.py` +- `app/schemas/proxmox/vm_id/mass_delete.py` +- `app/schemas/proxmox/vm_id/mass_start_stop_resume_pause.py` + +Keep both NEW names (e.g., `VmListRequest`) and OLD names (e.g., `Request_ProxmoxVms_VmList = VmListRequest`) as aliases so existing route imports don't break during migration. + +- [ ] **Step 5: Create consolidated `app/schemas/vm_config.py`** + +Consolidate from `app/schemas/proxmox/vm_id/config/` — all 5 files. + +- [ ] **Step 6: Create consolidated `app/schemas/snapshots.py`** + +Consolidate from `app/schemas/proxmox/vm_id/snapshot/` — all 4 files. + +- [ ] **Step 7: Create consolidated `app/schemas/firewall.py`** + +Consolidate from `app/schemas/proxmox/firewall/` — all files. + +- [ ] **Step 8: Create consolidated `app/schemas/network.py`** + +Consolidate from `app/schemas/proxmox/network/` — all files. + +- [ ] **Step 9: Create consolidated `app/schemas/storage.py`** + +Consolidate from `app/schemas/proxmox/storage/` — all files. + +- [ ] **Step 10: Create consolidated `app/schemas/bundles.py`** + +Consolidate from `app/schemas/bundles/` — all files. + +- [ ] **Step 11: Update `app/schemas/base.py`** + +Keep existing classes, add a shared `ProxmoxBaseRequest` that other schemas inherit from: + +```python +# Add to app/schemas/base.py +from pydantic import BaseModel, Field + + +class ProxmoxBaseRequest(BaseModel): + """Base request model with fields common to all Proxmox operations.""" + proxmox_node: str = Field(..., pattern=r"^[A-Za-z0-9-]*$") + as_json: bool = Field(default=True) +``` + +- [ ] **Step 12: Run schema tests, verify they pass** + +Run: `python -m pytest tests/test_schemas.py -v` +Expected: PASS + +- [ ] **Step 13: Commit** + +```bash +git add app/schemas/ tests/test_schemas.py +git commit -m "refactor: consolidate 52 schema files into domain-grouped modules (#53)" +``` + +### Task 1.6: Consolidate Routes — Flatten into Domain Files + +This is the largest task. Each domain's 5-20 route files collapse into one file. + +**CRITICAL:** All route paths, HTTP methods, tags, summaries, and response models must stay IDENTICAL. Only the file organization changes. + +**Files:** +- Create: `app/routes/vms.py` +- Create: `app/routes/vm_config.py` +- Create: `app/routes/snapshots.py` +- Create: `app/routes/firewall.py` +- Create: `app/routes/network.py` +- Create: `app/routes/storage.py` +- Create: `app/routes/bundles.py` +- Create: `app/routes/runner.py` (dynamic {name}/run endpoints) +- Create: `app/routes/debug.py` +- Modify: `app/routes/__init__.py` +- Keep: `app/routes/ws_status.py` (already well-structured) +- Test: `tests/test_routes_registered.py` + +- [ ] **Step 1: Write test that verifies route registration** + +```python +# tests/test_routes_registered.py +"""After consolidation, verify all routes are still registered with correct methods and paths. +Uses the golden reference file generated in Task 0.1 Step 4 — NOT a hand-coded list.""" + +import json +from pathlib import Path + + +def test_all_routes_preserved(client): + """After restructure, every route from the golden reference must still exist. + This catches all endpoints including bundle/scenario dynamic routes.""" + golden_path = Path(__file__).parent / "fixtures" / "routes_golden.json" + assert golden_path.exists(), "Golden reference missing — run Task 0.1 Step 4 first" + + with open(golden_path) as f: + golden_routes = json.load(f) + + resp = client.get("/docs/openapi.json") + schema = resp.json() + registered = {} + for path, methods in schema.get("paths", {}).items(): + for method in methods: + if method.upper() in ("GET", "POST", "PUT", "DELETE", "PATCH"): + registered.setdefault(path, []).append(method.upper()) + + missing = [] + for path, methods in golden_routes.items(): + for method in methods: + if method not in registered.get(path, []): + missing.append(f"{method} {path}") + + assert not missing, f"Missing {len(missing)} routes after restructure:\n" + "\n".join(sorted(missing)) +``` + +- [ ] **Step 2: Consolidate VM routes into `app/routes/vms.py`** + +Read every file under `app/routes/v0/proxmox/vms/` and consolidate into one file. Each route handler function must: +1. Keep the same `@router.post(path=..., summary=..., tags=..., response_model=...)` decorator +2. Use `run_playbook_core()` from `app.core.runner` (with vault_manager parameter) +3. Use `extract_action_results()` from `app.core.extractor` +4. Use `settings` from `app.core.config` instead of `Path(os.getenv("PROJECT_ROOT_DIR"))` +5. Keep the same response format (rc + result or rc + log_multiline) + +The consolidated file should define: +- A `_run_proxmox_action()` helper that encapsulates the common pattern (since all VM routes follow the same flow) +- Individual route handlers that call the helper with their specific action name and extravars + +- [ ] **Step 3: Consolidate VM config routes into `app/routes/vm_config.py`** + +- [ ] **Step 4: Consolidate snapshot routes into `app/routes/snapshots.py`** + +- [ ] **Step 5: Consolidate firewall routes into `app/routes/firewall.py`** + +- [ ] **Step 6: Consolidate network routes into `app/routes/network.py`** + +- [ ] **Step 7: Consolidate storage routes into `app/routes/storage.py`** + +- [ ] **Step 8: Consolidate bundle/scenario routes into `app/routes/bundles.py`** + +**IMPORTANT:** Bundle routes have THREE distinct patterns — do NOT use the `_run_proxmox_action()` helper for these: + +**Pattern A — Dynamic runner routes** (from `admin_run.py` → `actions_run.py`, `scenarios_run.py`): +- `/v0/admin/run/bundles/{bundles_name}/run` — uses path param, calls `utils.resolve_bundles_playbook()` +- `/v0/admin/run/scenarios/{scenario_name}/run` — uses path param, calls `utils.resolve_scenarios_playbook()` +- These use `Request_DebugPing` schema (not Proxmox schemas) +- They do NOT extract action results — just return raw logs + +**Pattern B — Core Ubuntu bundle routes** (from `admin_run_bundles_core.py` → `v0/admin/bundles/core/linux/ubuntu/`): +- Routes like `/v0/admin/run/bundles/core/linux/ubuntu/install/docker` +- Each has a hardcoded playbook path resolved via `utils.resolve_bundles_playbook("core/linux/ubuntu/install/docker", "public_github")` +- Standard request → run_playbook_core → response pattern + +**Pattern C — Core Proxmox multi-step bundle routes** (from `admin_run_bundles_core.py` → `v0/admin/bundles/core/proxmox/`): +- Routes like `/v0/admin/run/bundles/core/proxmox/configure/default/create-vms-admin` +- These iterate over `req.vms` dict and call `run_playbook_core()` MULTIPLE times per request +- Some call `init.yml` first, then `main.yml` for each VM +- Bundle schemas have nested per-VM models (e.g., `vms` dict with `vm_id`, `vm_ip`, `vm_description`) + +Read every file under `app/routes/v0/admin/bundles/core/proxmox/` to capture the multi-step patterns before consolidating. + +Also create `app/routes/runner.py` for the dynamic Pattern A routes, since they're architecturally different. + +- [ ] **Step 9: Consolidate debug routes into `app/routes/debug.py`** + +- [ ] **Step 10: Rewrite `app/routes/__init__.py` with new imports** + +```python +# app/routes/__init__.py +from fastapi import APIRouter + +from app.routes.vms import router as vms_router +from app.routes.vm_config import router as vm_config_router +from app.routes.snapshots import router as snapshots_router +from app.routes.firewall import router as firewall_router +from app.routes.network import router as network_router +from app.routes.storage import router as storage_router +from app.routes.bundles import router as bundles_router +from app.routes.runner import router as runner_router # Dynamic {name}/run routes +from app.routes.debug import router as debug_router + +router = APIRouter() + +router.include_router(debug_router) +router.include_router(bundles_router) +router.include_router(runner_router) +router.include_router(vms_router) +router.include_router(vm_config_router) +router.include_router(snapshots_router) +router.include_router(firewall_router) +router.include_router(network_router) +router.include_router(storage_router) +``` + +- [ ] **Step 11: Run route registration test** + +Run: `python -m pytest tests/test_routes_registered.py -v` +Expected: PASS — all routes still present + +- [ ] **Step 12: Commit** + +```bash +git add app/routes/ tests/test_routes_registered.py +git commit -m "refactor: consolidate 86 route files into 9 domain modules (#53)" +``` + +### Task 1.7: Rewrite `app/main.py` — Application Factory + +**Files:** +- Modify: `app/main.py` +- Test: `tests/test_app_factory.py` + +- [ ] **Step 1: Write failing test** + +```python +# tests/test_app_factory.py +from app.main import create_app + + +def test_create_app_returns_fastapi_instance(): + app = create_app() + assert app.title == "CR42 - API" + assert app.version == "v0.1" + + +def test_create_app_has_cors_middleware(): + app = create_app() + middleware_classes = [m.cls.__name__ for m in app.user_middleware] + assert "CORSMiddleware" in middleware_classes +``` + +- [ ] **Step 2: Run test, verify it fails** + +Run: `python -m pytest tests/test_app_factory.py -v` +Expected: FAIL + +- [ ] **Step 3: Rewrite main.py with application factory** + +```python +# app/main.py +"""FastAPI application factory for the Range42 Backend API.""" + +import logging +import os +import shutil +import stat +import tempfile +from contextlib import asynccontextmanager +from pathlib import Path + +from fastapi import FastAPI +from fastapi.exceptions import RequestValidationError +from starlette.middleware import Middleware +from starlette.middleware.cors import CORSMiddleware + +from app.core.config import settings +from app.core.exceptions import validation_exception_handler +from app.core.runner import vault_manager # Module-level instance shared with routes +from app.routes import router as api_router +from app.routes.ws_status import router as ws_router + +logger = logging.getLogger(__name__) + + +@asynccontextmanager +async def lifespan(app: FastAPI): + """Manage vault password lifecycle: setup on startup, cleanup on shutdown.""" + tmp_dir: Path | None = None + + vault_password_file = settings.vault_password_file + vault_password = settings.vault_password + + if vault_password_file: + p = Path(vault_password_file) + if not p.exists(): + raise RuntimeError(f"Vault password file not found: {p}") + vault_manager.set_vault_path(p) + logger.info("Using VAULT_PASSWORD_FILE=%s", p) + + elif vault_password: + tmp_dir = Path(tempfile.mkdtemp(prefix="vault-")) + f = tmp_dir / "vault_pass.txt" + f.write_text(vault_password) + os.chmod(f, stat.S_IRUSR | stat.S_IWUSR) + vault_manager.set_vault_path(f) + logger.info("Using VAULT_PASSWORD (temp file)") + + else: + logger.warning("No vault password provided") + + try: + yield + finally: + if tmp_dir and tmp_dir.exists(): + shutil.rmtree(tmp_dir, ignore_errors=True) + + +def create_app() -> FastAPI: + """Application factory. Creates and configures the FastAPI application.""" + middleware = [ + Middleware( + CORSMiddleware, + allow_origin_regex=settings.cors_origin_regex, + allow_credentials=True, + allow_methods=["GET", "POST", "DELETE", "OPTIONS"], + allow_headers=["Content-Type", "Accept", "Authorization"], + max_age=600, + ) + ] + + app = FastAPI( + title="CR42 - API", + lifespan=lifespan, + docs_url="/docs/swagger", + redoc_url="/docs/redoc", + openapi_url="/docs/openapi.json", + version="v0.1", + license_info={"name": "GPLv3"}, + contact={"email": "info@digisquad.com"}, + middleware=middleware, + ) + + # Register validation error handler (matches deployer-ui expected format) + app.add_exception_handler(RequestValidationError, validation_exception_handler) + + # Include routers + app.include_router(api_router) + app.include_router(ws_router) + + return app + + +# Module-level app instance for uvicorn +app = create_app() +``` + +- [ ] **Step 4: Run all tests** + +Run: `python -m pytest tests/ -v` +Expected: ALL PASS + +- [ ] **Step 5: Commit** + +```bash +git add app/main.py tests/test_app_factory.py +git commit -m "refactor: rewrite main.py with application factory pattern (#54)" +``` + +### Task 1.8: Clean Up — Remove Old Files + +**Files:** +- Remove: `app/routes/v0/` (entire directory) +- Remove: `app/routes/admin_proxmox.py` +- Remove: `app/routes/admin_run.py` +- Remove: `app/routes/admin_run_bundles_core.py` +- Remove: `app/routes/admin_debug.py` +- Remove: `app/schemas/proxmox/` (entire directory) +- Remove: `app/schemas/bundles/` (entire directory) +- Remove: `app/schemas/debug/` (keep `app/schemas/debug.py` — the new consolidated file) +- Remove: `app/vault/` (replaced by `app/core/vault.py`) +- Remove: `app/runner.py` (replaced by `app/core/runner.py`) +- Remove: `app/extract_actions.py` (replaced by `app/core/extractor.py`) + +- [ ] **Step 1: Run all tests BEFORE deleting anything** + +Run: `python -m pytest tests/ -v` +Expected: ALL PASS + +- [ ] **Step 2: Delete old route files** + +```bash +rm -rf app/routes/v0/ +rm -f app/routes/admin_proxmox.py app/routes/admin_run.py app/routes/admin_run_bundles_core.py app/routes/admin_debug.py +``` + +- [ ] **Step 3: Delete old schema files** + +```bash +rm -rf app/schemas/proxmox/ app/schemas/bundles/ app/schemas/debug/ +``` + +- [ ] **Step 4: Delete old vault and runner modules** + +```bash +rm -rf app/vault/ +rm -f app/runner.py app/extract_actions.py +``` + +- [ ] **Step 5: Run all tests AFTER deleting** + +Run: `python -m pytest tests/ -v` +Expected: ALL PASS — if any test fails, a stale import remains; fix it before proceeding + +- [ ] **Step 6: Commit** + +```bash +git add -A +git commit -m "chore: remove old nested files replaced by consolidated modules (#53)" +``` + +### Task 1.9: Fix Utils Logger Bug + +**Files:** +- Modify: `app/utils/checks_playbooks.py:2` +- Modify: `app/utils/checks_inventory.py` (if same bug exists) + +- [ ] **Step 1: Fix `from venv import logger` (line 2 of checks_playbooks.py)** + +This is a bug — `venv.logger` is Python's virtual environment module, not a logging logger. + +Change: +```python +from venv import logger +``` +To: +```python +import logging +logger = logging.getLogger(__name__) +``` + +- [ ] **Step 2: Check `checks_inventory.py` for the same bug** + +Read the file and apply the same fix if present. + +- [ ] **Step 3: Run all tests** + +Run: `python -m pytest tests/ -v` +Expected: PASS + +- [ ] **Step 4: Commit** + +```bash +git add app/utils/ +git commit -m "fix: replace broken venv.logger import with proper logging (#54)" +``` + +--- + +## Phase 2: Code Quality Refactor (#54) + +### Task 2.1: Replace Print Statements with Structured Logging + +**Files:** +- Modify: All files in `app/` that use `print()` +- Test: Manual verification via log output + +- [ ] **Step 1: Find all print statements** + +Run: `grep -rn "print(" app/ --include="*.py" | grep -v "__pycache__"` + +- [ ] **Step 2: Replace each print() with appropriate logger call** + +Pattern: +- `print(":: lifespan :: ...")` → `logger.info("...")` +- `print(":: err - ...")` → `logger.error("...")` +- `print(f":: work done :: {tmp_dir}")` → `logger.debug("Runner temp dir: %s", tmp_dir)` +- `print(":: REQUEST ::", ...)` → `logger.debug("Request: %s", ...)` +- Debug-only prints → `logger.debug()` + +- [ ] **Step 3: Remove `debug = 0` / `debug = 1` flags from all route files** + +These are no longer needed since logging is controlled by log level. + +- [ ] **Step 4: Run all tests** + +Run: `python -m pytest tests/ -v` +Expected: PASS + +- [ ] **Step 5: Commit** + +```bash +git add app/ +git commit -m "refactor: replace print statements with structured logging (#54)" +``` + +### Task 2.2: Add Type Hints to Core Modules + +**Files:** +- Modify: `app/core/runner.py` +- Modify: `app/utils/checks_playbooks.py` +- Modify: `app/utils/checks_inventory.py` +- Modify: `app/utils/vm_id_name_resolver.py` + +- [ ] **Step 1: Add type hints to all function signatures in utils/** + +Each function should have full parameter types and return types. + +- [ ] **Step 2: Run tests** + +Run: `python -m pytest tests/ -v` +Expected: PASS + +- [ ] **Step 3: Commit** + +```bash +git add app/ +git commit -m "refactor: add type hints to core and utility modules (#54)" +``` + +### Task 2.3: Fix Dead Code in Runner + +**Files:** +- Already handled in Task 1.3 (new runner doesn't have the bugs) +- Verify: No duplicate return statements, temp dir cleanup enabled + +- [ ] **Step 1: Verify app/core/runner.py has no dead code** + +Check that: +- `shutil.rmtree(tmp_dir)` is in the `finally` block (not commented out) +- No duplicate return statements +- No commented-out code blocks + +- [ ] **Step 2: Commit (if changes needed)** + +```bash +git add app/core/runner.py +git commit -m "fix: ensure temp dir cleanup and remove dead code in runner (#54)" +``` + +### Task 2.4: Create `start.sh` Template with Portable Env Vars + +**Files:** +- Modify: `start.sh` +- Create: `.env.example` + +- [ ] **Step 1: Create `.env.example`** + +```bash +# .env.example — Copy to .env and fill in values +# Required +PROJECT_ROOT_DIR= # Path to this project root +VAULT_PASSWORD_FILE= # Path to vault password file +# OR +# VAULT_PASSWORD= # Vault password string (alternative to file) + +# Playbook sources +API_BACKEND_WWWAPP_PLAYBOOKS_DIR= # Local playbooks directory (usually same as PROJECT_ROOT_DIR) +API_BACKEND_PUBLIC_PLAYBOOKS_DIR= # External playbooks repo path +API_BACKEND_INVENTORY_DIR= # Ansible inventory directory +API_BACKEND_VAULT_FILE= # Vault-encrypted variables file + +# Optional +CORS_ORIGIN_REGEX= # Custom CORS regex (default: localhost only) +HOST=0.0.0.0 # Server bind address +PORT=8000 # Server port +DEBUG=false # Enable debug mode (verbose 422 errors) +``` + +- [ ] **Step 2: Update `start.sh` to use `.env` file** + +```bash +#!/bin/bash +set -euo pipefail + +PROJECT_ROOT="$(realpath "$(dirname "${BASH_SOURCE[0]}")")" +export PROJECT_ROOT_DIR="$PROJECT_ROOT" + +# Load .env file if it exists +if [ -f "$PROJECT_ROOT/.env" ]; then + set -a + source "$PROJECT_ROOT/.env" + set +a +fi + +# Install Ansible collections if needed +if [ ! -d "$HOME/.ansible/collections/ansible_collections/community/general" ]; then + echo ":: Installing Ansible collections..." + ansible-galaxy collection install -r requirements.yml -p ~/.ansible/collections +fi + +# Set defaults for required vars +export API_BACKEND_WWWAPP_PLAYBOOKS_DIR="${API_BACKEND_WWWAPP_PLAYBOOKS_DIR:-$PROJECT_ROOT_DIR/}" +export API_BACKEND_INVENTORY_DIR="${API_BACKEND_INVENTORY_DIR:-$PROJECT_ROOT_DIR/inventory/}" + +HOST="${HOST:-0.0.0.0}" +PORT="${PORT:-8000}" + +cd "$PROJECT_ROOT_DIR" || exit +export PYTHONPATH="$PROJECT_ROOT:${PYTHONPATH:-}" + +echo ":: start :: app.main:app - $PROJECT_ROOT_DIR" +exec uvicorn app.main:app \ + --host "$HOST" \ + --port "$PORT" \ + --log-level info \ + --reload +``` + +- [ ] **Step 3: Add `.env` to `.gitignore`** + +Append to `.gitignore`: +``` +.env +``` + +- [ ] **Step 4: Commit** + +```bash +git add start.sh .env.example .gitignore +git commit -m "refactor: make start.sh portable with .env support (#54)" +``` + +--- + +## Phase 3: Docker Support (#51) + +### Task 3.1: Create Dockerfile + +**Files:** +- Create: `Dockerfile` +- Create: `.dockerignore` + +- [ ] **Step 1: Create `.dockerignore`** + +``` +.git +.gitignore +.env +__pycache__ +*.pyc +.venv +venv +*.egg-info +.pytest_cache +docs/ +curl_utils/ +*.md +!requirements.txt +!requirements.yml +``` + +- [ ] **Step 2: Create Dockerfile** + +```dockerfile +# Dockerfile +FROM python:3.12-slim AS base + +# Install system deps for ansible and ssh +RUN apt-get update && apt-get install -y --no-install-recommends \ + openssh-client \ + sshpass \ + git \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +# Install Python deps +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Install Ansible collections +COPY requirements.yml . +RUN ansible-galaxy collection install -r requirements.yml -p /usr/share/ansible/collections + +# Copy application +COPY app/ app/ +COPY playbooks/ playbooks/ +COPY inventory/ inventory/ +COPY start.sh . + +# Set env defaults +ENV PROJECT_ROOT_DIR=/app +ENV API_BACKEND_WWWAPP_PLAYBOOKS_DIR=/app/ +ENV API_BACKEND_INVENTORY_DIR=/app/inventory/ +ENV HOST=0.0.0.0 +ENV PORT=8000 +ENV PYTHONPATH=/app + +EXPOSE 8000 + +# Health check +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ + CMD python -c "import httpx; httpx.get('http://localhost:8000/docs/openapi.json').raise_for_status()" + +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--log-level", "info"] +``` + +- [ ] **Step 3: Build the image to verify** + +Run: `cd /home/ppa/projects/range42-base/range42-backend-api && docker build -t range42-backend-api:dev .` +Expected: Build completes without error + +- [ ] **Step 4: Commit** + +```bash +git add Dockerfile .dockerignore +git commit -m "feat(docker): add Dockerfile for containerized deployment (#51)" +``` + +### Task 3.2: Create docker-compose.yml + +**Files:** +- Create: `docker-compose.yml` + +- [ ] **Step 1: Create docker-compose.yml** + +```yaml +# docker-compose.yml +services: + api: + build: . + ports: + - "${PORT:-8000}:8000" + volumes: + # Mount local playbooks and inventory for development + - ./app:/app/app:ro + - ./playbooks:/app/playbooks:ro + - ./inventory:/app/inventory:ro + # Mount external playbooks repo (optional) + - ${API_BACKEND_PUBLIC_PLAYBOOKS_DIR:-./playbooks}:/external-playbooks:ro + # Mount vault password file (optional) + - ${VAULT_PASSWORD_FILE:-/dev/null}:/run/secrets/vault_pass:ro + environment: + - PROJECT_ROOT_DIR=/app + - API_BACKEND_WWWAPP_PLAYBOOKS_DIR=/app/ + - API_BACKEND_PUBLIC_PLAYBOOKS_DIR=/external-playbooks/ + - API_BACKEND_INVENTORY_DIR=/app/inventory/ + - API_BACKEND_VAULT_FILE=${API_BACKEND_VAULT_FILE:-} + - VAULT_PASSWORD_FILE=/run/secrets/vault_pass + - VAULT_PASSWORD=${VAULT_PASSWORD:-} + - CORS_ORIGIN_REGEX=${CORS_ORIGIN_REGEX:-} + - DEBUG=${DEBUG:-false} + restart: unless-stopped + healthcheck: + test: ["CMD", "python", "-c", "import httpx; httpx.get('http://localhost:8000/docs/openapi.json').raise_for_status()"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 10s +``` + +- [ ] **Step 2: Verify compose starts** + +Run: `cd /home/ppa/projects/range42-base/range42-backend-api && docker compose up --build -d && docker compose logs api && docker compose down` +Expected: Container starts, logs show "Uvicorn running on..." + +- [ ] **Step 3: Commit** + +```bash +git add docker-compose.yml +git commit -m "feat(docker): add docker-compose.yml for local development (#51)" +``` + +--- + +## Phase 4: Documentation (#52) + +### Task 4.1: Rewrite README.md + +**Files:** +- Modify: `README.md` + +- [ ] **Step 1: Read current README** + +Read `README.md` to understand what's already documented. + +- [ ] **Step 2: Rewrite README with comprehensive documentation** + +The README should cover: + +1. **Project overview** — What this API does, where it sits in Range42 architecture +2. **Quick start** — 3 options: Docker, docker-compose, manual setup +3. **Configuration** — Table of all environment variables with descriptions and defaults +4. **API documentation** — How to access Swagger/ReDoc, link to OpenAPI spec +5. **Project structure** — Updated directory tree reflecting new structure +6. **Development** — How to set up dev environment, run tests, lint +7. **Architecture** — Request flow diagram, key modules explained +8. **Deployment** — Production considerations (Kong gateway, SSL, scaling) +9. **Contributing** — Commit conventions, branch naming, PR guidelines + +- [ ] **Step 3: Commit** + +```bash +git add README.md +git commit -m "docs: comprehensive README with setup, architecture, and API guide (#52)" +``` + +### Task 4.2: Add Sphinx-Style Docstrings to All Modules + +**Files:** +- Modify: All `.py` files in `app/core/`, `app/routes/`, `app/schemas/`, `app/utils/` + +- [ ] **Step 1: Document `app/core/` modules** + +Add Sphinx-style docstrings (`:param:`, `:type:`, `:returns:`, `:rtype:`, `:raises:`) to every public function and class in: +- `app/core/config.py` — `Settings` class and its properties +- `app/core/runner.py` — `run_playbook_core()`, `build_logs()`, `_setup_temp_dir()`, `_build_cmdline()`, `_build_envvars()` +- `app/core/extractor.py` — `extract_action_results()` +- `app/core/vault.py` — `VaultManager` class +- `app/core/exceptions.py` — `validation_exception_handler()`, `make_validation_error_detail()` + +Example format: +```python +def run_playbook_core( + playbook: Path, + inventory: Path, + limit: str | None = None, + tags: str | None = None, + cmdline: str | None = None, + extravars: dict | None = None, + quiet: bool = False, +) -> tuple[int, list[dict], str, str]: + """Execute an Ansible playbook via ansible-runner and return structured results. + + Creates a temporary execution directory, copies the playbook tree and inventory, + sets Ansible environment variables, runs the playbook, and cleans up. + + :param playbook: Absolute path to the playbook YAML file. + :type playbook: Path + :param inventory: Absolute path to the inventory file. + :type inventory: Path + :param limit: Ansible ``--limit`` pattern to target specific hosts. + :type limit: str or None + :param tags: Comma-separated Ansible tags to select tasks. + :type tags: str or None + :param cmdline: Additional raw CLI arguments for ``ansible-playbook``. + :type cmdline: str or None + :param extravars: Extra variables passed to the playbook as ``-e`` args. + :type extravars: dict or None + :param quiet: Suppress ansible-runner stdout if True. + :type quiet: bool + :returns: Tuple of (return_code, events, log_plain, log_ansi). + :rtype: tuple[int, list[dict], str, str] + :raises FileNotFoundError: If playbook or inventory file does not exist. + """ +``` + +- [ ] **Step 2: Document `app/routes/` modules** + +Each route module gets a module-level docstring describing its domain and listing all endpoints: +```python +"""VM lifecycle routes for the Proxmox API. + +Endpoints: + POST /v0/admin/proxmox/vms/list + POST /v0/admin/proxmox/vms/list_usage + POST /v0/admin/proxmox/vms/vm_id/start + POST /v0/admin/proxmox/vms/vm_id/stop + ... +""" +``` + +Each route handler function gets a one-line summary + `:param:` for the request model: +```python +@router.post(path="/start", ...) +def proxmox_vms_vm_id_start(req: VmActionRequest): + """Start a specific virtual machine on Proxmox. + + :param req: Request body with ``proxmox_node`` and ``vm_id``. + :type req: VmActionRequest + :returns: JSON with ``rc`` (return code) and ``result`` or ``log_multiline``. + :rtype: JSONResponse + """ +``` + +- [ ] **Step 3: Document `app/schemas/` modules** + +Each Pydantic model class gets a docstring describing its purpose and example usage: +```python +class VmActionRequest(ProxmoxBaseRequest): + """Request body for single-VM lifecycle operations (start, stop, pause, resume). + + :param vm_id: Proxmox VM ID (numeric string). + :type vm_id: str + + Example:: + + {"proxmox_node": "pve01", "vm_id": "100", "as_json": true} + """ +``` + +- [ ] **Step 4: Document `app/utils/` modules** + +Add docstrings to `checks_playbooks.py`, `checks_inventory.py`, `text_cleaner.py`, `vm_id_name_resolver.py`. + +- [ ] **Step 5: Run tests to verify docstrings don't break anything** + +Run: `python -m pytest tests/ -v` +Expected: PASS + +- [ ] **Step 6: Commit** + +```bash +git add app/ +git commit -m "docs: add Sphinx-style docstrings to all modules (#52)" +``` + +### Task 4.3: Update curl_utils Scripts (if paths changed) + +**Files:** +- Review: All files in `curl_utils/` + +- [ ] **Step 1: Verify curl scripts still work** + +The API paths haven't changed (only the Python file organization), so curl scripts should still work. Verify by reading a few scripts and confirming the endpoint URLs match. + +- [ ] **Step 2: If no changes needed, skip this task** + +--- + +## Phase 5: Final Verification + +### Task 5.1: Full Test Suite Run + +- [ ] **Step 1: Run all tests** + +Run: `cd /home/ppa/projects/range42-base/range42-backend-api && python -m pytest tests/ -v --tb=short` +Expected: ALL PASS + +- [ ] **Step 2: Run the app and verify OpenAPI schema** + +Run: Start the app with `./start.sh`, then: +```bash +curl -s http://localhost:8000/docs/openapi.json | python -m json.tool | head -20 +``` +Expected: Valid OpenAPI JSON with all routes + +- [ ] **Step 3: Compare old vs new OpenAPI schemas** + +If you saved the old schema before restructuring, diff them: +```bash +diff <(curl -s http://localhost:8000/docs/openapi.json | python -m json.tool) old-openapi.json +``` +Expected: Paths identical (only schema descriptions may differ) + +- [ ] **Step 4: Docker build and run test** + +```bash +docker build -t range42-backend-api:test . && docker run --rm -p 8000:8000 range42-backend-api:test & +sleep 5 +curl -s http://localhost:8000/docs/openapi.json | python -m json.tool | head -5 +docker stop $(docker ps -q --filter ancestor=range42-backend-api:test) +``` + +### Task 5.2: Final Commit and Issue Closure + +- [ ] **Step 1: Review all changes** + +```bash +git log --oneline --since="today" +git diff --stat main +``` + +- [ ] **Step 2: Verify file count reduction** + +```bash +find app/ -name "*.py" | wc -l +``` +Expected: ~25 files (down from ~150) + +- [ ] **Step 3: Update GitHub issues with references** + +Each commit already references the issue numbers. The PR will close them. + +--- + +## Summary of Changes + +| Metric | Before | After | +|--------|--------|-------| +| Python files in `app/` | ~150 | ~25 | +| Route files | 86 | 10 (vms, vm_config, snapshots, firewall, network, storage, bundles, runner, debug, ws_status) | +| Schema files | 52 | 9 (base, vms, vm_config, snapshots, firewall, network, storage, bundles, debug) | +| Max directory depth | 7 levels | 3 levels | +| Test files | 0 | 7+ | +| Dockerfile | none | multi-stage | +| docker-compose | none | full dev setup | +| Config approach | hardcoded in start.sh | .env + Settings class | +| Logging | print() + debug flags | structured logging | +| Global state | vault._VAULT_PASS_PATH | VaultManager instance | +| Temp dir cleanup | disabled (memory leak) | enabled in finally block | +| Dead code | duplicate returns in runner | removed | +| Logger bug | `from venv import logger` | `logging.getLogger()` | + +## Risk Mitigation + +1. **API compatibility** — Smoke tests verify every route exists before AND after each change +2. **Incremental approach** — Old files kept with re-exports during migration, deleted only after tests pass +3. **Phase ordering** — Structure first (safe), refactor second (depends on structure), Docker third (wraps result), docs last (describes final state) +4. **No functionality changes** — This is purely structural and quality work; no new features, no behavior changes From 0dd358ccae8e0d6bf7099d898d57fc4defe55cbc Mon Sep 17 00:00:00 2001 From: Philippe Parage Date: Thu, 19 Mar 2026 15:29:44 +0100 Subject: [PATCH 14/33] refactor: make start.sh portable with .env support (#54) --- .env.example | 18 +++++++++++++++++ start.sh | 56 +++++++++++++++++++++------------------------------- 2 files changed, 41 insertions(+), 33 deletions(-) create mode 100644 .env.example diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..d70b039 --- /dev/null +++ b/.env.example @@ -0,0 +1,18 @@ +# .env.example — Copy to .env and fill in values +# Required +PROJECT_ROOT_DIR= # Path to this project root +VAULT_PASSWORD_FILE= # Path to vault password file +# OR +# VAULT_PASSWORD= # Vault password string (alternative to file) + +# Playbook sources +API_BACKEND_WWWAPP_PLAYBOOKS_DIR= # Local playbooks directory (usually same as PROJECT_ROOT_DIR) +API_BACKEND_PUBLIC_PLAYBOOKS_DIR= # External playbooks repo path +API_BACKEND_INVENTORY_DIR= # Ansible inventory directory +API_BACKEND_VAULT_FILE= # Vault-encrypted variables file + +# Optional +CORS_ORIGIN_REGEX= # Custom CORS regex (default: localhost only) +HOST=0.0.0.0 # Server bind address +PORT=8000 # Server port +DEBUG=false # Enable debug mode diff --git a/start.sh b/start.sh index ca975e8..ce505e2 100755 --- a/start.sh +++ b/start.sh @@ -1,45 +1,35 @@ #!/bin/bash - -# dirty & temp fix : - -ansible-galaxy collection install community.general -p ~/.ansible/collections -ansible-galaxy collection install ansible.posix -p ~/.ansible/collections -ansible-galaxy collection install ansible.windows -p ~/.ansible/collections - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +set -euo pipefail PROJECT_ROOT="$(realpath "$(dirname "${BASH_SOURCE[0]}")")" export PROJECT_ROOT_DIR="$PROJECT_ROOT" -APP_DIR="$PROJECT_ROOT_DIR/app" -APP_MODULE="app.main:app" +# Load .env file if it exists +if [ -f "$PROJECT_ROOT/.env" ]; then + set -a + source "$PROJECT_ROOT/.env" + set +a +fi -export API_BACKEND_PUBLIC_PLAYBOOKS_DIR="$HOME/_products.git-hyde-repo/range42-infrastructures-installers/" -export API_BACKEND_WWWAPP_PLAYBOOKS_DIR="$PROJECT_ROOT_DIR/" -export API_BACKEND_INVENTORY_DIR="$PROJECT_ROOT_DIR/inventory/" -export API_BACKEND_VAULT_FILE="$HOME/_products.git-hyde-repo/range42-ansible_roles-private-devkit//secrets/px-testing.cr42_tailscale.yaml" -# -# vault pwd -# -export VAULT_PASSWORD_FILE="/tmp/vault/vault_pass.txt" -#export VAULT_PASSWORD="redacted. +# Install Ansible collections if needed +if [ ! -d "$HOME/.ansible/collections/ansible_collections/community/general" ]; then + echo ":: Installing Ansible collections..." + ansible-galaxy collection install -r requirements.yml -p ~/.ansible/collections +fi -HOST="0.0.0.0" -PORT="8000" -WORKERS=1 +# Set defaults for required vars +export API_BACKEND_WWWAPP_PLAYBOOKS_DIR="${API_BACKEND_WWWAPP_PLAYBOOKS_DIR:-$PROJECT_ROOT_DIR/}" +export API_BACKEND_INVENTORY_DIR="${API_BACKEND_INVENTORY_DIR:-$PROJECT_ROOT_DIR/inventory/}" -#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +HOST="${HOST:-0.0.0.0}" +PORT="${PORT:-8000}" -# move to project + export cd "$PROJECT_ROOT_DIR" || exit export PYTHONPATH="$PROJECT_ROOT:${PYTHONPATH:-}" - -#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - -echo ":: start :: $APP_MODULE - $PROJECT_ROOT_DIR" -exec uvicorn "$APP_MODULE" \ - --host "$HOST" \ - --port "$PORT" \ - --workers "$WORKERS" \ - --log-level info --reload +echo ":: start :: app.main:app - $PROJECT_ROOT_DIR" +exec uvicorn app.main:app \ + --host "$HOST" \ + --port "$PORT" \ + --log-level info \ + --reload From 0b1622aabc35e5c9004b7b2a69d0391e48a6b67b Mon Sep 17 00:00:00 2001 From: Philippe Parage Date: Thu, 19 Mar 2026 15:30:20 +0100 Subject: [PATCH 15/33] refactor: replace print statements with structured logging (#54) --- app/routes/debug.py | 3 +-- app/utils/vm_id_name_resolver.py | 24 +++++++++--------------- 2 files changed, 10 insertions(+), 17 deletions(-) diff --git a/app/routes/debug.py b/app/routes/debug.py index 82fce05..17bdad8 100644 --- a/app/routes/debug.py +++ b/app/routes/debug.py @@ -63,5 +63,4 @@ def debug_func_test(): raise HTTPException(status_code=400, detail=f":: MISSING INVENTORY : {INVENTORY_SRC}") out = resolv_id_to_vm_name("px-testing", 1000) - print("GOT :::") - print(out["vm_name"]) + logger.debug("GOT: %s", out["vm_name"]) diff --git a/app/utils/vm_id_name_resolver.py b/app/utils/vm_id_name_resolver.py index 90780ab..78176ff 100644 --- a/app/utils/vm_id_name_resolver.py +++ b/app/utils/vm_id_name_resolver.py @@ -6,7 +6,7 @@ from app.core.runner import run_playbook_core from app.core.extractor import extract_action_results -debug =1 +logger = logging.getLogger(__name__) def hack_same_vm_id(a, b) -> bool: @@ -23,18 +23,17 @@ def resolv_id_to_vm_name(proxmox_node: str, target_vm_id: str) -> dict: PLAYBOOK_SRC = PROJECT_ROOT / "playbooks" / "generic.yml" INVENTORY_SRC = PROJECT_ROOT / "inventory" / "hosts.yml" - if debug == 1: - print(f":: PROJECT_ROOT :: {PROJECT_ROOT} ") - print(f":: PLAYBOOK_SRC :: {PLAYBOOK_SRC} ") - print(f":: INVENTORY_SRC :: {INVENTORY_SRC} ") + logger.debug("PROJECT_ROOT: %s", PROJECT_ROOT) + logger.debug("PLAYBOOK_SRC: %s", PLAYBOOK_SRC) + logger.debug("INVENTORY_SRC: %s", INVENTORY_SRC) if not PLAYBOOK_SRC.exists(): err = f":: err - MISSING PLAYBOOK : {PLAYBOOK_SRC}" - logging.error(err) + logger.error("Missing playbook: %s", PLAYBOOK_SRC) if not INVENTORY_SRC.exists(): err = f":: err - MISSING INVENTORY : {INVENTORY_SRC}" - logging.error(err) + logger.error("Missing inventory: %s", INVENTORY_SRC) extravars = {} extravars["proxmox_vm_action"] = "vm_list" @@ -63,7 +62,7 @@ def resolv_id_to_vm_name(proxmox_node: str, target_vm_id: str) -> dict: except json.JSONDecodeError as e: err = f":: err - INVALID actions_results JSONS" - logging.error(err) + logger.error("Invalid action_results JSON") raise HTTPException(status_code=500, detail=err) else: @@ -80,12 +79,7 @@ def resolv_id_to_vm_name(proxmox_node: str, target_vm_id: str) -> dict: # if isinstance(item, dict) and item.get("vm_id") == target_vm_id: if isinstance(item, dict) and hack_same_vm_id(item.get("vm_id"), target_vm_id): # hacky way - should be fixed. - if debug ==1 : - - print("=====================") - print( item.get("vm_id")) - print( item.get("vm_name")) - print("=====================") + logger.debug("Matched VM — vm_id: %s, vm_name: %s", item.get("vm_id"), item.get("vm_name")) return { "vm_id": item.get("vm_id"), @@ -95,7 +89,7 @@ def resolv_id_to_vm_name(proxmox_node: str, target_vm_id: str) -> dict: # return None err = f":: err - vm_id NOT FOUND" - logging.error(err) + logger.error("vm_id not found: %s", target_vm_id) raise HTTPException(status_code=500, detail=err) From 6fc23dfed8396f1def53aaaf90c57bc79eee2154 Mon Sep 17 00:00:00 2001 From: Philippe Parage Date: Thu, 19 Mar 2026 15:32:15 +0100 Subject: [PATCH 16/33] feat(docker): add Dockerfile and docker-compose for containerized deployment (#51) --- .dockerignore | 15 +++++++++++++++ Dockerfile | 39 +++++++++++++++++++++++++++++++++++++++ docker-compose.yml | 20 ++++++++++++++++++++ 3 files changed, 74 insertions(+) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 docker-compose.yml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..27bcb14 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,15 @@ +.git +.gitignore +.env +__pycache__ +*.pyc +.venv +venv +*.egg-info +.pytest_cache +docs/ +curl_utils/ +tests/ +*.md +!requirements.txt +!requirements.yml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2227a7c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,39 @@ +FROM python:3.12-slim AS base + +# Install system deps for ansible and ssh +RUN apt-get update && apt-get install -y --no-install-recommends \ + openssh-client \ + sshpass \ + git \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +# Install Python deps +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Install Ansible collections +COPY requirements.yml . +RUN ansible-galaxy collection install -r requirements.yml -p /usr/share/ansible/collections + +# Copy application +COPY app/ app/ +COPY playbooks/ playbooks/ +COPY inventory/ inventory/ +COPY start.sh . + +# Set env defaults +ENV PROJECT_ROOT_DIR=/app +ENV API_BACKEND_WWWAPP_PLAYBOOKS_DIR=/app/ +ENV API_BACKEND_INVENTORY_DIR=/app/inventory/ +ENV HOST=0.0.0.0 +ENV PORT=8000 +ENV PYTHONPATH=/app + +EXPOSE 8000 + +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ + CMD python -c "import httpx; httpx.get('http://localhost:8000/docs/openapi.json').raise_for_status()" + +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--log-level", "info"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..8bab87d --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,20 @@ +services: + api: + build: . + ports: + - "${PORT:-8000}:8000" + volumes: + - ./app:/app/app:ro + - ./playbooks:/app/playbooks:ro + - ./inventory:/app/inventory:ro + environment: + - PROJECT_ROOT_DIR=/app + - API_BACKEND_WWWAPP_PLAYBOOKS_DIR=/app/ + - API_BACKEND_PUBLIC_PLAYBOOKS_DIR=${API_BACKEND_PUBLIC_PLAYBOOKS_DIR:-/app/} + - API_BACKEND_INVENTORY_DIR=/app/inventory/ + - API_BACKEND_VAULT_FILE=${API_BACKEND_VAULT_FILE:-} + - VAULT_PASSWORD_FILE=${VAULT_PASSWORD_FILE:-} + - VAULT_PASSWORD=${VAULT_PASSWORD:-} + - CORS_ORIGIN_REGEX=${CORS_ORIGIN_REGEX:-} + - DEBUG=${DEBUG:-false} + restart: unless-stopped From 1142d753f0041638569cfe91d3e9b164e7575b53 Mon Sep 17 00:00:00 2001 From: Philippe Parage Date: Thu, 19 Mar 2026 15:40:31 +0100 Subject: [PATCH 17/33] docs: comprehensive README with setup, architecture, and API guide (#52) --- README.md | 229 ++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 197 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 4808807..f72cbc4 100644 --- a/README.md +++ b/README.md @@ -1,53 +1,218 @@ -# Table of Contents +# Range42 Backend API -- [Project Overview](#Project-Overview) -- [Repository Content](#Repository-Content) -- [Contributing](#Contributing) -- [License](#License) +FastAPI application that orchestrates Proxmox infrastructure deployments by executing Ansible playbooks via `ansible-runner`. Part of the [Range42](https://github.com/range42) cyber range platform. Designed to sit behind a Kong API gateway that handles authentication and ACLs. --- -# Project Overview +## Table of Contents -**RANGE42** is a modular cyber range platform designed for real-world readiness. -We build, deploy, and document offensive, defensive, and hybrid cyber training environments using reproducible, infrastructure-as-code methodologies. +- [Quick Start](#quick-start) +- [Configuration](#configuration) +- [API Documentation](#api-documentation) +- [Project Structure](#project-structure) +- [Architecture](#architecture) +- [Development](#development) +- [License](#license) -## What we build +--- + +## Quick Start + +### Option 1 -- Docker + +```bash +docker compose up +``` + +Builds the image, installs dependencies and Ansible collections, and starts the API on port `8000`. + +### Option 2 -- start.sh -- Proxmox-based cyber ranges with dynamic catalog -- Ansible roles for automated deployments (Wazuh, Kong, Docker, etc.) -- Private APIs for range orchestration and telemetry -- Developer and testing toolkits and JSON transformers for automation pipelines -- ... +```bash +./start.sh +``` -## Repository Overview +The script resolves `PROJECT_ROOT_DIR`, sources `.env` if present, installs Ansible collections on first run, and launches uvicorn with `--reload`. -- **RANGE42 deployer UI** : A web interface to visually design infrastructure schemas and trigger deployments. -- **RANGE42 deployer backend API** : Orchestrates deployments by executing playbooks and bundles from the catalog. -- **RANGE42 catalog** : A collection of Ansible roles and Docker/Docker Compose stacks, forming deployable bundles. -- **RANGE42 playbooks** : Centralized playbooks that can be invoked by the backend or CLI. -- **RANGE42 proxmox role** : An Ansible role for controlling Proxmox nodes via the Proxmox API. -- **RANGE42 devkit** : Helper scripts for testing, debugging, and development workflows. -- **RANGE42 kong API gateway** : A network service in front of the backend API, handling authentication, ACLs, and access control policies. -- **RANGE42 swagger API spec** : OpenAPI/Swagger JSON definition of the backend API. +### Option 3 -- Manual (development) -### Putting it all together +```bash +# Create and activate a virtual environment +python3 -m venv .venv +source .venv/bin/activate -These repositories provide a modular and extensible platform to design, manage and deploy infrastructures automatically either from the UI (coming soon) or from the CLI through the playbooks repository. +# Install Python and Ansible dependencies +pip install -r requirements.txt +ansible-galaxy collection install -r requirements.yml -p ~/.ansible/collections + +# Set required environment variables (or create a .env file) +export PROJECT_ROOT_DIR="$(pwd)" +export VAULT_PASSWORD_FILE="/path/to/vault-pass.txt" + +# Start the dev server +uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload +``` --- -# Repository Content +## Configuration -This repository contains the backend API built with FastAPI which orchestrates deployments by calling the Proxmox Ansible role to provision either reusable bundles (unit actions or infrastructure parts) or complete infrastructure scenarios (full infrastructures). +All settings are read from environment variables in `app/core/config.py`. Nothing is hard-coded. -## Contributing +| Variable | Required | Description | Default | +|---|---|---|---| +| `PROJECT_ROOT_DIR` | Yes | Absolute path to the project root | `.` (cwd) | +| `VAULT_PASSWORD_FILE` | Yes* | Path to the Ansible Vault password file | -- | +| `VAULT_PASSWORD` | Yes* | Ansible Vault password as a string | -- | +| `API_BACKEND_WWWAPP_PLAYBOOKS_DIR` | No | Local playbooks directory | `PROJECT_ROOT_DIR/` | +| `API_BACKEND_PUBLIC_PLAYBOOKS_DIR` | No | External playbooks repository path | -- | +| `API_BACKEND_INVENTORY_DIR` | No | Ansible inventory directory | `PROJECT_ROOT_DIR/inventory/` | +| `API_BACKEND_VAULT_FILE` | No | Path to vault-encrypted variables file | -- | +| `CORS_ORIGIN_REGEX` | No | Regex for allowed CORS origins | `localhost` / `127.0.0.1` / `[::1]` only | +| `HOST` | No | Server bind address | `0.0.0.0` | +| `PORT` | No | Server listen port | `8000` | +| `DEBUG` | No | Enable debug mode (`true`, `1`, or `yes`) | `false` | -This is a collaborative initiative, developed for applied security training, community integration, and internal capability building. -We use centralized community health files in Range42 community health. +> *One of `VAULT_PASSWORD_FILE` or `VAULT_PASSWORD` must be set for vault-encrypted operations. -## License +--- + +## API Documentation + +Once the server is running, interactive docs are available at: + +| Format | URL | +|---|---| +| Swagger UI | `/docs/swagger` | +| ReDoc | `/docs/redoc` | +| OpenAPI JSON | `/docs/openapi.json` | + +--- + +## Project Structure + +``` +range42-backend-api/ +|-- app/ +| |-- main.py # FastAPI app factory, CORS, vault lifespan +| |-- core/ +| | |-- config.py # Centralized settings from env vars +| | |-- runner.py # Ansible playbook execution engine +| | |-- extractor.py # Structured result extraction from events +| | |-- vault.py # Vault password file management +| | |-- exceptions.py # Custom exception handlers +| |-- routes/ +| | |-- __init__.py # Router assembly and prefix mapping +| | |-- vms.py # VM lifecycle (list, start, stop, create, delete, clone) +| | |-- vm_config.py # VM configuration (get config, set tags) +| | |-- snapshots.py # VM snapshots (list, create, delete, revert) +| | |-- firewall.py # Firewall (aliases, rules, enable/disable) +| | |-- network.py # Network interfaces (VM and node level) +| | |-- storage.py # Storage (list, download ISO, templates) +| | |-- bundles.py # Predefined bundles (Ubuntu setup, Proxmox VMs) +| | |-- runner.py # Generic bundle/scenario runner +| | |-- debug.py # Debug endpoints (ping, test functions) +| | |-- ws_status.py # WebSocket real-time VM status +| |-- schemas/ +| | |-- base.py # Shared Pydantic base models +| | |-- vms.py # VM request/response schemas +| | |-- vm_config.py # VM config schemas +| | |-- snapshots.py # Snapshot schemas +| | |-- firewall.py # Firewall schemas +| | |-- network.py # Network schemas +| | |-- storage.py # Storage schemas +| | |-- bundles/ # Bundle-specific schemas +| | |-- debug/ # Debug endpoint schemas +| |-- utils/ +| | |-- checks_playbooks.py # Playbook path validation and resolution +| | |-- checks_inventory.py # Inventory path validation and resolution +| | |-- text_cleaner.py # ANSI escape code stripper +| | |-- vm_id_name_resolver.py # VM ID to name resolution via Ansible +|-- tests/ # Pytest test suite +|-- curl_utils/ # Manual testing curl scripts +|-- playbooks/ # Local Ansible playbooks (generic, ping) +|-- inventory/ # Ansible inventory files +|-- Dockerfile # Multi-stage Docker build +|-- docker-compose.yml # Compose service definition +|-- start.sh # Development startup script +|-- requirements.txt # Python dependencies +|-- requirements.yml # Ansible Galaxy requirements +``` + +--- + +## Architecture + +### Request Flow + +``` +HTTP Request + --> FastAPI route handler + --> Pydantic schema validation + --> Path / inventory checks (checks_playbooks.py, checks_inventory.py) + --> runner.py (ansible-runner in temp directory) + --> Extract structured results (extractor.py) + --> JSONResponse (rc + result or log lines) +``` -- GPL-3.0 license +### Key Design Decisions +- **Temp directory per run** -- Each playbook execution creates an isolated temp directory containing a copy of the playbook tree, inventory, and environment variables. The directory is cleaned up in a `finally` block to prevent leaks. + +- **Vault lifecycle** -- On app startup, the lifespan context manager either reads `VAULT_PASSWORD_FILE` directly or writes `VAULT_PASSWORD` to a secure temp file. Both are cleaned up on shutdown. + +- **Two response modes** -- Endpoints that accept `as_json` can return either structured data extracted from Ansible events (`"result"` key) or raw log lines (`"log_multiline"` array). + +- **Path traversal protection** -- All playbook and inventory names are validated against `^[A-Za-z0-9_-]+(?:/[A-Za-z0-9_-]+)*$` and resolved paths are checked with `is_relative_to()` to prevent directory traversal attacks. + +- **No auth in this layer** -- Authentication, ACLs, and rate limiting are handled by the Kong API gateway in front of this API. CORS is restricted to localhost origins only. + +### Route Prefixes + +| Prefix | Module | Purpose | +|---|---|---| +| `/v0/admin/proxmox/vms/` | `vms.py` | VM list and lifecycle | +| `/v0/admin/proxmox/vms/vm_id/` | `vms.py` | Single VM operations | +| `/v0/admin/proxmox/vms/vm_ids/` | `vms.py` | Mass VM operations | +| `/v0/admin/proxmox/vms/vm_id/config/` | `vm_config.py` | VM configuration | +| `/v0/admin/proxmox/vms/vm_id/snapshot/` | `snapshots.py` | VM snapshots | +| `/v0/admin/proxmox/firewall/` | `firewall.py` | Firewall management | +| `/v0/admin/proxmox/network/` | `network.py` | Network interfaces | +| `/v0/admin/proxmox/storage/` | `storage.py` | Storage and ISOs | +| `/v0/admin/run/bundles/` | `bundles.py`, `runner.py` | Bundle execution | +| `/v0/admin/run/scenarios/` | `runner.py` | Scenario execution | +| `/v0/admin/debug/` | `debug.py` | Debug/test endpoints | +| `/ws/vm-status` | `ws_status.py` | WebSocket VM status | + +--- + +## Development + +### Running Tests + +```bash +python3 -m pytest tests/ -v +``` + +### Manual Testing + +Curl scripts for every endpoint are available in `curl_utils/`: + +```bash +# Example: list VMs +bash curl_utils/proxmox.vms.list.sh +``` + +### Code Conventions + +- **Imports**: Absolute from `app.` (no relative imports except in `__init__`) +- **Naming**: `req` for request objects, `rc` for return codes, `extravars` for Ansible extra variables +- **HTTP codes**: 200 for Ansible success (rc=0), 500 for failure, 400 for validation errors +- **Commit style**: Conventional commits -- `feat(scope):`, `fix(scope):`, `docs:`, `refactor:` +- **Branch naming**: `feature/description`, `fix/description`, `release/x.y.z` + +--- + +## License +[GPL-3.0](LICENSE) From 700af4e88fdcec7e36db1521660b5ffc47f47a6a Mon Sep 17 00:00:00 2001 From: Philippe Parage Date: Thu, 19 Mar 2026 15:40:38 +0100 Subject: [PATCH 18/33] docs: add Sphinx-style docstrings to all modules (#52) --- app/core/config.py | 47 +++++++++++++- app/core/exceptions.py | 31 +++++++++- app/core/extractor.py | 21 ++++++- app/core/runner.py | 86 ++++++++++++++++++++++---- app/core/vault.py | 30 ++++++++- app/main.py | 7 ++- app/routes/bundles.py | 63 ++++++++++++++++++- app/routes/debug.py | 14 ++++- app/routes/firewall.py | 75 ++++++++++++++++++++++- app/routes/network.py | 39 +++++++++++- app/routes/runner.py | 18 +++++- app/routes/snapshots.py | 27 ++++++++- app/routes/storage.py | 27 ++++++++- app/routes/vm_config.py | 33 +++++++++- app/routes/vms.py | 101 ++++++++++++++++++++++++++++++- app/routes/ws_status.py | 57 ++++++++++++++--- app/utils/checks_inventory.py | 41 +++++++++++-- app/utils/checks_playbooks.py | 100 ++++++++++++++++++++++++++++-- app/utils/text_cleaner.py | 21 ++++++- app/utils/vm_id_name_resolver.py | 34 +++++++++-- 20 files changed, 812 insertions(+), 60 deletions(-) diff --git a/app/core/config.py b/app/core/config.py index d57168f..2bcafbf 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -1,4 +1,9 @@ -"""Centralized application configuration. All env vars read here, nowhere else.""" +"""Centralized application configuration. + +All environment variables are read here and nowhere else. The :class:`Settings` +dataclass is frozen (immutable) and instantiated once at module level as the +``settings`` singleton. +""" import os from dataclasses import dataclass, field @@ -7,7 +12,35 @@ @dataclass(frozen=True) class Settings: - """Immutable application settings loaded from environment variables.""" + """Immutable application settings loaded from environment variables. + + Each field reads its value from the corresponding environment variable + at instantiation time. The ``frozen=True`` flag prevents accidental + mutation after startup. + + :param project_root: Resolved path to the project root directory. + :type project_root: Path + :param wwwapp_playbooks_dir: Local playbooks directory (``API_BACKEND_WWWAPP_PLAYBOOKS_DIR``). + :type wwwapp_playbooks_dir: str + :param public_playbooks_dir: External playbooks repository path (``API_BACKEND_PUBLIC_PLAYBOOKS_DIR``). + :type public_playbooks_dir: str + :param inventory_dir: Ansible inventory directory (``API_BACKEND_INVENTORY_DIR``). + :type inventory_dir: str + :param vault_file: Path to the vault-encrypted variables file (``API_BACKEND_VAULT_FILE``). + :type vault_file: str + :param vault_password_file: Path to the Ansible Vault password file (``VAULT_PASSWORD_FILE``). + :type vault_password_file: str + :param vault_password: Ansible Vault password as a plain string (``VAULT_PASSWORD``). + :type vault_password: str + :param cors_origin_regex: Regex for allowed CORS origins (``CORS_ORIGIN_REGEX``). + :type cors_origin_regex: str + :param host: Server bind address (``HOST``). + :type host: str + :param port: Server listen port (``PORT``). + :type port: int + :param debug: Whether debug mode is enabled (``DEBUG``). + :type debug: bool + """ project_root: Path = field(default_factory=lambda: Path(os.getenv("PROJECT_ROOT_DIR", ".")).resolve()) @@ -36,10 +69,20 @@ class Settings: @property def playbook_path(self) -> Path: + """Return the default generic playbook path. + + :returns: Resolved path to ``playbooks/generic.yml`` under the project root. + :rtype: Path + """ return self.project_root / "playbooks" / "generic.yml" @property def inventory_name(self) -> str: + """Return the default inventory filename. + + :returns: The string ``"hosts"``. + :rtype: str + """ return "hosts" diff --git a/app/core/exceptions.py b/app/core/exceptions.py index 2004155..ffba6a7 100644 --- a/app/core/exceptions.py +++ b/app/core/exceptions.py @@ -1,4 +1,9 @@ -"""Custom exception handlers for FastAPI.""" +"""Custom exception handlers for FastAPI. + +Provides a verbose 422 validation error handler that logs the full +request body and field-level error details in a format compatible +with the deployer-ui frontend. +""" import json import logging @@ -11,7 +16,15 @@ def make_validation_error_detail(err: dict) -> dict: - """Convert a Pydantic validation error to the response format the UI expects.""" + """Convert a Pydantic validation error dict to the response format the UI expects. + + :param err: Single error dict from ``exc.errors()`` containing ``loc``, + ``msg``, ``type``, ``input``, and ``ctx`` keys. + :type err: dict + :returns: Reformatted dict with ``field``, ``msg``, ``type``, ``input``, + and ``ctx`` keys. + :rtype: dict + """ return { "field": ".".join(str(p) for p in err.get("loc", [])), "msg": err.get("msg", ""), @@ -22,7 +35,19 @@ def make_validation_error_detail(err: dict) -> dict: async def validation_exception_handler(request: Request, exc: RequestValidationError) -> JSONResponse: - """Verbose 422 handler with debug logging. Matches deployer-ui expected format.""" + """Verbose 422 handler with debug logging. + + Logs the HTTP method, URL, raw request body, and each validation + error at the ``ERROR`` level. Returns a JSON response matching the + deployer-ui expected format. + + :param request: The incoming FastAPI request object. + :type request: Request + :param exc: The validation exception raised by Pydantic. + :type exc: RequestValidationError + :returns: A 422 JSON response with a ``detail`` array of error objects. + :rtype: JSONResponse + """ logger.error("422 on %s %s %s", request.method, request.url.path, request.url.query) try: diff --git a/app/core/extractor.py b/app/core/extractor.py index 4d56198..9cede13 100644 --- a/app/core/extractor.py +++ b/app/core/extractor.py @@ -1,8 +1,25 @@ -"""Extract structured results from Ansible runner events.""" +"""Extract structured results from Ansible runner events. + +Provides :func:`extract_action_results`, which filters ``runner_on_ok`` +events for a specific action key and collects the matching result data. +""" def extract_action_results(events: list[dict], action_to_search: str) -> list: - """Find all runner_on_ok events containing the specified action key.""" + """Find all ``runner_on_ok`` events containing the specified action key. + + Iterates over the Ansible runner event list and extracts the value + stored under ``event_data.res.`` for every + successful task event. + + :param events: List of Ansible runner event dicts. + :type events: list[dict] + :param action_to_search: The action key to search for in each event's + ``res`` dictionary (e.g. ``"vm_list"``, ``"vm_start"``). + :type action_to_search: str + :returns: List of extracted result values from matching events. + :rtype: list + """ out = [] for ev in events: if ev.get("event") != "runner_on_ok": diff --git a/app/core/runner.py b/app/core/runner.py index 8329ed3..3d47b87 100644 --- a/app/core/runner.py +++ b/app/core/runner.py @@ -1,4 +1,10 @@ -"""Clean playbook runner. Fixes temp-dir leak, removes print debugging, uses VaultManager.""" +"""Ansible playbook runner. + +Provides :func:`run_playbook_core`, the single entry point for executing +Ansible playbooks via ``ansible-runner``. Each invocation creates an +isolated temp directory that is cleaned up in a ``finally`` block to +prevent disk leaks. +""" import os import shutil @@ -14,7 +20,16 @@ def build_logs(events) -> tuple[str, str]: - """Build ansible log strings with and without ANSI escape codes.""" + """Build Ansible log strings with and without ANSI escape codes. + + Iterates over runner events, collects ``stdout`` lines, and produces + two variants of the combined output. + + :param events: Iterable of Ansible runner event dicts. + :type events: Iterable[dict] + :returns: A tuple of ``(text_with_ansi, text_without_ansi)``. + :rtype: tuple[str, str] + """ lines = [] for ev in events: stdout = ev.get("stdout") @@ -26,7 +41,16 @@ def build_logs(events) -> tuple[str, str]: def _build_envvars(vm: VaultManager) -> dict: - """Build the Ansible environment variables dict.""" + """Build the Ansible environment variables dict. + + Configures host-key checking, deprecation warnings, collection paths, + and vault password file for the runner environment. + + :param vm: Vault manager instance for vault password file resolution. + :type vm: VaultManager + :returns: Dictionary of environment variable key-value pairs. + :rtype: dict + """ home_collections = os.path.expanduser("~/.ansible/collections") sys_collections = "/usr/share/ansible/collections" coll_paths = f"{home_collections}:{sys_collections}" @@ -58,9 +82,19 @@ def _build_envvars(vm: VaultManager) -> dict: def _setup_temp_dir( inventory: Path, playbook: Path, vm: VaultManager, ) -> tuple[Path, Path, Path]: - """Create temp dir, copy playbook tree and inventory, write envvars. - - Returns (tmp_dir, inventory_path_in_tmp, playbook_relative_path). + """Create an isolated temp directory for a single playbook run. + + Copies the playbook tree and inventory into the temp directory and + writes an ``env/envvars`` file for ``ansible-runner``. + + :param inventory: Absolute path to the inventory file. + :type inventory: Path + :param playbook: Absolute path to the playbook file. + :type playbook: Path + :param vm: Vault manager instance for environment variable generation. + :type vm: VaultManager + :returns: A tuple of ``(tmp_dir, inventory_path_in_tmp, playbook_relative_path)``. + :rtype: tuple[Path, Path, Path] """ tmp_dir = Path(tempfile.mkdtemp(prefix="runner-")) @@ -95,7 +129,19 @@ def _build_cmdline( cmdline: str | None, tags: str | None, ) -> str | None: - """Build the ansible-runner cmdline string.""" + """Build the ``ansible-runner`` command-line string. + + Appends vault password file, extra vars file, and tag arguments as needed. + + :param vm: Vault manager instance for vault password file resolution. + :type vm: VaultManager + :param cmdline: Pre-existing command-line string, or ``None``. + :type cmdline: str or None + :param tags: Comma-separated Ansible tags to apply, or ``None``. + :type tags: str or None + :returns: The assembled command-line string, or ``None`` if empty. + :rtype: str or None + """ if not cmdline: vf = os.getenv("VAULT_PASSWORD_FILE") if not vf: @@ -123,10 +169,28 @@ def run_playbook_core( extravars: dict | None = None, quiet: bool = False, ) -> tuple[int, list, str, str]: - """Run an Ansible playbook and return (rc, events, log_plain, log_ansi). - - Uses the module-level vault_manager instance. - Cleans up the temp directory in a finally block. + """Run an Ansible playbook and return execution results. + + Creates an isolated temp directory, executes the playbook via + ``ansible_runner.run()``, collects events and logs, then cleans + up the temp directory in a ``finally`` block. + + :param playbook: Absolute path to the playbook YAML file. + :type playbook: Path + :param inventory: Absolute path to the inventory file. + :type inventory: Path + :param limit: Ansible ``--limit`` host pattern, or ``None`` for all hosts. + :type limit: str or None + :param tags: Comma-separated Ansible tags, or ``None``. + :type tags: str or None + :param cmdline: Additional command-line arguments for ansible-runner. + :type cmdline: str or None + :param extravars: Extra variables dict passed to the playbook. + :type extravars: dict or None + :param quiet: If ``True``, suppress ansible-runner console output. + :type quiet: bool + :returns: A tuple of ``(return_code, events_list, log_plain, log_ansi)``. + :rtype: tuple[int, list, str, str] """ tmp_dir, inv_dest, play_rel = _setup_temp_dir(inventory, playbook, vault_manager) try: diff --git a/app/core/vault.py b/app/core/vault.py index 2bec680..980af54 100644 --- a/app/core/vault.py +++ b/app/core/vault.py @@ -1,16 +1,42 @@ -"""Vault password management. No global state — uses a class instance.""" +"""Vault password management. + +Provides :class:`VaultManager`, a simple holder for the Ansible Vault +password file path. The path is set during the FastAPI lifespan startup +and read by the runner whenever a playbook execution requires vault +decryption. +""" from pathlib import Path class VaultManager: - """Manages the Ansible vault password file path.""" + """Manages the Ansible vault password file path. + + Stores a single :class:`Path` reference that points to the vault + password file on disk. The path may be a user-supplied file + (``VAULT_PASSWORD_FILE``) or a temp file written from + ``VAULT_PASSWORD`` during application startup. + + :param _vault_pass_path: Internal path storage, initially ``None``. + :type _vault_pass_path: Path or None + """ def __init__(self) -> None: + """Initialize the VaultManager with no vault path set.""" self._vault_pass_path: Path | None = None def set_vault_path(self, p: Path | None) -> None: + """Set the vault password file path. + + :param p: Absolute path to the vault password file, or ``None`` to clear. + :type p: Path or None + """ self._vault_pass_path = p def get_vault_path(self) -> Path | None: + """Return the current vault password file path. + + :returns: The stored path, or ``None`` if not set. + :rtype: Path or None + """ return self._vault_pass_path diff --git a/app/main.py b/app/main.py index a04ecc1..fd66111 100644 --- a/app/main.py +++ b/app/main.py @@ -1,4 +1,9 @@ -"""FastAPI application factory for the Range42 Backend API.""" +"""FastAPI application factory for the Range42 Backend API. + +Creates and configures the FastAPI app with CORS middleware, vault +lifecycle management, custom exception handlers, and route registration. +The module-level ``app`` object is the ASGI entry point used by uvicorn. +""" import logging import os diff --git a/app/routes/bundles.py b/app/routes/bundles.py index bb14ace..3c7507b 100644 --- a/app/routes/bundles.py +++ b/app/routes/bundles.py @@ -1,7 +1,26 @@ """Consolidated bundle routes. -Replaces: app/routes/v0/admin/bundles/core/linux/ubuntu/**/*.py - app/routes/v0/admin/bundles/proxmox/configure/default/vms/*.py +Endpoints +--------- +Ubuntu install bundles: + +- ``POST .../core/linux/ubuntu/install/docker`` -- Install Docker. +- ``POST .../core/linux/ubuntu/install/docker-compose`` -- Install Docker Compose. +- ``POST .../core/linux/ubuntu/install/basic-packages`` -- Install basic packages. +- ``POST .../core/linux/ubuntu/install/dot-files`` -- Install dotfiles. +- ``POST .../core/linux/ubuntu/configure/add-user`` -- Add a system user. + +Proxmox VM bundles (create/start/stop/pause/resume/delete/snapshot): + +- ``POST .../core/proxmox/configure/default/create-vms-admin`` +- ``POST .../core/proxmox/configure/default/create-vms-vuln`` +- ``POST .../core/proxmox/configure/default/create-vms-student`` +- ``POST .../core/proxmox/configure/default/{action}-vms-{role}`` +- ``DELETE .../core/proxmox/configure/default/delete-vms-{role}`` +- ``POST .../core/proxmox/configure/default/snapshot/create-vms-{role}`` +- ``POST .../core/proxmox/configure/default/snapshot/revert-vms-{role}`` + +All prefixed under ``/v0/admin/run/bundles``. """ import logging @@ -170,6 +189,11 @@ def _run_snapshot_bundle(req, action_name: str, action_key: str) -> JSONResponse @router.post(path="/core/linux/ubuntu/install/docker", summary="Install docker packages", description="Install and configure docker engine on the target ubuntu system", tags=["bundles - core - ubuntu "], response_model=Reply_BundlesCoreLinuxUbuntuInstall_Docker) def bundles_core_linux_ubuntu_install_docker(req: Request_BundlesCoreLinuxUbuntuInstall_Docker): + """Install and configure Docker on the target Ubuntu system. + + :param req: Request body with host, node, and package installation flags. + :returns: JSON with ``rc`` and ``log_multiline``. + """ extravars = {} if req.proxmox_node: extravars["proxmox_node"] = req.proxmox_node @@ -184,6 +208,11 @@ def bundles_core_linux_ubuntu_install_docker(req: Request_BundlesCoreLinuxUbuntu @router.post(path="/core/linux/ubuntu/install/docker-compose", summary="Install docker compose packages", description="Install and configure docker compose on the target ubuntu system", tags=["bundles - core - ubuntu "], response_model=Reply_BundlesCoreLinuxUbuntuInstall_DockerCompose) def bundles_core_linux_ubuntu_install_docker_compose(req: Request_BundlesCoreLinuxUbuntuInstall_DockerCompose): + """Install and configure Docker Compose on the target Ubuntu system. + + :param req: Request body with host, node, and package installation flags. + :returns: JSON with ``rc`` and ``log_multiline``. + """ extravars = {} if req.proxmox_node: extravars["proxmox_node"] = req.proxmox_node @@ -200,6 +229,11 @@ def bundles_core_linux_ubuntu_install_docker_compose(req: Request_BundlesCoreLin @router.post(path="/core/linux/ubuntu/install/basic-packages", summary="Install basics packages", description="Install and configure a base set of packages on the target Ubuntu system", tags=["bundles - core - ubuntu "], response_model=Reply_BundlesCoreLinuxUbuntuInstall_BasicPackages) def bundles_core_linux_ubuntu_install_basic_packages(req: Request_BundlesCoreLinuxUbuntuInstall_BasicPackages): + """Install a base set of packages on the target Ubuntu system. + + :param req: Request body with host, node, and package category flags. + :returns: JSON with ``rc`` and ``log_multiline``. + """ extravars = {} if req.proxmox_node: extravars["proxmox_node"] = req.proxmox_node @@ -212,6 +246,11 @@ def bundles_core_linux_ubuntu_install_basic_packages(req: Request_BundlesCoreLin @router.post(path="/core/linux/ubuntu/install/dot-files", summary="Install user dotfiles", description="Install and configure generic dotfiles - vimrc, zshrc, etc.", tags=["bundles - core - ubuntu "], response_model=Reply_BundlesCoreLinuxUbuntuInstall_DotFilesItem) def bundles_core_linux_ubuntu_install_dotfiles(req: Request_BundlesCoreLinuxUbuntuInstall_DotFiles): + """Install generic dotfiles (vimrc, zshrc, etc.) for a user. + + :param req: Request body with host, user, and dotfile selection flags. + :returns: JSON with ``rc`` and ``log_multiline``. + """ extravars = {} if req.proxmox_node: extravars["proxmox_node"] = req.proxmox_node @@ -234,6 +273,11 @@ def bundles_core_linux_ubuntu_install_dotfiles(req: Request_BundlesCoreLinuxUbun @router.post(path="/core/linux/ubuntu/configure/add-user", summary="Add system user", description="Create a new user with shell, home and password", tags=["bundles - core - ubuntu "], response_model=Reply_BundlesCoreLinuxUbuntuConfigure_AddUser) def bundles_core_linux_ubuntu_configure_add_user(req: Request_BundlesCoreLinuxUbuntuConfigure_AddUser): + """Create a new system user with shell, home directory, and password. + + :param req: Request body with host, user details, and password policy. + :returns: JSON with ``rc`` and ``log_multiline``. + """ extravars = {} if req.proxmox_node: extravars["proxmox_node"] = req.proxmox_node @@ -311,16 +355,31 @@ def _check_student_vms(req): @router.post(path="/core/proxmox/configure/default/create-vms-admin", summary="Create default admin VMs", description="Create the default set of admin virtual machines for initial configuration in Proxmox", tags=["bundles - core - proxmox - vms - default-configuration - admin"], response_model=Reply_BundlesCoreProxmoxConfigureDefaultVms_CreateAdminVms) def bundles_proxmox_create_vms_admin(req: Request_BundlesCoreProxmoxConfigureDefaultVms_CreateAdminVms): + """Create the default set of admin VMs on Proxmox. + + :param req: Request body with ``proxmox_node`` and ``vms`` dict. + :returns: JSON with ``rc`` and ``log_multiline``. + """ return _run_create_vms_bundle(req, "core/proxmox/configure/default/vms/create-vms-admin", _check_admin_vms) @router.post(path="/core/proxmox/configure/default/create-vms-vuln", summary="Create default vulnerable VMs", description="Create the default set of vulnerable virtual machines for initial configuration in Proxmox", tags=["bundles - core - proxmox - vms - default-configuration - vuln"], response_model=Reply_BundlesCoreProxmoxConfigureDefaultVms_CreateVulnVms) def bundles_proxmox_create_vms_vuln(req: Request_BundlesCoreProxmoxConfigureDefaultVms_CreateVulnVms): + """Create the default set of vulnerable VMs on Proxmox. + + :param req: Request body with ``proxmox_node`` and ``vms`` dict. + :returns: JSON with ``rc`` and ``log_multiline``. + """ return _run_create_vms_bundle(req, "core/proxmox/configure/default/vms/create-vms-vuln", _check_vuln_vms) @router.post(path="/core/proxmox/configure/default/create-vms-student", summary="Create default student VMs", description="Create the default set of student virtual machines for initial configuration in Proxmox", tags=["bundles - core - proxmox - vms - default-configuration - student"], response_model=Reply_BundlesCoreProxmoxConfigureDefaultVms_CreateStudentVms) def bundles_proxmox_create_vms_student(req: Request_BundlesCoreProxmoxConfigureDefaultVms_CreateStudentVms): + """Create the default set of student VMs on Proxmox. + + :param req: Request body with ``proxmox_node`` and ``vms`` dict. + :returns: JSON with ``rc`` and ``log_multiline``. + """ return _run_create_vms_bundle(req, "core/proxmox/configure/default/vms/create-vms-student", _check_student_vms) diff --git a/app/routes/debug.py b/app/routes/debug.py index 17bdad8..61b59b1 100644 --- a/app/routes/debug.py +++ b/app/routes/debug.py @@ -1,6 +1,9 @@ """Consolidated debug routes. -Replaces: app/routes/v0/debug/ping.py, app/routes/v0/debug/_test_func.py +Endpoints +--------- +- ``POST /v0/admin/debug/ping`` -- Ansible ping connectivity check. +- ``POST /v0/admin/debug/func_test`` -- Temporary test function. """ import logging @@ -31,6 +34,11 @@ tags=["runner"], ) def debug_ping(req: Request_DebugPing): + """Run Ansible ping to check connectivity with target hosts. + + :param req: Request body with ``hosts`` and optional ``proxmox_node``. + :returns: JSON with ``rc`` and ``log_multiline``. + """ if not PLAYBOOK_SRC.exists(): raise HTTPException(status_code=400, detail=f":: MISSING PLAYBOOK : {PLAYBOOK_SRC}") if not INVENTORY_SRC.exists(): @@ -57,6 +65,10 @@ def debug_ping(req: Request_DebugPing): tags=["__tmp_testing"], ) def debug_func_test(): + """Temporary test function for development. + + :returns: None (debug only). + """ if not PLAYBOOK_SRC.exists(): raise HTTPException(status_code=400, detail=f":: MISSING PLAYBOOK : {PLAYBOOK_SRC}") if not INVENTORY_SRC.exists(): diff --git a/app/routes/firewall.py b/app/routes/firewall.py index 32a187a..43d0eb0 100644 --- a/app/routes/firewall.py +++ b/app/routes/firewall.py @@ -1,6 +1,19 @@ """Consolidated firewall routes. -Replaces: app/routes/v0/proxmox/firewall/*.py +Endpoints +--------- +- ``POST /v0/admin/proxmox/firewall/vm/alias/list`` -- List VM aliases. +- ``POST /v0/admin/proxmox/firewall/vm/alias/add`` -- Add a VM alias. +- ``DELETE /v0/admin/proxmox/firewall/vm/alias/delete`` -- Delete a VM alias. +- ``POST /v0/admin/proxmox/firewall/vm/rules/list`` -- List VM rules. +- ``POST /v0/admin/proxmox/firewall/vm/rules/apply`` -- Apply VM rules. +- ``DELETE /v0/admin/proxmox/firewall/vm/rules/delete`` -- Delete a VM rule. +- ``POST /v0/admin/proxmox/firewall/vm/enable`` -- Enable VM firewall. +- ``POST /v0/admin/proxmox/firewall/vm/disable`` -- Disable VM firewall. +- ``POST /v0/admin/proxmox/firewall/node/enable`` -- Enable node firewall. +- ``POST /v0/admin/proxmox/firewall/node/disable`` -- Disable node firewall. +- ``POST /v0/admin/proxmox/firewall/datacenter/enable`` -- Enable DC firewall. +- ``POST /v0/admin/proxmox/firewall/datacenter/disable`` -- Disable DC firewall. """ import logging @@ -57,6 +70,11 @@ def _run_fw(req, action: str, extravars: dict) -> JSONResponse: @router.post(path="/vm/alias/list", summary="List VM firewall aliases", description="List firewall aliases for a specific virtual machine", tags=["proxmox - firewall"], response_model=Reply_ProxmoxFirewallWithStorageName_ListIptablesAlias, response_description="Details of the VM firewall aliases") def proxmox_vm_alias_list(req: Request_ProxmoxFirewall_ListIptablesAlias): + """List firewall aliases for a VM. + + :param req: Request body with ``proxmox_node`` and ``vm_id``. + :returns: JSON with ``rc`` and either ``result`` or ``log_multiline``. + """ extravars = {"proxmox_node": req.proxmox_node} if req.vm_id: extravars["vm_id"] = req.vm_id @@ -65,6 +83,11 @@ def proxmox_vm_alias_list(req: Request_ProxmoxFirewall_ListIptablesAlias): @router.post(path="/vm/alias/add", summary="Add a firewall alias", description="Add a new alias to the Proxmox firewall - IPs, subnets/networks, hostnames", tags=["proxmox - firewall"], response_model=Reply_ProxmoxFirewallWithStorageName_AddIptablesAlias, response_description="Information about the created firewall alias") def proxmox_firewall_vm_alias_add(req: Request_ProxmoxFirewall_AddIptablesAlias): + """Add a firewall alias (IP, subnet, or hostname) for a VM. + + :param req: Request body with node, VM ID, alias name, CIDR, and comment. + :returns: JSON with ``rc`` and either ``result`` or ``log_multiline``. + """ extravars = {"proxmox_node": req.proxmox_node} if req.vm_id is not None: extravars["vm_id"] = req.vm_id @@ -79,6 +102,11 @@ def proxmox_firewall_vm_alias_add(req: Request_ProxmoxFirewall_AddIptablesAlias) @router.delete(path="/vm/alias/delete", summary="Delete a firewall alias", description="Remove an existing alias from the proxmox firewall", tags=["proxmox - firewall"], response_model=Reply_ProxmoxFirewallWithStorageName_DeleteIptablesAlias, response_description="Details of the deleted firewall alias") def proxmox_firewall_vm_alias_delete(req: Request_ProxmoxFirewall_DeleteIptablesAlias): + """Delete a firewall alias from a VM. + + :param req: Request body with node, VM ID, and alias name. + :returns: JSON with ``rc`` and either ``result`` or ``log_multiline``. + """ extravars = {"proxmox_node": req.proxmox_node} if req.vm_id is not None: extravars["vm_id"] = req.vm_id @@ -91,6 +119,11 @@ def proxmox_firewall_vm_alias_delete(req: Request_ProxmoxFirewall_DeleteIptables @router.post(path="/vm/rules/list", summary="List VM firewall rules", description="List firewall rules for a specific virtual machine", tags=["proxmox - firewall"], response_model=Reply_ProxmoxFirewallWithStorageName_ListIptablesRules, response_description="Details of the VM firewall rules") def proxmox_vm_rules_list(req: Request_ProxmoxFirewall_ListIptablesRules): + """List firewall rules for a VM. + + :param req: Request body with ``proxmox_node`` and ``vm_id``. + :returns: JSON with ``rc`` and either ``result`` or ``log_multiline``. + """ extravars = {"proxmox_node": req.proxmox_node} if req.vm_id: extravars["vm_id"] = req.vm_id @@ -99,6 +132,11 @@ def proxmox_vm_rules_list(req: Request_ProxmoxFirewall_ListIptablesRules): @router.post(path="/vm/rules/apply", summary="Apply firewall rules", description="Apply the received firewall rules to the proxmox firewall", tags=["proxmox - firewall"], response_model=Reply_ProxmoxFirewallWithStorageName_ApplyIptablesRules, response_description="Details of the applied firewall rules") def proxmox_firewall_vm_rules_add(req: Request_ProxmoxFirewall_ApplyIptablesRules): + """Apply firewall rules to a VM. + + :param req: Request body with node, VM ID, and rule parameters. + :returns: JSON with ``rc`` and either ``result`` or ``log_multiline``. + """ extravars = {"proxmox_node": req.proxmox_node} if req.vm_id is not None: extravars["vm_id"] = req.vm_id @@ -111,6 +149,11 @@ def proxmox_firewall_vm_rules_add(req: Request_ProxmoxFirewall_ApplyIptablesRule @router.delete(path="/vm/rules/delete", summary="Delete a firewall rule", description="Remove an existing rule from the proxmox firewall configuration", tags=["proxmox - firewall"], response_model=Request_ProxmoxFirewall_DeleteIptablesRule, response_description="Details of the deleted firewall rule.") def proxmox_firewall_vm_rules_delete(req: Request_ProxmoxFirewall_DeleteIptablesRule): + """Delete a firewall rule from a VM by position. + + :param req: Request body with node, VM ID, and rule position. + :returns: JSON with ``rc`` and either ``result`` or ``log_multiline``. + """ extravars = {"proxmox_node": req.proxmox_node} if req.vm_id is not None: extravars["vm_id"] = req.vm_id @@ -123,6 +166,11 @@ def proxmox_firewall_vm_rules_delete(req: Request_ProxmoxFirewall_DeleteIptables @router.post(path="/vm/enable", summary="Enable VM firewall", description="Enable the proxmox firewall for a specific virtual machine", tags=["proxmox - firewall"], response_model=Reply_ProxmoxFirewallWithStorageName_EnableFirewallVm, response_description="Details of the enabled VM firewall") def proxmox_firewall_vm_enable(req: Request_ProxmoxFirewall_EnableFirewallVm): + """Enable the firewall on a VM. + + :param req: Request body with ``proxmox_node`` and ``vm_id``. + :returns: JSON with ``rc`` and either ``result`` or ``log_multiline``. + """ extravars = {"proxmox_node": req.proxmox_node} if req.vm_id: extravars["vm_id"] = req.vm_id @@ -132,6 +180,11 @@ def proxmox_firewall_vm_enable(req: Request_ProxmoxFirewall_EnableFirewallVm): @router.post(path="/vm/disable", summary="Disable VM firewall", description="Disable the proxmox firewall for a specific virtual machine", tags=["proxmox - firewall"], response_model=Reply_ProxmoxFirewallWithStorageName_DisableFirewallVm, response_description="Details of the disabled VM firewall") def proxmox_firewall_vm_disable(req: Request_ProxmoxFirewall_DistableFirewallVm): + """Disable the firewall on a VM. + + :param req: Request body with ``proxmox_node`` and ``vm_id``. + :returns: JSON with ``rc`` and either ``result`` or ``log_multiline``. + """ extravars = {"proxmox_node": req.proxmox_node} if req.vm_id: extravars["vm_id"] = req.vm_id @@ -143,12 +196,22 @@ def proxmox_firewall_vm_disable(req: Request_ProxmoxFirewall_DistableFirewallVm) @router.post(path="/node/enable", summary="Enable node firewall", description="Enable the proxmox firewall on a specific node", tags=["proxmox - firewall"], response_model=Reply_ProxmoxFirewallWithStorageName_EnableFirewallNode, response_description="Details of the enabled node firewall") def proxmox_firewall_node_enable(req: Request_ProxmoxFirewall_EnableFirewallNode): + """Enable the firewall on a Proxmox node. + + :param req: Request body with ``proxmox_node``. + :returns: JSON with ``rc`` and either ``result`` or ``log_multiline``. + """ extravars = {"proxmox_node": req.proxmox_node} return _run_fw(req, "firewall_node_enable", extravars) @router.post(path="/node/disable", summary="Disable node firewall", description="Disable the proxmox firewall on a specific node", tags=["proxmox - firewall"], response_model=Reply_ProxmoxFirewallWithStorageName_DisableFirewallNode, response_description="Details of the disabled node firewall") def proxmox_firewall_node_disable(req: Request_ProxmoxFirewall_DistableFirewallNode): + """Disable the firewall on a Proxmox node. + + :param req: Request body with ``proxmox_node``. + :returns: JSON with ``rc`` and either ``result`` or ``log_multiline``. + """ extravars = {"proxmox_node": req.proxmox_node} return _run_fw(req, "firewall_node_disable", extravars) @@ -157,6 +220,11 @@ def proxmox_firewall_node_disable(req: Request_ProxmoxFirewall_DistableFirewallN @router.post(path="/datacenter/enable", summary="Enable datacenter firewall", description="Enable the proxmox firewall at the datacenter level", tags=["proxmox - firewall"], response_model=Reply_ProxmoxFirewallWithStorageName_EnableFirewallDc, response_description="Details of the enabled datacenter firewall") def proxmox_firewall_dc_enable(req: Request_ProxmoxFirewall_EnableFirewallDc): + """Enable the firewall at the datacenter level. + + :param req: Request body with ``proxmox_api_host``. + :returns: JSON with ``rc`` and either ``result`` or ``log_multiline``. + """ extravars = {} if req.proxmox_api_host: extravars["proxmox_api_host"] = req.proxmox_api_host @@ -165,6 +233,11 @@ def proxmox_firewall_dc_enable(req: Request_ProxmoxFirewall_EnableFirewallDc): @router.post(path="/datacenter/disable", summary="Disable datacenter firewall", description="Disable the proxmox firewall at the datacenter level", tags=["proxmox - firewall"], response_model=Reply_ProxmoxFirewallWithStorageName_DisableFirewallDc, response_description="Details of the disabled datacenter firewall") def proxmox_firewall_dc_disable(req: Request_ProxmoxFirewall_DisableFirewallDc): + """Disable the firewall at the datacenter level. + + :param req: Request body with ``proxmox_api_host``. + :returns: JSON with ``rc`` and either ``result`` or ``log_multiline``. + """ extravars = {} if req.proxmox_api_host: extravars["proxmox_api_host"] = req.proxmox_api_host diff --git a/app/routes/network.py b/app/routes/network.py index 5ec52a6..df1d2cc 100644 --- a/app/routes/network.py +++ b/app/routes/network.py @@ -1,6 +1,13 @@ """Consolidated network routes. -Replaces: app/routes/v0/proxmox/network/vm/*.py, network/node/*.py +Endpoints +--------- +- ``POST /v0/admin/proxmox/network/vm/add`` -- Add VM network interface. +- ``POST /v0/admin/proxmox/network/vm/delete`` -- Delete VM network interface. +- ``POST /v0/admin/proxmox/network/vm/list`` -- List VM network interfaces. +- ``POST /v0/admin/proxmox/network/node/add`` -- Add node network interface. +- ``POST /v0/admin/proxmox/network/node/delete`` -- Delete node network interface. +- ``POST /v0/admin/proxmox/network//node/list`` -- List node network interfaces. """ import logging @@ -50,6 +57,11 @@ def _run_net(req, action: str, extravars: dict) -> JSONResponse: @router.post(path="/vm/add", summary="Add VM network interface", description="Create and attach a new network interface to a Proxmox VM.", tags=["proxmox - network - vm"], response_model=Reply_ProxmoxNetwork_WithVmId_AddNetworkInterface, response_description="Information about the added network interface.") def proxmox_network_vm_add_interface(req: Request_ProxmoxNetwork_WithVmId_AddNetwork): + """Create and attach a network interface to a VM. + + :param req: Request body with node, VM ID, and interface configuration. + :returns: JSON with ``rc`` and either ``result`` or ``log_multiline``. + """ extravars = {"proxmox_node": req.proxmox_node} if req.vm_id is not None: extravars["vm_id"] = req.vm_id @@ -62,6 +74,11 @@ def proxmox_network_vm_add_interface(req: Request_ProxmoxNetwork_WithVmId_AddNet @router.post(path="/vm/delete", summary="Delete VM network interface", description="Remove a network interface from a Proxmox VM.", tags=["proxmox - network - vm"], response_model=Reply_ProxmoxNetwork_WithVmId_DeleteNetworkInterface, response_description="Information about the deleted network interface.") def proxmox_network_vm_delete_interface(req: Request_ProxmoxNetwork_WithVmId_DeleteNetwork): + """Remove a network interface from a VM. + + :param req: Request body with node, VM ID, and network interface ID. + :returns: JSON with ``rc`` and either ``result`` or ``log_multiline``. + """ extravars = {"proxmox_node": req.proxmox_node} if req.vm_id is not None: extravars["vm_id"] = req.vm_id @@ -72,6 +89,11 @@ def proxmox_network_vm_delete_interface(req: Request_ProxmoxNetwork_WithVmId_Del @router.post(path="/vm/list", summary="List VM network interfaces", description="Retrieve all network interfaces attached to a Proxmox VM.", tags=["proxmox - network - vm"], response_model=Reply_ProxmoxNetwork_WithVmId_ListNetworkInterface, response_description="List of VM network interfaces.") def proxmox_network_vm_list_interface(req: Request_ProxmoxNetwork_WithVmId_ListNetwork): + """List all network interfaces attached to a VM. + + :param req: Request body with ``proxmox_node`` and ``vm_id``. + :returns: JSON with ``rc`` and either ``result`` or ``log_multiline``. + """ extravars = {"proxmox_node": req.proxmox_node} if req.vm_id is not None: extravars["vm_id"] = req.vm_id @@ -82,6 +104,11 @@ def proxmox_network_vm_list_interface(req: Request_ProxmoxNetwork_WithVmId_ListN @router.post(path="/node/add", summary="Add node network interface", description="Create and attach a new network interface to a Proxmox node.", tags=["proxmox - network - node"], response_model=Reply_ProxmoxNetwork_WithNodeName_AddNetworkInterface, response_description="Information about the added network interface.") def proxmox_network_node_add_interface(req: Request_ProxmoxNetwork_WithNodeName_AddNetworkInterface): + """Create and attach a network interface to a Proxmox node. + + :param req: Request body with node and interface configuration. + :returns: JSON with ``rc`` and either ``result`` or ``log_multiline``. + """ extravars = {"proxmox_node": req.proxmox_node} for field in ("bridge_ports", "iface_name", "iface_type", "iface_autostart", "ip_address", "ip_netmask", "ip_gateway", "ovs_bridge"): val = getattr(req, field, None) @@ -92,6 +119,11 @@ def proxmox_network_node_add_interface(req: Request_ProxmoxNetwork_WithNodeName_ @router.post(path="/node/delete", summary="Delete node network interface", description="Remove a network interface from a Proxmox node.", tags=["proxmox - network - node"], response_model=Reply_ProxmoxNetwork_WithNodeName_DeleteInterface, response_description="Information about the deleted network interface.") def proxmox_network_node_delete_interface(req: Request_ProxmoxNetwork_WithNodeName_DeleteInterface): + """Remove a network interface from a Proxmox node. + + :param req: Request body with node and interface name. + :returns: JSON with ``rc`` and either ``result`` or ``log_multiline``. + """ extravars = {"proxmox_node": req.proxmox_node} if req.iface_name is not None: extravars["iface_name"] = req.iface_name @@ -101,5 +133,10 @@ def proxmox_network_node_delete_interface(req: Request_ProxmoxNetwork_WithNodeNa # NOTE: original path has double slash "//node/list" - preserving exactly @router.post(path="//node/list", summary="List node network interfaces", description="Retrieve all network interfaces configured on a Proxmox node.", tags=["proxmox - network - node"], response_model=Reply_ProxmoxNetwork_WithNodeName_ListInterface, response_description="List of node network interfaces.") def proxmox_network_node_list_interface(req: Request_ProxmoxNetwork_WithNodeName_ListInterface): + """List all network interfaces on a Proxmox node. + + :param req: Request body with ``proxmox_node``. + :returns: JSON with ``rc`` and either ``result`` or ``log_multiline``. + """ extravars = {"proxmox_node": req.proxmox_node} return _run_net(req, "network_list_interfaces_node", extravars) diff --git a/app/routes/runner.py b/app/routes/runner.py index 1239cbe..1669655 100644 --- a/app/routes/runner.py +++ b/app/routes/runner.py @@ -1,7 +1,9 @@ """Consolidated dynamic runner routes. -Replaces: app/routes/v0/run/bundles/actions_run.py - app/routes/v0/run/scenarios/scenarios_run.py +Endpoints +--------- +- ``POST /v0/admin/run/bundles/{bundles_name}/run`` -- Run a named bundle. +- ``POST /v0/admin/run/scenarios/{scenario_name}/run`` -- Run a named scenario. """ import logging @@ -49,6 +51,12 @@ def _run_generic(req, name: str, resolver_fn) -> JSONResponse: tags=["runner"], ) def run_bundle(bundles_name: str, req: Request_DebugPing): + """Run a named bundle playbook from the external playbooks repository. + + :param bundles_name: Bundle path (e.g. ``"core/linux/ubuntu/install/docker"``). + :param req: Request body with ``hosts`` and optional ``proxmox_node``. + :returns: JSON with ``rc`` and ``log_multiline``. + """ return _run_generic(req, bundles_name, utils.resolve_bundles_playbook) @@ -59,4 +67,10 @@ def run_bundle(bundles_name: str, req: Request_DebugPing): tags=["runner"], ) def run_scenario(scenario_name: str, req: Request_DebugPing): + """Run a named scenario playbook from the external playbooks repository. + + :param scenario_name: Scenario path (e.g. ``"demo_lab"``). + :param req: Request body with ``hosts`` and optional ``proxmox_node``. + :returns: JSON with ``rc`` and ``log_multiline``. + """ return _run_generic(req, scenario_name, utils.resolve_scenarios_playbook) diff --git a/app/routes/snapshots.py b/app/routes/snapshots.py index 8959bc1..c16356c 100644 --- a/app/routes/snapshots.py +++ b/app/routes/snapshots.py @@ -1,6 +1,11 @@ """Consolidated snapshot routes. -Replaces: app/routes/v0/proxmox/vms/vm_id/snapshots/*.py +Endpoints +--------- +- ``POST /v0/admin/proxmox/vms/vm_id/snapshot/list`` -- List snapshots. +- ``POST /v0/admin/proxmox/vms/vm_id/snapshot/create`` -- Create a snapshot. +- ``DELETE /v0/admin/proxmox/vms/vm_id/snapshot/delete`` -- Delete a snapshot. +- ``POST /v0/admin/proxmox/vms/vm_id/snapshot/revert`` -- Revert to a snapshot. """ import logging @@ -62,6 +67,11 @@ def _run_snapshot_action(req, action: str, extravars: dict) -> JSONResponse: response_description="Snapshot list result", ) def proxmox_vms_vm_id_list_snapshot(req: Request_ProxmoxVmsVMID_ListSnapshot): + """List all snapshots for a VM. + + :param req: Request body with ``proxmox_node`` and ``vm_id``. + :returns: JSON with ``rc`` and either ``result`` or ``log_multiline``. + """ extravars = {} if req.vm_id is not None: extravars["vm_id"] = req.vm_id @@ -79,6 +89,11 @@ def proxmox_vms_vm_id_list_snapshot(req: Request_ProxmoxVmsVMID_ListSnapshot): response_description="Snapshot creation result", ) def proxmox_vms_vm_id_create_snapshot(req: Request_ProxmoxVmsVMID_CreateSnapshot): + """Create a named snapshot of a VM. + + :param req: Request body with node, VM ID, snapshot name, and description. + :returns: JSON with ``rc`` and either ``result`` or ``log_multiline``. + """ extravars = {"proxmox_node": req.proxmox_node} if req.vm_id is not None: extravars["vm_id"] = req.vm_id @@ -99,6 +114,11 @@ def proxmox_vms_vm_id_create_snapshot(req: Request_ProxmoxVmsVMID_CreateSnapshot response_description="Snapshot delete result", ) def proxmox_vms_vm_id_delete_snapshot(req: Request_ProxmoxVmsVMID_DeleteSnapshot): + """Delete a named snapshot from a VM. + + :param req: Request body with node, VM ID, and snapshot name. + :returns: JSON with ``rc`` and either ``result`` or ``log_multiline``. + """ extravars = {} if req.proxmox_node: extravars["proxmox_node"] = req.proxmox_node @@ -119,6 +139,11 @@ def proxmox_vms_vm_id_delete_snapshot(req: Request_ProxmoxVmsVMID_DeleteSnapshot response_description="Snapshot revert result", ) def proxmox_vms_vm_id_revert_snapshot(req: Request_ProxmoxVmsVMID_RevertSnapshot): + """Revert a VM to a named snapshot. + + :param req: Request body with node, VM ID, and snapshot name. + :returns: JSON with ``rc`` and either ``result`` or ``log_multiline``. + """ extravars = {} if req.proxmox_node: extravars["proxmox_node"] = req.proxmox_node diff --git a/app/routes/storage.py b/app/routes/storage.py index 4e744ce..c2fb204 100644 --- a/app/routes/storage.py +++ b/app/routes/storage.py @@ -1,6 +1,11 @@ """Consolidated storage routes. -Replaces: app/routes/v0/proxmox/storage/*.py +Endpoints +--------- +- ``POST /v0/admin/proxmox/storage/list`` -- List storage pools. +- ``POST /v0/admin/proxmox/storage/download_iso`` -- Download an ISO file. +- ``POST /v0/admin/proxmox/storage/storage_name/list_iso`` -- List ISOs in storage. +- ``POST /v0/admin/proxmox/storage/storage_name/list_template`` -- List templates. """ import logging @@ -50,6 +55,11 @@ def _run_storage(req, action: str, extravars: dict) -> JSONResponse: @storage_router.post(path="/list", summary="Retrieve configuration of a VM", description="Returns the configuration details of the specified virtual machine (VM).", tags=["proxmox - storage"], response_model=Reply_ProxmoxStorage_ListItem, response_description="VM configuration details") def proxmox_storage_list(req: Request_ProxmoxStorage_List): + """List storage pools on the Proxmox node. + + :param req: Request body with ``proxmox_node`` and optional ``storage_name``. + :returns: JSON with ``rc`` and either ``result`` or ``log_multiline``. + """ extravars = {"proxmox_node": req.proxmox_node} if req.storage_name is not None: extravars["storage_name"] = req.storage_name @@ -58,6 +68,11 @@ def proxmox_storage_list(req: Request_ProxmoxStorage_List): @storage_router.post(path="/download_iso", summary="Retrieve configuration of a VM", description="Returns the configuration details of the specified virtual machine (VM).", tags=["proxmox - storage"], response_model=Reply_ProxmoxStorage_DownloadIsoItem, response_description="VM configuration details") def proxmox_storage_download_iso(req: Request_ProxmoxStorage_DownloadIso): + """Download an ISO file to a Proxmox storage pool. + + :param req: Request body with node, storage name, ISO URL, and metadata. + :returns: JSON with ``rc`` and either ``result`` or ``log_multiline``. + """ extravars = {"proxmox_node": req.proxmox_node} if req.proxmox_storage is not None: extravars["proxmox_storage"] = req.proxmox_storage @@ -74,6 +89,11 @@ def proxmox_storage_download_iso(req: Request_ProxmoxStorage_DownloadIso): @storage_name_router.post(path="/list_iso", summary="Retrieve configuration of a VM", description="Returns the configuration details of the specified virtual machine (VM).", tags=["proxmox - storage"], response_model=Reply_ProxmoxStorageWithStorageName_ListIsoItem, response_description="VM configuration details") def proxmox_storage_with_storage_name_list_iso(req: Request_ProxmoxStorage_ListIso): + """List ISO files in a named storage pool. + + :param req: Request body with ``proxmox_node`` and ``storage_name``. + :returns: JSON with ``rc`` and either ``result`` or ``log_multiline``. + """ extravars = {"proxmox_node": req.proxmox_node} if req.storage_name is not None: extravars["storage_name"] = req.storage_name @@ -82,6 +102,11 @@ def proxmox_storage_with_storage_name_list_iso(req: Request_ProxmoxStorage_ListI @storage_name_router.post(path="/list_template", summary="Retrieve configuration of a VM", description="Returns the configuration details of the specified virtual machine (VM).", tags=["proxmox - storage"], response_model=Reply_ProxmoxStorageWithStorageName_ListTemplate, response_description="VM configuration details") def proxmox_storage_with_storage_name_list_template(req: Request_ProxmoxStorage_ListTemplate): + """List VM templates in a named storage pool. + + :param req: Request body with ``proxmox_node`` and ``storage_name``. + :returns: JSON with ``rc`` and either ``result`` or ``log_multiline``. + """ extravars = {"proxmox_node": req.proxmox_node} if req.storage_name is not None: extravars["storage_name"] = req.storage_name diff --git a/app/routes/vm_config.py b/app/routes/vm_config.py index 9293b73..9d5961d 100644 --- a/app/routes/vm_config.py +++ b/app/routes/vm_config.py @@ -1,6 +1,12 @@ """Consolidated VM configuration routes. -Replaces: app/routes/v0/proxmox/vms/vm_id/config/*.py +Endpoints +--------- +- ``POST /v0/admin/proxmox/vms/vm_id/config/vm_get_config`` -- Full VM config. +- ``POST /v0/admin/proxmox/vms/vm_id/config/vm_get_config_cdrom`` -- CD-ROM config. +- ``POST /v0/admin/proxmox/vms/vm_id/config/vm_get_config_cpu`` -- CPU config. +- ``POST /v0/admin/proxmox/vms/vm_id/config/vm_get_config_ram`` -- RAM config. +- ``POST /v0/admin/proxmox/vms/vm_id/config/vm_set_tag`` -- Set VM tags. """ import logging @@ -63,6 +69,11 @@ def _run_config_action(req, action: str, extravars: dict) -> JSONResponse: response_description="VM configuration details", ) def proxmox_vms_vm_id_vm_get_config(req: Request_ProxmoxVmsVMID_VmGetConfig): + """Retrieve the full configuration of a VM. + + :param req: Request body with ``proxmox_node`` and ``vm_id``. + :returns: JSON with ``rc`` and either ``result`` or ``log_multiline``. + """ extravars = {"proxmox_node": req.proxmox_node} if req.vm_id is not None: extravars["vm_id"] = req.vm_id @@ -78,6 +89,11 @@ def proxmox_vms_vm_id_vm_get_config(req: Request_ProxmoxVmsVMID_VmGetConfig): response_description="cdrom configuration details", ) def proxmox_vms_vm_id_vm_get_config_cdrom(req: Request_ProxmoxVmsVMID_VmGetConfigCdrom): + """Retrieve the CD-ROM configuration of a VM. + + :param req: Request body with ``proxmox_node`` and ``vm_id``. + :returns: JSON with ``rc`` and either ``result`` or ``log_multiline``. + """ extravars = {"proxmox_node": req.proxmox_node} if req.vm_id is not None: extravars["vm_id"] = req.vm_id @@ -93,6 +109,11 @@ def proxmox_vms_vm_id_vm_get_config_cdrom(req: Request_ProxmoxVmsVMID_VmGetConfi response_description="cpu configuration details", ) def proxmox_vms_vm_id_vm_get_config_cpu(req: Request_ProxmoxVmsVMID_VmGetConfigCpu): + """Retrieve the CPU configuration of a VM. + + :param req: Request body with ``proxmox_node`` and ``vm_id``. + :returns: JSON with ``rc`` and either ``result`` or ``log_multiline``. + """ extravars = {"proxmox_node": req.proxmox_node} if req.vm_id is not None: extravars["vm_id"] = req.vm_id @@ -108,6 +129,11 @@ def proxmox_vms_vm_id_vm_get_config_cpu(req: Request_ProxmoxVmsVMID_VmGetConfigC response_description="ram configuration details", ) def proxmox_vms_vm_id_vm_get_config_ram(req: Request_ProxmoxVmsVMID_VmGetConfigRam): + """Retrieve the RAM configuration of a VM. + + :param req: Request body with ``proxmox_node`` and ``vm_id``. + :returns: JSON with ``rc`` and either ``result`` or ``log_multiline``. + """ extravars = {"proxmox_node": req.proxmox_node} if req.vm_id is not None: extravars["vm_id"] = req.vm_id @@ -123,6 +149,11 @@ def proxmox_vms_vm_id_vm_get_config_ram(req: Request_ProxmoxVmsVMID_VmGetConfigR response_description="VM configuration details", ) def proxmox_vms_vm_id_vm_set_tags(req: Request_ProxmoxVmsVMID_VmSetTag): + """Set tags on a VM. + + :param req: Request body with ``proxmox_node``, ``vm_id``, and ``vm_tag_name``. + :returns: JSON with ``rc`` and either ``result`` or ``log_multiline``. + """ extravars = {"proxmox_node": req.proxmox_node} if req.vm_id is not None: extravars["vm_id"] = req.vm_id diff --git a/app/routes/vms.py b/app/routes/vms.py index f64cb68..35e1d8e 100644 --- a/app/routes/vms.py +++ b/app/routes/vms.py @@ -1,8 +1,23 @@ """Consolidated VM lifecycle routes. -Replaces: app/routes/v0/proxmox/vms/list.py, list_usage.py, - vm_id/{start,stop,stop_force,pause,resume,create,delete,clone}.py, - vm_ids/{mass_start_stop_pause_resume,mass_delete}.py +Endpoints +--------- +- ``POST /v0/admin/proxmox/vms/list`` -- List VMs and LXC containers. +- ``POST /v0/admin/proxmox/vms/list_usage`` -- Resource usage of VMs. +- ``POST /v0/admin/proxmox/vms/vm_id/start`` -- Start a VM. +- ``POST /v0/admin/proxmox/vms/vm_id/stop`` -- Stop a VM. +- ``POST /v0/admin/proxmox/vms/vm_id/stop_force`` -- Force stop a VM. +- ``POST /v0/admin/proxmox/vms/vm_id/pause`` -- Pause a VM. +- ``POST /v0/admin/proxmox/vms/vm_id/resume`` -- Resume a VM. +- ``POST /v0/admin/proxmox/vms/vm_id/create`` -- Create a VM. +- ``DELETE /v0/admin/proxmox/vms/vm_id/delete`` -- Delete a VM. +- ``POST /v0/admin/proxmox/vms/vm_id/clone`` -- Clone a VM. +- ``POST /v0/admin/proxmox/vms/vm_ids/start`` -- Mass start VMs. +- ``POST /v0/admin/proxmox/vms/vm_ids/stop`` -- Mass stop VMs. +- ``POST /v0/admin/proxmox/vms/vm_ids/stop_force`` -- Mass force stop VMs. +- ``POST /v0/admin/proxmox/vms/vm_ids/pause`` -- Mass pause VMs. +- ``POST /v0/admin/proxmox/vms/vm_ids/resume`` -- Mass resume VMs. +- ``DELETE /v0/admin/proxmox/vms/vm_ids/delete`` -- Mass delete VMs. """ import logging @@ -80,6 +95,11 @@ def _run_proxmox_action(req, action: str, extravars: dict) -> JSONResponse: response_description="List VM result", ) def proxmox_vms_list(req: Request_ProxmoxVms_VmList): + """List all VMs and LXC containers on the Proxmox node. + + :param req: Request body with optional ``proxmox_node`` filter. + :returns: JSON with ``rc`` and either ``result`` or ``log_multiline``. + """ extravars = {} if req.proxmox_node: extravars["proxmox_node"] = req.proxmox_node @@ -95,6 +115,11 @@ def proxmox_vms_list(req: Request_ProxmoxVms_VmList): response_description="Resource usage details", ) def proxmox_vms_list_usage(req: Request_ProxmoxVms_VmListUsage): + """Retrieve current resource usage (RAM, CPU, disk) for all VMs. + + :param req: Request body with optional ``proxmox_node`` filter. + :returns: JSON with ``rc`` and either ``result`` or ``log_multiline``. + """ extravars = {} if req.proxmox_node: extravars["proxmox_node"] = req.proxmox_node @@ -116,6 +141,11 @@ def proxmox_vms_list_usage(req: Request_ProxmoxVms_VmListUsage): response_description="Start result", ) def proxmox_vms_vm_id_start(req: Request_ProxmoxVmsVMID_StartStopPauseResume): + """Start a specific VM. + + :param req: Request body with ``proxmox_node`` and ``vm_id``. + :returns: JSON with ``rc`` and either ``result`` or ``log_multiline``. + """ extravars = {"proxmox_node": req.proxmox_node} if req.vm_id is not None: extravars["vm_id"] = req.vm_id @@ -131,6 +161,11 @@ def proxmox_vms_vm_id_start(req: Request_ProxmoxVmsVMID_StartStopPauseResume): response_description="Start result", ) def proxmox_vms_vm_id_stop(req: Request_ProxmoxVmsVMID_StartStopPauseResume): + """Stop a specific VM gracefully. + + :param req: Request body with ``proxmox_node`` and ``vm_id``. + :returns: JSON with ``rc`` and either ``result`` or ``log_multiline``. + """ extravars = {"proxmox_node": req.proxmox_node} if req.vm_id is not None: extravars["vm_id"] = req.vm_id @@ -146,6 +181,11 @@ def proxmox_vms_vm_id_stop(req: Request_ProxmoxVmsVMID_StartStopPauseResume): response_description="Start result", ) def proxmox_vms_vm_id_stop_force(req: Request_ProxmoxVmsVMID_StartStopPauseResume): + """Force stop a specific VM (equivalent to power off). + + :param req: Request body with ``proxmox_node`` and ``vm_id``. + :returns: JSON with ``rc`` and either ``result`` or ``log_multiline``. + """ extravars = {"proxmox_node": req.proxmox_node} if req.vm_id is not None: extravars["vm_id"] = req.vm_id @@ -161,6 +201,11 @@ def proxmox_vms_vm_id_stop_force(req: Request_ProxmoxVmsVMID_StartStopPauseResum response_description="Start result", ) def proxmox_vms_vm_id_pause(req: Request_ProxmoxVmsVMID_StartStopPauseResume): + """Pause a specific VM. + + :param req: Request body with ``proxmox_node`` and ``vm_id``. + :returns: JSON with ``rc`` and either ``result`` or ``log_multiline``. + """ extravars = {"proxmox_node": req.proxmox_node} if req.vm_id is not None: extravars["vm_id"] = req.vm_id @@ -176,6 +221,11 @@ def proxmox_vms_vm_id_pause(req: Request_ProxmoxVmsVMID_StartStopPauseResume): response_description="Start result", ) def proxmox_vms_vm_id_resume(req: Request_ProxmoxVmsVMID_StartStopPauseResume): + """Resume a paused VM. + + :param req: Request body with ``proxmox_node`` and ``vm_id``. + :returns: JSON with ``rc`` and either ``result`` or ``log_multiline``. + """ extravars = {"proxmox_node": req.proxmox_node} if req.vm_id is not None: extravars["vm_id"] = req.vm_id @@ -191,6 +241,11 @@ def proxmox_vms_vm_id_resume(req: Request_ProxmoxVmsVMID_StartStopPauseResume): response_description="Delete result", ) def proxmox_vms_vm_id_create(req: Request_ProxmoxVmsVMID_Create): + """Create a new VM with the specified configuration. + + :param req: Request body with node, VM ID, name, CPU, memory, disk, and ISO options. + :returns: JSON with ``rc`` and either ``result`` or ``log_multiline``. + """ extravars = {"proxmox_node": req.proxmox_node} if req.vm_id is not None: extravars["vm_id"] = req.vm_id @@ -220,6 +275,11 @@ def proxmox_vms_vm_id_create(req: Request_ProxmoxVmsVMID_Create): response_description="Delete result", ) def proxmox_vms_vm_id_delete(req: Request_ProxmoxVmsVMID_Delete): + """Delete a specific VM. Resolves the VM name before deletion. + + :param req: Request body with ``proxmox_node`` and ``vm_id``. + :returns: JSON with ``rc`` and either ``result`` or ``log_multiline``. + """ extravars = {"proxmox_node": req.proxmox_node} if req.vm_id is not None: extravars["vm_id"] = req.vm_id @@ -236,6 +296,11 @@ def proxmox_vms_vm_id_delete(req: Request_ProxmoxVmsVMID_Delete): response_description="Delete result", ) def proxmox_vms_vm_id_clone(req: Request_ProxmoxVmsVMID_Clone): + """Clone a VM to create a new VM with a different ID and name. + + :param req: Request body with source VM ID, new VM ID, name, and description. + :returns: JSON with ``rc`` and either ``result`` or ``log_multiline``. + """ extravars = {"proxmox_node": req.proxmox_node} if req.vm_id is not None: extravars["vm_id"] = req.vm_id @@ -294,6 +359,11 @@ def _run_mass_action(req, action_name: str, proxmox_vm_action: str) -> JSONRespo response_model=Reply_ProxmoxVmsVMID_StartStopPauseResume, ) def proxmox_vms_vm_ids_mass_stop(req: Request_ProxmoxVmsVmIds_MassStartStopPauseResume): + """Stop multiple VMs by ID list. + + :param req: Request body with ``proxmox_node`` and ``vm_ids`` list. + :returns: JSON with ``rc`` and either ``result`` or ``log_multiline``. + """ return _run_mass_action(req, _MASS_ACTION_NAME, "vm_stop") @@ -305,6 +375,11 @@ def proxmox_vms_vm_ids_mass_stop(req: Request_ProxmoxVmsVmIds_MassStartStopPause response_model=Reply_ProxmoxVmsVMID_StartStopPauseResume, ) def proxmox_vms_vm_ids_mass_stop_force(req: Request_ProxmoxVmsVmIds_MassStartStopPauseResume): + """Force stop multiple VMs by ID list. + + :param req: Request body with ``proxmox_node`` and ``vm_ids`` list. + :returns: JSON with ``rc`` and either ``result`` or ``log_multiline``. + """ return _run_mass_action(req, _MASS_ACTION_NAME, "vm_stop_force") @@ -316,6 +391,11 @@ def proxmox_vms_vm_ids_mass_stop_force(req: Request_ProxmoxVmsVmIds_MassStartSto response_model=Reply_ProxmoxVmsVMID_StartStopPauseResume, ) def proxmox_vms_vm_ids_mass_start(req: Request_ProxmoxVmsVmIds_MassStartStopPauseResume): + """Start multiple VMs by ID list. + + :param req: Request body with ``proxmox_node`` and ``vm_ids`` list. + :returns: JSON with ``rc`` and either ``result`` or ``log_multiline``. + """ return _run_mass_action(req, _MASS_ACTION_NAME, "vm_start") @@ -327,6 +407,11 @@ def proxmox_vms_vm_ids_mass_start(req: Request_ProxmoxVmsVmIds_MassStartStopPaus response_model=Reply_ProxmoxVmsVMID_StartStopPauseResume, ) def proxmox_vms_vm_ids_mass_pause(req: Request_ProxmoxVmsVmIds_MassStartStopPauseResume): + """Pause multiple VMs by ID list. + + :param req: Request body with ``proxmox_node`` and ``vm_ids`` list. + :returns: JSON with ``rc`` and either ``result`` or ``log_multiline``. + """ return _run_mass_action(req, _MASS_ACTION_NAME, "vm_pause") @@ -338,6 +423,11 @@ def proxmox_vms_vm_ids_mass_pause(req: Request_ProxmoxVmsVmIds_MassStartStopPaus response_model=Reply_ProxmoxVmsVMID_StartStopPauseResume, ) def proxmox_vms_vm_ids_mass_resume(req: Request_ProxmoxVmsVmIds_MassStartStopPauseResume): + """Resume multiple paused VMs by ID list. + + :param req: Request body with ``proxmox_node`` and ``vm_ids`` list. + :returns: JSON with ``rc`` and either ``result`` or ``log_multiline``. + """ return _run_mass_action(req, _MASS_ACTION_NAME, "vm_resume") @@ -349,6 +439,11 @@ def proxmox_vms_vm_ids_mass_resume(req: Request_ProxmoxVmsVmIds_MassStartStopPau response_model=Reply_ProxmoxVmsVmIds_MassDelete, ) def proxmox_vms_vm_ids_mass_delete(req: Request_ProxmoxVmsVmIds_MassDelete): + """Delete multiple VMs by name/ID pairs. + + :param req: Request body with ``proxmox_node`` and ``vms`` list. + :returns: JSON with ``rc`` and either ``result`` or ``log_multiline``. + """ action_name = "core/proxmox/configure/default/vms/delete-vms-vuln" checked_inventory_filepath = utils.resolve_inventory(INVENTORY_NAME) checked_playbook_filepath = utils.resolve_bundles_playbook(action_name, "public_github") diff --git a/app/routes/ws_status.py b/app/routes/ws_status.py index 88f0e21..d5c3dc1 100644 --- a/app/routes/ws_status.py +++ b/app/routes/ws_status.py @@ -1,15 +1,15 @@ -""" -WebSocket endpoint for real-time VM status updates. +"""WebSocket endpoint for real-time VM status updates. -Polls Proxmox API directly via httpx (not Ansible — too slow for real-time) -and pushes status changes to connected clients. +Polls the Proxmox API directly via httpx (not Ansible -- too slow for +real-time) and pushes status changes to connected WebSocket clients. Proxmox credentials are read from the backend's own inventory file so the frontend never needs to handle API tokens. -Usage: - ws://host:8000/ws/vm-status - ws://host:8000/ws/vm-status?node=pve01 +Endpoints +--------- +- ``ws://host:8000/ws/vm-status`` -- Full VM status stream. +- ``ws://host:8000/ws/vm-status?node=pve01`` -- Filtered by node. """ import asyncio @@ -31,7 +31,12 @@ def load_proxmox_credentials() -> dict: - """Read Proxmox API credentials from the backend's inventory file.""" + """Read Proxmox API credentials from the backend's inventory file. + + :returns: Dict with ``api_host``, ``node``, ``token_id``, and + ``token_secret`` keys, or an empty dict on failure. + :rtype: dict + """ inv_dir = os.getenv("API_BACKEND_INVENTORY_DIR", "") inv_path = Path(inv_dir) / "hosts.yml" if inv_dir else Path("inventory/hosts.yml") @@ -62,7 +67,22 @@ async def fetch_vm_status( token_id: str, token_secret: str, ) -> list[dict]: - """Fetch VM list directly from Proxmox API (bypasses Ansible for speed).""" + """Fetch VM list directly from the Proxmox API (bypasses Ansible for speed). + + :param client: Reusable async HTTP client. + :type client: httpx.AsyncClient + :param api_host: Proxmox API hostname or IP. + :type api_host: str + :param node: Proxmox node name. + :type node: str + :param token_id: PVE API token identifier. + :type token_id: str + :param token_secret: PVE API token secret. + :type token_secret: str + :returns: List of VM status dicts with keys ``vmid``, ``name``, + ``status``, ``cpu``, ``mem``, ``maxmem``, ``uptime``, ``template``, ``tags``. + :rtype: list[dict] + """ url = f"https://{api_host}/api2/json/nodes/{node}/qemu" headers = {"Authorization": f"PVEAPIToken={token_id}={token_secret}"} @@ -92,7 +112,16 @@ async def fetch_vm_status( def compute_diff( prev: Dict[int, dict], current: Dict[int, dict] ) -> Optional[dict]: - """Compare previous and current VM states, return changes.""" + """Compare previous and current VM states, return changes. + + :param prev: Previous poll's VM state keyed by ``vmid``. + :type prev: Dict[int, dict] + :param current: Current poll's VM state keyed by ``vmid``. + :type current: Dict[int, dict] + :returns: Dict of changes keyed by ``vmid`` with ``type`` field + (``"added"``, ``"changed"``, ``"removed"``), or ``None`` if no changes. + :rtype: dict or None + """ changes = {} for vmid, vm in current.items(): @@ -111,6 +140,14 @@ def compute_diff( @router.websocket("/ws/vm-status") async def vm_status_websocket(ws: WebSocket): + """WebSocket handler for real-time VM status streaming. + + Sends a full state on first connection, then sends only diffs + on subsequent polls (every ``POLL_INTERVAL`` seconds). + + :param ws: The WebSocket connection. + :type ws: WebSocket + """ await ws.accept() # Read Proxmox credentials from backend inventory (not from client) diff --git a/app/utils/checks_inventory.py b/app/utils/checks_inventory.py index 27b3bd3..e741746 100644 --- a/app/utils/checks_inventory.py +++ b/app/utils/checks_inventory.py @@ -1,3 +1,10 @@ +"""Inventory path validation and resolution. + +Provides :func:`resolve_inventory`, which validates an inventory name +against a strict regex, resolves the file path under the project's +``inventory/`` directory, and checks for path traversal before returning +the absolute path. +""" import os from pathlib import Path @@ -9,7 +16,20 @@ def resolve_inventory(inventory_name: str) -> Path: - """ resolve inventory file path """ + """Resolve an inventory file path from a logical name. + + Constructs the path ``/inventory/.yml``, + validates the name format, and checks for traversal or missing files. + + :param inventory_name: Logical inventory name (e.g. ``"hosts"``). + Must match ``^[A-Za-z0-9_-]+(?:/[A-Za-z0-9_-]+)*$``. + :type inventory_name: str + :returns: Absolute resolved path to the inventory YAML file. + :rtype: Path + :raises HTTPException: 400 if the name format is invalid, a path + traversal is detected, or the file does not exist. + 500 if the inventory directory itself is missing. + """ project_root = Path(os.getenv("PROJECT_ROOT_DIR")).resolve() inventory_dir = (project_root / "inventory").resolve() @@ -27,6 +47,22 @@ def resolve_inventory(inventory_name: str) -> Path: def _resolve_inventory_file(inventory_dir: Path, inventory_name: str, name_pattern: re.Pattern[str]) -> Path: + """Validate and resolve an inventory file path. + + Performs regex validation, traversal detection, directory existence + check, and strict file resolution. + + :param inventory_dir: Absolute path to the inventory directory. + :type inventory_dir: Path + :param inventory_name: The inventory name to resolve. + :type inventory_name: str + :param name_pattern: Compiled regex pattern for name validation. + :type name_pattern: re.Pattern[str] + :returns: Absolute resolved path to the inventory file. + :rtype: Path + :raises HTTPException: 400 if the name is invalid, traversal is detected, + or the file is not found. 500 if the inventory directory is missing. + """ if not name_pattern.fullmatch(str(inventory_name)): @@ -72,6 +108,3 @@ def _resolve_inventory_file(inventory_dir: Path, logger.info(f":: ok - resolved inventory : {inventory_filepath}") return inventory_filepath - - - diff --git a/app/utils/checks_playbooks.py b/app/utils/checks_playbooks.py index 02bc749..961fd6d 100644 --- a/app/utils/checks_playbooks.py +++ b/app/utils/checks_playbooks.py @@ -1,3 +1,11 @@ +"""Playbook path validation and resolution. + +Provides resolver functions for actions, bundles, and scenarios. Each +function validates the action name against a strict regex, resolves the +file path, and checks for path traversal before returning the absolute +path to the playbook YAML file. +""" + import re import logging logger = logging.getLogger(__name__) @@ -9,6 +17,18 @@ #### def _warmup_checks(playbooks_dir_type: str) -> Path: + """Validate and resolve the playbooks base directory. + + Reads the appropriate environment variable based on the directory + type and verifies that the resolved path exists and is a directory. + + :param playbooks_dir_type: Either ``"www_app"`` or ``"public_github"``. + :type playbooks_dir_type: str + :returns: Resolved absolute path to the playbooks directory. + :rtype: Path + :raises HTTPException: 400 if the directory type is unknown, the + environment variable is missing, or the directory does not exist. + """ playbooks_dir: Path if playbooks_dir_type == "www_app": @@ -36,7 +56,20 @@ def _warmup_checks(playbooks_dir_type: str) -> Path: def resolve_actions_playbook(action_name: str, playbooks_dir_type: str) -> Path: - """ resolve actions file path """ + """Resolve an action playbook file path. + + Looks for ``/actions//main.yml`` after + validating the action name format. + + :param action_name: Slash-separated action path (e.g. ``"vm/clone-template"``). + :type action_name: str + :param playbooks_dir_type: Either ``"www_app"`` or ``"public_github"``. + :type playbooks_dir_type: str + :returns: Absolute path to the action's ``main.yml``. + :rtype: Path + :raises HTTPException: 400 if the name is invalid, the file is missing, + or a path traversal is detected. + """ playbooks_dir = _warmup_checks(playbooks_dir_type) actions_dir = (playbooks_dir / "actions").resolve() @@ -57,7 +90,20 @@ def resolve_actions_playbook(action_name: str, playbooks_dir_type: str) -> Path: return main_filepath def resolve_bundles_playbook(action_name: str, playbooks_dir_type: str) -> Path: - """ resolve bundles file path """ + """Resolve a bundle playbook file path. + + Looks for ``/bundles//main.yml`` after + validating the action name format. + + :param action_name: Slash-separated bundle path (e.g. ``"core/linux/ubuntu/install/docker"``). + :type action_name: str + :param playbooks_dir_type: Either ``"www_app"`` or ``"public_github"``. + :type playbooks_dir_type: str + :returns: Absolute path to the bundle's ``main.yml``. + :rtype: Path + :raises HTTPException: 400 if the name is invalid, the file is missing, + or a path traversal is detected. + """ playbooks_dir = _warmup_checks(playbooks_dir_type) actions_dir = (playbooks_dir / "bundles").resolve() @@ -70,7 +116,21 @@ def resolve_bundles_playbook(action_name: str, playbooks_dir_type: str) -> Path: return main_filepath def resolve_bundles_playbook_init_file(action_name: str, playbooks_dir_type: str) -> Path: - """ resolve bundles file path """ + """Resolve a bundle's init playbook file path. + + Looks for ``/bundles//init.yml`` instead + of the default ``main.yml``. Used by multi-step bundle execution + that runs per-VM initialization before the main playbook. + + :param action_name: Slash-separated bundle path. + :type action_name: str + :param playbooks_dir_type: Either ``"www_app"`` or ``"public_github"``. + :type playbooks_dir_type: str + :returns: Absolute path to the bundle's ``init.yml``. + :rtype: Path + :raises HTTPException: 400 if the name is invalid, the file is missing, + or a path traversal is detected. + """ playbooks_dir = _warmup_checks(playbooks_dir_type) actions_dir = (playbooks_dir / "bundles").resolve() @@ -84,7 +144,20 @@ def resolve_bundles_playbook_init_file(action_name: str, playbooks_dir_type: str def resolve_scenarios_playbook(action_name: str, playbooks_dir_type: str) -> Path: - """ resolve scenarios file path """ + """Resolve a scenario playbook file path. + + Looks for ``/scenarios//main.yml`` after + validating the action name format. + + :param action_name: Slash-separated scenario path (e.g. ``"demo_lab"``). + :type action_name: str + :param playbooks_dir_type: Either ``"www_app"`` or ``"public_github"``. + :type playbooks_dir_type: str + :returns: Absolute path to the scenario's ``main.yml``. + :rtype: Path + :raises HTTPException: 400 if the name is invalid, the file is missing, + or a path traversal is detected. + """ playbooks_dir = _warmup_checks(playbooks_dir_type) scenarios_dir = (playbooks_dir / "scenarios").resolve() @@ -103,6 +176,24 @@ def _resolve_file(actions_dir: Path, action_name: str, *, is_init_yaml: bool = False) -> Path: + """Validate an action name and resolve the corresponding playbook file. + + Performs regex validation, path resolution, traversal detection, and + existence checks. + + :param actions_dir: Base directory for the action type (actions/bundles/scenarios). + :type actions_dir: Path + :param actions_regex_pattern: Compiled regex pattern for name validation. + :type actions_regex_pattern: re.Pattern[str] + :param action_name: The action name to resolve (e.g. ``"core/linux/ubuntu/install/docker"``). + :type action_name: str + :param is_init_yaml: If ``True``, resolve ``init.yml`` instead of ``main.yml``. + :type is_init_yaml: bool + :returns: Absolute path to the resolved playbook file. + :rtype: Path + :raises HTTPException: 400 if the name format is invalid, a path + traversal is detected, or the file does not exist. + """ # # REGEX CHECKS # @@ -146,4 +237,3 @@ def _resolve_file(actions_dir: Path, raise HTTPException(status_code=400, detail=err) return main_filepath - diff --git a/app/utils/text_cleaner.py b/app/utils/text_cleaner.py index 82d2e7f..5ab596f 100644 --- a/app/utils/text_cleaner.py +++ b/app/utils/text_cleaner.py @@ -1,9 +1,24 @@ +"""Text cleaning utilities. + +Provides :func:`strip_ansi` for removing ANSI escape sequences (colors, +cursor movement, etc.) from strings returned by Ansible runner output. +""" + import re ANSI_RE = re.compile(r'(?:\x1B[@-_][0-?]*[ -/]*[@-~])') -# ANSI_RE = re.compile(r"\x1b\[[0-9;]*[A-Za-z]") + def strip_ansi(s: str) -> str: - """ delete ansi sequences (colors ect) from string """ - return ANSI_RE.sub("", s) + """Remove ANSI escape sequences from a string. + + Strips terminal color codes, cursor positioning, and other control + sequences so that log output is safe for plain-text storage and + JSON serialization. + :param s: Input string potentially containing ANSI escape sequences. + :type s: str + :returns: The input string with all ANSI sequences removed. + :rtype: str + """ + return ANSI_RE.sub("", s) diff --git a/app/utils/vm_id_name_resolver.py b/app/utils/vm_id_name_resolver.py index 78176ff..b121c80 100644 --- a/app/utils/vm_id_name_resolver.py +++ b/app/utils/vm_id_name_resolver.py @@ -1,3 +1,10 @@ +"""VM ID to name resolution. + +Provides :func:`resolv_id_to_vm_name`, which queries the Proxmox VM list +via Ansible and returns the ``vm_id`` / ``vm_name`` pair for a given VM ID. +Used by routes that need the VM name for operations like delete, clone, +and snapshot management. +""" from pathlib import Path import os, json, logging @@ -10,6 +17,15 @@ def hack_same_vm_id(a, b) -> bool: + """Compare two VM IDs that may be int or str. + + Attempts integer comparison first, falls back to string comparison. + + :param a: First VM ID value. + :param b: Second VM ID value. + :returns: ``True`` if the IDs are equal after type coercion. + :rtype: bool + """ try: return int(a) == int(b) @@ -18,6 +34,20 @@ def hack_same_vm_id(a, b) -> bool: return str(a) == str(b) def resolv_id_to_vm_name(proxmox_node: str, target_vm_id: str) -> dict: + """Resolve a VM ID to its name by querying the Proxmox VM list. + + Runs the ``vm_list`` action via Ansible, iterates over the results, + and returns the matching ``vm_id`` / ``vm_name`` pair. + + :param proxmox_node: The Proxmox node name to query. + :type proxmox_node: str + :param target_vm_id: The VM ID to look up. + :type target_vm_id: str + :returns: Dict with ``"vm_id"`` and ``"vm_name"`` keys. + :rtype: dict + :raises HTTPException: 500 if the VM ID is not found in the results + or the result data cannot be parsed. + """ PROJECT_ROOT = Path(os.getenv("PROJECT_ROOT_DIR")).resolve() PLAYBOOK_SRC = PROJECT_ROOT / "playbooks" / "generic.yml" @@ -91,7 +121,3 @@ def resolv_id_to_vm_name(proxmox_node: str, target_vm_id: str) -> dict: err = f":: err - vm_id NOT FOUND" logger.error("vm_id not found: %s", target_vm_id) raise HTTPException(status_code=500, detail=err) - - - - From 119c2f6b318fb21e9f1831bd97a1c222098f94c4 Mon Sep 17 00:00:00 2001 From: t0kubetsu Date: Mon, 23 Mar 2026 11:33:50 +0100 Subject: [PATCH 19/33] format --- README.md | 66 ++--- app/core/config.py | 28 +- app/core/exceptions.py | 16 +- app/core/runner.py | 18 +- app/routes/__init__.py | 24 +- app/routes/bundles.py | 324 ++++++++++++++++++----- app/routes/debug.py | 24 +- app/routes/firewall.py | 184 ++++++++++--- app/routes/network.py | 128 +++++++-- app/routes/runner.py | 8 +- app/routes/snapshots.py | 40 ++- app/routes/storage.py | 67 ++++- app/routes/vm_config.py | 29 ++- app/routes/vms.py | 70 +++-- app/routes/ws_status.py | 51 ++-- app/schemas/base.py | 7 +- app/schemas/bundles/__init__.py | 428 ++++++++++++------------------- app/schemas/debug/__init__.py | 32 +-- app/schemas/firewall.py | 295 ++++++++++----------- app/schemas/network.py | 192 ++++++-------- app/schemas/snapshots.py | 113 ++++---- app/schemas/storage.py | 163 ++++++------ app/schemas/vm_config.py | 125 ++++----- app/schemas/vms.py | 250 ++++++++---------- app/utils/__init__.py | 12 - app/utils/checks_inventory.py | 18 +- app/utils/checks_playbooks.py | 33 ++- app/utils/text_cleaner.py | 2 +- app/utils/vm_id_name_resolver.py | 34 ++- tests/conftest.py | 10 +- tests/test_app_factory.py | 1 + tests/test_config.py | 7 +- tests/test_extractor.py | 7 +- tests/test_schemas.py | 131 ++++++++-- tests/test_vault.py | 1 + 35 files changed, 1660 insertions(+), 1278 deletions(-) diff --git a/README.md b/README.md index f72cbc4..67b9bca 100644 --- a/README.md +++ b/README.md @@ -59,21 +59,21 @@ uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload All settings are read from environment variables in `app/core/config.py`. Nothing is hard-coded. -| Variable | Required | Description | Default | -|---|---|---|---| -| `PROJECT_ROOT_DIR` | Yes | Absolute path to the project root | `.` (cwd) | -| `VAULT_PASSWORD_FILE` | Yes* | Path to the Ansible Vault password file | -- | -| `VAULT_PASSWORD` | Yes* | Ansible Vault password as a string | -- | -| `API_BACKEND_WWWAPP_PLAYBOOKS_DIR` | No | Local playbooks directory | `PROJECT_ROOT_DIR/` | -| `API_BACKEND_PUBLIC_PLAYBOOKS_DIR` | No | External playbooks repository path | -- | -| `API_BACKEND_INVENTORY_DIR` | No | Ansible inventory directory | `PROJECT_ROOT_DIR/inventory/` | -| `API_BACKEND_VAULT_FILE` | No | Path to vault-encrypted variables file | -- | -| `CORS_ORIGIN_REGEX` | No | Regex for allowed CORS origins | `localhost` / `127.0.0.1` / `[::1]` only | -| `HOST` | No | Server bind address | `0.0.0.0` | -| `PORT` | No | Server listen port | `8000` | -| `DEBUG` | No | Enable debug mode (`true`, `1`, or `yes`) | `false` | - -> *One of `VAULT_PASSWORD_FILE` or `VAULT_PASSWORD` must be set for vault-encrypted operations. +| Variable | Required | Description | Default | +| ---------------------------------- | -------- | ----------------------------------------- | ---------------------------------------- | +| `PROJECT_ROOT_DIR` | Yes | Absolute path to the project root | `.` (cwd) | +| `VAULT_PASSWORD_FILE` | Yes\* | Path to the Ansible Vault password file | -- | +| `VAULT_PASSWORD` | Yes\* | Ansible Vault password as a string | -- | +| `API_BACKEND_WWWAPP_PLAYBOOKS_DIR` | No | Local playbooks directory | `PROJECT_ROOT_DIR/` | +| `API_BACKEND_PUBLIC_PLAYBOOKS_DIR` | No | External playbooks repository path | -- | +| `API_BACKEND_INVENTORY_DIR` | No | Ansible inventory directory | `PROJECT_ROOT_DIR/inventory/` | +| `API_BACKEND_VAULT_FILE` | No | Path to vault-encrypted variables file | -- | +| `CORS_ORIGIN_REGEX` | No | Regex for allowed CORS origins | `localhost` / `127.0.0.1` / `[::1]` only | +| `HOST` | No | Server bind address | `0.0.0.0` | +| `PORT` | No | Server listen port | `8000` | +| `DEBUG` | No | Enable debug mode (`true`, `1`, or `yes`) | `false` | + +> \*One of `VAULT_PASSWORD_FILE` or `VAULT_PASSWORD` must be set for vault-encrypted operations. --- @@ -81,10 +81,10 @@ All settings are read from environment variables in `app/core/config.py`. Nothin Once the server is running, interactive docs are available at: -| Format | URL | -|---|---| -| Swagger UI | `/docs/swagger` | -| ReDoc | `/docs/redoc` | +| Format | URL | +| ------------ | -------------------- | +| Swagger UI | `/docs/swagger` | +| ReDoc | `/docs/redoc` | | OpenAPI JSON | `/docs/openapi.json` | --- @@ -169,20 +169,20 @@ HTTP Request ### Route Prefixes -| Prefix | Module | Purpose | -|---|---|---| -| `/v0/admin/proxmox/vms/` | `vms.py` | VM list and lifecycle | -| `/v0/admin/proxmox/vms/vm_id/` | `vms.py` | Single VM operations | -| `/v0/admin/proxmox/vms/vm_ids/` | `vms.py` | Mass VM operations | -| `/v0/admin/proxmox/vms/vm_id/config/` | `vm_config.py` | VM configuration | -| `/v0/admin/proxmox/vms/vm_id/snapshot/` | `snapshots.py` | VM snapshots | -| `/v0/admin/proxmox/firewall/` | `firewall.py` | Firewall management | -| `/v0/admin/proxmox/network/` | `network.py` | Network interfaces | -| `/v0/admin/proxmox/storage/` | `storage.py` | Storage and ISOs | -| `/v0/admin/run/bundles/` | `bundles.py`, `runner.py` | Bundle execution | -| `/v0/admin/run/scenarios/` | `runner.py` | Scenario execution | -| `/v0/admin/debug/` | `debug.py` | Debug/test endpoints | -| `/ws/vm-status` | `ws_status.py` | WebSocket VM status | +| Prefix | Module | Purpose | +| --------------------------------------- | ------------------------- | --------------------- | +| `/v0/admin/proxmox/vms/` | `vms.py` | VM list and lifecycle | +| `/v0/admin/proxmox/vms/vm_id/` | `vms.py` | Single VM operations | +| `/v0/admin/proxmox/vms/vm_ids/` | `vms.py` | Mass VM operations | +| `/v0/admin/proxmox/vms/vm_id/config/` | `vm_config.py` | VM configuration | +| `/v0/admin/proxmox/vms/vm_id/snapshot/` | `snapshots.py` | VM snapshots | +| `/v0/admin/proxmox/firewall/` | `firewall.py` | Firewall management | +| `/v0/admin/proxmox/network/` | `network.py` | Network interfaces | +| `/v0/admin/proxmox/storage/` | `storage.py` | Storage and ISOs | +| `/v0/admin/run/bundles/` | `bundles.py`, `runner.py` | Bundle execution | +| `/v0/admin/run/scenarios/` | `runner.py` | Scenario execution | +| `/v0/admin/debug/` | `debug.py` | Debug/test endpoints | +| `/ws/vm-status` | `ws_status.py` | WebSocket VM status | --- diff --git a/app/core/config.py b/app/core/config.py index 2bcafbf..6e22ac0 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -42,16 +42,28 @@ class Settings: :type debug: bool """ - project_root: Path = field(default_factory=lambda: Path(os.getenv("PROJECT_ROOT_DIR", ".")).resolve()) + project_root: Path = field( + default_factory=lambda: Path(os.getenv("PROJECT_ROOT_DIR", ".")).resolve() + ) # Playbook paths - wwwapp_playbooks_dir: str = field(default_factory=lambda: os.getenv("API_BACKEND_WWWAPP_PLAYBOOKS_DIR", "")) - public_playbooks_dir: str = field(default_factory=lambda: os.getenv("API_BACKEND_PUBLIC_PLAYBOOKS_DIR", "")) - inventory_dir: str = field(default_factory=lambda: os.getenv("API_BACKEND_INVENTORY_DIR", "")) - vault_file: str = field(default_factory=lambda: os.getenv("API_BACKEND_VAULT_FILE", "")) + wwwapp_playbooks_dir: str = field( + default_factory=lambda: os.getenv("API_BACKEND_WWWAPP_PLAYBOOKS_DIR", "") + ) + public_playbooks_dir: str = field( + default_factory=lambda: os.getenv("API_BACKEND_PUBLIC_PLAYBOOKS_DIR", "") + ) + inventory_dir: str = field( + default_factory=lambda: os.getenv("API_BACKEND_INVENTORY_DIR", "") + ) + vault_file: str = field( + default_factory=lambda: os.getenv("API_BACKEND_VAULT_FILE", "") + ) # Vault credentials - vault_password_file: str = field(default_factory=lambda: os.getenv("VAULT_PASSWORD_FILE", "")) + vault_password_file: str = field( + default_factory=lambda: os.getenv("VAULT_PASSWORD_FILE", "") + ) vault_password: str = field(default_factory=lambda: os.getenv("VAULT_PASSWORD", "")) # CORS @@ -65,7 +77,9 @@ class Settings: # Server host: str = field(default_factory=lambda: os.getenv("HOST", "0.0.0.0")) port: int = field(default_factory=lambda: int(os.getenv("PORT", "8000"))) - debug: bool = field(default_factory=lambda: os.getenv("DEBUG", "").lower() in ("1", "true", "yes")) + debug: bool = field( + default_factory=lambda: os.getenv("DEBUG", "").lower() in ("1", "true", "yes") + ) @property def playbook_path(self) -> Path: diff --git a/app/core/exceptions.py b/app/core/exceptions.py index ffba6a7..2b84fd3 100644 --- a/app/core/exceptions.py +++ b/app/core/exceptions.py @@ -34,7 +34,9 @@ def make_validation_error_detail(err: dict) -> dict: } -async def validation_exception_handler(request: Request, exc: RequestValidationError) -> JSONResponse: +async def validation_exception_handler( + request: Request, exc: RequestValidationError +) -> JSONResponse: """Verbose 422 handler with debug logging. Logs the HTTP method, URL, raw request body, and each validation @@ -57,7 +59,10 @@ async def validation_exception_handler(request: Request, exc: RequestValidationE if body_text.strip(): try: parsed = json.loads(body_text) - logger.error("Request body:\n%s", json.dumps(parsed, indent=2, ensure_ascii=False)) + logger.error( + "Request body:\n%s", + json.dumps(parsed, indent=2, ensure_ascii=False), + ) except json.JSONDecodeError: logger.error("Request body (raw): %s", body_text) else: @@ -68,7 +73,12 @@ async def validation_exception_handler(request: Request, exc: RequestValidationE details = [] for err in exc.errors(): detail = make_validation_error_detail(err) - logger.error("field=%s | msg=%s | type=%s", detail["field"], detail["msg"], detail["type"]) + logger.error( + "field=%s | msg=%s | type=%s", + detail["field"], + detail["msg"], + detail["type"], + ) details.append(detail) return JSONResponse(status_code=422, content={"detail": details}) diff --git a/app/core/runner.py b/app/core/runner.py index 3d47b87..9382185 100644 --- a/app/core/runner.py +++ b/app/core/runner.py @@ -62,8 +62,12 @@ def _build_envvars(vm: VaultManager) -> dict: "PYTHONWARNINGS": "ignore::DeprecationWarning", "ANSIBLE_ROLES_PATH": os.environ.get("ANSIBLE_ROLES_PATH", ""), "ANSIBLE_FILTER_PLUGINS": os.environ.get("ANSIBLE_FILTER_PLUGINS", ""), - "ANSIBLE_COLLECTIONS_PATH": os.environ.get("ANSIBLE_COLLECTIONS_PATH", coll_paths), - "ANSIBLE_COLLECTIONS_PATHS": os.environ.get("ANSIBLE_COLLECTIONS_PATHS", coll_paths), + "ANSIBLE_COLLECTIONS_PATH": os.environ.get( + "ANSIBLE_COLLECTIONS_PATH", coll_paths + ), + "ANSIBLE_COLLECTIONS_PATHS": os.environ.get( + "ANSIBLE_COLLECTIONS_PATHS", coll_paths + ), "ANSIBLE_LIBRARY": os.environ.get("ANSIBLE_LIBRARY", ""), } @@ -80,7 +84,9 @@ def _build_envvars(vm: VaultManager) -> dict: def _setup_temp_dir( - inventory: Path, playbook: Path, vm: VaultManager, + inventory: Path, + playbook: Path, + vm: VaultManager, ) -> tuple[Path, Path, Path]: """Create an isolated temp directory for a single playbook run. @@ -117,9 +123,7 @@ def _setup_temp_dir( env_dir = tmp_dir / "env" env_dir.mkdir(parents=True, exist_ok=True) env_file = env_dir / "envvars" - env_file.write_text( - "\n".join(f"{k}={v}" for k, v in envvars.items()) + "\n" - ) + env_file.write_text("\n".join(f"{k}={v}" for k, v in envvars.items()) + "\n") return tmp_dir, inv_dest, play_rel @@ -155,7 +159,7 @@ def _build_cmdline( cmdline = f'{(cmdline or "").strip()} -e "@{vars_file}"'.strip() if tags: - cmdline = f'{(cmdline or "").strip()} --tags {tags}'.strip() + cmdline = f"{(cmdline or '').strip()} --tags {tags}".strip() return cmdline diff --git a/app/routes/__init__.py b/app/routes/__init__.py index 5c9f9d5..7bcc7ac 100644 --- a/app/routes/__init__.py +++ b/app/routes/__init__.py @@ -1,14 +1,14 @@ from fastapi import APIRouter -from app.routes.vms import vms_router, vm_id_router, vm_ids_router -from app.routes.vm_config import router as vm_config_router -from app.routes.snapshots import router as snapshots_router +from app.routes.bundles import router as bundles_router +from app.routes.debug import router as debug_router from app.routes.firewall import router as firewall_router from app.routes.network import router as network_router -from app.routes.storage import storage_router, storage_name_router -from app.routes.bundles import router as bundles_router from app.routes.runner import run_bundle, run_scenario -from app.routes.debug import router as debug_router +from app.routes.snapshots import router as snapshots_router +from app.routes.storage import storage_name_router, storage_router +from app.routes.vm_config import router as vm_config_router +from app.routes.vms import vm_id_router, vm_ids_router, vms_router router = APIRouter() @@ -21,7 +21,9 @@ # /v0/admin/run/bundles/{name}/run _bundles_runner = APIRouter() _bundles_runner.add_api_route( - "/{bundles_name}/run", run_bundle, methods=["POST"], + "/{bundles_name}/run", + run_bundle, + methods=["POST"], summary="Run bundles", description="Run generic bundles with default (and static) extras_vars ", tags=["runner"], @@ -31,7 +33,9 @@ # /v0/admin/run/scenarios/{name}/run _scenarios_runner = APIRouter() _scenarios_runner.add_api_route( - "/{scenario_name}/run", run_scenario, methods=["POST"], + "/{scenario_name}/run", + run_scenario, + methods=["POST"], summary="Run scenario", description="Run generic scenario with default (and static) extras_vars ", tags=["runner"], @@ -54,7 +58,9 @@ router.include_router(snapshots_router, prefix="/v0/admin/proxmox/vms/vm_id/snapshot") # /v0/admin/proxmox/storage/storage_name -router.include_router(storage_name_router, prefix="/v0/admin/proxmox/storage/storage_name") +router.include_router( + storage_name_router, prefix="/v0/admin/proxmox/storage/storage_name" +) # /v0/admin/proxmox/storage router.include_router(storage_router, prefix="/v0/admin/proxmox/storage") diff --git a/app/routes/bundles.py b/app/routes/bundles.py index 3c7507b..6a25a73 100644 --- a/app/routes/bundles.py +++ b/app/routes/bundles.py @@ -26,31 +26,41 @@ import logging import os from pathlib import Path -from typing import Any from fastapi import APIRouter, HTTPException from fastapi.responses import JSONResponse -from app.core.runner import run_playbook_core -from app.core.extractor import extract_action_results from app import utils +from app.core.extractor import extract_action_results +from app.core.runner import run_playbook_core # --- Linux/Ubuntu bundle schemas --- from app.schemas.bundles import ( - Request_BundlesCoreLinuxUbuntuInstall_Docker, Reply_BundlesCoreLinuxUbuntuInstall_Docker, - Request_BundlesCoreLinuxUbuntuInstall_DockerCompose, Reply_BundlesCoreLinuxUbuntuInstall_DockerCompose, - Request_BundlesCoreLinuxUbuntuInstall_BasicPackages, Reply_BundlesCoreLinuxUbuntuInstall_BasicPackages, - Request_BundlesCoreLinuxUbuntuInstall_DotFiles, Reply_BundlesCoreLinuxUbuntuInstall_DotFilesItem, - Request_BundlesCoreLinuxUbuntuConfigure_AddUser, Reply_BundlesCoreLinuxUbuntuConfigure_AddUser, + Reply_BundlesCoreLinuxUbuntuConfigure_AddUser, + Reply_BundlesCoreLinuxUbuntuInstall_BasicPackages, + Reply_BundlesCoreLinuxUbuntuInstall_Docker, + Reply_BundlesCoreLinuxUbuntuInstall_DockerCompose, + Reply_BundlesCoreLinuxUbuntuInstall_DotFilesItem, + Reply_BundlesCoreProxmoxConfigureDefaultVms_CreateAdminVms, + Reply_BundlesCoreProxmoxConfigureDefaultVms_CreateStudentVms, + Reply_BundlesCoreProxmoxConfigureDefaultVms_CreateVulnVms, + Request_BundlesCoreLinuxUbuntuConfigure_AddUser, + Request_BundlesCoreLinuxUbuntuInstall_BasicPackages, + Request_BundlesCoreLinuxUbuntuInstall_Docker, + Request_BundlesCoreLinuxUbuntuInstall_DockerCompose, + Request_BundlesCoreLinuxUbuntuInstall_DotFiles, # --- Proxmox bundle schemas --- - Request_BundlesCoreProxmoxConfigureDefaultVms_CreateAdminVms, Reply_BundlesCoreProxmoxConfigureDefaultVms_CreateAdminVms, - Request_BundlesCoreProxmoxConfigureDefaultVms_CreateVulnVms, Reply_BundlesCoreProxmoxConfigureDefaultVms_CreateVulnVms, - Request_BundlesCoreProxmoxConfigureDefaultVms_CreateStudentVms, Reply_BundlesCoreProxmoxConfigureDefaultVms_CreateStudentVms, - Request_BundlesCoreProxmoxConfigureDefaultVms_StartStopPauseResumeAdminVulnStudentVms, + Request_BundlesCoreProxmoxConfigureDefaultVms_CreateAdminVms, + Request_BundlesCoreProxmoxConfigureDefaultVms_CreateStudentVms, + Request_BundlesCoreProxmoxConfigureDefaultVms_CreateVulnVms, Request_BundlesCoreProxmoxConfigureDefaultVms_RevertSnapshotAdminVulnStudentVms, + Request_BundlesCoreProxmoxConfigureDefaultVms_StartStopPauseResumeAdminVulnStudentVms, +) +from app.schemas.snapshots import ( + Reply_ProxmoxVmsVMID_CreateSnapshot, + Reply_ProxmoxVmsVMID_RevertSnapshot, ) from app.schemas.vms import Reply_ProxmoxVmsVMID_StartStopPauseResume -from app.schemas.snapshots import Reply_ProxmoxVmsVMID_CreateSnapshot, Reply_ProxmoxVmsVMID_RevertSnapshot logger = logging.getLogger(__name__) @@ -64,13 +74,17 @@ # Pattern B helpers: simple playbook run (Ubuntu bundles) # --------------------------------------------------------------------------- + def _run_bundle_simple(req, action_name: str, extravars: dict) -> JSONResponse: """Run a single bundle playbook (Pattern B).""" checked_inventory = utils.resolve_inventory(INVENTORY_NAME) checked_playbook = utils.resolve_bundles_playbook(action_name, "public_github") rc, events, log_plain, _ = run_playbook_core( - checked_playbook, checked_inventory, limit=req.hosts, extravars=extravars, + checked_playbook, + checked_inventory, + limit=req.hosts, + extravars=extravars, ) payload = {"rc": rc, "log_multiline": log_plain.splitlines()} @@ -81,10 +95,13 @@ def _run_bundle_simple(req, action_name: str, extravars: dict) -> JSONResponse: # Pattern C helpers: multi-step Proxmox bundles # --------------------------------------------------------------------------- + def _run_create_vms_bundle(req, action_name: str, request_checks_fn) -> JSONResponse: """Multi-step create VMs: init.yml per VM, then main.yml (Pattern C).""" checked_inventory = utils.resolve_inventory(INVENTORY_NAME) - checked_playbook_init = utils.resolve_bundles_playbook_init_file(action_name, "public_github") + checked_playbook_init = utils.resolve_bundles_playbook_init_file( + action_name, "public_github" + ) request_checks_fn(req) @@ -97,7 +114,10 @@ def _run_create_vms_bundle(req, action_name: str, request_checks_fn) -> JSONResp "global_vm_description": item.vm_description, } rc, events, log_plain, _ = run_playbook_core( - checked_playbook_init, checked_inventory, tags=vm_key, extravars=extravars, + checked_playbook_init, + checked_inventory, + tags=vm_key, + extravars=extravars, ) if rc != 0: payload = {"rc": rc, "log_multiline": log_plain.splitlines()} @@ -107,13 +127,17 @@ def _run_create_vms_bundle(req, action_name: str, request_checks_fn) -> JSONResp checked_playbook_main = utils.resolve_bundles_playbook(action_name, "public_github") extravars = {"proxmox_node": req.proxmox_node} rc, events, log_plain, _ = run_playbook_core( - checked_playbook_main, checked_inventory, extravars=extravars, + checked_playbook_main, + checked_inventory, + extravars=extravars, ) payload = {"rc": rc, "log_multiline": log_plain.splitlines()} return JSONResponse(payload, status_code=200 if rc == 0 else 500) -def _run_start_stop_bundle(req, action_name: str, proxmox_vm_action: str) -> JSONResponse: +def _run_start_stop_bundle( + req, action_name: str, proxmox_vm_action: str +) -> JSONResponse: """Start/stop/pause/resume bundle (Pattern C variant).""" checked_inventory = utils.resolve_inventory(INVENTORY_NAME) checked_playbook = utils.resolve_bundles_playbook(action_name, "public_github") @@ -123,7 +147,10 @@ def _run_start_stop_bundle(req, action_name: str, proxmox_vm_action: str) -> JSO extravars["proxmox_node"] = req.proxmox_node rc, events, log_plain, _ = run_playbook_core( - checked_playbook, checked_inventory, limit=req.proxmox_node, extravars=extravars, + checked_playbook, + checked_inventory, + limit=req.proxmox_node, + extravars=extravars, ) if req.as_json: @@ -145,7 +172,10 @@ def _run_delete_bundle(req, action_name: str) -> JSONResponse: extravars["proxmox_node"] = req.proxmox_node rc, events, log_plain, _ = run_playbook_core( - checked_playbook, checked_inventory, limit=req.proxmox_node, extravars=extravars, + checked_playbook, + checked_inventory, + limit=req.proxmox_node, + extravars=extravars, ) if req.as_json: @@ -170,7 +200,10 @@ def _run_snapshot_bundle(req, action_name: str, action_key: str) -> JSONResponse extravars["VM_SNAPSHOT_NAME"] = req.vm_snapshot_name rc, events, log_plain, _ = run_playbook_core( - checked_playbook, checked_inventory, limit=req.proxmox_node, extravars=extravars, + checked_playbook, + checked_inventory, + limit=req.proxmox_node, + extravars=extravars, ) if req.as_json: @@ -187,8 +220,17 @@ def _run_snapshot_bundle(req, action_name: str, action_key: str) -> JSONResponse # Linux/Ubuntu install bundles # =========================================================================== -@router.post(path="/core/linux/ubuntu/install/docker", summary="Install docker packages", description="Install and configure docker engine on the target ubuntu system", tags=["bundles - core - ubuntu "], response_model=Reply_BundlesCoreLinuxUbuntuInstall_Docker) -def bundles_core_linux_ubuntu_install_docker(req: Request_BundlesCoreLinuxUbuntuInstall_Docker): + +@router.post( + path="/core/linux/ubuntu/install/docker", + summary="Install docker packages", + description="Install and configure docker engine on the target ubuntu system", + tags=["bundles - core - ubuntu "], + response_model=Reply_BundlesCoreLinuxUbuntuInstall_Docker, +) +def bundles_core_linux_ubuntu_install_docker( + req: Request_BundlesCoreLinuxUbuntuInstall_Docker, +): """Install and configure Docker on the target Ubuntu system. :param req: Request body with host, node, and package installation flags. @@ -200,14 +242,26 @@ def bundles_core_linux_ubuntu_install_docker(req: Request_BundlesCoreLinuxUbuntu if req.install_package_docker is not None: extravars["INSTALL_PACKAGES_DOCKER"] = req.install_package_docker if req.install_ntpclient_and_update_time is not None: - extravars["INSTALL_PACKAGES_NTP_AND_UPDATE_TIME"] = req.install_ntpclient_and_update_time + extravars["INSTALL_PACKAGES_NTP_AND_UPDATE_TIME"] = ( + req.install_ntpclient_and_update_time + ) if req.packages_cleaning is not None: extravars["SPECIFIC_PACKAGES_CLEANING"] = req.packages_cleaning - return _run_bundle_simple(req, "core/linux/ubuntu/install/docker", extravars or None) + return _run_bundle_simple( + req, "core/linux/ubuntu/install/docker", extravars or None + ) -@router.post(path="/core/linux/ubuntu/install/docker-compose", summary="Install docker compose packages", description="Install and configure docker compose on the target ubuntu system", tags=["bundles - core - ubuntu "], response_model=Reply_BundlesCoreLinuxUbuntuInstall_DockerCompose) -def bundles_core_linux_ubuntu_install_docker_compose(req: Request_BundlesCoreLinuxUbuntuInstall_DockerCompose): +@router.post( + path="/core/linux/ubuntu/install/docker-compose", + summary="Install docker compose packages", + description="Install and configure docker compose on the target ubuntu system", + tags=["bundles - core - ubuntu "], + response_model=Reply_BundlesCoreLinuxUbuntuInstall_DockerCompose, +) +def bundles_core_linux_ubuntu_install_docker_compose( + req: Request_BundlesCoreLinuxUbuntuInstall_DockerCompose, +): """Install and configure Docker Compose on the target Ubuntu system. :param req: Request body with host, node, and package installation flags. @@ -219,16 +273,30 @@ def bundles_core_linux_ubuntu_install_docker_compose(req: Request_BundlesCoreLin if req.install_package_docker is not None: extravars["INSTALL_PACKAGES_DOCKER"] = req.install_package_docker if req.install_package_docker_compose is not None: - extravars["INSTALL_PACKAGES_DOCKER_COMPOSE"] = req.install_package_docker_compose + extravars["INSTALL_PACKAGES_DOCKER_COMPOSE"] = ( + req.install_package_docker_compose + ) if req.install_ntpclient_and_update_time is not None: - extravars["INSTALL_PACKAGES_NTP_AND_UPDATE_TIME"] = req.install_ntpclient_and_update_time + extravars["INSTALL_PACKAGES_NTP_AND_UPDATE_TIME"] = ( + req.install_ntpclient_and_update_time + ) if req.packages_cleaning is not None: extravars["SPECIFIC_PACKAGES_CLEANING"] = req.packages_cleaning - return _run_bundle_simple(req, "core/linux/ubuntu/install/docker", extravars or None) + return _run_bundle_simple( + req, "core/linux/ubuntu/install/docker", extravars or None + ) -@router.post(path="/core/linux/ubuntu/install/basic-packages", summary="Install basics packages", description="Install and configure a base set of packages on the target Ubuntu system", tags=["bundles - core - ubuntu "], response_model=Reply_BundlesCoreLinuxUbuntuInstall_BasicPackages) -def bundles_core_linux_ubuntu_install_basic_packages(req: Request_BundlesCoreLinuxUbuntuInstall_BasicPackages): +@router.post( + path="/core/linux/ubuntu/install/basic-packages", + summary="Install basics packages", + description="Install and configure a base set of packages on the target Ubuntu system", + tags=["bundles - core - ubuntu "], + response_model=Reply_BundlesCoreLinuxUbuntuInstall_BasicPackages, +) +def bundles_core_linux_ubuntu_install_basic_packages( + req: Request_BundlesCoreLinuxUbuntuInstall_BasicPackages, +): """Install a base set of packages on the target Ubuntu system. :param req: Request body with host, node, and package category flags. @@ -237,15 +305,34 @@ def bundles_core_linux_ubuntu_install_basic_packages(req: Request_BundlesCoreLin extravars = {} if req.proxmox_node: extravars["proxmox_node"] = req.proxmox_node - for field, key in [("install_package_basics", "INSTALL_PACKAGES_BASICS"), ("install_package_firewalls", "INSTALL_PACKAGES_FIREWALLS"), ("install_package_docker", "INSTALL_PACKAGES_DOCKER"), ("install_package_docker_compose", "INSTALL_PACKAGES_DOCKER_COMPOSE"), ("install_package_utils_json", "INSTALL_PACKAGES_UTILS_JSON"), ("install_package_utils_network", "INSTALL_PACKAGES_UTILS_NETWORK"), ("install_ntpclient_and_update_time", "INSTALL_PACKAGES_NTP_AND_UPDATE_TIME"), ("packages_cleaning", "SPECIFIC_PACKAGES_CLEANING")]: + for field, key in [ + ("install_package_basics", "INSTALL_PACKAGES_BASICS"), + ("install_package_firewalls", "INSTALL_PACKAGES_FIREWALLS"), + ("install_package_docker", "INSTALL_PACKAGES_DOCKER"), + ("install_package_docker_compose", "INSTALL_PACKAGES_DOCKER_COMPOSE"), + ("install_package_utils_json", "INSTALL_PACKAGES_UTILS_JSON"), + ("install_package_utils_network", "INSTALL_PACKAGES_UTILS_NETWORK"), + ("install_ntpclient_and_update_time", "INSTALL_PACKAGES_NTP_AND_UPDATE_TIME"), + ("packages_cleaning", "SPECIFIC_PACKAGES_CLEANING"), + ]: val = getattr(req, field, None) if val is not None: extravars[key] = val - return _run_bundle_simple(req, "core/linux/ubuntu/install/basic-packages", extravars or None) + return _run_bundle_simple( + req, "core/linux/ubuntu/install/basic-packages", extravars or None + ) -@router.post(path="/core/linux/ubuntu/install/dot-files", summary="Install user dotfiles", description="Install and configure generic dotfiles - vimrc, zshrc, etc.", tags=["bundles - core - ubuntu "], response_model=Reply_BundlesCoreLinuxUbuntuInstall_DotFilesItem) -def bundles_core_linux_ubuntu_install_dotfiles(req: Request_BundlesCoreLinuxUbuntuInstall_DotFiles): +@router.post( + path="/core/linux/ubuntu/install/dot-files", + summary="Install user dotfiles", + description="Install and configure generic dotfiles - vimrc, zshrc, etc.", + tags=["bundles - core - ubuntu "], + response_model=Reply_BundlesCoreLinuxUbuntuInstall_DotFilesItem, +) +def bundles_core_linux_ubuntu_install_dotfiles( + req: Request_BundlesCoreLinuxUbuntuInstall_DotFiles, +): """Install generic dotfiles (vimrc, zshrc, etc.) for a user. :param req: Request body with host, user, and dotfile selection flags. @@ -264,15 +351,26 @@ def bundles_core_linux_ubuntu_install_dotfiles(req: Request_BundlesCoreLinuxUbun extravars["INSTALL_ZSH_DOTFILES"] = req.install_zsh_dot_files if req.apply_for_root is not None: extravars["APPLY_FOR_ROOT"] = req.apply_for_root - return _run_bundle_simple(req, "core/linux/ubuntu/install/dot-files", extravars or None) + return _run_bundle_simple( + req, "core/linux/ubuntu/install/dot-files", extravars or None + ) # =========================================================================== # Linux/Ubuntu configure bundles # =========================================================================== -@router.post(path="/core/linux/ubuntu/configure/add-user", summary="Add system user", description="Create a new user with shell, home and password", tags=["bundles - core - ubuntu "], response_model=Reply_BundlesCoreLinuxUbuntuConfigure_AddUser) -def bundles_core_linux_ubuntu_configure_add_user(req: Request_BundlesCoreLinuxUbuntuConfigure_AddUser): + +@router.post( + path="/core/linux/ubuntu/configure/add-user", + summary="Add system user", + description="Create a new user with shell, home and password", + tags=["bundles - core - ubuntu "], + response_model=Reply_BundlesCoreLinuxUbuntuConfigure_AddUser, +) +def bundles_core_linux_ubuntu_configure_add_user( + req: Request_BundlesCoreLinuxUbuntuConfigure_AddUser, +): """Create a new system user with shell, home directory, and password. :param req: Request body with host, user details, and password policy. @@ -289,17 +387,26 @@ def bundles_core_linux_ubuntu_configure_add_user(req: Request_BundlesCoreLinuxUb extravars["TARGET_SHELL_PATH"] = req.shell_path if req.change_pwd_at_logon is not None: extravars["CHANGE_PWD_AT_LOGON"] = req.change_pwd_at_logon - return _run_bundle_simple(req, "core/linux/ubuntu/configure/add-user", extravars or None) + return _run_bundle_simple( + req, "core/linux/ubuntu/configure/add-user", extravars or None + ) # =========================================================================== # Proxmox create VMs bundles (Pattern C) # =========================================================================== + def _check_admin_vms(req): if not req.vms or len(req.vms) == 0: raise HTTPException(status_code=400, detail="Field vms must not be empty") - allowed = {"admin-wazuh", "admin-web-api-kong", "admin-web-builder-api", "admin-web-deployer-ui", "admin-web-emp"} + allowed = { + "admin-wazuh", + "admin-web-api-kong", + "admin-web-builder-api", + "admin-web-deployer-ui", + "admin-web-emp", + } for r in allowed: if r not in req.vms: raise HTTPException(status_code=400, detail=f"Missing required vm key {r}") @@ -308,17 +415,29 @@ def _check_admin_vms(req): raise HTTPException(status_code=400, detail=f"Unauthorized vm key {vm}") for vm_name, vm_spec in req.vms.items(): if vm_spec.vm_id is None: - raise HTTPException(status_code=400, detail=f"missing key vm_id for {vm_name}") + raise HTTPException( + status_code=400, detail=f"missing key vm_id for {vm_name}" + ) if vm_spec.vm_description is None: - raise HTTPException(status_code=400, detail=f"missing key vm_description for {vm_name}") + raise HTTPException( + status_code=400, detail=f"missing key vm_description for {vm_name}" + ) if vm_spec.vm_ip is None: - raise HTTPException(status_code=400, detail=f"missing key vm_ip for {vm_name}") + raise HTTPException( + status_code=400, detail=f"missing key vm_ip for {vm_name}" + ) def _check_vuln_vms(req): if not req.vms or len(req.vms) == 0: raise HTTPException(status_code=500, detail="Field vms must not be empty") - allowed = {"vuln-box-00", "vuln-box-01", "vuln-box-02", "vuln-box-03", "vuln-box-04"} + allowed = { + "vuln-box-00", + "vuln-box-01", + "vuln-box-02", + "vuln-box-03", + "vuln-box-04", + } for r in allowed: if r not in req.vms: raise HTTPException(status_code=500, detail=f"Missing required vm key {r}") @@ -327,11 +446,17 @@ def _check_vuln_vms(req): raise HTTPException(status_code=500, detail=f"Unauthorized vm key {vm}") for vm_name, vm_spec in req.vms.items(): if vm_spec.vm_id is None: - raise HTTPException(status_code=500, detail=f"missing key vm_id for {vm_name}") + raise HTTPException( + status_code=500, detail=f"missing key vm_id for {vm_name}" + ) if vm_spec.vm_description is None: - raise HTTPException(status_code=500, detail=f"missing key vm_description for {vm_name}") + raise HTTPException( + status_code=500, detail=f"missing key vm_description for {vm_name}" + ) if vm_spec.vm_ip is None: - raise HTTPException(status_code=500, detail=f"missing key vm_ip for {vm_name}") + raise HTTPException( + status_code=500, detail=f"missing key vm_ip for {vm_name}" + ) def _check_student_vms(req): @@ -346,41 +471,77 @@ def _check_student_vms(req): raise HTTPException(status_code=400, detail=f"Unauthorized vm key {vm}") for vm_name, vm_spec in req.vms.items(): if vm_spec.vm_id is None: - raise HTTPException(status_code=400, detail=f"missing key vm_id for {vm_name}") + raise HTTPException( + status_code=400, detail=f"missing key vm_id for {vm_name}" + ) if vm_spec.vm_description is None: - raise HTTPException(status_code=400, detail=f"missing key vm_description for {vm_name}") + raise HTTPException( + status_code=400, detail=f"missing key vm_description for {vm_name}" + ) if vm_spec.vm_ip is None: - raise HTTPException(status_code=400, detail=f"missing key vm_ip for {vm_name}") + raise HTTPException( + status_code=400, detail=f"missing key vm_ip for {vm_name}" + ) -@router.post(path="/core/proxmox/configure/default/create-vms-admin", summary="Create default admin VMs", description="Create the default set of admin virtual machines for initial configuration in Proxmox", tags=["bundles - core - proxmox - vms - default-configuration - admin"], response_model=Reply_BundlesCoreProxmoxConfigureDefaultVms_CreateAdminVms) -def bundles_proxmox_create_vms_admin(req: Request_BundlesCoreProxmoxConfigureDefaultVms_CreateAdminVms): +@router.post( + path="/core/proxmox/configure/default/create-vms-admin", + summary="Create default admin VMs", + description="Create the default set of admin virtual machines for initial configuration in Proxmox", + tags=["bundles - core - proxmox - vms - default-configuration - admin"], + response_model=Reply_BundlesCoreProxmoxConfigureDefaultVms_CreateAdminVms, +) +def bundles_proxmox_create_vms_admin( + req: Request_BundlesCoreProxmoxConfigureDefaultVms_CreateAdminVms, +): """Create the default set of admin VMs on Proxmox. :param req: Request body with ``proxmox_node`` and ``vms`` dict. :returns: JSON with ``rc`` and ``log_multiline``. """ - return _run_create_vms_bundle(req, "core/proxmox/configure/default/vms/create-vms-admin", _check_admin_vms) + return _run_create_vms_bundle( + req, "core/proxmox/configure/default/vms/create-vms-admin", _check_admin_vms + ) -@router.post(path="/core/proxmox/configure/default/create-vms-vuln", summary="Create default vulnerable VMs", description="Create the default set of vulnerable virtual machines for initial configuration in Proxmox", tags=["bundles - core - proxmox - vms - default-configuration - vuln"], response_model=Reply_BundlesCoreProxmoxConfigureDefaultVms_CreateVulnVms) -def bundles_proxmox_create_vms_vuln(req: Request_BundlesCoreProxmoxConfigureDefaultVms_CreateVulnVms): +@router.post( + path="/core/proxmox/configure/default/create-vms-vuln", + summary="Create default vulnerable VMs", + description="Create the default set of vulnerable virtual machines for initial configuration in Proxmox", + tags=["bundles - core - proxmox - vms - default-configuration - vuln"], + response_model=Reply_BundlesCoreProxmoxConfigureDefaultVms_CreateVulnVms, +) +def bundles_proxmox_create_vms_vuln( + req: Request_BundlesCoreProxmoxConfigureDefaultVms_CreateVulnVms, +): """Create the default set of vulnerable VMs on Proxmox. :param req: Request body with ``proxmox_node`` and ``vms`` dict. :returns: JSON with ``rc`` and ``log_multiline``. """ - return _run_create_vms_bundle(req, "core/proxmox/configure/default/vms/create-vms-vuln", _check_vuln_vms) + return _run_create_vms_bundle( + req, "core/proxmox/configure/default/vms/create-vms-vuln", _check_vuln_vms + ) -@router.post(path="/core/proxmox/configure/default/create-vms-student", summary="Create default student VMs", description="Create the default set of student virtual machines for initial configuration in Proxmox", tags=["bundles - core - proxmox - vms - default-configuration - student"], response_model=Reply_BundlesCoreProxmoxConfigureDefaultVms_CreateStudentVms) -def bundles_proxmox_create_vms_student(req: Request_BundlesCoreProxmoxConfigureDefaultVms_CreateStudentVms): +@router.post( + path="/core/proxmox/configure/default/create-vms-student", + summary="Create default student VMs", + description="Create the default set of student virtual machines for initial configuration in Proxmox", + tags=["bundles - core - proxmox - vms - default-configuration - student"], + response_model=Reply_BundlesCoreProxmoxConfigureDefaultVms_CreateStudentVms, +) +def bundles_proxmox_create_vms_student( + req: Request_BundlesCoreProxmoxConfigureDefaultVms_CreateStudentVms, +): """Create the default set of student VMs on Proxmox. :param req: Request body with ``proxmox_node`` and ``vms`` dict. :returns: JSON with ``rc`` and ``log_multiline``. """ - return _run_create_vms_bundle(req, "core/proxmox/configure/default/vms/create-vms-student", _check_student_vms) + return _run_create_vms_bundle( + req, "core/proxmox/configure/default/vms/create-vms-student", _check_student_vms + ) # =========================================================================== @@ -394,18 +555,28 @@ def bundles_proxmox_create_vms_student(req: Request_BundlesCoreProxmoxConfigureD } for _role in ("admin", "vuln", "student"): - for _action, _verb in [("start", "vm_start"), ("stop", "vm_stop"), ("pause", "vm_pause"), ("resume", "vm_resume")]: + for _action, _verb in [ + ("start", "vm_start"), + ("stop", "vm_stop"), + ("pause", "vm_pause"), + ("resume", "vm_resume"), + ]: _path = f"/core/proxmox/configure/default/{_action}-vms-{_role}" _tag = f"bundles - core - proxmox - vms - default-configuration - {_role}" _action_name = _SSPR[_role] def _make_handler(_an=_action_name, _v=_verb): - def handler(req: Request_BundlesCoreProxmoxConfigureDefaultVms_StartStopPauseResumeAdminVulnStudentVms): + def handler( + req: Request_BundlesCoreProxmoxConfigureDefaultVms_StartStopPauseResumeAdminVulnStudentVms, + ): return _run_start_stop_bundle(req, _an, _v) + return handler router.add_api_route( - _path, _make_handler(), methods=["POST"], + _path, + _make_handler(), + methods=["POST"], summary=f"{_action.capitalize()} {_role} vms ", description=f"{_action.capitalize()} all {_role} virtual machines", tags=[_tag], @@ -428,12 +599,17 @@ def handler(req: Request_BundlesCoreProxmoxConfigureDefaultVms_StartStopPauseRes _tag = f"bundles - core - proxmox - vms - default-configuration - {_role}" def _make_delete_handler(_an=_an): - def handler(req: Request_BundlesCoreProxmoxConfigureDefaultVms_StartStopPauseResumeAdminVulnStudentVms): + def handler( + req: Request_BundlesCoreProxmoxConfigureDefaultVms_StartStopPauseResumeAdminVulnStudentVms, + ): return _run_delete_bundle(req, _an) + return handler router.add_api_route( - _path, _make_delete_handler(), methods=["DELETE"], + _path, + _make_delete_handler(), + methods=["DELETE"], summary=f"Delete {_role} vms ", description=f"Delete all {_role} virtual machines", tags=[_tag], @@ -456,12 +632,17 @@ def handler(req: Request_BundlesCoreProxmoxConfigureDefaultVms_StartStopPauseRes _tag = f"bundles - core - proxmox - vms - default-configuration - {_role}" def _make_snap_create_handler(_an=_an): - def handler(req: Request_BundlesCoreProxmoxConfigureDefaultVms_StartStopPauseResumeAdminVulnStudentVms): + def handler( + req: Request_BundlesCoreProxmoxConfigureDefaultVms_StartStopPauseResumeAdminVulnStudentVms, + ): return _run_snapshot_bundle(req, _an, "snapshot_vm_create") + return handler router.add_api_route( - _path, _make_snap_create_handler(), methods=["POST"], + _path, + _make_snap_create_handler(), + methods=["POST"], summary=f"Snapshot {_role} vms ", description=f"Snapshot all {_role} virtual machines", tags=[_tag], @@ -484,12 +665,17 @@ def handler(req: Request_BundlesCoreProxmoxConfigureDefaultVms_StartStopPauseRes _tag = f"bundles - core - proxmox - vms - default-configuration - {_role}" def _make_snap_revert_handler(_an=_an): - def handler(req: Request_BundlesCoreProxmoxConfigureDefaultVms_RevertSnapshotAdminVulnStudentVms): + def handler( + req: Request_BundlesCoreProxmoxConfigureDefaultVms_RevertSnapshotAdminVulnStudentVms, + ): return _run_snapshot_bundle(req, _an, "snapshot_vm_revert") + return handler router.add_api_route( - _path, _make_snap_revert_handler(), methods=["POST"], + _path, + _make_snap_revert_handler(), + methods=["POST"], summary=f"Snapshot {_role} vms ", description=f"Snapshot all {_role} virtual machines", tags=[_tag], diff --git a/app/routes/debug.py b/app/routes/debug.py index 61b59b1..70f7b58 100644 --- a/app/routes/debug.py +++ b/app/routes/debug.py @@ -9,14 +9,13 @@ import logging import os from pathlib import Path -from typing import Any from fastapi import APIRouter, HTTPException from fastapi.responses import JSONResponse from app.core.runner import run_playbook_core from app.schemas.debug import Request_DebugPing -from app.utils.vm_id_name_resolver import * +from app.utils.vm_id_name_resolver import resolv_id_to_vm_name logger = logging.getLogger(__name__) @@ -40,9 +39,13 @@ def debug_ping(req: Request_DebugPing): :returns: JSON with ``rc`` and ``log_multiline``. """ if not PLAYBOOK_SRC.exists(): - raise HTTPException(status_code=400, detail=f":: MISSING PLAYBOOK : {PLAYBOOK_SRC}") + raise HTTPException( + status_code=400, detail=f":: MISSING PLAYBOOK : {PLAYBOOK_SRC}" + ) if not INVENTORY_SRC.exists(): - raise HTTPException(status_code=400, detail=f":: MISSING INVENTORY : {INVENTORY_SRC}") + raise HTTPException( + status_code=400, detail=f":: MISSING INVENTORY : {INVENTORY_SRC}" + ) extravars = {} if req.proxmox_node: @@ -51,7 +54,10 @@ def debug_ping(req: Request_DebugPing): extravars = None rc, events, log_plain, log_ansi = run_playbook_core( - PLAYBOOK_SRC, INVENTORY_SRC, limit=req.hosts, extravars=extravars, + PLAYBOOK_SRC, + INVENTORY_SRC, + limit=req.hosts, + extravars=extravars, ) payload = {"rc": rc, "log_multiline": log_plain.splitlines()} @@ -70,9 +76,13 @@ def debug_func_test(): :returns: None (debug only). """ if not PLAYBOOK_SRC.exists(): - raise HTTPException(status_code=400, detail=f":: MISSING PLAYBOOK : {PLAYBOOK_SRC}") + raise HTTPException( + status_code=400, detail=f":: MISSING PLAYBOOK : {PLAYBOOK_SRC}" + ) if not INVENTORY_SRC.exists(): - raise HTTPException(status_code=400, detail=f":: MISSING INVENTORY : {INVENTORY_SRC}") + raise HTTPException( + status_code=400, detail=f":: MISSING INVENTORY : {INVENTORY_SRC}" + ) out = resolv_id_to_vm_name("px-testing", 1000) logger.debug("GOT: %s", out["vm_name"]) diff --git a/app/routes/firewall.py b/app/routes/firewall.py index 43d0eb0..57f77fb 100644 --- a/app/routes/firewall.py +++ b/app/routes/firewall.py @@ -23,25 +23,35 @@ from fastapi import APIRouter, HTTPException from fastapi.responses import JSONResponse -from app.core.runner import run_playbook_core -from app.core.extractor import extract_action_results -from app.utils.vm_id_name_resolver import resolv_id_to_vm_name from app import utils - +from app.core.extractor import extract_action_results +from app.core.runner import run_playbook_core from app.schemas.firewall import ( - Request_ProxmoxFirewall_ListIptablesAlias, Reply_ProxmoxFirewallWithStorageName_ListIptablesAlias, - Request_ProxmoxFirewall_AddIptablesAlias, Reply_ProxmoxFirewallWithStorageName_AddIptablesAlias, - Request_ProxmoxFirewall_DeleteIptablesAlias, Reply_ProxmoxFirewallWithStorageName_DeleteIptablesAlias, - Request_ProxmoxFirewall_ListIptablesRules, Reply_ProxmoxFirewallWithStorageName_ListIptablesRules, - Request_ProxmoxFirewall_ApplyIptablesRules, Reply_ProxmoxFirewallWithStorageName_ApplyIptablesRules, + Reply_ProxmoxFirewallWithStorageName_AddIptablesAlias, + Reply_ProxmoxFirewallWithStorageName_ApplyIptablesRules, + Reply_ProxmoxFirewallWithStorageName_DeleteIptablesAlias, + Reply_ProxmoxFirewallWithStorageName_DisableFirewallDc, + Reply_ProxmoxFirewallWithStorageName_DisableFirewallNode, + Reply_ProxmoxFirewallWithStorageName_DisableFirewallVm, + Reply_ProxmoxFirewallWithStorageName_EnableFirewallDc, + Reply_ProxmoxFirewallWithStorageName_EnableFirewallNode, + Reply_ProxmoxFirewallWithStorageName_EnableFirewallVm, + Reply_ProxmoxFirewallWithStorageName_ListIptablesAlias, + Reply_ProxmoxFirewallWithStorageName_ListIptablesRules, + Request_ProxmoxFirewall_AddIptablesAlias, + Request_ProxmoxFirewall_ApplyIptablesRules, + Request_ProxmoxFirewall_DeleteIptablesAlias, Request_ProxmoxFirewall_DeleteIptablesRule, - Request_ProxmoxFirewall_EnableFirewallVm, Reply_ProxmoxFirewallWithStorageName_EnableFirewallVm, - Request_ProxmoxFirewall_DistableFirewallVm, Reply_ProxmoxFirewallWithStorageName_DisableFirewallVm, - Request_ProxmoxFirewall_EnableFirewallNode, Reply_ProxmoxFirewallWithStorageName_EnableFirewallNode, - Request_ProxmoxFirewall_DistableFirewallNode, Reply_ProxmoxFirewallWithStorageName_DisableFirewallNode, - Request_ProxmoxFirewall_EnableFirewallDc, Reply_ProxmoxFirewallWithStorageName_EnableFirewallDc, - Request_ProxmoxFirewall_DisableFirewallDc, Reply_ProxmoxFirewallWithStorageName_DisableFirewallDc, + Request_ProxmoxFirewall_DisableFirewallDc, + Request_ProxmoxFirewall_DistableFirewallNode, + Request_ProxmoxFirewall_DistableFirewallVm, + Request_ProxmoxFirewall_EnableFirewallDc, + Request_ProxmoxFirewall_EnableFirewallNode, + Request_ProxmoxFirewall_EnableFirewallVm, + Request_ProxmoxFirewall_ListIptablesAlias, + Request_ProxmoxFirewall_ListIptablesRules, ) +from app.utils.vm_id_name_resolver import resolv_id_to_vm_name logger = logging.getLogger(__name__) @@ -56,9 +66,13 @@ def _run_fw(req, action: str, extravars: dict) -> JSONResponse: extravars["proxmox_vm_action"] = action extravars["hosts"] = "proxmox" if not PLAYBOOK_SRC.exists(): - raise HTTPException(status_code=400, detail=f":: err - MISSING PLAYBOOK : {PLAYBOOK_SRC}") + raise HTTPException( + status_code=400, detail=f":: err - MISSING PLAYBOOK : {PLAYBOOK_SRC}" + ) inventory = utils.resolve_inventory(INVENTORY_NAME) - rc, events, log_plain, _ = run_playbook_core(PLAYBOOK_SRC, inventory, limit=extravars["hosts"], extravars=extravars) + rc, events, log_plain, _ = run_playbook_core( + PLAYBOOK_SRC, inventory, limit=extravars["hosts"], extravars=extravars + ) if req.as_json: payload = {"rc": rc, "result": extract_action_results(events, action)} else: @@ -68,7 +82,15 @@ def _run_fw(req, action: str, extravars: dict) -> JSONResponse: # --- Alias routes --- -@router.post(path="/vm/alias/list", summary="List VM firewall aliases", description="List firewall aliases for a specific virtual machine", tags=["proxmox - firewall"], response_model=Reply_ProxmoxFirewallWithStorageName_ListIptablesAlias, response_description="Details of the VM firewall aliases") + +@router.post( + path="/vm/alias/list", + summary="List VM firewall aliases", + description="List firewall aliases for a specific virtual machine", + tags=["proxmox - firewall"], + response_model=Reply_ProxmoxFirewallWithStorageName_ListIptablesAlias, + response_description="Details of the VM firewall aliases", +) def proxmox_vm_alias_list(req: Request_ProxmoxFirewall_ListIptablesAlias): """List firewall aliases for a VM. @@ -81,7 +103,14 @@ def proxmox_vm_alias_list(req: Request_ProxmoxFirewall_ListIptablesAlias): return _run_fw(req, "firewall_vm_list_iptables_alias", extravars) -@router.post(path="/vm/alias/add", summary="Add a firewall alias", description="Add a new alias to the Proxmox firewall - IPs, subnets/networks, hostnames", tags=["proxmox - firewall"], response_model=Reply_ProxmoxFirewallWithStorageName_AddIptablesAlias, response_description="Information about the created firewall alias") +@router.post( + path="/vm/alias/add", + summary="Add a firewall alias", + description="Add a new alias to the Proxmox firewall - IPs, subnets/networks, hostnames", + tags=["proxmox - firewall"], + response_model=Reply_ProxmoxFirewallWithStorageName_AddIptablesAlias, + response_description="Information about the created firewall alias", +) def proxmox_firewall_vm_alias_add(req: Request_ProxmoxFirewall_AddIptablesAlias): """Add a firewall alias (IP, subnet, or hostname) for a VM. @@ -100,7 +129,14 @@ def proxmox_firewall_vm_alias_add(req: Request_ProxmoxFirewall_AddIptablesAlias) return _run_fw(req, "firewall_vm_add_iptables_alias", extravars) -@router.delete(path="/vm/alias/delete", summary="Delete a firewall alias", description="Remove an existing alias from the proxmox firewall", tags=["proxmox - firewall"], response_model=Reply_ProxmoxFirewallWithStorageName_DeleteIptablesAlias, response_description="Details of the deleted firewall alias") +@router.delete( + path="/vm/alias/delete", + summary="Delete a firewall alias", + description="Remove an existing alias from the proxmox firewall", + tags=["proxmox - firewall"], + response_model=Reply_ProxmoxFirewallWithStorageName_DeleteIptablesAlias, + response_description="Details of the deleted firewall alias", +) def proxmox_firewall_vm_alias_delete(req: Request_ProxmoxFirewall_DeleteIptablesAlias): """Delete a firewall alias from a VM. @@ -117,7 +153,15 @@ def proxmox_firewall_vm_alias_delete(req: Request_ProxmoxFirewall_DeleteIptables # --- Rules routes --- -@router.post(path="/vm/rules/list", summary="List VM firewall rules", description="List firewall rules for a specific virtual machine", tags=["proxmox - firewall"], response_model=Reply_ProxmoxFirewallWithStorageName_ListIptablesRules, response_description="Details of the VM firewall rules") + +@router.post( + path="/vm/rules/list", + summary="List VM firewall rules", + description="List firewall rules for a specific virtual machine", + tags=["proxmox - firewall"], + response_model=Reply_ProxmoxFirewallWithStorageName_ListIptablesRules, + response_description="Details of the VM firewall rules", +) def proxmox_vm_rules_list(req: Request_ProxmoxFirewall_ListIptablesRules): """List firewall rules for a VM. @@ -130,7 +174,14 @@ def proxmox_vm_rules_list(req: Request_ProxmoxFirewall_ListIptablesRules): return _run_fw(req, "firewall_vm_list_iptables_rule", extravars) -@router.post(path="/vm/rules/apply", summary="Apply firewall rules", description="Apply the received firewall rules to the proxmox firewall", tags=["proxmox - firewall"], response_model=Reply_ProxmoxFirewallWithStorageName_ApplyIptablesRules, response_description="Details of the applied firewall rules") +@router.post( + path="/vm/rules/apply", + summary="Apply firewall rules", + description="Apply the received firewall rules to the proxmox firewall", + tags=["proxmox - firewall"], + response_model=Reply_ProxmoxFirewallWithStorageName_ApplyIptablesRules, + response_description="Details of the applied firewall rules", +) def proxmox_firewall_vm_rules_add(req: Request_ProxmoxFirewall_ApplyIptablesRules): """Apply firewall rules to a VM. @@ -140,14 +191,34 @@ def proxmox_firewall_vm_rules_add(req: Request_ProxmoxFirewall_ApplyIptablesRule extravars = {"proxmox_node": req.proxmox_node} if req.vm_id is not None: extravars["vm_id"] = req.vm_id - for field in ("vm_fw_action", "vm_fw_dport", "vm_fw_enable", "vm_fw_proto", "vm_fw_type", "vm_fw_log", "vm_fw_iface", "vm_fw_source", "vm_fw_dest", "vm_fw_sport", "vm_fw_comment", "vm_fw_pos"): + for field in ( + "vm_fw_action", + "vm_fw_dport", + "vm_fw_enable", + "vm_fw_proto", + "vm_fw_type", + "vm_fw_log", + "vm_fw_iface", + "vm_fw_source", + "vm_fw_dest", + "vm_fw_sport", + "vm_fw_comment", + "vm_fw_pos", + ): val = getattr(req, field, None) if val is not None: extravars[field] = val return _run_fw(req, "firewall_vm_apply_iptables_rule", extravars) -@router.delete(path="/vm/rules/delete", summary="Delete a firewall rule", description="Remove an existing rule from the proxmox firewall configuration", tags=["proxmox - firewall"], response_model=Request_ProxmoxFirewall_DeleteIptablesRule, response_description="Details of the deleted firewall rule.") +@router.delete( + path="/vm/rules/delete", + summary="Delete a firewall rule", + description="Remove an existing rule from the proxmox firewall configuration", + tags=["proxmox - firewall"], + response_model=Request_ProxmoxFirewall_DeleteIptablesRule, + response_description="Details of the deleted firewall rule.", +) def proxmox_firewall_vm_rules_delete(req: Request_ProxmoxFirewall_DeleteIptablesRule): """Delete a firewall rule from a VM by position. @@ -164,7 +235,15 @@ def proxmox_firewall_vm_rules_delete(req: Request_ProxmoxFirewall_DeleteIptables # --- VM enable/disable --- -@router.post(path="/vm/enable", summary="Enable VM firewall", description="Enable the proxmox firewall for a specific virtual machine", tags=["proxmox - firewall"], response_model=Reply_ProxmoxFirewallWithStorageName_EnableFirewallVm, response_description="Details of the enabled VM firewall") + +@router.post( + path="/vm/enable", + summary="Enable VM firewall", + description="Enable the proxmox firewall for a specific virtual machine", + tags=["proxmox - firewall"], + response_model=Reply_ProxmoxFirewallWithStorageName_EnableFirewallVm, + response_description="Details of the enabled VM firewall", +) def proxmox_firewall_vm_enable(req: Request_ProxmoxFirewall_EnableFirewallVm): """Enable the firewall on a VM. @@ -174,11 +253,20 @@ def proxmox_firewall_vm_enable(req: Request_ProxmoxFirewall_EnableFirewallVm): extravars = {"proxmox_node": req.proxmox_node} if req.vm_id: extravars["vm_id"] = req.vm_id - extravars["vm_name"] = resolv_id_to_vm_name(extravars["proxmox_node"], extravars["vm_id"]) + extravars["vm_name"] = resolv_id_to_vm_name( + extravars["proxmox_node"], extravars["vm_id"] + ) return _run_fw(req, "firewall_vm_enable", extravars) -@router.post(path="/vm/disable", summary="Disable VM firewall", description="Disable the proxmox firewall for a specific virtual machine", tags=["proxmox - firewall"], response_model=Reply_ProxmoxFirewallWithStorageName_DisableFirewallVm, response_description="Details of the disabled VM firewall") +@router.post( + path="/vm/disable", + summary="Disable VM firewall", + description="Disable the proxmox firewall for a specific virtual machine", + tags=["proxmox - firewall"], + response_model=Reply_ProxmoxFirewallWithStorageName_DisableFirewallVm, + response_description="Details of the disabled VM firewall", +) def proxmox_firewall_vm_disable(req: Request_ProxmoxFirewall_DistableFirewallVm): """Disable the firewall on a VM. @@ -188,13 +276,23 @@ def proxmox_firewall_vm_disable(req: Request_ProxmoxFirewall_DistableFirewallVm) extravars = {"proxmox_node": req.proxmox_node} if req.vm_id: extravars["vm_id"] = req.vm_id - extravars["vm_name"] = resolv_id_to_vm_name(extravars["proxmox_node"], extravars["vm_id"]) + extravars["vm_name"] = resolv_id_to_vm_name( + extravars["proxmox_node"], extravars["vm_id"] + ) return _run_fw(req, "firewall_vm_disable", extravars) # --- Node enable/disable --- -@router.post(path="/node/enable", summary="Enable node firewall", description="Enable the proxmox firewall on a specific node", tags=["proxmox - firewall"], response_model=Reply_ProxmoxFirewallWithStorageName_EnableFirewallNode, response_description="Details of the enabled node firewall") + +@router.post( + path="/node/enable", + summary="Enable node firewall", + description="Enable the proxmox firewall on a specific node", + tags=["proxmox - firewall"], + response_model=Reply_ProxmoxFirewallWithStorageName_EnableFirewallNode, + response_description="Details of the enabled node firewall", +) def proxmox_firewall_node_enable(req: Request_ProxmoxFirewall_EnableFirewallNode): """Enable the firewall on a Proxmox node. @@ -205,7 +303,14 @@ def proxmox_firewall_node_enable(req: Request_ProxmoxFirewall_EnableFirewallNode return _run_fw(req, "firewall_node_enable", extravars) -@router.post(path="/node/disable", summary="Disable node firewall", description="Disable the proxmox firewall on a specific node", tags=["proxmox - firewall"], response_model=Reply_ProxmoxFirewallWithStorageName_DisableFirewallNode, response_description="Details of the disabled node firewall") +@router.post( + path="/node/disable", + summary="Disable node firewall", + description="Disable the proxmox firewall on a specific node", + tags=["proxmox - firewall"], + response_model=Reply_ProxmoxFirewallWithStorageName_DisableFirewallNode, + response_description="Details of the disabled node firewall", +) def proxmox_firewall_node_disable(req: Request_ProxmoxFirewall_DistableFirewallNode): """Disable the firewall on a Proxmox node. @@ -218,7 +323,15 @@ def proxmox_firewall_node_disable(req: Request_ProxmoxFirewall_DistableFirewallN # --- Datacenter enable/disable --- -@router.post(path="/datacenter/enable", summary="Enable datacenter firewall", description="Enable the proxmox firewall at the datacenter level", tags=["proxmox - firewall"], response_model=Reply_ProxmoxFirewallWithStorageName_EnableFirewallDc, response_description="Details of the enabled datacenter firewall") + +@router.post( + path="/datacenter/enable", + summary="Enable datacenter firewall", + description="Enable the proxmox firewall at the datacenter level", + tags=["proxmox - firewall"], + response_model=Reply_ProxmoxFirewallWithStorageName_EnableFirewallDc, + response_description="Details of the enabled datacenter firewall", +) def proxmox_firewall_dc_enable(req: Request_ProxmoxFirewall_EnableFirewallDc): """Enable the firewall at the datacenter level. @@ -231,7 +344,14 @@ def proxmox_firewall_dc_enable(req: Request_ProxmoxFirewall_EnableFirewallDc): return _run_fw(req, "firewall_dc_enable", extravars) -@router.post(path="/datacenter/disable", summary="Disable datacenter firewall", description="Disable the proxmox firewall at the datacenter level", tags=["proxmox - firewall"], response_model=Reply_ProxmoxFirewallWithStorageName_DisableFirewallDc, response_description="Details of the disabled datacenter firewall") +@router.post( + path="/datacenter/disable", + summary="Disable datacenter firewall", + description="Disable the proxmox firewall at the datacenter level", + tags=["proxmox - firewall"], + response_model=Reply_ProxmoxFirewallWithStorageName_DisableFirewallDc, + response_description="Details of the disabled datacenter firewall", +) def proxmox_firewall_dc_disable(req: Request_ProxmoxFirewall_DisableFirewallDc): """Disable the firewall at the datacenter level. diff --git a/app/routes/network.py b/app/routes/network.py index df1d2cc..b6576eb 100644 --- a/app/routes/network.py +++ b/app/routes/network.py @@ -17,17 +17,22 @@ from fastapi import APIRouter, HTTPException from fastapi.responses import JSONResponse -from app.core.runner import run_playbook_core -from app.core.extractor import extract_action_results from app import utils - +from app.core.extractor import extract_action_results +from app.core.runner import run_playbook_core from app.schemas.network import ( - Request_ProxmoxNetwork_WithVmId_AddNetwork, Reply_ProxmoxNetwork_WithVmId_AddNetworkInterface, - Request_ProxmoxNetwork_WithVmId_DeleteNetwork, Reply_ProxmoxNetwork_WithVmId_DeleteNetworkInterface, - Request_ProxmoxNetwork_WithVmId_ListNetwork, Reply_ProxmoxNetwork_WithVmId_ListNetworkInterface, - Request_ProxmoxNetwork_WithNodeName_AddNetworkInterface, Reply_ProxmoxNetwork_WithNodeName_AddNetworkInterface, - Request_ProxmoxNetwork_WithNodeName_DeleteInterface, Reply_ProxmoxNetwork_WithNodeName_DeleteInterface, - Request_ProxmoxNetwork_WithNodeName_ListInterface, Reply_ProxmoxNetwork_WithNodeName_ListInterface, + Reply_ProxmoxNetwork_WithNodeName_AddNetworkInterface, + Reply_ProxmoxNetwork_WithNodeName_DeleteInterface, + Reply_ProxmoxNetwork_WithNodeName_ListInterface, + Reply_ProxmoxNetwork_WithVmId_AddNetworkInterface, + Reply_ProxmoxNetwork_WithVmId_DeleteNetworkInterface, + Reply_ProxmoxNetwork_WithVmId_ListNetworkInterface, + Request_ProxmoxNetwork_WithNodeName_AddNetworkInterface, + Request_ProxmoxNetwork_WithNodeName_DeleteInterface, + Request_ProxmoxNetwork_WithNodeName_ListInterface, + Request_ProxmoxNetwork_WithVmId_AddNetwork, + Request_ProxmoxNetwork_WithVmId_DeleteNetwork, + Request_ProxmoxNetwork_WithVmId_ListNetwork, ) logger = logging.getLogger(__name__) @@ -43,9 +48,13 @@ def _run_net(req, action: str, extravars: dict) -> JSONResponse: extravars["proxmox_vm_action"] = action extravars["hosts"] = "proxmox" if not PLAYBOOK_SRC.exists(): - raise HTTPException(status_code=400, detail=f":: err - MISSING PLAYBOOK : {PLAYBOOK_SRC}") + raise HTTPException( + status_code=400, detail=f":: err - MISSING PLAYBOOK : {PLAYBOOK_SRC}" + ) inventory = utils.resolve_inventory(INVENTORY_NAME) - rc, events, log_plain, _ = run_playbook_core(PLAYBOOK_SRC, inventory, limit=extravars["hosts"], extravars=extravars) + rc, events, log_plain, _ = run_playbook_core( + PLAYBOOK_SRC, inventory, limit=extravars["hosts"], extravars=extravars + ) if req.as_json: payload = {"rc": rc, "result": extract_action_results(events, action)} else: @@ -55,7 +64,15 @@ def _run_net(req, action: str, extravars: dict) -> JSONResponse: # --- VM network routes --- -@router.post(path="/vm/add", summary="Add VM network interface", description="Create and attach a new network interface to a Proxmox VM.", tags=["proxmox - network - vm"], response_model=Reply_ProxmoxNetwork_WithVmId_AddNetworkInterface, response_description="Information about the added network interface.") + +@router.post( + path="/vm/add", + summary="Add VM network interface", + description="Create and attach a new network interface to a Proxmox VM.", + tags=["proxmox - network - vm"], + response_model=Reply_ProxmoxNetwork_WithVmId_AddNetworkInterface, + response_description="Information about the added network interface.", +) def proxmox_network_vm_add_interface(req: Request_ProxmoxNetwork_WithVmId_AddNetwork): """Create and attach a network interface to a VM. @@ -65,15 +82,36 @@ def proxmox_network_vm_add_interface(req: Request_ProxmoxNetwork_WithVmId_AddNet extravars = {"proxmox_node": req.proxmox_node} if req.vm_id is not None: extravars["vm_id"] = req.vm_id - for field in ("iface_model", "iface_bridge", "vm_vmnet_id", "iface_trunks", "iface_tag", "iface_rate", "iface_queues", "iface_mtu", "iface_macaddr", "iface_link_down", "iface_firewall"): + for field in ( + "iface_model", + "iface_bridge", + "vm_vmnet_id", + "iface_trunks", + "iface_tag", + "iface_rate", + "iface_queues", + "iface_mtu", + "iface_macaddr", + "iface_link_down", + "iface_firewall", + ): val = getattr(req, field, None) if val is not None: extravars[field] = val return _run_net(req, "network_add_interfaces_vm", extravars) -@router.post(path="/vm/delete", summary="Delete VM network interface", description="Remove a network interface from a Proxmox VM.", tags=["proxmox - network - vm"], response_model=Reply_ProxmoxNetwork_WithVmId_DeleteNetworkInterface, response_description="Information about the deleted network interface.") -def proxmox_network_vm_delete_interface(req: Request_ProxmoxNetwork_WithVmId_DeleteNetwork): +@router.post( + path="/vm/delete", + summary="Delete VM network interface", + description="Remove a network interface from a Proxmox VM.", + tags=["proxmox - network - vm"], + response_model=Reply_ProxmoxNetwork_WithVmId_DeleteNetworkInterface, + response_description="Information about the deleted network interface.", +) +def proxmox_network_vm_delete_interface( + req: Request_ProxmoxNetwork_WithVmId_DeleteNetwork, +): """Remove a network interface from a VM. :param req: Request body with node, VM ID, and network interface ID. @@ -87,7 +125,14 @@ def proxmox_network_vm_delete_interface(req: Request_ProxmoxNetwork_WithVmId_Del return _run_net(req, "network_delete_interfaces_vm", extravars) -@router.post(path="/vm/list", summary="List VM network interfaces", description="Retrieve all network interfaces attached to a Proxmox VM.", tags=["proxmox - network - vm"], response_model=Reply_ProxmoxNetwork_WithVmId_ListNetworkInterface, response_description="List of VM network interfaces.") +@router.post( + path="/vm/list", + summary="List VM network interfaces", + description="Retrieve all network interfaces attached to a Proxmox VM.", + tags=["proxmox - network - vm"], + response_model=Reply_ProxmoxNetwork_WithVmId_ListNetworkInterface, + response_description="List of VM network interfaces.", +) def proxmox_network_vm_list_interface(req: Request_ProxmoxNetwork_WithVmId_ListNetwork): """List all network interfaces attached to a VM. @@ -102,23 +147,51 @@ def proxmox_network_vm_list_interface(req: Request_ProxmoxNetwork_WithVmId_ListN # --- Node network routes --- -@router.post(path="/node/add", summary="Add node network interface", description="Create and attach a new network interface to a Proxmox node.", tags=["proxmox - network - node"], response_model=Reply_ProxmoxNetwork_WithNodeName_AddNetworkInterface, response_description="Information about the added network interface.") -def proxmox_network_node_add_interface(req: Request_ProxmoxNetwork_WithNodeName_AddNetworkInterface): + +@router.post( + path="/node/add", + summary="Add node network interface", + description="Create and attach a new network interface to a Proxmox node.", + tags=["proxmox - network - node"], + response_model=Reply_ProxmoxNetwork_WithNodeName_AddNetworkInterface, + response_description="Information about the added network interface.", +) +def proxmox_network_node_add_interface( + req: Request_ProxmoxNetwork_WithNodeName_AddNetworkInterface, +): """Create and attach a network interface to a Proxmox node. :param req: Request body with node and interface configuration. :returns: JSON with ``rc`` and either ``result`` or ``log_multiline``. """ extravars = {"proxmox_node": req.proxmox_node} - for field in ("bridge_ports", "iface_name", "iface_type", "iface_autostart", "ip_address", "ip_netmask", "ip_gateway", "ovs_bridge"): + for field in ( + "bridge_ports", + "iface_name", + "iface_type", + "iface_autostart", + "ip_address", + "ip_netmask", + "ip_gateway", + "ovs_bridge", + ): val = getattr(req, field, None) if val is not None: extravars[field] = val return _run_net(req, "network_add_interfaces_node", extravars) -@router.post(path="/node/delete", summary="Delete node network interface", description="Remove a network interface from a Proxmox node.", tags=["proxmox - network - node"], response_model=Reply_ProxmoxNetwork_WithNodeName_DeleteInterface, response_description="Information about the deleted network interface.") -def proxmox_network_node_delete_interface(req: Request_ProxmoxNetwork_WithNodeName_DeleteInterface): +@router.post( + path="/node/delete", + summary="Delete node network interface", + description="Remove a network interface from a Proxmox node.", + tags=["proxmox - network - node"], + response_model=Reply_ProxmoxNetwork_WithNodeName_DeleteInterface, + response_description="Information about the deleted network interface.", +) +def proxmox_network_node_delete_interface( + req: Request_ProxmoxNetwork_WithNodeName_DeleteInterface, +): """Remove a network interface from a Proxmox node. :param req: Request body with node and interface name. @@ -131,8 +204,17 @@ def proxmox_network_node_delete_interface(req: Request_ProxmoxNetwork_WithNodeNa # NOTE: original path has double slash "//node/list" - preserving exactly -@router.post(path="//node/list", summary="List node network interfaces", description="Retrieve all network interfaces configured on a Proxmox node.", tags=["proxmox - network - node"], response_model=Reply_ProxmoxNetwork_WithNodeName_ListInterface, response_description="List of node network interfaces.") -def proxmox_network_node_list_interface(req: Request_ProxmoxNetwork_WithNodeName_ListInterface): +@router.post( + path="//node/list", + summary="List node network interfaces", + description="Retrieve all network interfaces configured on a Proxmox node.", + tags=["proxmox - network - node"], + response_model=Reply_ProxmoxNetwork_WithNodeName_ListInterface, + response_description="List of node network interfaces.", +) +def proxmox_network_node_list_interface( + req: Request_ProxmoxNetwork_WithNodeName_ListInterface, +): """List all network interfaces on a Proxmox node. :param req: Request body with ``proxmox_node``. diff --git a/app/routes/runner.py b/app/routes/runner.py index 1669655..dd8fb90 100644 --- a/app/routes/runner.py +++ b/app/routes/runner.py @@ -9,14 +9,13 @@ import logging import os from pathlib import Path -from typing import Any from fastapi import APIRouter from fastapi.responses import JSONResponse +from app import utils from app.core.runner import run_playbook_core from app.schemas.debug import Request_DebugPing -from app import utils logger = logging.getLogger(__name__) @@ -37,7 +36,10 @@ def _run_generic(req, name: str, resolver_fn) -> JSONResponse: extravars = None rc, events, log_plain, _ = run_playbook_core( - checked_playbook, checked_inventory, limit=req.hosts, extravars=extravars, + checked_playbook, + checked_inventory, + limit=req.hosts, + extravars=extravars, ) payload = {"rc": rc, "log_multiline": log_plain.splitlines()} diff --git a/app/routes/snapshots.py b/app/routes/snapshots.py index c16356c..6435614 100644 --- a/app/routes/snapshots.py +++ b/app/routes/snapshots.py @@ -15,17 +15,20 @@ from fastapi import APIRouter, HTTPException from fastapi.responses import JSONResponse -from app.core.runner import run_playbook_core -from app.core.extractor import extract_action_results -from app.utils.vm_id_name_resolver import resolv_id_to_vm_name from app import utils - +from app.core.extractor import extract_action_results +from app.core.runner import run_playbook_core from app.schemas.snapshots import ( - Request_ProxmoxVmsVMID_ListSnapshot, Reply_ProxmoxVmsVMID_ListSnapshot, - Request_ProxmoxVmsVMID_CreateSnapshot, Reply_ProxmoxVmsVMID_CreateSnapshot, - Request_ProxmoxVmsVMID_DeleteSnapshot, Reply_ProxmoxVmsVMID_DeleteSnapshot, - Request_ProxmoxVmsVMID_RevertSnapshot, Reply_ProxmoxVmsVMID_RevertSnapshot, + Reply_ProxmoxVmsVMID_CreateSnapshot, + Reply_ProxmoxVmsVMID_DeleteSnapshot, + Reply_ProxmoxVmsVMID_ListSnapshot, + Reply_ProxmoxVmsVMID_RevertSnapshot, + Request_ProxmoxVmsVMID_CreateSnapshot, + Request_ProxmoxVmsVMID_DeleteSnapshot, + Request_ProxmoxVmsVMID_ListSnapshot, + Request_ProxmoxVmsVMID_RevertSnapshot, ) +from app.utils.vm_id_name_resolver import resolv_id_to_vm_name logger = logging.getLogger(__name__) @@ -41,12 +44,17 @@ def _run_snapshot_action(req, action: str, extravars: dict) -> JSONResponse: extravars["hosts"] = "proxmox" if not PLAYBOOK_SRC.exists(): - raise HTTPException(status_code=400, detail=f":: err - MISSING PLAYBOOK : {PLAYBOOK_SRC}") + raise HTTPException( + status_code=400, detail=f":: err - MISSING PLAYBOOK : {PLAYBOOK_SRC}" + ) inventory = utils.resolve_inventory(INVENTORY_NAME) rc, events, log_plain, log_ansi = run_playbook_core( - PLAYBOOK_SRC, inventory, limit=extravars["hosts"], extravars=extravars, + PLAYBOOK_SRC, + inventory, + limit=extravars["hosts"], + extravars=extravars, ) if req.as_json: @@ -97,7 +105,9 @@ def proxmox_vms_vm_id_create_snapshot(req: Request_ProxmoxVmsVMID_CreateSnapshot extravars = {"proxmox_node": req.proxmox_node} if req.vm_id is not None: extravars["vm_id"] = req.vm_id - extravars["vm_name"] = resolv_id_to_vm_name(extravars["proxmox_node"], extravars["vm_id"]) + extravars["vm_name"] = resolv_id_to_vm_name( + extravars["proxmox_node"], extravars["vm_id"] + ) if req.vm_snapshot_name is not None: extravars["vm_snapshot_name"] = req.vm_snapshot_name if req.vm_snapshot_description is not None: @@ -124,7 +134,9 @@ def proxmox_vms_vm_id_delete_snapshot(req: Request_ProxmoxVmsVMID_DeleteSnapshot extravars["proxmox_node"] = req.proxmox_node if req.vm_id is not None: extravars["vm_id"] = req.vm_id - extravars["vm_name"] = resolv_id_to_vm_name(extravars["proxmox_node"], extravars["vm_id"]) + extravars["vm_name"] = resolv_id_to_vm_name( + extravars["proxmox_node"], extravars["vm_id"] + ) if req.vm_snapshot_name: extravars["vm_snapshot_name"] = req.vm_snapshot_name return _run_snapshot_action(req, "snapshot_vm_delete", extravars) @@ -149,7 +161,9 @@ def proxmox_vms_vm_id_revert_snapshot(req: Request_ProxmoxVmsVMID_RevertSnapshot extravars["proxmox_node"] = req.proxmox_node if req.vm_id is not None: extravars["vm_id"] = req.vm_id - extravars["vm_name"] = resolv_id_to_vm_name(extravars["proxmox_node"], extravars["vm_id"]) + extravars["vm_name"] = resolv_id_to_vm_name( + extravars["proxmox_node"], extravars["vm_id"] + ) if req.vm_snapshot_name is not None: extravars["vm_snapshot_name"] = req.vm_snapshot_name return _run_snapshot_action(req, "snapshot_vm_revert", extravars) diff --git a/app/routes/storage.py b/app/routes/storage.py index c2fb204..63734f3 100644 --- a/app/routes/storage.py +++ b/app/routes/storage.py @@ -15,15 +15,18 @@ from fastapi import APIRouter, HTTPException from fastapi.responses import JSONResponse -from app.core.runner import run_playbook_core -from app.core.extractor import extract_action_results from app import utils - +from app.core.extractor import extract_action_results +from app.core.runner import run_playbook_core from app.schemas.storage import ( - Request_ProxmoxStorage_List, Reply_ProxmoxStorage_ListItem, - Request_ProxmoxStorage_DownloadIso, Reply_ProxmoxStorage_DownloadIsoItem, - Request_ProxmoxStorage_ListIso, Reply_ProxmoxStorageWithStorageName_ListIsoItem, - Request_ProxmoxStorage_ListTemplate, Reply_ProxmoxStorageWithStorageName_ListTemplate, + Reply_ProxmoxStorage_DownloadIsoItem, + Reply_ProxmoxStorage_ListItem, + Reply_ProxmoxStorageWithStorageName_ListIsoItem, + Reply_ProxmoxStorageWithStorageName_ListTemplate, + Request_ProxmoxStorage_DownloadIso, + Request_ProxmoxStorage_List, + Request_ProxmoxStorage_ListIso, + Request_ProxmoxStorage_ListTemplate, ) logger = logging.getLogger(__name__) @@ -41,9 +44,13 @@ def _run_storage(req, action: str, extravars: dict) -> JSONResponse: extravars["proxmox_vm_action"] = action extravars["hosts"] = "proxmox" if not PLAYBOOK_SRC.exists(): - raise HTTPException(status_code=400, detail=f":: err - MISSING PLAYBOOK : {PLAYBOOK_SRC}") + raise HTTPException( + status_code=400, detail=f":: err - MISSING PLAYBOOK : {PLAYBOOK_SRC}" + ) inventory = utils.resolve_inventory(INVENTORY_NAME) - rc, events, log_plain, _ = run_playbook_core(PLAYBOOK_SRC, inventory, limit=extravars["hosts"], extravars=extravars) + rc, events, log_plain, _ = run_playbook_core( + PLAYBOOK_SRC, inventory, limit=extravars["hosts"], extravars=extravars + ) if req.as_json: payload = {"rc": rc, "result": extract_action_results(events, action)} else: @@ -53,7 +60,15 @@ def _run_storage(req, action: str, extravars: dict) -> JSONResponse: # --- /v0/admin/proxmox/storage/ --- -@storage_router.post(path="/list", summary="Retrieve configuration of a VM", description="Returns the configuration details of the specified virtual machine (VM).", tags=["proxmox - storage"], response_model=Reply_ProxmoxStorage_ListItem, response_description="VM configuration details") + +@storage_router.post( + path="/list", + summary="Retrieve configuration of a VM", + description="Returns the configuration details of the specified virtual machine (VM).", + tags=["proxmox - storage"], + response_model=Reply_ProxmoxStorage_ListItem, + response_description="VM configuration details", +) def proxmox_storage_list(req: Request_ProxmoxStorage_List): """List storage pools on the Proxmox node. @@ -66,7 +81,14 @@ def proxmox_storage_list(req: Request_ProxmoxStorage_List): return _run_storage(req, "storage_list", extravars) -@storage_router.post(path="/download_iso", summary="Retrieve configuration of a VM", description="Returns the configuration details of the specified virtual machine (VM).", tags=["proxmox - storage"], response_model=Reply_ProxmoxStorage_DownloadIsoItem, response_description="VM configuration details") +@storage_router.post( + path="/download_iso", + summary="Retrieve configuration of a VM", + description="Returns the configuration details of the specified virtual machine (VM).", + tags=["proxmox - storage"], + response_model=Reply_ProxmoxStorage_DownloadIsoItem, + response_description="VM configuration details", +) def proxmox_storage_download_iso(req: Request_ProxmoxStorage_DownloadIso): """Download an ISO file to a Proxmox storage pool. @@ -87,7 +109,15 @@ def proxmox_storage_download_iso(req: Request_ProxmoxStorage_DownloadIso): # --- /v0/admin/proxmox/storage/storage_name/ --- -@storage_name_router.post(path="/list_iso", summary="Retrieve configuration of a VM", description="Returns the configuration details of the specified virtual machine (VM).", tags=["proxmox - storage"], response_model=Reply_ProxmoxStorageWithStorageName_ListIsoItem, response_description="VM configuration details") + +@storage_name_router.post( + path="/list_iso", + summary="Retrieve configuration of a VM", + description="Returns the configuration details of the specified virtual machine (VM).", + tags=["proxmox - storage"], + response_model=Reply_ProxmoxStorageWithStorageName_ListIsoItem, + response_description="VM configuration details", +) def proxmox_storage_with_storage_name_list_iso(req: Request_ProxmoxStorage_ListIso): """List ISO files in a named storage pool. @@ -100,8 +130,17 @@ def proxmox_storage_with_storage_name_list_iso(req: Request_ProxmoxStorage_ListI return _run_storage(req, "storage_list_iso", extravars) -@storage_name_router.post(path="/list_template", summary="Retrieve configuration of a VM", description="Returns the configuration details of the specified virtual machine (VM).", tags=["proxmox - storage"], response_model=Reply_ProxmoxStorageWithStorageName_ListTemplate, response_description="VM configuration details") -def proxmox_storage_with_storage_name_list_template(req: Request_ProxmoxStorage_ListTemplate): +@storage_name_router.post( + path="/list_template", + summary="Retrieve configuration of a VM", + description="Returns the configuration details of the specified virtual machine (VM).", + tags=["proxmox - storage"], + response_model=Reply_ProxmoxStorageWithStorageName_ListTemplate, + response_description="VM configuration details", +) +def proxmox_storage_with_storage_name_list_template( + req: Request_ProxmoxStorage_ListTemplate, +): """List VM templates in a named storage pool. :param req: Request body with ``proxmox_node`` and ``storage_name``. diff --git a/app/routes/vm_config.py b/app/routes/vm_config.py index 9d5961d..c1b51eb 100644 --- a/app/routes/vm_config.py +++ b/app/routes/vm_config.py @@ -16,16 +16,20 @@ from fastapi import APIRouter, HTTPException from fastapi.responses import JSONResponse -from app.core.runner import run_playbook_core -from app.core.extractor import extract_action_results from app import utils - +from app.core.extractor import extract_action_results +from app.core.runner import run_playbook_core from app.schemas.vm_config import ( - Request_ProxmoxVmsVMID_VmGetConfig, Reply_ProxmoxVmsVMID_VmGetConfig, - Request_ProxmoxVmsVMID_VmGetConfigCdrom, Reply_ProxmoxVmsVMID_VmGetConfigCdrom, - Request_ProxmoxVmsVMID_VmGetConfigCpu, Reply_ProxmoxVmsVMID_VmGetConfigCpu, - Request_ProxmoxVmsVMID_VmGetConfigRam, Reply_ProxmoxVmsVMID_VmGetConfigRam, - Request_ProxmoxVmsVMID_VmSetTag, Reply_ProxmoxVmsVMID_VmSetTag, + Reply_ProxmoxVmsVMID_VmGetConfig, + Reply_ProxmoxVmsVMID_VmGetConfigCdrom, + Reply_ProxmoxVmsVMID_VmGetConfigCpu, + Reply_ProxmoxVmsVMID_VmGetConfigRam, + Reply_ProxmoxVmsVMID_VmSetTag, + Request_ProxmoxVmsVMID_VmGetConfig, + Request_ProxmoxVmsVMID_VmGetConfigCdrom, + Request_ProxmoxVmsVMID_VmGetConfigCpu, + Request_ProxmoxVmsVMID_VmGetConfigRam, + Request_ProxmoxVmsVMID_VmSetTag, ) logger = logging.getLogger(__name__) @@ -43,12 +47,17 @@ def _run_config_action(req, action: str, extravars: dict) -> JSONResponse: extravars["hosts"] = "proxmox" if not PLAYBOOK_SRC.exists(): - raise HTTPException(status_code=400, detail=f":: err - MISSING PLAYBOOK : {PLAYBOOK_SRC}") + raise HTTPException( + status_code=400, detail=f":: err - MISSING PLAYBOOK : {PLAYBOOK_SRC}" + ) inventory = utils.resolve_inventory(INVENTORY_NAME) rc, events, log_plain, log_ansi = run_playbook_core( - PLAYBOOK_SRC, inventory, limit=extravars["hosts"], extravars=extravars, + PLAYBOOK_SRC, + inventory, + limit=extravars["hosts"], + extravars=extravars, ) if req.as_json: diff --git a/app/routes/vms.py b/app/routes/vms.py index 35e1d8e..168f16d 100644 --- a/app/routes/vms.py +++ b/app/routes/vms.py @@ -23,26 +23,31 @@ import logging import os from pathlib import Path -from typing import Any from fastapi import APIRouter, HTTPException from fastapi.responses import JSONResponse -from app.core.runner import run_playbook_core -from app.core.extractor import extract_action_results -from app.utils.vm_id_name_resolver import resolv_id_to_vm_name from app import utils - +from app.core.extractor import extract_action_results +from app.core.runner import run_playbook_core from app.schemas.vms import ( - Request_ProxmoxVms_VmList, Reply_ProxmoxVmList, - Request_ProxmoxVms_VmListUsage, Reply_ProxmoxVms_VmListUsage, - Request_ProxmoxVmsVMID_StartStopPauseResume, Reply_ProxmoxVmsVMID_StartStopPauseResume, - Request_ProxmoxVmsVMID_Create, Reply_ProxmoxVmsVMID_Create, - Request_ProxmoxVmsVMID_Delete, Reply_ProxmoxVmsVMID_Delete, - Request_ProxmoxVmsVMID_Clone, Reply_ProxmoxVmsVMID_Clone, + Reply_ProxmoxVmList, + Reply_ProxmoxVms_VmListUsage, + Reply_ProxmoxVmsVMID_Clone, + Reply_ProxmoxVmsVMID_Create, + Reply_ProxmoxVmsVMID_Delete, + Reply_ProxmoxVmsVMID_StartStopPauseResume, + Reply_ProxmoxVmsVmIds_MassDelete, + Request_ProxmoxVms_VmList, + Request_ProxmoxVms_VmListUsage, + Request_ProxmoxVmsVMID_Clone, + Request_ProxmoxVmsVMID_Create, + Request_ProxmoxVmsVMID_Delete, + Request_ProxmoxVmsVMID_StartStopPauseResume, + Request_ProxmoxVmsVmIds_MassDelete, Request_ProxmoxVmsVmIds_MassStartStopPauseResume, - Request_ProxmoxVmsVmIds_MassDelete, Reply_ProxmoxVmsVmIds_MassDelete, ) +from app.utils.vm_id_name_resolver import resolv_id_to_vm_name logger = logging.getLogger(__name__) @@ -54,13 +59,16 @@ # Shared helpers # --------------------------------------------------------------------------- + def _run_proxmox_action(req, action: str, extravars: dict) -> JSONResponse: """Common pattern for all standard Proxmox action routes.""" extravars["proxmox_vm_action"] = action extravars["hosts"] = "proxmox" if not PLAYBOOK_SRC.exists(): - raise HTTPException(status_code=400, detail=f":: err - MISSING PLAYBOOK : {PLAYBOOK_SRC}") + raise HTTPException( + status_code=400, detail=f":: err - MISSING PLAYBOOK : {PLAYBOOK_SRC}" + ) inventory = utils.resolve_inventory(INVENTORY_NAME) @@ -283,7 +291,9 @@ def proxmox_vms_vm_id_delete(req: Request_ProxmoxVmsVMID_Delete): extravars = {"proxmox_node": req.proxmox_node} if req.vm_id is not None: extravars["vm_id"] = req.vm_id - extravars["vm_name"] = resolv_id_to_vm_name(extravars["proxmox_node"], extravars["vm_id"]) + extravars["vm_name"] = resolv_id_to_vm_name( + extravars["proxmox_node"], extravars["vm_id"] + ) return _run_proxmox_action(req, "vm_delete", extravars) @@ -304,7 +314,9 @@ def proxmox_vms_vm_id_clone(req: Request_ProxmoxVmsVMID_Clone): extravars = {"proxmox_node": req.proxmox_node} if req.vm_id is not None: extravars["vm_id"] = req.vm_id - extravars["vm_name"] = resolv_id_to_vm_name(extravars["proxmox_node"], extravars["vm_id"]) + extravars["vm_name"] = resolv_id_to_vm_name( + extravars["proxmox_node"], extravars["vm_id"] + ) if req.vm_new_id is not None: extravars["vm_new_id"] = req.vm_new_id if req.vm_name is not None: @@ -323,7 +335,9 @@ def proxmox_vms_vm_id_clone(req: Request_ProxmoxVmsVMID_Clone): def _run_mass_action(req, action_name: str, proxmox_vm_action: str) -> JSONResponse: """Helper for mass start/stop/pause/resume.""" checked_inventory_filepath = utils.resolve_inventory(INVENTORY_NAME) - checked_playbook_filepath = utils.resolve_bundles_playbook(action_name, "public_github") + checked_playbook_filepath = utils.resolve_bundles_playbook( + action_name, "public_github" + ) extravars = {} extravars["PROXMOX_VM_ACTION"] = proxmox_vm_action @@ -348,7 +362,9 @@ def _run_mass_action(req, action_name: str, proxmox_vm_action: str) -> JSONRespo return JSONResponse(payload, status_code=200 if rc == 0 else 500) -_MASS_ACTION_NAME = "core/proxmox/configure/default/vms/start-stop-pause-resume-vms-vuln" +_MASS_ACTION_NAME = ( + "core/proxmox/configure/default/vms/start-stop-pause-resume-vms-vuln" +) @vm_ids_router.post( @@ -374,7 +390,9 @@ def proxmox_vms_vm_ids_mass_stop(req: Request_ProxmoxVmsVmIds_MassStartStopPause tags=["proxmox - vm lifecycle"], response_model=Reply_ProxmoxVmsVMID_StartStopPauseResume, ) -def proxmox_vms_vm_ids_mass_stop_force(req: Request_ProxmoxVmsVmIds_MassStartStopPauseResume): +def proxmox_vms_vm_ids_mass_stop_force( + req: Request_ProxmoxVmsVmIds_MassStartStopPauseResume, +): """Force stop multiple VMs by ID list. :param req: Request body with ``proxmox_node`` and ``vm_ids`` list. @@ -390,7 +408,9 @@ def proxmox_vms_vm_ids_mass_stop_force(req: Request_ProxmoxVmsVmIds_MassStartSto tags=["proxmox - vm lifecycle"], response_model=Reply_ProxmoxVmsVMID_StartStopPauseResume, ) -def proxmox_vms_vm_ids_mass_start(req: Request_ProxmoxVmsVmIds_MassStartStopPauseResume): +def proxmox_vms_vm_ids_mass_start( + req: Request_ProxmoxVmsVmIds_MassStartStopPauseResume, +): """Start multiple VMs by ID list. :param req: Request body with ``proxmox_node`` and ``vm_ids`` list. @@ -406,7 +426,9 @@ def proxmox_vms_vm_ids_mass_start(req: Request_ProxmoxVmsVmIds_MassStartStopPaus tags=["proxmox - vm lifecycle"], response_model=Reply_ProxmoxVmsVMID_StartStopPauseResume, ) -def proxmox_vms_vm_ids_mass_pause(req: Request_ProxmoxVmsVmIds_MassStartStopPauseResume): +def proxmox_vms_vm_ids_mass_pause( + req: Request_ProxmoxVmsVmIds_MassStartStopPauseResume, +): """Pause multiple VMs by ID list. :param req: Request body with ``proxmox_node`` and ``vm_ids`` list. @@ -422,7 +444,9 @@ def proxmox_vms_vm_ids_mass_pause(req: Request_ProxmoxVmsVmIds_MassStartStopPaus tags=["proxmox - vm lifecycle"], response_model=Reply_ProxmoxVmsVMID_StartStopPauseResume, ) -def proxmox_vms_vm_ids_mass_resume(req: Request_ProxmoxVmsVmIds_MassStartStopPauseResume): +def proxmox_vms_vm_ids_mass_resume( + req: Request_ProxmoxVmsVmIds_MassStartStopPauseResume, +): """Resume multiple paused VMs by ID list. :param req: Request body with ``proxmox_node`` and ``vm_ids`` list. @@ -446,7 +470,9 @@ def proxmox_vms_vm_ids_mass_delete(req: Request_ProxmoxVmsVmIds_MassDelete): """ action_name = "core/proxmox/configure/default/vms/delete-vms-vuln" checked_inventory_filepath = utils.resolve_inventory(INVENTORY_NAME) - checked_playbook_filepath = utils.resolve_bundles_playbook(action_name, "public_github") + checked_playbook_filepath = utils.resolve_bundles_playbook( + action_name, "public_github" + ) extravars = {} if req.proxmox_node: diff --git a/app/routes/ws_status.py b/app/routes/ws_status.py index d5c3dc1..e792cdd 100644 --- a/app/routes/ws_status.py +++ b/app/routes/ws_status.py @@ -13,7 +13,6 @@ """ import asyncio -import json import logging import os from pathlib import Path @@ -45,7 +44,14 @@ def load_proxmox_credentials() -> dict: inv = yaml.safe_load(f) # Navigate to proxmox host vars - px = inv.get("all", {}).get("children", {}).get("range42_infrastructure", {}).get("children", {}).get("proxmox", {}).get("hosts", {}) + px = ( + inv.get("all", {}) + .get("children", {}) + .get("range42_infrastructure", {}) + .get("children", {}) + .get("proxmox", {}) + .get("hosts", {}) + ) for host_name, host_vars in px.items(): if host_vars and host_vars.get("proxmox_api_host"): return { @@ -109,9 +115,7 @@ async def fetch_vm_status( return [] -def compute_diff( - prev: Dict[int, dict], current: Dict[int, dict] -) -> Optional[dict]: +def compute_diff(prev: Dict[int, dict], current: Dict[int, dict]) -> Optional[dict]: """Compare previous and current VM states, return changes. :param prev: Previous poll's VM state keyed by ``vmid``. @@ -128,7 +132,10 @@ def compute_diff( old = prev.get(vmid) if old is None: changes[vmid] = {"type": "added", **vm} - elif old["status"] != vm["status"] or abs(old.get("cpu", 0) - vm.get("cpu", 0)) > 2: + elif ( + old["status"] != vm["status"] + or abs(old.get("cpu", 0) - vm.get("cpu", 0)) > 2 + ): changes[vmid] = {"type": "changed", **vm} for vmid in prev: @@ -153,7 +160,9 @@ async def vm_status_websocket(ws: WebSocket): # Read Proxmox credentials from backend inventory (not from client) creds = load_proxmox_credentials() if not creds: - await ws.send_json({"error": "Proxmox credentials not found in backend inventory"}) + await ws.send_json( + {"error": "Proxmox credentials not found in backend inventory"} + ) await ws.close() return @@ -170,24 +179,32 @@ async def vm_status_websocket(ws: WebSocket): async with httpx.AsyncClient(verify=False) as client: try: while True: - vms = await fetch_vm_status(client, api_host, node, token_id, token_secret) + vms = await fetch_vm_status( + client, api_host, node, token_id, token_secret + ) - current_state = {vm["vmid"]: vm for vm in vms if vm.get("template", 0) != 1} + current_state = { + vm["vmid"]: vm for vm in vms if vm.get("template", 0) != 1 + } # First message: send full state if not prev_state: - await ws.send_json({ - "type": "full", - "vms": list(current_state.values()), - }) + await ws.send_json( + { + "type": "full", + "vms": list(current_state.values()), + } + ) else: # Subsequent: send only changes diff = compute_diff(prev_state, current_state) if diff: - await ws.send_json({ - "type": "diff", - "changes": diff, - }) + await ws.send_json( + { + "type": "diff", + "changes": diff, + } + ) prev_state = current_state await asyncio.sleep(POLL_INTERVAL) diff --git a/app/schemas/base.py b/app/schemas/base.py index 936c518..e14aa29 100644 --- a/app/schemas/base.py +++ b/app/schemas/base.py @@ -1,16 +1,17 @@ - -from typing import Optional, Literal from pydantic import BaseModel, Field class ProxmoxBaseRequest(BaseModel): """Base request model with fields common to all Proxmox operations.""" + proxmox_node: str = Field(..., pattern=r"^[A-Za-z0-9-]*$") as_json: bool = Field(default=True) class PingRequest(BaseModel): - hosts: str | None + hosts: str | None + + # # class ListRequest(BaseModel): # hosts: Optional[str] = None diff --git a/app/schemas/bundles/__init__.py b/app/schemas/bundles/__init__.py index 31707d4..4b1100e 100644 --- a/app/schemas/bundles/__init__.py +++ b/app/schemas/bundles/__init__.py @@ -5,9 +5,9 @@ 2. Exposes consolidated schema classes for new code to import from app.schemas.bundles. """ -from typing import Dict, List, Literal -from pydantic import BaseModel, Field +from typing import Dict +from pydantic import BaseModel, Field # =========================================================================== # Ubuntu Bundles @@ -17,46 +17,39 @@ # Add User # --------------------------------------------------------------------------- -class BundleAddUserRequest(BaseModel): +class BundleAddUserRequest(BaseModel): proxmox_node: str = Field( ..., # default= "px-testing", - description = "Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" + description="Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$", ) hosts: str = Field( - ..., - description= "Hosts or groups", - pattern = r"^[a-zA-Z0-9._:-]+$" + ..., description="Hosts or groups", pattern=r"^[a-zA-Z0-9._:-]+$" ) #### user: str = Field( ..., - description = "New user", - pattern = r"^[a-z_][a-z0-9_-]*$", + description="New user", + pattern=r"^[a-z_][a-z0-9_-]*$", ) - password: str = Field( ..., - description = "New password", - pattern = r"^[A-Za-z0-9@._-]*$" # dangerous chars removed. + description="New password", + pattern=r"^[A-Za-z0-9@._-]*$", # dangerous chars removed. ) - - change_pwd_at_logon : bool = Field( - ..., - description = "Force user to change password on first login" + change_pwd_at_logon: bool = Field( + ..., description="Force user to change password on first login" ) shell_path: str = Field( - ..., - description = "Default user shell ", - pattern = r"^/[a-z/]*$" + ..., description="Default user shell ", pattern=r"^/[a-z/]*$" ) model_config = { @@ -75,7 +68,6 @@ class BundleAddUserRequest(BaseModel): class BundleAddUserItemReply(BaseModel): - # action: Literal["vm_get_config"] # source: Literal["proxmox"] proxmox_node: str @@ -85,83 +77,70 @@ class BundleAddUserItemReply(BaseModel): class BundleAddUserReply(BaseModel): - rc: int = Field(0, description="RETURN code (0 = OK)") result: list[BundleAddUserItemReply] - model_config = { - "json_schema_extra": { - "example": { - "rc": 0, - "result": [ - { - } - ] - } - } - } + model_config = {"json_schema_extra": {"example": {"rc": 0, "result": [{}]}}} # --------------------------------------------------------------------------- # Install Basic Packages # --------------------------------------------------------------------------- -class BundleBasicPackagesRequest(BaseModel): +class BundleBasicPackagesRequest(BaseModel): proxmox_node: str = Field( ..., # default= "px-testing", - description = "Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" + description="Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$", ) hosts: str = Field( - ..., - description= "Hosts or groups", - pattern = r"^[a-zA-Z0-9._:-]+$" + ..., description="Hosts or groups", pattern=r"^[a-zA-Z0-9._:-]+$" ) #### - install_package_basics : bool = Field( - ..., - description="", + install_package_basics: bool = Field( + ..., + description="", ) - install_package_firewalls : bool = Field( - ..., - description="", + install_package_firewalls: bool = Field( + ..., + description="", ) - install_package_docker : bool = Field( - ..., - description="", + install_package_docker: bool = Field( + ..., + description="", ) - install_package_docker_compose: bool = Field( - ..., - description="", + install_package_docker_compose: bool = Field( + ..., + description="", ) - install_package_utils_json : bool = Field( - ..., - description="", + install_package_utils_json: bool = Field( + ..., + description="", ) - install_package_utils_network : bool = Field( - ..., - description="", + install_package_utils_network: bool = Field( + ..., + description="", ) #### - install_ntpclient_and_update_time: bool = Field( - ..., + install_ntpclient_and_update_time: bool = Field( + ..., description="", ) - packages_cleaning: bool = Field( - ..., + packages_cleaning: bool = Field( + ..., description="", ) @@ -171,22 +150,20 @@ class BundleBasicPackagesRequest(BaseModel): "proxmox_node": "px-testing", "hosts": "r42.vuln-box-00", # - "install_package_basics" : True, - "install_package_firewalls" : False, - "install_package_docker" : False, - "install_package_docker_compose" : False, - "install_package_utils_json" : False, - "install_package_utils_network" : False, - "install_ntpclient_and_update_time" : True, - "packages_cleaning" : True, - + "install_package_basics": True, + "install_package_firewalls": False, + "install_package_docker": False, + "install_package_docker_compose": False, + "install_package_utils_json": False, + "install_package_utils_network": False, + "install_ntpclient_and_update_time": True, + "packages_cleaning": True, } } } class BundleBasicPackagesItemReply(BaseModel): - # action: Literal["vm_get_config"] # source: Literal["proxmox"] proxmox_node: str @@ -196,58 +173,45 @@ class BundleBasicPackagesItemReply(BaseModel): class BundleBasicPackagesReply(BaseModel): - rc: int = Field(0, description="RETURN code (0 = OK)") result: list[BundleBasicPackagesItemReply] - model_config = { - "json_schema_extra": { - "example": { - "rc": 0, - "result": [ - { - } - ] - } - } - } + model_config = {"json_schema_extra": {"example": {"rc": 0, "result": [{}]}}} # --------------------------------------------------------------------------- # Install Docker # --------------------------------------------------------------------------- -class BundleDockerRequest(BaseModel): +class BundleDockerRequest(BaseModel): proxmox_node: str = Field( ..., # default= "px-testing", - description = "Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" + description="Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$", ) hosts: str = Field( - ..., - description= "Hosts or groups", - pattern = r"^[a-zA-Z0-9._:-]+$" + ..., description="Hosts or groups", pattern=r"^[a-zA-Z0-9._:-]+$" ) #### - install_package_docker : bool = Field( - ..., - description="", + install_package_docker: bool = Field( + ..., + description="", ) #### - install_ntpclient_and_update_time: bool = Field( - ..., + install_ntpclient_and_update_time: bool = Field( + ..., description="", ) - packages_cleaning: bool = Field( - ..., + packages_cleaning: bool = Field( + ..., description="", ) @@ -257,17 +221,15 @@ class BundleDockerRequest(BaseModel): "proxmox_node": "px-testing", "hosts": "r42.vuln-box-00", # - "install_package_docker" : True, - "install_ntpclient_and_update_time" : True, - "packages_cleaning" : True, - + "install_package_docker": True, + "install_ntpclient_and_update_time": True, + "packages_cleaning": True, } } } class BundleDockerItemReply(BaseModel): - # action: Literal["vm_get_config"] # source: Literal["proxmox"] proxmox_node: str @@ -277,64 +239,50 @@ class BundleDockerItemReply(BaseModel): class BundleDockerReply(BaseModel): - rc: int = Field(0, description="RETURN code (0 = OK)") result: list[BundleDockerItemReply] - model_config = { - "json_schema_extra": { - "example": { - "rc": 0, - "result": [ - { - } - ] - } - } - } + model_config = {"json_schema_extra": {"example": {"rc": 0, "result": [{}]}}} # --------------------------------------------------------------------------- # Install Docker Compose # --------------------------------------------------------------------------- -class BundleDockerComposeRequest(BaseModel): +class BundleDockerComposeRequest(BaseModel): proxmox_node: str = Field( ..., # default= "px-testing", - description = "Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" + description="Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$", ) hosts: str = Field( - ..., - description= "Hosts or groups", - pattern = r"^[a-zA-Z0-9._:-]+$" - + ..., description="Hosts or groups", pattern=r"^[a-zA-Z0-9._:-]+$" ) #### - install_package_docker : bool = Field( - ..., - description="", + install_package_docker: bool = Field( + ..., + description="", ) - install_package_docker_compose: bool = Field( - ..., - description="", + install_package_docker_compose: bool = Field( + ..., + description="", ) #### - install_ntpclient_and_update_time: bool = Field( - ..., + install_ntpclient_and_update_time: bool = Field( + ..., description="", ) - packages_cleaning: bool = Field( - ..., + packages_cleaning: bool = Field( + ..., description="", ) @@ -344,18 +292,16 @@ class BundleDockerComposeRequest(BaseModel): "proxmox_node": "px-testing", "hosts": "r42.vuln-box-00", # - "install_package_docker" : True, - "install_package_docker_compose" : True, - "install_ntpclient_and_update_time" : True, - "packages_cleaning" : True, - + "install_package_docker": True, + "install_package_docker_compose": True, + "install_ntpclient_and_update_time": True, + "packages_cleaning": True, } } } class BundleDockerComposeItemReply(BaseModel): - # action: Literal["vm_get_config"] # source: Literal["proxmox"] proxmox_node: str @@ -365,40 +311,27 @@ class BundleDockerComposeItemReply(BaseModel): class BundleDockerComposeReply(BaseModel): - rc: int = Field(0, description="RETURN code (0 = OK)") result: list[BundleDockerComposeItemReply] - model_config = { - "json_schema_extra": { - "example": { - "rc": 0, - "result": [ - { - } - ] - } - } - } + model_config = {"json_schema_extra": {"example": {"rc": 0, "result": [{}]}}} # --------------------------------------------------------------------------- # Install Dot Files # --------------------------------------------------------------------------- -class BundleDotFilesRequest(BaseModel): +class BundleDotFilesRequest(BaseModel): proxmox_node: str = Field( ..., # default= "px-testing", - description = "Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" + description="Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$", ) hosts: str = Field( - ..., - description= "Hosts or groups", - pattern = r"^[a-zA-Z0-9._:-]+$" + ..., description="Hosts or groups", pattern=r"^[a-zA-Z0-9._:-]+$" ) #### @@ -406,23 +339,17 @@ class BundleDotFilesRequest(BaseModel): user: str = Field( ..., description="targeted username", - ) install_vim_dot_files: bool = Field( - ..., - description= "Install vim dot file in user directory" + ..., description="Install vim dot file in user directory" ) install_zsh_dot_files: bool = Field( - ..., - description= "Install zsh dot file in user directory" + ..., description="Install zsh dot file in user directory" ) - apply_for_root: bool = Field( - ..., - description= "Install dot files in /root" - ) + apply_for_root: bool = Field(..., description="Install dot files in /root") model_config = { "json_schema_extra": { @@ -440,7 +367,6 @@ class BundleDotFilesRequest(BaseModel): class BundleDotFilesItemReply(BaseModel): - # action: Literal["vm_get_config"] # source: Literal["proxmox"] proxmox_node: str @@ -453,21 +379,10 @@ class BundleDotFilesItemReply(BaseModel): # Reply_BundlesCoreLinuxUbuntuInstall_DotFilesItem (same as Item reply). # We preserve both the Item and the Reply under their correct new names. class BundleDotFilesReply(BaseModel): - rc: int = Field(0, description="RETURN code (0 = OK)") result: list[BundleDotFilesItemReply] - model_config = { - "json_schema_extra": { - "example": { - "rc": 0, - "result": [ - { - } - ] - } - } - } + model_config = {"json_schema_extra": {"example": {"rc": 0, "result": [{}]}}} # =========================================================================== @@ -478,8 +393,8 @@ class BundleDotFilesReply(BaseModel): # Create Admin VMs (Default) # --------------------------------------------------------------------------- -class BundleCreateAdminVmsItemRequest(BaseModel): +class BundleCreateAdminVmsItemRequest(BaseModel): vm_id: int = Field( ..., ge=1, @@ -487,9 +402,7 @@ class BundleCreateAdminVmsItemRequest(BaseModel): ) vm_ip: str = Field( - ..., - description="vm ipv4", - pattern=r"^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$" + ..., description="vm ipv4", pattern=r"^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$" ) vm_description: str = Field( @@ -497,21 +410,21 @@ class BundleCreateAdminVmsItemRequest(BaseModel): strip_whitespace=True, max_length=200, # pattern=VM_DESCRIPTION_RE, - description="Description" + description="Description", ) -class BundleCreateAdminVmsRequest(BaseModel): +class BundleCreateAdminVmsRequest(BaseModel): proxmox_node: str = Field( ..., # default= "px-testing", - description = "Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" + description="Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$", ) vms: Dict[str, BundleCreateAdminVmsItemRequest] = Field( ..., - description="Map - vm override vm_id vm_ip vm_description, ... " + description="Map - vm override vm_id vm_ip vm_description, ... ", ) model_config = { @@ -529,14 +442,13 @@ class BundleCreateAdminVmsRequest(BaseModel): "vm_description": "API gateway", "vm_ip": "192.168.42.120", }, - } + }, } } } class BundleCreateAdminVmsItemReply(BaseModel): - # action: Literal["vm_get_config"] # source: Literal["proxmox"] proxmox_node: str @@ -546,29 +458,18 @@ class BundleCreateAdminVmsItemReply(BaseModel): class BundleCreateAdminVmsReply(BaseModel): - rc: int = Field(0, description="RETURN code (0 = OK)") result: list[BundleCreateAdminVmsItemReply] - model_config = { - "json_schema_extra": { - "example": { - "rc": 0, - "result": [ - { - } - ] - } - } - } + model_config = {"json_schema_extra": {"example": {"rc": 0, "result": [{}]}}} # --------------------------------------------------------------------------- # Create Student VMs (Default) # --------------------------------------------------------------------------- -class BundleCreateStudentVmsItemRequest(BaseModel): +class BundleCreateStudentVmsItemRequest(BaseModel): vm_id: int = Field( ..., ge=1, @@ -576,9 +477,7 @@ class BundleCreateStudentVmsItemRequest(BaseModel): ) vm_ip: str = Field( - ..., - description="vm ipv4", - pattern=r"^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$" + ..., description="vm ipv4", pattern=r"^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$" ) vm_description: str = Field( @@ -586,24 +485,23 @@ class BundleCreateStudentVmsItemRequest(BaseModel): strip_whitespace=True, max_length=200, # pattern=VM_DESCRIPTION_RE, - description="Description" + description="Description", ) -class BundleCreateStudentVmsRequest(BaseModel): +class BundleCreateStudentVmsRequest(BaseModel): proxmox_node: str = Field( ..., # default= "px-testing", - description = "Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" + description="Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$", ) vms: Dict[str, BundleCreateStudentVmsItemRequest] = Field( ..., - description="Map - vm override vm_id vm_ip vm_description, ... " + description="Map - vm override vm_id vm_ip vm_description, ... ", ) - model_config = { "json_schema_extra": { "example": { @@ -612,16 +510,15 @@ class BundleCreateStudentVmsRequest(BaseModel): "student-box-01": { "vm_id": 3001, "vm_description": "student R42 student vm", - "vm_ip": "192.168.42.160" , + "vm_ip": "192.168.42.160", } - } + }, } } } class BundleCreateStudentVmsItemReply(BaseModel): - # action: Literal["vm_get_config"] # source: Literal["proxmox"] proxmox_node: str @@ -631,29 +528,18 @@ class BundleCreateStudentVmsItemReply(BaseModel): class BundleCreateStudentVmsReply(BaseModel): - rc: int = Field(0, description="RETURN code (0 = OK)") result: list[BundleCreateStudentVmsItemReply] - model_config = { - "json_schema_extra": { - "example": { - "rc": 0, - "result": [ - { - } - ] - } - } - } + model_config = {"json_schema_extra": {"example": {"rc": 0, "result": [{}]}}} # --------------------------------------------------------------------------- # Create Vuln VMs (Default) # --------------------------------------------------------------------------- -class BundleCreateVulnVmsItemRequest(BaseModel): +class BundleCreateVulnVmsItemRequest(BaseModel): vm_id: int = Field( ..., ge=1, @@ -661,9 +547,7 @@ class BundleCreateVulnVmsItemRequest(BaseModel): ) vm_ip: str = Field( - ..., - description="vm ipv4", - pattern=r"^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$" + ..., description="vm ipv4", pattern=r"^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$" ) vm_description: str = Field( @@ -671,21 +555,21 @@ class BundleCreateVulnVmsItemRequest(BaseModel): strip_whitespace=True, max_length=200, # pattern=VM_DESCRIPTION_RE, - description="Description" + description="Description", ) -class BundleCreateVulnVmsRequest(BaseModel): +class BundleCreateVulnVmsRequest(BaseModel): proxmox_node: str = Field( ..., # default= "px-testing", - description = "Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" + description="Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$", ) vms: Dict[str, BundleCreateVulnVmsItemRequest] = Field( ..., - description="Map - vm override vm_id vm_ip vm_description, ... " + description="Map - vm override vm_id vm_ip vm_description, ... ", ) model_config = { @@ -698,14 +582,13 @@ class BundleCreateVulnVmsRequest(BaseModel): "vm_description": "vulnerable vm 00", "vm_ip": "192.168.42.170", }, - } + }, } } } class BundleCreateVulnVmsItemReply(BaseModel): - # action: Literal["vm_get_config"] # source: Literal["proxmox"] proxmox_node: str @@ -715,45 +598,33 @@ class BundleCreateVulnVmsItemReply(BaseModel): class BundleCreateVulnVmsReply(BaseModel): - rc: int = Field(0, description="RETURN code (0 = OK)") result: list[BundleCreateVulnVmsItemReply] - model_config = { - "json_schema_extra": { - "example": { - "rc": 0, - "result": [ - { - } - ] - } - } - } + model_config = {"json_schema_extra": {"example": {"rc": 0, "result": [{}]}}} # --------------------------------------------------------------------------- # Revert Snapshot Default (Admin/Vuln/Student VMs) # --------------------------------------------------------------------------- -class BundleRevertSnapshotDefaultRequest(BaseModel): +class BundleRevertSnapshotDefaultRequest(BaseModel): proxmox_node: str = Field( ..., # default= "px-testing", - description = "Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" + description="Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$", ) as_json: bool = Field( - default=True, - description="If true : JSON output else : raw output" + default=True, description="If true : JSON output else : raw output" ) vm_snapshot_name: str | None = Field( default=None, description="Name of the snapshot to create", - pattern=r"^[A-Za-z0-9_-]+$" + pattern=r"^[A-Za-z0-9_-]+$", ) # @@ -763,7 +634,6 @@ class BundleRevertSnapshotDefaultRequest(BaseModel): "proxmox_node": "px-testing", "vm_snapshot_name": "default-snapshot-from-API-220925-1734", "as_json": True, - } } } @@ -773,18 +643,17 @@ class BundleRevertSnapshotDefaultRequest(BaseModel): # Start/Stop/Pause/Resume Default (Admin/Vuln/Student VMs) # --------------------------------------------------------------------------- -class BundleStartStopDefaultRequest(BaseModel): +class BundleStartStopDefaultRequest(BaseModel): proxmox_node: str = Field( ..., # default= "px-testing", - description = "Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" + description="Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$", ) as_json: bool = Field( - default=True, - description="If true : JSON output else : raw output" + default=True, description="If true : JSON output else : raw output" ) # @@ -793,7 +662,6 @@ class BundleStartStopDefaultRequest(BaseModel): "example": { "proxmox_node": "px-testing", "as_json": True, - } } } @@ -830,25 +698,45 @@ class BundleStartStopDefaultRequest(BaseModel): # Reply_BundlesCoreLinuxUbuntuInstall_DotFilesItem. We alias the Reply class too. # bundles/core/proxmox/configure/default/vms/create_vms_admin_default.py -Request_BundlesCoreProxmoxConfigureDefaultVms_CreateAdminVmsItem = BundleCreateAdminVmsItemRequest -Request_BundlesCoreProxmoxConfigureDefaultVms_CreateAdminVms = BundleCreateAdminVmsRequest -Reply_BundlesCoreProxmoxConfigureDefaultVms_CreateAdminVmsItem = BundleCreateAdminVmsItemReply +Request_BundlesCoreProxmoxConfigureDefaultVms_CreateAdminVmsItem = ( + BundleCreateAdminVmsItemRequest +) +Request_BundlesCoreProxmoxConfigureDefaultVms_CreateAdminVms = ( + BundleCreateAdminVmsRequest +) +Reply_BundlesCoreProxmoxConfigureDefaultVms_CreateAdminVmsItem = ( + BundleCreateAdminVmsItemReply +) Reply_BundlesCoreProxmoxConfigureDefaultVms_CreateAdminVms = BundleCreateAdminVmsReply # bundles/core/proxmox/configure/default/vms/create_vms_student_default.py -Request_BundlesCoreProxmoxConfigureDefaultVms_CreateStudentVmsItem = BundleCreateStudentVmsItemRequest -Request_BundlesCoreProxmoxConfigureDefaultVms_CreateStudentVms = BundleCreateStudentVmsRequest -Reply_BundlesCoreProxmoxConfigureDefaultVms_CreateStudentVmsItem = BundleCreateStudentVmsItemReply -Reply_BundlesCoreProxmoxConfigureDefaultVms_CreateStudentVms = BundleCreateStudentVmsReply +Request_BundlesCoreProxmoxConfigureDefaultVms_CreateStudentVmsItem = ( + BundleCreateStudentVmsItemRequest +) +Request_BundlesCoreProxmoxConfigureDefaultVms_CreateStudentVms = ( + BundleCreateStudentVmsRequest +) +Reply_BundlesCoreProxmoxConfigureDefaultVms_CreateStudentVmsItem = ( + BundleCreateStudentVmsItemReply +) +Reply_BundlesCoreProxmoxConfigureDefaultVms_CreateStudentVms = ( + BundleCreateStudentVmsReply +) # bundles/core/proxmox/configure/default/vms/create_vms_vuln_default.py -Request_BundlesCoreProxmoxConfigureDefaultVms_CreateVulnVmsItem = BundleCreateVulnVmsItemRequest +Request_BundlesCoreProxmoxConfigureDefaultVms_CreateVulnVmsItem = ( + BundleCreateVulnVmsItemRequest +) Request_BundlesCoreProxmoxConfigureDefaultVms_CreateVulnVms = BundleCreateVulnVmsRequest -Reply_BundlesCoreProxmoxConfigureDefaultVms_CreateVulnVmsItem = BundleCreateVulnVmsItemReply +Reply_BundlesCoreProxmoxConfigureDefaultVms_CreateVulnVmsItem = ( + BundleCreateVulnVmsItemReply +) Reply_BundlesCoreProxmoxConfigureDefaultVms_CreateVulnVms = BundleCreateVulnVmsReply # bundles/core/proxmox/configure/default/vms/revert_snapshot_default.py -Request_BundlesCoreProxmoxConfigureDefaultVms_RevertSnapshotAdminVulnStudentVms = BundleRevertSnapshotDefaultRequest +Request_BundlesCoreProxmoxConfigureDefaultVms_RevertSnapshotAdminVulnStudentVms = ( + BundleRevertSnapshotDefaultRequest +) # bundles/core/proxmox/configure/default/vms/start_stop_resume_pause_default.py Request_BundlesCoreProxmoxConfigureDefaultVms_StartStopPauseResumeAdminVulnStudentVms = BundleStartStopDefaultRequest diff --git a/app/schemas/debug/__init__.py b/app/schemas/debug/__init__.py index 21f6735..e3b94a3 100644 --- a/app/schemas/debug/__init__.py +++ b/app/schemas/debug/__init__.py @@ -6,25 +6,24 @@ """ from typing import List -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field # --------------------------------------------------------------------------- # Debug Ping # --------------------------------------------------------------------------- -class DebugPingRequest(BaseModel): +class DebugPingRequest(BaseModel): proxmox_node: str = Field( ..., # default="px-testing", description="Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" + pattern=r"^[A-Za-z0-9-]*$", ) as_json: bool | None = Field( - default=False, - description="If true : JSON output else : raw output" + default=False, description="If true : JSON output else : raw output" ) # @@ -32,31 +31,24 @@ class DebugPingRequest(BaseModel): ..., # default="all", description="Targeted ansible hosts", - pattern=r"^[A-Za-z0-9\._-]+$" + pattern=r"^[A-Za-z0-9\._-]+$", ) - - model_config = { "json_schema_extra": { - "example": { - "proxmox_node": "px-testing", - "hosts": "all", - "as_json": False - } + "example": {"proxmox_node": "px-testing", "hosts": "all", "as_json": False} } } class DebugPingReply(BaseModel): - rc: int = Field( - ..., # mandatory field. - description="Return code of the job (0 = success, >0 = error/warning)" + ..., # mandatory field. + description="Return code of the job (0 = success, >0 = error/warning)", ) log_multiline: List[str] = Field( - ..., # mandatory field. - description="Execution log as a list of lines (chronological order)" + ..., # mandatory field. + description="Execution log as a list of lines (chronological order)", ) model_config = { @@ -70,8 +62,8 @@ class DebugPingReply(BaseModel): "ok: [something-1]", "", "PLAY RECAP *********************************************************************", - "something-1 : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0" - ] + "something-1 : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0", + ], } } } diff --git a/app/schemas/firewall.py b/app/schemas/firewall.py index 93c5ac0..626695f 100644 --- a/app/schemas/firewall.py +++ b/app/schemas/firewall.py @@ -1,25 +1,24 @@ """Consolidated firewall schemas: rules, aliases, enable/disable at DC/node/VM level.""" -from typing import List, Literal -from pydantic import BaseModel, Field, ConfigDict +from typing import Literal +from pydantic import BaseModel, ConfigDict, Field # --------------------------------------------------------------------------- # Add Iptables Alias # --------------------------------------------------------------------------- -class FirewallAliasAddRequest(BaseModel): +class FirewallAliasAddRequest(BaseModel): proxmox_node: str = Field( ..., # default= "px-testing", - description = "Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" + description="Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$", ) as_json: bool = Field( - default=True, - description="If true : JSON output else : raw output" + default=True, description="If true : JSON output else : raw output" ) # @@ -27,7 +26,7 @@ class FirewallAliasAddRequest(BaseModel): ..., # default="4000", description="Virtual machine id", - pattern=r"^[0-9]+$" + pattern=r"^[0-9]+$", ) vm_fw_alias_name: str = Field( @@ -54,18 +53,17 @@ class FirewallAliasAddRequest(BaseModel): "proxmox_node": "px-testing", "as_json": True, # - "vm_id":"1000", + "vm_id": "1000", # - "vm_fw_alias_name":"test", - "vm_fw_alias_cidr":"192.168.123.0/24", - "vm_fw_alias_comment":"this_comment" + "vm_fw_alias_name": "test", + "vm_fw_alias_cidr": "192.168.123.0/24", + "vm_fw_alias_comment": "this_comment", } } } class FirewallAliasAddItemReply(BaseModel): - action: Literal["vm_ListIso_usage"] source: Literal["proxmox"] proxmox_node: str @@ -75,8 +73,8 @@ class FirewallAliasAddItemReply(BaseModel): vm_fw_alias_name: str vm_id: str -class FirewallAliasAddReply(BaseModel): +class FirewallAliasAddReply(BaseModel): rc: int = Field(0, description="RETURN code (0 = OK)") result: list[FirewallAliasAddItemReply] @@ -92,9 +90,9 @@ class FirewallAliasAddReply(BaseModel): ## "vm_fw_alias_cidr": "192.168.123.0/24", "vm_fw_alias_name": "test", - "vm_id": "1000" + "vm_id": "1000", } - ] + ], } } } @@ -104,8 +102,8 @@ class FirewallAliasAddReply(BaseModel): # Apply Iptables Rules # --------------------------------------------------------------------------- -class FirewallRuleApplyRequest(BaseModel): +class FirewallRuleApplyRequest(BaseModel): proxmox_node: str = Field( ..., description="Target Proxmox node name.", @@ -122,7 +120,7 @@ class FirewallRuleApplyRequest(BaseModel): ..., # default="4000", description="Virtual machine id", - pattern=r"^[0-9]+$" + pattern=r"^[0-9]+$", ) vm_fw_action: str = Field( @@ -197,24 +195,24 @@ class FirewallRuleApplyRequest(BaseModel): model_config = ConfigDict( json_schema_extra={ - "example":[ + "example": [ { - "proxmox_node": "px-node-01", - "as_json": True, - # - "vm_id": "1000", - "vm_fw_action": "ACCEPT", - "vm_fw_type": "in", - "vm_fw_proto": "tcp", - "vm_fw_dport": "22", - "vm_fw_enable": 1, - "vm_fw_iface": "net0", - "vm_fw_source": "192.168.1.0/24", - "vm_fw_dest": "0.0.0.0/0", - "vm_fw_sport": "1024", - "vm_fw_comment": "Test comment", - "vm_fw_pos": 5, - "vm_fw_log": "debug", + "proxmox_node": "px-node-01", + "as_json": True, + # + "vm_id": "1000", + "vm_fw_action": "ACCEPT", + "vm_fw_type": "in", + "vm_fw_proto": "tcp", + "vm_fw_dport": "22", + "vm_fw_enable": 1, + "vm_fw_iface": "net0", + "vm_fw_source": "192.168.1.0/24", + "vm_fw_dest": "0.0.0.0/0", + "vm_fw_sport": "1024", + "vm_fw_comment": "Test comment", + "vm_fw_pos": 5, + "vm_fw_log": "debug", }, ] } @@ -222,28 +220,27 @@ class FirewallRuleApplyRequest(BaseModel): class FirewallRuleApplyItemReply(BaseModel): - action: Literal["vm_ApplyIptablesRules_usage"] source: Literal["proxmox"] proxmox_node: str ## # vm_id: int = Field(..., ge=1) - vm_fw_action : str - vm_fw_comment : str - vm_fw_dest : str - vm_fw_dport : str - vm_fw_enable : int - vm_fw_iface : str - vm_fw_log : str - vm_fw_pos : int - vm_fw_proto : str - vm_fw_source : str - vm_fw_sport : str - vm_fw_type : str - vm_id : str + vm_fw_action: str + vm_fw_comment: str + vm_fw_dest: str + vm_fw_dport: str + vm_fw_enable: int + vm_fw_iface: str + vm_fw_log: str + vm_fw_pos: int + vm_fw_proto: str + vm_fw_source: str + vm_fw_sport: str + vm_fw_type: str + vm_id: str -class FirewallRuleApplyReply(BaseModel): +class FirewallRuleApplyReply(BaseModel): rc: int = Field(0, description="RETURN code (0 = OK)") result: list[FirewallRuleApplyItemReply] @@ -262,9 +259,9 @@ class FirewallRuleApplyReply(BaseModel): "vm_fw_enable": "1", "vm_fw_proto": "tcp", "vm_fw_type": "out", - "vm_id": "100" + "vm_id": "100", }, - ] + ], } } } @@ -274,18 +271,17 @@ class FirewallRuleApplyReply(BaseModel): # Delete Iptables Alias # --------------------------------------------------------------------------- -class FirewallAliasDeleteRequest(BaseModel): +class FirewallAliasDeleteRequest(BaseModel): proxmox_node: str = Field( ..., # default= "px-testing", - description = "Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" + description="Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$", ) as_json: bool = Field( - default=True, - description="If true : JSON output else : raw output" + default=True, description="If true : JSON output else : raw output" ) # @@ -293,7 +289,7 @@ class FirewallAliasDeleteRequest(BaseModel): ..., # default="4000", description="Virtual machine id", - pattern=r"^[0-9]+$" + pattern=r"^[0-9]+$", ) vm_fw_alias_name: str = Field( @@ -316,7 +312,6 @@ class FirewallAliasDeleteRequest(BaseModel): class FirewallAliasDeleteItemReply(BaseModel): - action: Literal["firewall_vm_delete_iptables_alias"] source: Literal["proxmox"] proxmox_node: str @@ -327,7 +322,6 @@ class FirewallAliasDeleteItemReply(BaseModel): class FirewallAliasDeleteReply(BaseModel): - rc: int = Field(0, description="RETURN code (0 = OK)") result: list[FirewallAliasDeleteItemReply] @@ -344,7 +338,7 @@ class FirewallAliasDeleteReply(BaseModel): "vm_fw_alias_name": "test", "vm_id": "1000", } - ] + ], } } } @@ -354,18 +348,17 @@ class FirewallAliasDeleteReply(BaseModel): # Delete Iptables Rule # --------------------------------------------------------------------------- -class FirewallRuleDeleteRequest(BaseModel): +class FirewallRuleDeleteRequest(BaseModel): proxmox_node: str = Field( ..., # default= "px-testing", - description = "Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" + description="Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$", ) as_json: bool = Field( - default=True, - description="If true : JSON output else : raw output" + default=True, description="If true : JSON output else : raw output" ) # @@ -373,7 +366,7 @@ class FirewallRuleDeleteRequest(BaseModel): ..., # default="4000", description="Virtual machine id", - pattern=r"^[0-9]+$" + pattern=r"^[0-9]+$", ) vm_fw_pos: int | None = Field( @@ -387,16 +380,14 @@ class FirewallRuleDeleteRequest(BaseModel): "proxmox_node": "px-testing", "as_json": True, # - "vm_id":"1000", - "vm_fw_pos":1, - + "vm_id": "1000", + "vm_fw_pos": 1, } } } class FirewallRuleDeleteItemReply(BaseModel): - action: Literal["vm_DeleteIptablesRule_usage"] source: Literal["proxmox"] proxmox_node: str @@ -405,8 +396,8 @@ class FirewallRuleDeleteItemReply(BaseModel): vm_id: str vm_fw_pos: int -class FirewallRuleDeleteReply(BaseModel): +class FirewallRuleDeleteReply(BaseModel): rc: int = Field(0, description="RETURN code (0 = OK)") result: list[FirewallRuleDeleteItemReply] @@ -421,9 +412,9 @@ class FirewallRuleDeleteReply(BaseModel): "proxmox_node": "px-testing", ## "vm_fw_pos": "0", - "vm_id": "1000" + "vm_id": "1000", } - ] + ], } } } @@ -433,18 +424,17 @@ class FirewallRuleDeleteReply(BaseModel): # List Iptables Alias # --------------------------------------------------------------------------- -class FirewallAliasListRequest(BaseModel): +class FirewallAliasListRequest(BaseModel): proxmox_node: str = Field( ..., # default= "px-testing", - description = "Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" + description="Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$", ) as_json: bool = Field( - default=True, - description="If true : JSON output else : raw output" + default=True, description="If true : JSON output else : raw output" ) # @@ -452,7 +442,7 @@ class FirewallAliasListRequest(BaseModel): ..., # default="4000", description="Virtual machine id", - pattern=r"^[0-9]+$" + pattern=r"^[0-9]+$", ) model_config = { @@ -468,7 +458,6 @@ class FirewallAliasListRequest(BaseModel): class FirewallAliasListItemReply(BaseModel): - action: Literal["vm_ListIptablesAlias_usage"] source: Literal["proxmox"] proxmox_node: str @@ -478,8 +467,8 @@ class FirewallAliasListItemReply(BaseModel): vm_fw_alias_name: int vm_id: str -class FirewallAliasListReply(BaseModel): +class FirewallAliasListReply(BaseModel): rc: int = Field(0, description="RETURN code (0 = OK)") result: list[FirewallAliasListItemReply] @@ -495,9 +484,9 @@ class FirewallAliasListReply(BaseModel): ## "vm_fw_alias_cidr": "192.168.123.0/24", "vm_fw_alias_name": "test", - "vm_id": "1000" + "vm_id": "1000", } - ] + ], } } } @@ -507,18 +496,17 @@ class FirewallAliasListReply(BaseModel): # List Iptables Rules # --------------------------------------------------------------------------- -class FirewallRuleListRequest(BaseModel): +class FirewallRuleListRequest(BaseModel): proxmox_node: str = Field( ..., # default= "px-testing", - description = "Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" + description="Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$", ) as_json: bool = Field( - default=True, - description="If true : JSON output else : raw output" + default=True, description="If true : JSON output else : raw output" ) # @@ -526,7 +514,7 @@ class FirewallRuleListRequest(BaseModel): ..., # default="4000", description="Virtual machine id", - pattern=r"^[0-9]+$" + pattern=r"^[0-9]+$", ) model_config = { @@ -536,14 +524,12 @@ class FirewallRuleListRequest(BaseModel): "as_json": True, # "vm_id": "1000", - } } } class FirewallRuleListItemReply(BaseModel): - action: Literal["vm_ListIptablesRules_usage"] source: Literal["proxmox"] proxmox_node: str @@ -563,8 +549,8 @@ class FirewallRuleListItemReply(BaseModel): vm_fw_type: str vm_id: str -class FirewallRuleListReply(BaseModel): +class FirewallRuleListReply(BaseModel): rc: int = Field(0, description="RETURN code (0 = OK)") result: list[FirewallRuleListItemReply] @@ -582,9 +568,9 @@ class FirewallRuleListReply(BaseModel): "vm_fw_log": "nolog", "vm_fw_pos": 0, "vm_fw_type": "in", - "vm_id": "100" + "vm_id": "100", }, - ] + ], } } } @@ -594,18 +580,17 @@ class FirewallRuleListReply(BaseModel): # Enable / Disable Firewall — Datacenter # --------------------------------------------------------------------------- -class FirewallEnableDcRequest(BaseModel): +class FirewallEnableDcRequest(BaseModel): proxmox_api_host: str = Field( ..., # default= "px-testing", - description = "Proxmox api - ip:port", - pattern=r"^[A-Za-z0-9\.:-]*$" + description="Proxmox api - ip:port", + pattern=r"^[A-Za-z0-9\.:-]*$", ) as_json: bool = Field( - default=True, - description="If true : JSON output else : raw output" + default=True, description="If true : JSON output else : raw output" ) # @@ -622,7 +607,6 @@ class FirewallEnableDcRequest(BaseModel): class FirewallEnableDcItemReply(BaseModel): - action: Literal["vm_EnableFirewallDc_usage"] source: Literal["proxmox"] proxmox_node: str @@ -630,8 +614,8 @@ class FirewallEnableDcItemReply(BaseModel): # vm_id: int = Field(..., ge=1) proxmox_api_host: str -class FirewallEnableDcReply(BaseModel): +class FirewallEnableDcReply(BaseModel): rc: int = Field(0, description="RETURN code (0 = OK)") result: list[FirewallEnableDcItemReply] @@ -647,34 +631,32 @@ class FirewallEnableDcReply(BaseModel): ## "vm_id": "100", "vm_firewall": "disable", - "vm_name": "test" + "vm_name": "test", } - ] + ], } } } class FirewallDisableDcRequest(BaseModel): - proxmox_node: str = Field( ..., # default= "px-testing", - description = "Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" + description="Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$", ) as_json: bool = Field( - default=True, - description="If true : JSON output else : raw output" + default=True, description="If true : JSON output else : raw output" ) # proxmox_api_host: str = Field( ..., # default= "px-testing", - description = "Proxmox api - ip:port", - pattern=r"^[A-Za-z0-9\.:-]*$" + description="Proxmox api - ip:port", + pattern=r"^[A-Za-z0-9\.:-]*$", ) model_config = { @@ -684,21 +666,19 @@ class FirewallDisableDcRequest(BaseModel): "as_json": True, # "proxmox_api_host": "127.0.0.1:1234", - } } } class FirewallDisableDcItemReply(BaseModel): - action: Literal["vm_DisableFirewallDc_usage"] source: Literal["proxmox"] proxmox_node: str ## -class FirewallDisableDcReply(BaseModel): +class FirewallDisableDcReply(BaseModel): rc: int = Field(0, description="RETURN code (0 = OK)") result: list[FirewallDisableDcItemReply] @@ -713,7 +693,7 @@ class FirewallDisableDcReply(BaseModel): "proxmox_node": "px-testing", ## } - ] + ], } } } @@ -723,33 +703,28 @@ class FirewallDisableDcReply(BaseModel): # Enable / Disable Firewall — Node # --------------------------------------------------------------------------- -class FirewallEnableNodeRequest(BaseModel): +class FirewallEnableNodeRequest(BaseModel): proxmox_node: str = Field( ..., # default= "px-testing", description="Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" + pattern=r"^[A-Za-z0-9-]*$", ) as_json: bool = Field( - default=True, - description="If true : JSON output else : raw output" + default=True, description="If true : JSON output else : raw output" ) # model_config = { "json_schema_extra": { - "example": { - "proxmox_node": "px-testing", - "as_json": True - } + "example": {"proxmox_node": "px-testing", "as_json": True} } } class FirewallEnableNodeItemReply(BaseModel): - action: Literal["vm_EnableFirewallNode_usage"] source: Literal["proxmox"] proxmox_node: str @@ -757,8 +732,8 @@ class FirewallEnableNodeItemReply(BaseModel): # vm_id: int = Field(..., ge=1) node_firewall: str -class FirewallEnableNodeReply(BaseModel): +class FirewallEnableNodeReply(BaseModel): rc: int = Field(0, description="RETURN code (0 = OK)") result: list[FirewallEnableNodeItemReply] @@ -773,41 +748,34 @@ class FirewallEnableNodeReply(BaseModel): "proxmox_node": "px-testing", ## "node_firewall": "enabled", - } - ] + ], } } } class FirewallDisableNodeRequest(BaseModel): - proxmox_node: str = Field( ..., # default= "px-testing", description="Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" + pattern=r"^[A-Za-z0-9-]*$", ) as_json: bool = Field( - default=True, - description="If true : JSON output else : raw output" + default=True, description="If true : JSON output else : raw output" ) # model_config = { "json_schema_extra": { - "example": { - "proxmox_node": "px-testing", - "as_json": True - } + "example": {"proxmox_node": "px-testing", "as_json": True} } } class FirewallDisableNodeItemReply(BaseModel): - action: Literal["vm_EnableFirewallNode_usage"] source: Literal["proxmox"] proxmox_node: str @@ -815,8 +783,8 @@ class FirewallDisableNodeItemReply(BaseModel): # vm_id: int = Field(..., ge=1) node_firewall: str -class FirewallDisableNodeReply(BaseModel): +class FirewallDisableNodeReply(BaseModel): rc: int = Field(0, description="RETURN code (0 = OK)") result: list[FirewallDisableNodeItemReply] @@ -835,7 +803,7 @@ class FirewallDisableNodeReply(BaseModel): ## "node_firewall": "disabled", } - ] + ], } } } @@ -845,18 +813,17 @@ class FirewallDisableNodeReply(BaseModel): # Enable / Disable Firewall — VM # --------------------------------------------------------------------------- -class FirewallEnableVmRequest(BaseModel): +class FirewallEnableVmRequest(BaseModel): proxmox_node: str = Field( ..., # default= "px-testing", - description = "Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" + description="Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$", ) as_json: bool = Field( - default=True, - description="If true : JSON output else : raw output" + default=True, description="If true : JSON output else : raw output" ) # @@ -864,7 +831,7 @@ class FirewallEnableVmRequest(BaseModel): ..., # default="4000", description="Virtual machine id", - pattern=r"^[0-9]+$" + pattern=r"^[0-9]+$", ) model_config = { @@ -881,7 +848,6 @@ class FirewallEnableVmRequest(BaseModel): class FirewallEnableVmItemReply(BaseModel): - action: Literal["vm_EnableFirewallVm_usage"] source: Literal["proxmox"] proxmox_node: str @@ -891,8 +857,8 @@ class FirewallEnableVmItemReply(BaseModel): vm_name: str vm_firewall: str -class FirewallEnableVmReply(BaseModel): +class FirewallEnableVmReply(BaseModel): rc: int = Field(0, description="RETURN code (0 = OK)") result: list[FirewallEnableVmItemReply] @@ -908,26 +874,24 @@ class FirewallEnableVmReply(BaseModel): ## "vm_id": "100", "vm_firewall": "enabled", - "vm_name": "test" + "vm_name": "test", } - ] + ], } } } class FirewallDisableVmRequest(BaseModel): - proxmox_node: str = Field( ..., # default= "px-testing", - description = "Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" + description="Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$", ) as_json: bool = Field( - default=True, - description="If true : JSON output else : raw output" + default=True, description="If true : JSON output else : raw output" ) # @@ -935,7 +899,7 @@ class FirewallDisableVmRequest(BaseModel): ..., # default="4000", description="Virtual machine id", - pattern=r"^[0-9]+$" + pattern=r"^[0-9]+$", ) model_config = { @@ -951,7 +915,6 @@ class FirewallDisableVmRequest(BaseModel): class FirewallDisableVmItemReply(BaseModel): - action: Literal["vm_EnableFirewallDc_usage"] source: Literal["proxmox"] proxmox_node: str @@ -959,8 +922,8 @@ class FirewallDisableVmItemReply(BaseModel): # vm_id: int = Field(..., ge=1) proxmox_api_host: str -class FirewallDisableVmReply(BaseModel): +class FirewallDisableVmReply(BaseModel): rc: int = Field(0, description="RETURN code (0 = OK)") result: list[FirewallDisableVmItemReply] @@ -976,9 +939,9 @@ class FirewallDisableVmReply(BaseModel): ## "vm_id": "1000", "vm_firewall": "disable", - "vm_name": "test" + "vm_name": "test", } - ] + ], } } } @@ -1000,12 +963,16 @@ class FirewallDisableVmReply(BaseModel): # firewall/delete_iptables_alias.py Request_ProxmoxFirewall_DeleteIptablesAlias = FirewallAliasDeleteRequest -Reply_ProxmoxFirewallWithStorageName_DeleteIptablesAliasItem = FirewallAliasDeleteItemReply +Reply_ProxmoxFirewallWithStorageName_DeleteIptablesAliasItem = ( + FirewallAliasDeleteItemReply +) Reply_ProxmoxFirewallWithStorageName_DeleteIptablesAlias = FirewallAliasDeleteReply # firewall/delete_iptables_rule.py Request_ProxmoxFirewall_DeleteIptablesRule = FirewallRuleDeleteRequest -Reply_ProxmoxFirewallWithStorageName_DeleteIptablesRuleItem = FirewallRuleDeleteItemReply +Reply_ProxmoxFirewallWithStorageName_DeleteIptablesRuleItem = ( + FirewallRuleDeleteItemReply +) Reply_ProxmoxFirewallWithStorageName_DeleteIptablesRule = FirewallRuleDeleteReply # firewall/list_iptables_alias.py @@ -1030,12 +997,16 @@ class FirewallDisableVmReply(BaseModel): # firewall/enable_firewall_node.py Request_ProxmoxFirewall_EnableFirewallNode = FirewallEnableNodeRequest -Reply_ProxmoxFirewallWithStorageName_EnableFirewallNodeItem = FirewallEnableNodeItemReply +Reply_ProxmoxFirewallWithStorageName_EnableFirewallNodeItem = ( + FirewallEnableNodeItemReply +) Reply_ProxmoxFirewallWithStorageName_EnableFirewallNode = FirewallEnableNodeReply # firewall/disable_firewall_node.py Request_ProxmoxFirewall_DistableFirewallNode = FirewallDisableNodeRequest -Reply_ProxmoxFirewallWithStorageName_DistableFirewallNodeItem = FirewallDisableNodeItemReply +Reply_ProxmoxFirewallWithStorageName_DistableFirewallNodeItem = ( + FirewallDisableNodeItemReply +) Reply_ProxmoxFirewallWithStorageName_DisableFirewallNode = FirewallDisableNodeReply # firewall/enable_firewall_vm.py diff --git a/app/schemas/network.py b/app/schemas/network.py index 1c7b231..6f5230f 100644 --- a/app/schemas/network.py +++ b/app/schemas/network.py @@ -1,96 +1,88 @@ """Consolidated network schemas: node and VM network interface operations.""" -from typing import List, Literal -from pydantic import BaseModel, Field +from typing import Literal +from pydantic import BaseModel, Field # --------------------------------------------------------------------------- # Node — Add Network Interface # --------------------------------------------------------------------------- -class NodeNetworkAddRequest(BaseModel): +class NodeNetworkAddRequest(BaseModel): proxmox_node: str = Field( ..., # default= "px-testing", - description = "Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" + description="Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$", ) as_json: bool = Field( - default=True, - description="If true : JSON output else : raw output" + default=True, description="If true : JSON output else : raw output" ) # bridge_ports: str | None = Field( - default=None, - description="Bridge ports", - pattern=r"^[a-zA-Z0-9._-]+$" + default=None, description="Bridge ports", pattern=r"^[a-zA-Z0-9._-]+$" ) iface_name: str | None = Field( - ..., - description="Interface name", - pattern=r"^[a-zA-Z0-9._-]+$" + ..., description="Interface name", pattern=r"^[a-zA-Z0-9._-]+$" ) iface_type: str | None = Field( ..., description="Interface type - ethernet, ovs, bridge", - pattern=r"^[a-zA-Z]+$" + pattern=r"^[a-zA-Z]+$", ) iface_autostart: int | None = Field( - ..., - description="Autostart flag - 0 = no, 1 = yes" + ..., description="Autostart flag - 0 = no, 1 = yes" ) ip_address: str | None = Field( default=None, description="ipv4 address", - pattern=r"^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$" + pattern=r"^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$", ) ip_netmask: str | None = Field( default=None, description="ipv4 netmask", - pattern=r"^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$|^\/[0-9]{1,2}$" + pattern=r"^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$|^\/[0-9]{1,2}$", ) ip_gateway: str | None = Field( default=None, description="ipv4 gateway", - pattern=r"^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$" + pattern=r"^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$", ) ovs_bridge: str | None = Field( - default=None, - description="OVS bridge name", - pattern=r"^[a-zA-Z0-9._-]+$" + default=None, description="OVS bridge name", pattern=r"^[a-zA-Z0-9._-]+$" ) model_config = { "json_schema_extra": { - "example":[ { - "proxmox_node": "px-testing", - "as_json": "true", - # - "iface_name": "vmbr142", - "iface_type": "bridge", - "bridge_ports": "enp87s0", - "iface_autostart": 1, - "ip_address": "192.168.99.2", - "ip_netmask": "255.255.255.0" - }, + "example": [ + { + "proxmox_node": "px-testing", + "as_json": "true", + # + "iface_name": "vmbr142", + "iface_type": "bridge", + "bridge_ports": "enp87s0", + "iface_autostart": 1, + "ip_address": "192.168.99.2", + "ip_netmask": "255.255.255.0", + }, ] } } class NodeNetworkAddItemReply(BaseModel): - action: Literal["vm_DeleteIptablesRule_usage"] source: Literal["proxmox"] proxmox_node: str @@ -107,7 +99,6 @@ class NodeNetworkAddItemReply(BaseModel): class NodeNetworkAddReply(BaseModel): - rc: int = Field(0, description="RETURN code (0 = OK)") result: list[NodeNetworkAddItemReply] @@ -117,7 +108,6 @@ class NodeNetworkAddReply(BaseModel): "rc": 0, "result": [ { - "action": "network_add_interfaces_node", "proxmox_node": "px-testing", "source": "proxmox", @@ -128,7 +118,7 @@ class NodeNetworkAddReply(BaseModel): "ip_address": "192.168.99.2", "ip_netmask": "255.255.255.0", } - ] + ], } } } @@ -138,24 +128,22 @@ class NodeNetworkAddReply(BaseModel): # Node — Delete Network Interface # --------------------------------------------------------------------------- -class NodeNetworkDeleteRequest(BaseModel): +class NodeNetworkDeleteRequest(BaseModel): proxmox_node: str = Field( ..., # default= "px-testing", - description = "Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" + description="Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$", ) # iface_name: str | None = Field( - description="Interface name", - pattern=r"^[a-zA-Z0-9._-]+$" + description="Interface name", pattern=r"^[a-zA-Z0-9._-]+$" ) as_json: bool = Field( - default=True, - description="If true : JSON output else : raw output" + default=True, description="If true : JSON output else : raw output" ) model_config = { @@ -165,22 +153,21 @@ class NodeNetworkDeleteRequest(BaseModel): "storage_name": "local", "as_json": True, # - "iface_name":"vmbr42", + "iface_name": "vmbr42", } } } class NodeNetworkDeleteItemReply(BaseModel): - action: Literal["vm_DeleteIptablesRule_usage"] source: Literal["proxmox"] proxmox_node: str ## iface_name: str -class NodeNetworkDeleteReply(BaseModel): +class NodeNetworkDeleteReply(BaseModel): rc: int = Field(0, description="RETURN code (0 = OK)") result: list[NodeNetworkDeleteItemReply] @@ -196,7 +183,7 @@ class NodeNetworkDeleteReply(BaseModel): ## "iface_name": "vmbr42", } - ] + ], } } } @@ -206,18 +193,17 @@ class NodeNetworkDeleteReply(BaseModel): # Node — List Network Interfaces # --------------------------------------------------------------------------- -class NodeNetworkListRequest(BaseModel): +class NodeNetworkListRequest(BaseModel): proxmox_node: str = Field( ..., # default= "px-testing", - description = "Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" + description="Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$", ) as_json: bool = Field( - default=True, - description="If true : JSON output else : raw output" + default=True, description="If true : JSON output else : raw output" ) # @@ -232,7 +218,6 @@ class NodeNetworkListRequest(BaseModel): class NodeNetworkListItemReply(BaseModel): - action: Literal["vm_DeleteIptablesRule_usage"] source: Literal["proxmox"] proxmox_node: str @@ -241,8 +226,8 @@ class NodeNetworkListItemReply(BaseModel): vm_id: str vm_fw_pos: int -class NodeNetworkListReply(BaseModel): +class NodeNetworkListReply(BaseModel): rc: int = Field(0, description="RETURN code (0 = OK)") result: list[NodeNetworkListItemReply] @@ -258,9 +243,9 @@ class NodeNetworkListReply(BaseModel): "ip_settings_method": "manual", "ip_settings_method6": "manual", "proxmox_node": "px-testing", - "source": "proxmox" + "source": "proxmox", }, - ] + ], } } } @@ -270,18 +255,17 @@ class NodeNetworkListReply(BaseModel): # VM — Add Network Interface # --------------------------------------------------------------------------- -class VmNetworkAddRequest(BaseModel): +class VmNetworkAddRequest(BaseModel): proxmox_node: str = Field( ..., # default= "px-testing", - description = "Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" + description="Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$", ) as_json: bool = Field( - default=True, - description="If true : JSON output else : raw output" + default=True, description="If true : JSON output else : raw output" ) # @@ -289,24 +273,22 @@ class VmNetworkAddRequest(BaseModel): ..., # default="4000", description="Virtual machine id", - pattern=r"^[0-9]+$" + pattern=r"^[0-9]+$", ) # quick classic fields iface_model: str | None = Field( description="Interface model- virtio, e1000, rtl8139", - pattern=r"^[A-Za-z0-9._-]+$" + pattern=r"^[A-Za-z0-9._-]+$", ) iface_bridge: str | None = Field( description="Bridge name for interface - vmbr0, vmbr142", - pattern=r"^[A-Za-z0-9._-]+$" + pattern=r"^[A-Za-z0-9._-]+$", ) - vm_vmnet_id: int | None = Field( - description="Network device index - 0, 1, 2, ..." - ) + vm_vmnet_id: int | None = Field(description="Network device index - 0, 1, 2, ...") #### below fields to test : @@ -314,30 +296,22 @@ class VmNetworkAddRequest(BaseModel): description="Enable trunk - allow multiple vlan on interface" ) - iface_tag: int | None = Field( - description="VLAN tag id" - ) + iface_tag: int | None = Field(description="VLAN tag id") - iface_rate: float | None = Field( - description="Limit bandwith - Mbps - 0 to x" - ) + iface_rate: float | None = Field(description="Limit bandwith - Mbps - 0 to x") iface_queues: int | None = Field( description="Allocated amount allocated tx/rx on interface" ) - iface_mtu: int | None = Field( - description="MTU" - ) + iface_mtu: int | None = Field(description="MTU") iface_macaddr: str | None = Field( description="MAC address - hexa format", - pattern = r'^(?:[0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$' + pattern=r"^(?:[0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$", ) - iface_link_down: bool | None = Field( - description="Force to set down the interface" - ) + iface_link_down: bool | None = Field(description="Force to set down the interface") iface_firewall: bool | None = Field( description="Apply firewall rules on this interface" @@ -360,7 +334,6 @@ class VmNetworkAddRequest(BaseModel): class VmNetworkAddItemReply(BaseModel): - action: Literal["vm_DeleteIptablesRule_usage"] source: Literal["proxmox"] proxmox_node: str @@ -369,8 +342,8 @@ class VmNetworkAddItemReply(BaseModel): vm_id: str vm_fw_pos: int -class VmNetworkAddReply(BaseModel): +class VmNetworkAddReply(BaseModel): rc: int = Field(0, description="RETURN code (0 = OK)") result: list[VmNetworkAddItemReply] @@ -387,7 +360,7 @@ class VmNetworkAddReply(BaseModel): "vm_id": "1000", "iface_model": "virtio", } - ] + ], } } } @@ -397,18 +370,17 @@ class VmNetworkAddReply(BaseModel): # VM — Delete Network Interface # --------------------------------------------------------------------------- -class VmNetworkDeleteRequest(BaseModel): +class VmNetworkDeleteRequest(BaseModel): proxmox_node: str = Field( ..., # default= "px-testing", - description = "Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" + description="Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$", ) as_json: bool = Field( - default=True, - description="If true : JSON output else : raw output" + default=True, description="If true : JSON output else : raw output" ) # @@ -416,12 +388,10 @@ class VmNetworkDeleteRequest(BaseModel): ..., # default="4000", description="Virtual machine id", - pattern=r"^[0-9]+$" + pattern=r"^[0-9]+$", ) - vm_vmnet_id: int | None = Field( - description="Network device index - 0, 1, 2, ..." - ) + vm_vmnet_id: int | None = Field(description="Network device index - 0, 1, 2, ...") model_config = { "json_schema_extra": { @@ -429,17 +399,15 @@ class VmNetworkDeleteRequest(BaseModel): "proxmox_node": "px-testing", "storage_name": "local", "as_json": True, -# - "vm_id":"1000", - "vm_vmnet_id":1, - + # + "vm_id": "1000", + "vm_vmnet_id": 1, } } } class VmNetworkDeleteItemReply(BaseModel): - action: Literal["vm_DeleteIptablesRule_usage"] source: Literal["proxmox"] proxmox_node: str @@ -448,8 +416,8 @@ class VmNetworkDeleteItemReply(BaseModel): vm_id: str vm_fw_pos: int -class VmNetworkDeleteReply(BaseModel): +class VmNetworkDeleteReply(BaseModel): rc: int = Field(0, description="RETURN code (0 = OK)") result: list[VmNetworkDeleteItemReply] @@ -464,10 +432,9 @@ class VmNetworkDeleteReply(BaseModel): "proxmox_node": "px-testing", ## "vm_id": "1000", - "iface_model": "virtio" - - } - ] + "iface_model": "virtio", + } + ], } } } @@ -477,18 +444,17 @@ class VmNetworkDeleteReply(BaseModel): # VM — List Network Interfaces # --------------------------------------------------------------------------- -class VmNetworkListRequest(BaseModel): +class VmNetworkListRequest(BaseModel): proxmox_node: str = Field( ..., # default= "px-testing", - description = "Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" + description="Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$", ) as_json: bool = Field( - default=True, - description="If true : JSON output else : raw output" + default=True, description="If true : JSON output else : raw output" ) # @@ -496,7 +462,7 @@ class VmNetworkListRequest(BaseModel): ..., # default="4000", description="Virtual machine id", - pattern=r"^[0-9]+$" + pattern=r"^[0-9]+$", ) model_config = { @@ -511,7 +477,6 @@ class VmNetworkListRequest(BaseModel): class VmNetworkListItemReply(BaseModel): - action: Literal["vm_DeleteIptablesRule_usage"] source: Literal["proxmox"] proxmox_node: str @@ -519,8 +484,8 @@ class VmNetworkListItemReply(BaseModel): # vm_id: int = Field(..., ge=1) vm_id: str -class VmNetworkListReply(BaseModel): +class VmNetworkListReply(BaseModel): rc: int = Field(0, description="RETURN code (0 = OK)") result: list[VmNetworkListItemReply] @@ -538,10 +503,9 @@ class VmNetworkListReply(BaseModel): "vm_network_bridge": "vmbr0", "vm_network_device": "net0", "vm_network_mac": "AA:BB:CC:DD:EE:FF", - "vm_network_type": "virtio" - + "vm_network_type": "virtio", } - ] + ], } } } diff --git a/app/schemas/snapshots.py b/app/schemas/snapshots.py index 25ab9e2..a06882d 100644 --- a/app/schemas/snapshots.py +++ b/app/schemas/snapshots.py @@ -1,25 +1,24 @@ """Consolidated snapshot schemas: create, delete, list, revert.""" from typing import Literal -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field # --------------------------------------------------------------------------- # Snapshot Create # --------------------------------------------------------------------------- -class SnapshotCreateRequest(BaseModel): +class SnapshotCreateRequest(BaseModel): proxmox_node: str = Field( ..., # default= "px-testing", - description = "Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" + description="Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$", ) as_json: bool = Field( - default=True, - description="If true : JSON output else : raw output" + default=True, description="If true : JSON output else : raw output" ) # @@ -27,19 +26,19 @@ class SnapshotCreateRequest(BaseModel): ..., # default="4000", description="Virtual machine id", - pattern=r"^[0-9]+$" + pattern=r"^[0-9]+$", ) vm_snapshot_name: str | None = Field( default=None, description="Name of the snapshot to create", - pattern=r"^[A-Za-z0-9_-]+$" + pattern=r"^[A-Za-z0-9_-]+$", ) vm_snapshot_description: str | None = Field( default=None, description="Optional description for the snapshot", - pattern=r"^[A-Za-z0-9_-]+$" + pattern=r"^[A-Za-z0-9_-]+$", ) model_config = { @@ -47,20 +46,19 @@ class SnapshotCreateRequest(BaseModel): "example": { "proxmox_node": "px-testing", "vm_id": "1111", - "vm_snapshot_name":"MY_VM_SNAPSHOT", - "vm_snapshot_description":"MY_DESCRIPTION", - "as_json": True + "vm_snapshot_name": "MY_VM_SNAPSHOT", + "vm_snapshot_description": "MY_DESCRIPTION", + "as_json": True, } } } class SnapshotCreateItemReply(BaseModel): - action: Literal["vm_get_config"] proxmox_node: str source: Literal["proxmox"] - vm_id: str # int = Field(..., ge=1) + vm_id: str # int = Field(..., ge=1) vm_name: str vm_snapshot_description: str @@ -70,7 +68,6 @@ class SnapshotCreateItemReply(BaseModel): class SnapshotCreateReply(BaseModel): - rc: int = Field(0, description="RETURN code (0 = OK)") result: list[SnapshotCreateItemReply] @@ -83,14 +80,15 @@ class SnapshotCreateReply(BaseModel): "action": "snapshot_vm_create", "proxmox_node": "px-testing", "source": "proxmox", - "vm_id": "1000", "vm_name": "admin-wazuh", "vm_snapshot_description": "MY_DESCRIPTION", "vm_snapshot_name": "MY_VM_SNAPSHOT", - "raw_data": { "data": "UPID:px-testing:002D5E30:1706941B:68C196E9:qmsnapshot:1000:API_master@pam!API_master:" } + "raw_data": { + "data": "UPID:px-testing:002D5E30:1706941B:68C196E9:qmsnapshot:1000:API_master@pam!API_master:" + }, } - ] + ], } } } @@ -100,18 +98,17 @@ class SnapshotCreateReply(BaseModel): # Snapshot Delete # --------------------------------------------------------------------------- -class SnapshotDeleteRequest(BaseModel): +class SnapshotDeleteRequest(BaseModel): proxmox_node: str = Field( ..., # default= "px-testing", - description = "Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" + description="Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$", ) as_json: bool = Field( - default=True, - description="If true : JSON output else : raw output" + default=True, description="If true : JSON output else : raw output" ) # @@ -119,13 +116,13 @@ class SnapshotDeleteRequest(BaseModel): ..., # default="4000", description="Virtual machine id", - pattern=r"^[0-9]+$" + pattern=r"^[0-9]+$", ) vm_snapshot_name: str | None = Field( default=None, description="Name of the snapshot to delete", - pattern=r"^[A-Za-z0-9_-]+$" + pattern=r"^[A-Za-z0-9_-]+$", ) model_config = { @@ -134,14 +131,13 @@ class SnapshotDeleteRequest(BaseModel): "proxmox_node": "px-testing", "vm_id": "1111", "vm_snapshot_name": "MY_VM_SNAPSHOT", - "as_json": True + "as_json": True, } } } class SnapshotDeleteItemReply(BaseModel): - action: Literal["vm_get_config"] source: Literal["proxmox"] # proxmox_node: str @@ -151,7 +147,6 @@ class SnapshotDeleteItemReply(BaseModel): class SnapshotDeleteReply(BaseModel): - rc: int = Field(0, description="RETURN code (0 = OK)") result: list[SnapshotDeleteItemReply] @@ -159,18 +154,19 @@ class SnapshotDeleteReply(BaseModel): "json_schema_extra": { "example": { "rc": 0, - "result": [ + "result": [ { "action": "snapshot_vm_delete", "proxmox_node": "px-testing", "source": "proxmox", - "vm_id": "1000", "vm_name": "admin-wazuh", "vm_snapshot_name": "BBBB", - "raw_data": { "data": "UPID:px-testing:002D6878:17077370:68C19925:qmdelsnapshot:1000:API_master@pam!API_master:"}, + "raw_data": { + "data": "UPID:px-testing:002D6878:17077370:68C19925:qmdelsnapshot:1000:API_master@pam!API_master:" + }, } - ] + ], } } } @@ -180,18 +176,17 @@ class SnapshotDeleteReply(BaseModel): # Snapshot List # --------------------------------------------------------------------------- -class SnapshotListRequest(BaseModel): +class SnapshotListRequest(BaseModel): proxmox_node: str = Field( ..., # default= "px-testing", - description = "Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" + description="Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$", ) as_json: bool = Field( - default=True, - description="If true : JSON output else : raw output" + default=True, description="If true : JSON output else : raw output" ) # @@ -199,21 +194,17 @@ class SnapshotListRequest(BaseModel): ..., # default="4000", description="Virtual machine id", - pattern=r"^[0-9]+$" + pattern=r"^[0-9]+$", ) model_config = { "json_schema_extra": { - "example": { - "proxmox_node": "px-testing", - "vm_id": "1111" - } + "example": {"proxmox_node": "px-testing", "vm_id": "1111"} } } class SnapshotListItemReply(BaseModel): - action: Literal["vm_get_config"] source: Literal["proxmox"] # proxmox_node: str @@ -223,7 +214,6 @@ class SnapshotListItemReply(BaseModel): class SnapshotListReply(BaseModel): - rc: int = Field(0, description="RETURN code (0 = OK)") result: list[SnapshotListItemReply] @@ -231,31 +221,28 @@ class SnapshotListReply(BaseModel): "json_schema_extra": { "example": { "rc": 0, - "result": [ { "action": "snapshot_vm_list", "proxmox_node": "px-testing", "source": "proxmox", - "vm_id": "1000", "vm_snapshot_description": "MY_DESCRIPTION", "vm_snapshot_name": "MY_VM_SNAPSHOT", "vm_snapshot_parent": "", - "vm_snapshot_time": 1757517545 + "vm_snapshot_time": 1757517545, }, { "action": "snapshot_vm_list", "proxmox_node": "px-testing", "source": "proxmox", - "vm_id": "1000", "vm_snapshot_description": "You are here!", "vm_snapshot_name": "current", "vm_snapshot_parent": "MY_VM_SNAPSHOT", - "vm_snapshot_sha1": "7cc59c988bb8f18601fe076ad239f8b760667270" - } - ] + "vm_snapshot_sha1": "7cc59c988bb8f18601fe076ad239f8b760667270", + }, + ], } } } @@ -265,18 +252,17 @@ class SnapshotListReply(BaseModel): # Snapshot Revert # --------------------------------------------------------------------------- -class SnapshotRevertRequest(BaseModel): +class SnapshotRevertRequest(BaseModel): proxmox_node: str = Field( ..., # default= "px-testing", - description = "Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" + description="Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$", ) as_json: bool = Field( - default=True, - description="If true : JSON output else : raw output" + default=True, description="If true : JSON output else : raw output" ) # @@ -284,13 +270,13 @@ class SnapshotRevertRequest(BaseModel): ..., # default="4000", description="Virtual machine id", - pattern=r"^[0-9]+$" + pattern=r"^[0-9]+$", ) vm_snapshot_name: str | None = Field( default=None, description="Name of the snapshot to create", - pattern=r"^[A-Za-z0-9_-]+$" + pattern=r"^[A-Za-z0-9_-]+$", ) model_config = { @@ -298,7 +284,7 @@ class SnapshotRevertRequest(BaseModel): "example": { "proxmox_node": "px-testing", "vm_id": "1111", - "vm_snapshot_name":"CCCC", + "vm_snapshot_name": "CCCC", "as_json": True, } } @@ -306,7 +292,6 @@ class SnapshotRevertRequest(BaseModel): class SnapshotRevertItemReply(BaseModel): - action: Literal["vm_get_config"] source: Literal["proxmox"] # proxmox_node: str @@ -316,7 +301,6 @@ class SnapshotRevertItemReply(BaseModel): class SnapshotRevertReply(BaseModel): - rc: int = Field(0, description="RETURN code (0 = OK)") result: list[SnapshotRevertItemReply] @@ -329,13 +313,14 @@ class SnapshotRevertReply(BaseModel): "action": "snapshot_vm_revert", "source": "proxmox", "proxmox_node": "px-testing", - "vm_id": "1000", "vm_name": "admin-wazuh", "vm_snapshot_name": "CCCC", - "raw_data": {"data": "UPID:px-testing:002D7C57:17096777:68C19E25:qmrollback:1000:API_master@pam!API_master:"}, + "raw_data": { + "data": "UPID:px-testing:002D7C57:17096777:68C19E25:qmrollback:1000:API_master@pam!API_master:" + }, } - ] + ], } } } diff --git a/app/schemas/storage.py b/app/schemas/storage.py index 6e9e593..2295a6c 100644 --- a/app/schemas/storage.py +++ b/app/schemas/storage.py @@ -1,25 +1,24 @@ """Consolidated storage schemas: list, download ISO, list ISO, list templates.""" -from typing import List, Literal -from pydantic import BaseModel, Field +from typing import Literal +from pydantic import BaseModel, Field # --------------------------------------------------------------------------- # Storage List # --------------------------------------------------------------------------- -class StorageListRequest(BaseModel): +class StorageListRequest(BaseModel): proxmox_node: str = Field( ..., # default= "px-testing", description="Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" + pattern=r"^[A-Za-z0-9-]*$", ) as_json: bool = Field( - default=True, - description="If true : JSON output else : raw output" + default=True, description="If true : JSON output else : raw output" ) # @@ -27,7 +26,7 @@ class StorageListRequest(BaseModel): ..., # default= "px-testing", description="Proxmox storage name", - pattern=r"^[A-Za-z0-9-]*$" + pattern=r"^[A-Za-z0-9-]*$", ) model_config = { @@ -35,14 +34,13 @@ class StorageListRequest(BaseModel): "example": { "proxmox_node": "px-testing", "storage_name": "local", - "as_json": True + "as_json": True, } } } class StorageListItemReply(BaseModel): - action: Literal["storage_list"] source: Literal["proxmox"] proxmox_node: str @@ -51,15 +49,15 @@ class StorageListItemReply(BaseModel): storage_content_types: str storage_is_enable: int storage_is_share: int - storage_name : str + storage_name: str storage_space_available: int storage_space_total: int storage_space_used: int storage_space_used_fraction: float storage_type: str -class StorageListReply(BaseModel): +class StorageListReply(BaseModel): rc: int = Field(0, description="RETURN code (0 = OK)") result: list[StorageListItemReply] @@ -69,22 +67,22 @@ class StorageListReply(BaseModel): "rc": 0, "result": [ { - "action": "storage_list", - "proxmox_node": "px-testing", - "source": "proxmox", - # - "storage_active": 1, - "storage_content_types": "images,rootdir", - "storage_is_enable": 1, - "storage_is_share": 0, - "storage_name": "local-lvm", - "storage_space_available": 3600935440666, - "storage_space_total": 3836496314368, - "storage_space_used": 235560873702, - "storage_space_used_fraction": 0.0613999999999491, - "storage_type": "lvmthin" + "action": "storage_list", + "proxmox_node": "px-testing", + "source": "proxmox", + # + "storage_active": 1, + "storage_content_types": "images,rootdir", + "storage_is_enable": 1, + "storage_is_share": 0, + "storage_name": "local-lvm", + "storage_space_available": 3600935440666, + "storage_space_total": 3836496314368, + "storage_space_used": 235560873702, + "storage_space_used_fraction": 0.0613999999999491, + "storage_type": "lvmthin", } - ] + ], } } } @@ -94,18 +92,17 @@ class StorageListReply(BaseModel): # Download ISO # --------------------------------------------------------------------------- -class StorageDownloadIsoRequest(BaseModel): +class StorageDownloadIsoRequest(BaseModel): proxmox_node: str = Field( ..., # default= "px-testing", - description = "Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" + description="Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$", ) as_json: bool = Field( - default=True, - description="If true : JSON output else : raw output" + default=True, description="If true : JSON output else : raw output" ) # @@ -118,7 +115,7 @@ class StorageDownloadIsoRequest(BaseModel): iso_file_content_type: str = Field( ..., description="MIME type of the ISO file", - pattern=r"^[A-Za-z0-9-]*$" + pattern=r"^[A-Za-z0-9-]*$", # pattern = r"^application/(?:x-)?iso9660-image$", ) @@ -150,7 +147,6 @@ class StorageDownloadIsoRequest(BaseModel): class StorageDownloadIsoItemReply(BaseModel): - action: Literal["storage_download_iso"] source: Literal["proxmox"] proxmox_node: str @@ -170,8 +166,8 @@ class StorageDownloadIsoItemReply(BaseModel): vm_status: str vm_uptime: int -class StorageDownloadIsoReply(BaseModel): +class StorageDownloadIsoReply(BaseModel): rc: int = Field(0, description="RETURN code (0 = OK)") result: list[StorageDownloadIsoItemReply] @@ -181,22 +177,22 @@ class StorageDownloadIsoReply(BaseModel): "rc": 0, "result": [ { - "cpu_allocated":1, - "cpu_current_usage":0, - "disk_current_usage":0, - "disk_max":34359738368, - "disk_read":0, - "disk_write":0, - "net_in":280531583, - "net_out":6330590, - "ram_current_usage":1910544625, - "ram_max":4294967296, - "vm_id":1020, - "vm_name":"admin-web-api-kong", - "vm_status":"running", - "vm_uptime":79940 + "cpu_allocated": 1, + "cpu_current_usage": 0, + "disk_current_usage": 0, + "disk_max": 34359738368, + "disk_read": 0, + "disk_write": 0, + "net_in": 280531583, + "net_out": 6330590, + "ram_current_usage": 1910544625, + "ram_max": 4294967296, + "vm_id": 1020, + "vm_name": "admin-web-api-kong", + "vm_status": "running", + "vm_uptime": 79940, } - ] + ], } } } @@ -206,26 +202,25 @@ class StorageDownloadIsoReply(BaseModel): # List ISO # --------------------------------------------------------------------------- -class StorageListIsoRequest(BaseModel): +class StorageListIsoRequest(BaseModel): proxmox_node: str = Field( ..., # default= "px-testing", - description = "Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" + description="Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$", ) as_json: bool = Field( - default=True, - description="If true : JSON output else : raw output" + default=True, description="If true : JSON output else : raw output" ) # storage_name: str = Field( ..., # default= "px-testing", - description = "Proxmox storage name", - pattern=r"^[A-Za-z0-9-]*$" + description="Proxmox storage name", + pattern=r"^[A-Za-z0-9-]*$", ) model_config = { @@ -233,15 +228,13 @@ class StorageListIsoRequest(BaseModel): "example": { "proxmox_node": "px-testing", "storage_name": "local", - "as_json": True - + "as_json": True, } } } class StorageListIsoItemReply(BaseModel): - action: Literal["storage_list_iso"] source: Literal["proxmox"] proxmox_node: str @@ -255,8 +248,8 @@ class StorageListIsoItemReply(BaseModel): local: str storage_name: str -class StorageListIsoReply(BaseModel): +class StorageListIsoReply(BaseModel): rc: int = Field(0, description="RETURN code (0 = OK)") result: list[StorageListIsoItemReply] @@ -274,11 +267,10 @@ class StorageListIsoReply(BaseModel): "iso_ctime": 1753343734, "iso_format": "iso", "iso_size": 614746112, - "iso_vol_id": - "local:iso/noble-server-cloudimg-amd64.img", + "iso_vol_id": "local:iso/noble-server-cloudimg-amd64.img", "storage_name": "local", } - ] + ], } } } @@ -288,26 +280,25 @@ class StorageListIsoReply(BaseModel): # List Templates # --------------------------------------------------------------------------- -class StorageListTemplateRequest(BaseModel): +class StorageListTemplateRequest(BaseModel): proxmox_node: str = Field( ..., # default= "px-testing", - description = "Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" + description="Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$", ) as_json: bool = Field( - default=True, - description="If true : JSON output else : raw output" + default=True, description="If true : JSON output else : raw output" ) # storage_name: str = Field( ..., # default= "px-testing", - description = "Proxmox storage name", - pattern=r"^[A-Za-z0-9-]*$" + description="Proxmox storage name", + pattern=r"^[A-Za-z0-9-]*$", ) model_config = { @@ -315,15 +306,13 @@ class StorageListTemplateRequest(BaseModel): "example": { "proxmox_node": "px-testing", "storage_name": "local", - "as_json": True - + "as_json": True, } } } class StorageListTemplateItemReply(BaseModel): - action: Literal["storage_list_template"] source: Literal["proxmox"] proxmox_node: str @@ -336,8 +325,8 @@ class StorageListTemplateItemReply(BaseModel): template_size: int template_vol_id: str -class StorageListTemplateReply(BaseModel): +class StorageListTemplateReply(BaseModel): rc: int = Field(0, description="RETURN code (0 = OK)") result: list[StorageListTemplateItemReply] @@ -346,19 +335,19 @@ class StorageListTemplateReply(BaseModel): "example": { "rc": 0, "result": [ - { - "action": "storage_list_template", - "proxmox_node": "px-testing", - "source": "proxmox", - # - "storage_name": "local", - "template_content": "vztmpl", - "template_ctime": 1749734175, - "template_format": "tzst", - "template_size": 126515062, - "template_vol_id": "local:vztmpl/debian-12-standard_12.7-1_amd64.tar.zst" - } - ] + { + "action": "storage_list_template", + "proxmox_node": "px-testing", + "source": "proxmox", + # + "storage_name": "local", + "template_content": "vztmpl", + "template_ctime": 1749734175, + "template_format": "tzst", + "template_size": 126515062, + "template_vol_id": "local:vztmpl/debian-12-standard_12.7-1_amd64.tar.zst", + } + ], } } } diff --git a/app/schemas/vm_config.py b/app/schemas/vm_config.py index fb44612..16a7dee 100644 --- a/app/schemas/vm_config.py +++ b/app/schemas/vm_config.py @@ -1,25 +1,24 @@ """Consolidated VM config schemas: get config, get cdrom, get cpu, get ram, set tag.""" from typing import Literal -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field # --------------------------------------------------------------------------- # VM Get Config # --------------------------------------------------------------------------- -class VmGetConfigRequest(BaseModel): +class VmGetConfigRequest(BaseModel): proxmox_node: str = Field( ..., # default= "px-testing", - description = "Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" + description="Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$", ) as_json: bool = Field( - default=True, - description="If true : JSON output else : raw output" + default=True, description="If true : JSON output else : raw output" ) # @@ -27,7 +26,7 @@ class VmGetConfigRequest(BaseModel): ..., # default="4000", description="Virtual machine id", - pattern=r"^[0-9]+$" + pattern=r"^[0-9]+$", ) model_config = { @@ -42,7 +41,6 @@ class VmGetConfigRequest(BaseModel): class VmGetConfigItemReply(BaseModel): - action: Literal["vm_get_config"] source: Literal["proxmox"] proxmox_node: str @@ -52,7 +50,6 @@ class VmGetConfigItemReply(BaseModel): class VmGetConfigReply(BaseModel): - rc: int = Field(0, description="RETURN code (0 = OK)") result: list[VmGetConfigItemReply] @@ -88,13 +85,13 @@ class VmGetConfigReply(BaseModel): "sshkeys": "ssh-ed25519%20AAAAC....redacted", "tags": "admin", "vga": "serial0", - "vmgenid": "c7426562-ad4b-4719-81a1-72328f7ec018" + "vmgenid": "c7426562-ad4b-4719-81a1-72328f7ec018", } }, "source": "proxmox", - "vm_id": "1000" + "vm_id": "1000", } - ] + ], } } } @@ -104,18 +101,17 @@ class VmGetConfigReply(BaseModel): # VM Get Config CDROM # --------------------------------------------------------------------------- -class VmGetConfigCdromRequest(BaseModel): +class VmGetConfigCdromRequest(BaseModel): proxmox_node: str = Field( ..., # default= "px-testing", - description = "Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" + description="Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$", ) as_json: bool = Field( - default=True, - description="If true : JSON output else : raw output" + default=True, description="If true : JSON output else : raw output" ) # @@ -123,7 +119,7 @@ class VmGetConfigCdromRequest(BaseModel): ..., # default="4000", description="Virtual machine id", - pattern=r"^[0-9]+$" + pattern=r"^[0-9]+$", ) model_config = { @@ -138,22 +134,20 @@ class VmGetConfigCdromRequest(BaseModel): class VmGetConfigCdromItemReply(BaseModel): - action: Literal["vm_get_config_cdrom"] source: Literal["proxmox"] - proxmox_node : str - vm_id : str # int = Field(..., ge=1) + proxmox_node: str + vm_id: str # int = Field(..., ge=1) vm_cdrom_device: str - vm_cdrom_iso : str - vm_cdrom_media : str - vm_cdrom_size : str + vm_cdrom_iso: str + vm_cdrom_media: str + vm_cdrom_size: str # vm_name: str # raw_data: str = Field(..., description="Raw string returned by proxmox") class VmGetConfigCdromReply(BaseModel): - rc: int = Field(0, description="RETURN code (0 = OK)") result: list[VmGetConfigCdromItemReply] @@ -170,9 +164,9 @@ class VmGetConfigCdromReply(BaseModel): "vm_cdrom_iso": "local:1000/vm-1000-cloudinit.qcow2", "vm_cdrom_media": "cdrom", "vm_cdrom_size": "4M", - "vm_id": "1000" + "vm_id": "1000", } - ] + ], } } } @@ -182,18 +176,17 @@ class VmGetConfigCdromReply(BaseModel): # VM Get Config CPU # --------------------------------------------------------------------------- -class VmGetConfigCpuRequest(BaseModel): +class VmGetConfigCpuRequest(BaseModel): proxmox_node: str = Field( ..., # default= "px-testing", - description = "Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" + description="Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$", ) as_json: bool = Field( - default=True, - description="If true : JSON output else : raw output" + default=True, description="If true : JSON output else : raw output" ) # @@ -201,7 +194,7 @@ class VmGetConfigCpuRequest(BaseModel): ..., # default="4000", description="Virtual machine id", - pattern=r"^[0-9]+$" + pattern=r"^[0-9]+$", ) model_config = { @@ -216,20 +209,18 @@ class VmGetConfigCpuRequest(BaseModel): class VmGetConfigCpuItemReply(BaseModel): - action: Literal["vm_get_config_cpu"] source: Literal["proxmox"] proxmox_node: str - vm_id : str # int = Field(..., ge=1) - vm_arch : str #to fix ? - vm_cores : str #to fix ? - vm_sockets : str #to fix ? + vm_id: str # int = Field(..., ge=1) + vm_arch: str # to fix ? + vm_cores: str # to fix ? + vm_sockets: str # to fix ? # vm_name: str # raw_data: str = Field(..., description="Raw string returned by proxmox") class VmGetConfigCpuReply(BaseModel): - rc: int = Field(0, description="RETURN code (0 = OK)") result: list[VmGetConfigCpuItemReply] @@ -239,15 +230,15 @@ class VmGetConfigCpuReply(BaseModel): "rc": 0, "result": [ { - "action":"vm_get_config_cpu", - "proxmox_node":"px-testing", - "source":"proxmox", - "vm_arch":"host", - "vm_cores":"2", - "vm_id":"1000", - "vm_sockets":"1" + "action": "vm_get_config_cpu", + "proxmox_node": "px-testing", + "source": "proxmox", + "vm_arch": "host", + "vm_cores": "2", + "vm_id": "1000", + "vm_sockets": "1", } - ] + ], } } } @@ -257,18 +248,17 @@ class VmGetConfigCpuReply(BaseModel): # VM Get Config RAM # --------------------------------------------------------------------------- -class VmGetConfigRamRequest(BaseModel): +class VmGetConfigRamRequest(BaseModel): proxmox_node: str = Field( ..., # default= "px-testing", - description = "Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" + description="Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$", ) as_json: bool = Field( - default=True, - description="If true : JSON output else : raw output" + default=True, description="If true : JSON output else : raw output" ) # @@ -276,7 +266,7 @@ class VmGetConfigRamRequest(BaseModel): ..., # default="4000", description="Virtual machine id", - pattern=r"^[0-9]+$" + pattern=r"^[0-9]+$", ) model_config = { @@ -291,19 +281,17 @@ class VmGetConfigRamRequest(BaseModel): class VmGetConfigRamItemReply(BaseModel): - action: Literal["vm_get_config_ram"] source: Literal["proxmox"] proxmox_node: str - vm_id : str # int = Field(..., ge=1) - vm_ram_allocated: str # wtf... - fix todo + vm_id: str # int = Field(..., ge=1) + vm_ram_allocated: str # wtf... - fix todo # vm_name: str # raw_data: str = Field(..., description="Raw string returned by proxmox") class VmGetConfigRamReply(BaseModel): - rc: int = Field(0, description="RETURN code (0 = OK)") result: list[VmGetConfigRamItemReply] @@ -317,9 +305,9 @@ class VmGetConfigRamReply(BaseModel): "proxmox_node": "px-testing", "source": "proxmox", "vm_id": "1000", - "vm_ram_allocated": "8192" + "vm_ram_allocated": "8192", } - ] + ], } } } @@ -329,17 +317,16 @@ class VmGetConfigRamReply(BaseModel): # VM Set Tag # --------------------------------------------------------------------------- -class VmSetTagRequest(BaseModel): +class VmSetTagRequest(BaseModel): proxmox_node: str = Field( ..., # default= "px-testing", - description = "Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" + description="Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$", ) as_json: bool = Field( - default=True, - description="If true : JSON output else : raw output" + default=True, description="If true : JSON output else : raw output" ) # @@ -347,13 +334,13 @@ class VmSetTagRequest(BaseModel): ..., # default="4000", description="Virtual machine id", - pattern=r"^[0-9]+$" + pattern=r"^[0-9]+$", ) vm_tag_name: str = Field( ..., description="Comma separated list of tags to assign to the virtual machine", - pattern=r"^[A-Za-z0-9_, -]+$" + pattern=r"^[A-Za-z0-9_, -]+$", ) model_config = { @@ -361,7 +348,7 @@ class VmSetTagRequest(BaseModel): "example": { "proxmox_node": "px-testing", "vm_id": "1111", - "vm_tag_name":"group_01,group_02", + "vm_tag_name": "group_01,group_02", "as_json": True, } } @@ -369,7 +356,6 @@ class VmSetTagRequest(BaseModel): class VmSetTagItemReply(BaseModel): - action: Literal["vm_get_config"] source: Literal["proxmox"] # proxmox_node: str @@ -379,7 +365,6 @@ class VmSetTagItemReply(BaseModel): class VmSetTagReply(BaseModel): - rc: int = Field(0, description="RETURN code (0 = OK)") result: list[VmSetTagItemReply] @@ -393,7 +378,7 @@ class VmSetTagReply(BaseModel): "source": "proxmox", "tags": "group_01,group_02", } - ] + ], } } } diff --git a/app/schemas/vms.py b/app/schemas/vms.py index ab4ddd8..1983139 100644 --- a/app/schemas/vms.py +++ b/app/schemas/vms.py @@ -2,39 +2,34 @@ from enum import Enum from typing import List, Literal -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field # --------------------------------------------------------------------------- # VM List # --------------------------------------------------------------------------- -class VmListRequest(BaseModel): +class VmListRequest(BaseModel): proxmox_node: str = Field( ..., # default= "px-testing", - description = "Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" + description="Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$", ) as_json: bool = Field( - default=True, - description="If true : JSON output else : raw output" + default=True, description="If true : JSON output else : raw output" ) model_config = { "json_schema_extra": { - "example": { - "proxmox_node": "px-testing", - "as_json": True - } + "example": {"proxmox_node": "px-testing", "as_json": True} } } class VmListActionEnum(str, Enum): - LIST = "vm_list" START = "vm_start" STOP = "vm_stop" @@ -44,14 +39,12 @@ class VmListActionEnum(str, Enum): class VmListStatusEnum(str, Enum): - RUNNING = "running" STOPPED = "stopped" PAUSED = "paused" class VmListMetaReply(BaseModel): - cpu_current_usage: int cpu_allocated: int disk_current_usage: int @@ -65,53 +58,46 @@ class VmListMetaReply(BaseModel): class VmListInfoReply(BaseModel): - - action: VmListActionEnum + action: VmListActionEnum source: str = Field("proxmox", description="data source provider") proxmox_node: str vm_name: str - vm_status: VmListStatusEnum + vm_status: VmListStatusEnum vm_id: int vm_uptime: int - vm_meta: VmListMetaReply + vm_meta: VmListMetaReply class VmListReply(BaseModel): - rc: int = Field(..., description="RETURN CODE (0 = OK) ") - result: List[List[ VmListInfoReply]] + result: List[List[VmListInfoReply]] # --------------------------------------------------------------------------- # VM List Usage # --------------------------------------------------------------------------- -class VmListUsageRequest(BaseModel): +class VmListUsageRequest(BaseModel): proxmox_node: str = Field( ..., # default= "px-testing", - description = "Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" + description="Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$", ) as_json: bool = Field( - default=True, - description="If true : JSON output else : raw output" + default=True, description="If true : JSON output else : raw output" ) model_config = { "json_schema_extra": { - "example": { - "proxmox_node": "px-testing", - "as_json": True - } + "example": {"proxmox_node": "px-testing", "as_json": True} } } class VmListUsageItemReply(BaseModel): - action: Literal["vm_list_usage"] source: Literal["proxmox"] proxmox_node: str @@ -133,7 +119,6 @@ class VmListUsageItemReply(BaseModel): class VmListUsageReply(BaseModel): - rc: int = Field(0, description="RETURN code (0 = OK)") result: list[VmListUsageItemReply] @@ -143,22 +128,22 @@ class VmListUsageReply(BaseModel): "rc": 0, "result": [ { - "cpu_allocated":1, - "cpu_current_usage":0, - "disk_current_usage":0, - "disk_max":34359738368, - "disk_read":0, - "disk_write":0, - "net_in":280531583, - "net_out":6330590, - "ram_current_usage":1910544625, - "ram_max":4294967296, - "vm_id":1020, - "vm_name":"admin-web-api-kong", - "vm_status":"running", - "vm_uptime":79940 + "cpu_allocated": 1, + "cpu_current_usage": 0, + "disk_current_usage": 0, + "disk_max": 34359738368, + "disk_read": 0, + "disk_write": 0, + "net_in": 280531583, + "net_out": 6330590, + "ram_current_usage": 1910544625, + "ram_max": 4294967296, + "vm_id": 1020, + "vm_name": "admin-web-api-kong", + "vm_status": "running", + "vm_uptime": 79940, } - ] + ], } } } @@ -168,18 +153,17 @@ class VmListUsageReply(BaseModel): # VM Create # --------------------------------------------------------------------------- -class VmCreateRequest(BaseModel): +class VmCreateRequest(BaseModel): proxmox_node: str = Field( ..., # default= "px-testing", - description = "Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" + description="Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$", ) as_json: bool = Field( - default=True, - description="If true : JSON output else : raw output" + default=True, description="If true : JSON output else : raw output" ) # @@ -187,54 +171,52 @@ class VmCreateRequest(BaseModel): ..., # default="4000", description="Virtual machine id", - pattern=r"^[0-9]+$" + pattern=r"^[0-9]+$", ) - vm_name: str = Field( + vm_name: str = Field( ..., # default="new-vm", description="Virtual machine meta name", - pattern = r"^[A-Za-z0-9-]*$" + pattern=r"^[A-Za-z0-9-]*$", ) vm_cpu: str = Field( ..., # default= "host", - description='CPU type/model - host)', - pattern=r"^[A-Za-z0-9._-]+$" + description="CPU type/model - host)", + pattern=r"^[A-Za-z0-9._-]+$", ) vm_cores: int = Field( ..., # default=1, ge=1, - description="Number of cores per socket" + description="Number of cores per socket", ) vm_sockets: int = Field( ..., # default=1, ge=1, - description="Number of CPU sockets" + description="Number of CPU sockets", ) vm_memory: int = Field( ..., # default=1024, ge=128, - description="Memory in MiB" + description="Memory in MiB", ) vm_disk_size: int | None = Field( - default=None, - ge=1, - description="Disk size in GiB - optional" + default=None, ge=1, description="Disk size in GiB - optional" ) vm_iso: str | None = Field( default=None, description="ISO volume path like 'local:iso/xxx.iso' - optional", - pattern=r"^[A-Za-z0-9._-]+:iso/.+\.iso$" + pattern=r"^[A-Za-z0-9._-]+:iso/.+\.iso$", ) model_config = { @@ -248,30 +230,28 @@ class VmCreateRequest(BaseModel): "vm_sockets": 1, "vm_memory": 2042, "vm_disk_size": 42, - "vm_iso": "local:iso/ubuntu-24.04.2-live-server-amd64.iso" + "vm_iso": "local:iso/ubuntu-24.04.2-live-server-amd64.iso", } } } class VmCreateItemReply(BaseModel): - action: Literal["vm_create"] source: Literal["proxmox"] proxmox_node: str - vm_id : int = Field(..., ge=1) - vm_name : str - vm_cpu : str - vm_cores : int = Field(..., ge=1) + vm_id: int = Field(..., ge=1) + vm_name: str + vm_cpu: str + vm_cores: int = Field(..., ge=1) vm_sockets: int = Field(..., ge=1) - vm_memory : int = Field(..., ge=1) - vm_net0 : str - vm_scsi0 : str - raw_data : str = Field(..., description="Raw string returned by Proxmox") + vm_memory: int = Field(..., ge=1) + vm_net0: str + vm_scsi0: str + raw_data: str = Field(..., description="Raw string returned by Proxmox") class VmCreateReply(BaseModel): - rc: int = Field(0, description="RETURN code (0 = OK)") result: list[VmCreateItemReply] @@ -292,9 +272,9 @@ class VmCreateReply(BaseModel): "vm_name": "vm-with-local-iso-2", "vm_net0": "virtio,bridge=vmbr0", "vm_scsi0": "local-lvm:42,format=raw", - "vm_sockets": 1 + "vm_sockets": 1, } - ] + ], } } } @@ -304,18 +284,17 @@ class VmCreateReply(BaseModel): # VM Delete # --------------------------------------------------------------------------- -class VmDeleteRequest(BaseModel): +class VmDeleteRequest(BaseModel): proxmox_node: str = Field( ..., # default= "px-testing", - description = "Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" + description="Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$", ) as_json: bool = Field( - default=True, - description="If true : JSON output else : raw output" + default=True, description="If true : JSON output else : raw output" ) # @@ -323,7 +302,7 @@ class VmDeleteRequest(BaseModel): ..., # default="4000", description="Virtual machine id", - pattern=r"^[0-9]+$" + pattern=r"^[0-9]+$", ) model_config = { @@ -338,7 +317,6 @@ class VmDeleteRequest(BaseModel): class VmDeleteItemReply(BaseModel): - action: Literal["vm_delete"] source: Literal["proxmox"] proxmox_node: str @@ -347,8 +325,8 @@ class VmDeleteItemReply(BaseModel): vm_name: str raw_data: str = Field(..., description="Raw string returned by proxmox") -class VmDeleteReply(BaseModel): +class VmDeleteReply(BaseModel): rc: int = Field(0, description="RETURN code (0 = OK)") result: list[VmDeleteItemReply] @@ -363,9 +341,9 @@ class VmDeleteReply(BaseModel): "proxmox_node": "px-testing", "vm_id": 1023, "vm_name": "admin-web-deployer-ui", - "raw_data": "UPID:px-testing:123123:1123D4:68BFF2C7:qmdestroy:1023:API_master@pam!API_master:" + "raw_data": "UPID:px-testing:123123:1123D4:68BFF2C7:qmdestroy:1023:API_master@pam!API_master:", } - ] + ], } } } @@ -375,18 +353,17 @@ class VmDeleteReply(BaseModel): # VM Clone # --------------------------------------------------------------------------- -class VmCloneRequest(BaseModel): +class VmCloneRequest(BaseModel): proxmox_node: str = Field( ..., # default= "px-testing", - description = "Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" + description="Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$", ) as_json: bool = Field( - default=True, - description="If true : JSON output else : raw output" + default=True, description="If true : JSON output else : raw output" ) # @@ -394,27 +371,27 @@ class VmCloneRequest(BaseModel): ..., # default="4000", description="Virtual machine id", - pattern=r"^[0-9]+$" + pattern=r"^[0-9]+$", ) vm_new_id: str = Field( ..., # default="5005", description="New virtual machine id", - pattern=r"^[0-9]+$" + pattern=r"^[0-9]+$", ) vm_description: str | None = Field( default="cloned-vm", description="Virtual machine meta description field", - pattern = r"^[A-Za-z0-9\s.,_\-]*$" + pattern=r"^[A-Za-z0-9\s.,_\-]*$", ) - vm_name: str = Field( + vm_name: str = Field( ..., # default="new-vm", description="Virtual machine meta name", - pattern = r"^[A-Za-z0-9-]*$" + pattern=r"^[A-Za-z0-9-]*$", ) model_config = { @@ -423,15 +400,14 @@ class VmCloneRequest(BaseModel): "proxmox_node": "px-testing", "vm_id": "2000", "vm_new_id": "3000", - "vm_name":"test-cloned", - "vm_description":"my description" + "vm_name": "test-cloned", + "vm_description": "my description", } } } class VmCloneItemReply(BaseModel): - action: Literal["vm_clone"] source: Literal["proxmox"] proxmox_node: str @@ -443,7 +419,6 @@ class VmCloneItemReply(BaseModel): class VmCloneReply(BaseModel): - rc: int = Field(0, description="RETURN code (0 = OK)") result: list[VmCloneItemReply] @@ -456,14 +431,15 @@ class VmCloneReply(BaseModel): "action": "vm_clone", "proxmox_node": "px-testing", "raw_info": { - "data": "UPID:px-testing:0027CE9B:167F1A2C:68C03C17:qmclone:4004:API_master@pam!API_master:"}, + "data": "UPID:px-testing:0027CE9B:167F1A2C:68C03C17:qmclone:4004:API_master@pam!API_master:" + }, "source": "proxmox", "vm_description": "my description", "vm_id": "5004", "vm_id_clone_from": "4004", - "vm_name": "test-cloned" + "vm_name": "test-cloned", } - ] + ], } } } @@ -473,18 +449,17 @@ class VmCloneReply(BaseModel): # VM Action (Start / Stop / Resume / Pause) # --------------------------------------------------------------------------- -class VmActionRequest(BaseModel): +class VmActionRequest(BaseModel): proxmox_node: str = Field( ..., # default= "px-testing", - description = "Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" + description="Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$", ) as_json: bool = Field( - default=True, - description="If true : JSON output else : raw output" + default=True, description="If true : JSON output else : raw output" ) # @@ -492,10 +467,9 @@ class VmActionRequest(BaseModel): ..., # default="1000", description="Virtual machine id", - pattern=r"^[0-9]+$" + pattern=r"^[0-9]+$", ) - model_config = { "json_schema_extra": { "example": { @@ -507,17 +481,15 @@ class VmActionRequest(BaseModel): class VmActionItemReply(BaseModel): - - action: Literal["vm_start", "vm_stop", "vm_resume", "vm_pause", "vm_stop_force" ] + action: Literal["vm_start", "vm_stop", "vm_resume", "vm_pause", "vm_stop_force"] source: Literal["proxmox"] proxmox_node: str - vm_id : str # int = Field(..., ge=1) - vm_name : str + vm_id: str # int = Field(..., ge=1) + vm_name: str class VmActionReply(BaseModel): - rc: int = Field(0, description="RETURN code (0 = OK)") result: list[VmActionItemReply] @@ -526,7 +498,6 @@ class VmActionReply(BaseModel): "example": { "rc": 0, "result": [ - { "action": "vm_delete", "source": "proxmox", @@ -535,7 +506,7 @@ class VmActionReply(BaseModel): "vm_name": "vuln-box-01", "raw_data": { "data": "UPID:px-testing:0033649C:1D2619CC:68D143C5:qmdestroy:4001:API_master@pam!API_master:" - } + }, }, { "action": "vm_delete", @@ -545,9 +516,9 @@ class VmActionReply(BaseModel): "vm_name": "vuln-box-02", "raw_data": { "data": "UPID:px-testing:003364A6:1D261A84:68D143C6:qmdestroy:4002:API_master@pam!API_master:" - } + }, }, - ] + ], } } } @@ -557,34 +528,28 @@ class VmActionReply(BaseModel): # Mass Delete # --------------------------------------------------------------------------- -class MassDeleteVmItem(BaseModel): - id: str = Field( - ..., - description="Virtual machine id", - pattern=r"^[0-9]+$" - ) +class MassDeleteVmItem(BaseModel): + id: str = Field(..., description="Virtual machine id", pattern=r"^[0-9]+$") name: str = Field( ..., description="Virtual machine meta name", - pattern="^[A-Za-z0-9-]+$" # deny void name + pattern="^[A-Za-z0-9-]+$", # deny void name # pattern=r"^[A-Za-z0-9-]*$", ) class MassDeleteRequest(BaseModel): - proxmox_node: str = Field( ..., # default= "px-testing", - description = "Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" + description="Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$", ) as_json: bool = Field( - default=True, - description="If true : JSON output else : raw output" + default=True, description="If true : JSON output else : raw output" ) vms: List[MassDeleteVmItem] = Field( @@ -599,9 +564,9 @@ class MassDeleteRequest(BaseModel): "proxmox_node": "px-testing", "as_json": True, "vms": [ - {"id":"4000", "name":"vuln-box-00"}, - {"id":"4001", "name":"vuln-box-01"}, - {"id":"4002", "name":"vuln-box-02"}, + {"id": "4000", "name": "vuln-box-00"}, + {"id": "4001", "name": "vuln-box-01"}, + {"id": "4002", "name": "vuln-box-02"}, ], } } @@ -609,18 +574,17 @@ class MassDeleteRequest(BaseModel): class MassDeleteItemReply(BaseModel): - - action: Literal["vm_start", "vm_stop", "vm_resume", "vm_pause", "vm_stop_force" ] + action: Literal["vm_start", "vm_stop", "vm_resume", "vm_pause", "vm_stop_force"] source: Literal["proxmox"] proxmox_node: str - vm_id : str # int = Field(..., ge=1) + vm_id: str # int = Field(..., ge=1) # vm_new_id : str # int = Field(..., ge=1) - vm_name : str + vm_name: str vm_status: Literal["running", "stopped", "paused"] -class MassDeleteReply(BaseModel): +class MassDeleteReply(BaseModel): rc: int = Field(0, description="RETURN code (0 = OK)") result: list[MassDeleteItemReply] @@ -629,18 +593,17 @@ class MassDeleteReply(BaseModel): # Mass Start / Stop / Resume / Pause # --------------------------------------------------------------------------- -class MassActionRequest(BaseModel): +class MassActionRequest(BaseModel): proxmox_node: str = Field( ..., # default= "px-testing", - description = "Proxmox node name", - pattern=r"^[A-Za-z0-9-]*$" + description="Proxmox node name", + pattern=r"^[A-Za-z0-9-]*$", ) as_json: bool = Field( - default=True, - description="If true : JSON output else : raw output" + default=True, description="If true : JSON output else : raw output" ) # @@ -652,7 +615,6 @@ class MassActionRequest(BaseModel): # pattern=r"^[0-9]+$" ) - model_config = { "json_schema_extra": { "example": { diff --git a/app/utils/__init__.py b/app/utils/__init__.py index b883df4..e69de29 100644 --- a/app/utils/__init__.py +++ b/app/utils/__init__.py @@ -1,12 +0,0 @@ -# - -from .text_cleaner import * - -from .checks_playbooks import ( - resolve_actions_playbook, - resolve_scenarios_playbook, - resolve_bundles_playbook, - resolve_bundles_playbook_init_file -) - -from .checks_inventory import resolve_inventory diff --git a/app/utils/checks_inventory.py b/app/utils/checks_inventory.py index e741746..23e5789 100644 --- a/app/utils/checks_inventory.py +++ b/app/utils/checks_inventory.py @@ -6,14 +6,15 @@ the absolute path. """ +import logging import os -from pathlib import Path import re -import logging -logger = logging.getLogger(__name__) +from pathlib import Path from fastapi import HTTPException +logger = logging.getLogger(__name__) + def resolve_inventory(inventory_name: str) -> Path: """Resolve an inventory file path from a logical name. @@ -44,9 +45,10 @@ def resolve_inventory(inventory_name: str) -> Path: name_pattern=inventory_name_pattern, ) -def _resolve_inventory_file(inventory_dir: Path, - inventory_name: str, - name_pattern: re.Pattern[str]) -> Path: + +def _resolve_inventory_file( + inventory_dir: Path, inventory_name: str, name_pattern: re.Pattern[str] +) -> Path: """Validate and resolve an inventory file path. Performs regex validation, traversal detection, directory existence @@ -65,7 +67,6 @@ def _resolve_inventory_file(inventory_dir: Path, """ if not name_pattern.fullmatch(str(inventory_name)): - err = f":: err - INVALID INVENTORY NAME FORMAT {inventory_name!r}" logger.error(err) raise HTTPException(status_code=400, detail=err) @@ -78,7 +79,6 @@ def _resolve_inventory_file(inventory_dir: Path, # if not inventory_filepath.is_relative_to(inventory_dir): - err = f":: err - POTENTIAL PATH TRAVERSAL DETECTED : {inventory_filepath}" logger.error(err) raise HTTPException(status_code=400, detail=err) @@ -88,7 +88,6 @@ def _resolve_inventory_file(inventory_dir: Path, # if not inventory_dir.exists(): - err = f":: err - INVENTORY DIR NOT FOUND : {inventory_dir}" logger.error(err) raise HTTPException(status_code=500, detail=err) @@ -100,7 +99,6 @@ def _resolve_inventory_file(inventory_dir: Path, inventory_filepath = inventory_filepath.resolve(strict=True) except FileNotFoundError: - err = f":: err - INVENTORY NOT FOUND : {inventory_filepath}" logger.error(err) raise HTTPException(status_code=400, detail=err) diff --git a/app/utils/checks_playbooks.py b/app/utils/checks_playbooks.py index 961fd6d..982f042 100644 --- a/app/utils/checks_playbooks.py +++ b/app/utils/checks_playbooks.py @@ -6,15 +6,15 @@ path to the playbook YAML file. """ -import re import logging -logger = logging.getLogger(__name__) import os +import re +from pathlib import Path from fastapi import HTTPException -from pathlib import Path -#### +logger = logging.getLogger(__name__) + def _warmup_checks(playbooks_dir_type: str) -> Path: """Validate and resolve the playbooks base directory. @@ -89,6 +89,7 @@ def resolve_actions_playbook(action_name: str, playbooks_dir_type: str) -> Path: return main_filepath + def resolve_bundles_playbook(action_name: str, playbooks_dir_type: str) -> Path: """Resolve a bundle playbook file path. @@ -115,7 +116,10 @@ def resolve_bundles_playbook(action_name: str, playbooks_dir_type: str) -> Path: return main_filepath -def resolve_bundles_playbook_init_file(action_name: str, playbooks_dir_type: str) -> Path: + +def resolve_bundles_playbook_init_file( + action_name: str, playbooks_dir_type: str +) -> Path: """Resolve a bundle's init playbook file path. Looks for ``/bundles//init.yml`` instead @@ -138,7 +142,9 @@ def resolve_bundles_playbook_init_file(action_name: str, playbooks_dir_type: str # print (actions_dir) actions_regex_pattern = re.compile(r"^[A-Za-z0-9_-]+(?:/[A-Za-z0-9_-]+)*$") - main_filepath = _resolve_file(actions_dir, actions_regex_pattern, action_name, is_init_yaml=True ) + main_filepath = _resolve_file( + actions_dir, actions_regex_pattern, action_name, is_init_yaml=True + ) return main_filepath @@ -171,11 +177,13 @@ def resolve_scenarios_playbook(action_name: str, playbooks_dir_type: str) -> Pat #### -def _resolve_file(actions_dir: Path, - actions_regex_pattern: re.Pattern[str], - action_name: str, - *, - is_init_yaml: bool = False) -> Path: +def _resolve_file( + actions_dir: Path, + actions_regex_pattern: re.Pattern[str], + action_name: str, + *, + is_init_yaml: bool = False, +) -> Path: """Validate an action name and resolve the corresponding playbook file. Performs regex validation, path resolution, traversal detection, and @@ -199,7 +207,6 @@ def _resolve_file(actions_dir: Path, # if not actions_regex_pattern.fullmatch(action_name): - err = f":: err - INVALID ACTION NAME FORMAT {action_name!r}" logger.error(err) raise HTTPException(status_code=400, detail=err) @@ -219,7 +226,6 @@ def _resolve_file(actions_dir: Path, # if not main_filepath.is_relative_to(actions_dir): - err = f":: err - POTENTIAL PATH TRAVERSAL DETECTED : {main_filepath}" logger.error(err) raise HTTPException(status_code=400, detail=err) @@ -231,7 +237,6 @@ def _resolve_file(actions_dir: Path, # raise HTTPException(status_code=400, detail=err) if not main_filepath.exists(): - err = f":: err - PLAYBOOK NOT FOUND : {main_filepath}" logger.error(err) raise HTTPException(status_code=400, detail=err) diff --git a/app/utils/text_cleaner.py b/app/utils/text_cleaner.py index 5ab596f..ae6a90a 100644 --- a/app/utils/text_cleaner.py +++ b/app/utils/text_cleaner.py @@ -6,7 +6,7 @@ import re -ANSI_RE = re.compile(r'(?:\x1B[@-_][0-?]*[ -/]*[@-~])') +ANSI_RE = re.compile(r"(?:\x1B[@-_][0-?]*[ -/]*[@-~])") def strip_ansi(s: str) -> str: diff --git a/app/utils/vm_id_name_resolver.py b/app/utils/vm_id_name_resolver.py index b121c80..3cfb5ed 100644 --- a/app/utils/vm_id_name_resolver.py +++ b/app/utils/vm_id_name_resolver.py @@ -6,12 +6,15 @@ and snapshot management. """ -from pathlib import Path -import os, json, logging +import json +import logging +import os +from pathlib import Path + from fastapi import HTTPException -from app.core.runner import run_playbook_core from app.core.extractor import extract_action_results +from app.core.runner import run_playbook_core logger = logging.getLogger(__name__) @@ -33,6 +36,7 @@ def hack_same_vm_id(a, b) -> bool: except (TypeError, ValueError): return str(a) == str(b) + def resolv_id_to_vm_name(proxmox_node: str, target_vm_id: str) -> dict: """Resolve a VM ID to its name by querying the Proxmox VM list. @@ -86,30 +90,32 @@ def resolv_id_to_vm_name(proxmox_node: str, target_vm_id: str) -> dict: # cross check json / py object if isinstance(action_result, str): - try: data = json.loads(action_result) - except json.JSONDecodeError as e: - err = f":: err - INVALID actions_results JSONS" + except json.JSONDecodeError: + err = ":: err - INVALID actions_results JSONS" logger.error("Invalid action_results JSON") raise HTTPException(status_code=500, detail=err) else: data = action_result - for outer in data: # first [] - + for outer in data: # first [] if not isinstance(outer, list): continue - for item in outer: # second[] - + for item in outer: # second[] # if isinstance(item, dict) and str(item.get("vm_id")) == target_vm_id: # if isinstance(item, dict) and item.get("vm_id") == target_vm_id: - if isinstance(item, dict) and hack_same_vm_id(item.get("vm_id"), target_vm_id): # hacky way - should be fixed. - - logger.debug("Matched VM — vm_id: %s, vm_name: %s", item.get("vm_id"), item.get("vm_name")) + if isinstance(item, dict) and hack_same_vm_id( + item.get("vm_id"), target_vm_id + ): # hacky way - should be fixed. + logger.debug( + "Matched VM — vm_id: %s, vm_name: %s", + item.get("vm_id"), + item.get("vm_name"), + ) return { "vm_id": item.get("vm_id"), @@ -118,6 +124,6 @@ def resolv_id_to_vm_name(proxmox_node: str, target_vm_id: str) -> dict: # return None - err = f":: err - vm_id NOT FOUND" + err = ":: err - vm_id NOT FOUND" logger.error("vm_id not found: %s", target_vm_id) raise HTTPException(status_code=500, detail=err) diff --git a/tests/conftest.py b/tests/conftest.py index 9cfcbb2..277efb0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,7 +1,7 @@ -import json import os -import pytest from pathlib import Path + +import pytest from fastapi.testclient import TestClient _PROJECT_ROOT = str(Path(__file__).resolve().parents[1]) @@ -11,18 +11,22 @@ os.environ.setdefault("PROJECT_ROOT_DIR", _PROJECT_ROOT) os.environ.setdefault("API_BACKEND_WWWAPP_PLAYBOOKS_DIR", _PROJECT_ROOT) os.environ.setdefault("API_BACKEND_PUBLIC_PLAYBOOKS_DIR", _PROJECT_ROOT) -os.environ.setdefault("API_BACKEND_INVENTORY_DIR", str(Path(_PROJECT_ROOT) / "inventory")) +os.environ.setdefault( + "API_BACKEND_INVENTORY_DIR", str(Path(_PROJECT_ROOT) / "inventory") +) @pytest.fixture def client(): from app.main import app + return TestClient(app) @pytest.fixture(scope="session") def openapi_schema(): from app.main import app + c = TestClient(app) resp = c.get("/docs/openapi.json") return resp.json() diff --git a/tests/test_app_factory.py b/tests/test_app_factory.py index 4dbba15..a7acb68 100644 --- a/tests/test_app_factory.py +++ b/tests/test_app_factory.py @@ -1,5 +1,6 @@ def test_create_app_returns_fastapi_instance(): from app.main import create_app + app = create_app() assert app.title == "CR42 - API" assert app.version == "v0.1" diff --git a/tests/test_config.py b/tests/test_config.py index a6ac978..8c80284 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,4 +1,3 @@ -import os from pathlib import Path @@ -7,7 +6,9 @@ def test_settings_loads_from_env(monkeypatch): monkeypatch.setenv("CORS_ORIGIN_REGEX", r"^https?://example\.com$") import importlib + import app.core.config as config_mod + importlib.reload(config_mod) from app.core.config import settings @@ -20,7 +21,9 @@ def test_settings_defaults(monkeypatch): monkeypatch.delenv("CORS_ORIGIN_REGEX", raising=False) import importlib + import app.core.config as config_mod + importlib.reload(config_mod) assert "localhost" in config_mod.Settings().cors_origin_regex @@ -30,7 +33,9 @@ def test_settings_playbook_path(monkeypatch): monkeypatch.setenv("PROJECT_ROOT_DIR", "/tmp/test-project") import importlib + import app.core.config as config_mod + importlib.reload(config_mod) s = config_mod.Settings() diff --git a/tests/test_extractor.py b/tests/test_extractor.py index 346f376..3a0d3ad 100644 --- a/tests/test_extractor.py +++ b/tests/test_extractor.py @@ -17,7 +17,12 @@ def test_extract_returns_empty_for_no_match(): def test_extract_skips_non_ok_events(): - events = [{"event": "runner_on_failed", "event_data": {"res": {"vm_list": [{"vmid": 100}]}}}] + events = [ + { + "event": "runner_on_failed", + "event_data": {"res": {"vm_list": [{"vmid": 100}]}}, + } + ] result = extract_action_results(events, "vm_list") assert result == [] diff --git a/tests/test_schemas.py b/tests/test_schemas.py index eb33e81..61fd3eb 100644 --- a/tests/test_schemas.py +++ b/tests/test_schemas.py @@ -1,15 +1,16 @@ """Tests for consolidated schema modules — validates fields, patterns, and backward-compat aliases.""" -from pydantic import ValidationError import pytest - +from pydantic import ValidationError # =========================================================================== # vms.py # =========================================================================== + def test_vm_list_request(): from app.schemas.vms import VmListRequest + req = VmListRequest(proxmox_node="px-testing") assert req.proxmox_node == "px-testing" assert req.as_json is True @@ -17,18 +18,21 @@ def test_vm_list_request(): def test_vm_action_request_validates_vm_id(): from app.schemas.vms import VmActionRequest + req = VmActionRequest(proxmox_node="px-testing", vm_id="100") assert req.vm_id == "100" def test_vm_action_request_rejects_invalid_vm_id(): from app.schemas.vms import VmActionRequest + with pytest.raises(ValidationError): VmActionRequest(proxmox_node="px-testing", vm_id="abc") def test_vm_create_request(): from app.schemas.vms import VmCreateRequest + req = VmCreateRequest( proxmox_node="px-testing", vm_id="200", @@ -43,6 +47,7 @@ def test_vm_create_request(): def test_vm_create_request_rejects_low_memory(): from app.schemas.vms import VmCreateRequest + with pytest.raises(ValidationError): VmCreateRequest( proxmox_node="px-testing", @@ -57,18 +62,21 @@ def test_vm_create_request_rejects_low_memory(): def test_proxmox_node_rejects_special_chars(): from app.schemas.vms import VmListRequest + with pytest.raises(ValidationError): VmListRequest(proxmox_node="node; rm -rf /") def test_vm_delete_request(): from app.schemas.vms import VmDeleteRequest + req = VmDeleteRequest(proxmox_node="px-testing", vm_id="1111") assert req.vm_id == "1111" def test_vm_clone_request(): from app.schemas.vms import VmCloneRequest + req = VmCloneRequest( proxmox_node="px-testing", vm_id="2000", @@ -81,6 +89,7 @@ def test_vm_clone_request(): def test_mass_delete_request(): from app.schemas.vms import MassDeleteRequest, MassDeleteVmItem + req = MassDeleteRequest( proxmox_node="px-testing", vms=[MassDeleteVmItem(id="4000", name="vuln-box-00")], @@ -90,6 +99,7 @@ def test_mass_delete_request(): def test_mass_delete_request_rejects_empty_vms(): from app.schemas.vms import MassDeleteRequest + with pytest.raises(ValidationError): MassDeleteRequest(proxmox_node="px-testing", vms=[]) @@ -98,14 +108,17 @@ def test_mass_delete_request_rejects_empty_vms(): # vm_config.py # =========================================================================== + def test_vm_get_config_request(): from app.schemas.vm_config import VmGetConfigRequest + req = VmGetConfigRequest(proxmox_node="px-testing", vm_id="1000") assert req.as_json is True def test_vm_set_tag_request(): from app.schemas.vm_config import VmSetTagRequest + req = VmSetTagRequest( proxmox_node="px-testing", vm_id="1111", @@ -116,6 +129,7 @@ def test_vm_set_tag_request(): def test_vm_set_tag_rejects_bad_pattern(): from app.schemas.vm_config import VmSetTagRequest + with pytest.raises(ValidationError): VmSetTagRequest( proxmox_node="px-testing", @@ -128,8 +142,10 @@ def test_vm_set_tag_rejects_bad_pattern(): # snapshots.py # =========================================================================== + def test_snapshot_create_request(): from app.schemas.snapshots import SnapshotCreateRequest + req = SnapshotCreateRequest( proxmox_node="px-testing", vm_id="1000", @@ -140,6 +156,7 @@ def test_snapshot_create_request(): def test_snapshot_list_request(): from app.schemas.snapshots import SnapshotListRequest + req = SnapshotListRequest(proxmox_node="px-testing", vm_id="1000") assert req.vm_id == "1000" @@ -148,8 +165,10 @@ def test_snapshot_list_request(): # firewall.py # =========================================================================== + def test_firewall_rule_request(): from app.schemas.firewall import FirewallRuleApplyRequest + req = FirewallRuleApplyRequest( proxmox_node="px-testing", vm_id="100", @@ -164,6 +183,7 @@ def test_firewall_rule_request(): def test_firewall_rule_rejects_invalid_action(): from app.schemas.firewall import FirewallRuleApplyRequest + with pytest.raises(ValidationError): FirewallRuleApplyRequest( proxmox_node="px-testing", @@ -178,6 +198,7 @@ def test_firewall_rule_rejects_invalid_action(): def test_firewall_alias_add_request(): from app.schemas.firewall import FirewallAliasAddRequest + req = FirewallAliasAddRequest( proxmox_node="px-testing", vm_id="1000", @@ -192,14 +213,17 @@ def test_firewall_alias_add_request(): # network.py # =========================================================================== + def test_node_network_list_request(): from app.schemas.network import NodeNetworkListRequest + req = NodeNetworkListRequest(proxmox_node="px-testing") assert req.as_json is True def test_vm_network_list_request(): from app.schemas.network import VmNetworkListRequest + req = VmNetworkListRequest(proxmox_node="px-testing", vm_id="1001") assert req.vm_id == "1001" @@ -208,8 +232,10 @@ def test_vm_network_list_request(): # storage.py # =========================================================================== + def test_storage_list_request(): from app.schemas.storage import StorageListRequest + req = StorageListRequest( proxmox_node="px-testing", storage_name="local", @@ -219,6 +245,7 @@ def test_storage_list_request(): def test_storage_download_iso_request(): from app.schemas.storage import StorageDownloadIsoRequest + req = StorageDownloadIsoRequest( proxmox_node="px-testing", proxmox_storage="local", @@ -233,8 +260,10 @@ def test_storage_download_iso_request(): # bundles.py # =========================================================================== + def test_bundle_add_user_request(): from app.schemas.bundles import BundleAddUserRequest + req = BundleAddUserRequest( proxmox_node="px-testing", hosts="r42.vuln-box-00", @@ -247,7 +276,11 @@ def test_bundle_add_user_request(): def test_bundle_create_admin_vms_request(): - from app.schemas.bundles import BundleCreateAdminVmsRequest, BundleCreateAdminVmsItemRequest + from app.schemas.bundles import ( + BundleCreateAdminVmsItemRequest, + BundleCreateAdminVmsRequest, + ) + req = BundleCreateAdminVmsRequest( proxmox_node="px-testing", vms={ @@ -266,8 +299,10 @@ def test_bundle_create_admin_vms_request(): # debug.py # =========================================================================== + def test_debug_ping_request(): from app.schemas.debug import DebugPingRequest + req = DebugPingRequest(proxmox_node="px-testing", hosts="all") assert req.as_json is False # default @@ -276,14 +311,17 @@ def test_debug_ping_request(): # base.py — ProxmoxBaseRequest # =========================================================================== + def test_proxmox_base_request(): from app.schemas.base import ProxmoxBaseRequest + req = ProxmoxBaseRequest(proxmox_node="px-testing") assert req.as_json is True def test_proxmox_base_request_rejects_bad_node(): from app.schemas.base import ProxmoxBaseRequest + with pytest.raises(ValidationError): ProxmoxBaseRequest(proxmox_node="node; rm -rf /") @@ -292,78 +330,133 @@ def test_proxmox_base_request_rejects_bad_node(): # Backward-compatibility aliases # =========================================================================== + def test_backward_compat_aliases_vms(): """Old class names must still be importable.""" from app.schemas.vms import Request_ProxmoxVms_VmList, VmListRequest + assert Request_ProxmoxVms_VmList is VmListRequest from app.schemas.vms import Reply_ProxmoxVmList, VmListReply + assert Reply_ProxmoxVmList is VmListReply from app.schemas.vms import Request_ProxmoxVmsVMID_Create, VmCreateRequest + assert Request_ProxmoxVmsVMID_Create is VmCreateRequest - from app.schemas.vms import Request_ProxmoxVmsVMID_StartStopPauseResume, VmActionRequest + from app.schemas.vms import ( + Request_ProxmoxVmsVMID_StartStopPauseResume, + VmActionRequest, + ) + assert Request_ProxmoxVmsVMID_StartStopPauseResume is VmActionRequest - from app.schemas.vms import Request_ProxmoxVmsVmIds_MassDelete, MassDeleteRequest + from app.schemas.vms import MassDeleteRequest, Request_ProxmoxVmsVmIds_MassDelete + assert Request_ProxmoxVmsVmIds_MassDelete is MassDeleteRequest def test_backward_compat_aliases_vm_config(): - from app.schemas.vm_config import Request_ProxmoxVmsVMID_VmGetConfig, VmGetConfigRequest + from app.schemas.vm_config import ( + Request_ProxmoxVmsVMID_VmGetConfig, + VmGetConfigRequest, + ) + assert Request_ProxmoxVmsVMID_VmGetConfig is VmGetConfigRequest from app.schemas.vm_config import Request_ProxmoxVmsVMID_VmSetTag, VmSetTagRequest + assert Request_ProxmoxVmsVMID_VmSetTag is VmSetTagRequest def test_backward_compat_aliases_snapshots(): - from app.schemas.snapshots import Request_ProxmoxVmsVMID_CreateSnapshot, SnapshotCreateRequest + from app.schemas.snapshots import ( + Request_ProxmoxVmsVMID_CreateSnapshot, + SnapshotCreateRequest, + ) + assert Request_ProxmoxVmsVMID_CreateSnapshot is SnapshotCreateRequest - from app.schemas.snapshots import Request_ProxmoxVmsVMID_RevertSnapshot, SnapshotRevertRequest + from app.schemas.snapshots import ( + Request_ProxmoxVmsVMID_RevertSnapshot, + SnapshotRevertRequest, + ) + assert Request_ProxmoxVmsVMID_RevertSnapshot is SnapshotRevertRequest def test_backward_compat_aliases_firewall(): - from app.schemas.firewall import Request_ProxmoxFirewall_ApplyIptablesRules, FirewallRuleApplyRequest + from app.schemas.firewall import ( + FirewallRuleApplyRequest, + Request_ProxmoxFirewall_ApplyIptablesRules, + ) + assert Request_ProxmoxFirewall_ApplyIptablesRules is FirewallRuleApplyRequest - from app.schemas.firewall import Request_ProxmoxFirewall_EnableFirewallVm, FirewallEnableVmRequest + from app.schemas.firewall import ( + FirewallEnableVmRequest, + Request_ProxmoxFirewall_EnableFirewallVm, + ) + assert Request_ProxmoxFirewall_EnableFirewallVm is FirewallEnableVmRequest def test_backward_compat_aliases_network(): - from app.schemas.network import Request_ProxmoxNetwork_WithNodeName_AddNetworkInterface, NodeNetworkAddRequest - assert Request_ProxmoxNetwork_WithNodeName_AddNetworkInterface is NodeNetworkAddRequest + from app.schemas.network import ( + NodeNetworkAddRequest, + Request_ProxmoxNetwork_WithNodeName_AddNetworkInterface, + ) + + assert ( + Request_ProxmoxNetwork_WithNodeName_AddNetworkInterface is NodeNetworkAddRequest + ) + + from app.schemas.network import ( + Request_ProxmoxNetwork_WithVmId_ListNetwork, + VmNetworkListRequest, + ) - from app.schemas.network import Request_ProxmoxNetwork_WithVmId_ListNetwork, VmNetworkListRequest assert Request_ProxmoxNetwork_WithVmId_ListNetwork is VmNetworkListRequest def test_backward_compat_aliases_storage(): from app.schemas.storage import Request_ProxmoxStorage_List, StorageListRequest + assert Request_ProxmoxStorage_List is StorageListRequest - from app.schemas.storage import Request_ProxmoxStorage_ListIso, StorageListIsoRequest + from app.schemas.storage import ( + Request_ProxmoxStorage_ListIso, + StorageListIsoRequest, + ) + assert Request_ProxmoxStorage_ListIso is StorageListIsoRequest def test_backward_compat_aliases_bundles(): - from app.schemas.bundles import Request_BundlesCoreLinuxUbuntuConfigure_AddUser, BundleAddUserRequest + from app.schemas.bundles import ( + BundleAddUserRequest, + Request_BundlesCoreLinuxUbuntuConfigure_AddUser, + ) + assert Request_BundlesCoreLinuxUbuntuConfigure_AddUser is BundleAddUserRequest from app.schemas.bundles import ( - Request_BundlesCoreProxmoxConfigureDefaultVms_CreateAdminVms, BundleCreateAdminVmsRequest, + Request_BundlesCoreProxmoxConfigureDefaultVms_CreateAdminVms, + ) + + assert ( + Request_BundlesCoreProxmoxConfigureDefaultVms_CreateAdminVms + is BundleCreateAdminVmsRequest ) - assert Request_BundlesCoreProxmoxConfigureDefaultVms_CreateAdminVms is BundleCreateAdminVmsRequest def test_backward_compat_aliases_debug(): - from app.schemas.debug import Request_DebugPing, DebugPingRequest + from app.schemas.debug import DebugPingRequest, Request_DebugPing + assert Request_DebugPing is DebugPingRequest - from app.schemas.debug import Reply_DebugPing, DebugPingReply + from app.schemas.debug import DebugPingReply, Reply_DebugPing + assert Reply_DebugPing is DebugPingReply diff --git a/tests/test_vault.py b/tests/test_vault.py index c621a18..f3c4594 100644 --- a/tests/test_vault.py +++ b/tests/test_vault.py @@ -1,4 +1,5 @@ from pathlib import Path + from app.core.vault import VaultManager From 2d99e7a877b1b280e18cefc17f3b326475857196 Mon Sep 17 00:00:00 2001 From: t0kubetsu Date: Mon, 23 Mar 2026 11:35:35 +0100 Subject: [PATCH 20/33] switch to slim from 3.12 --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 2227a7c..5f688db 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.12-slim AS base +FROM python:slim AS base # Install system deps for ansible and ssh RUN apt-get update && apt-get install -y --no-install-recommends \ From a0869808b8007635bd339f4466c4128476b97116 Mon Sep 17 00:00:00 2001 From: t0kubetsu Date: Mon, 23 Mar 2026 11:46:14 +0100 Subject: [PATCH 21/33] fix: resolve pydantic v2 deprecation warnings in vm schemas --- app/schemas/bundles/__init__.py | 219 +++++--------------------------- app/schemas/vms.py | 4 +- 2 files changed, 32 insertions(+), 191 deletions(-) diff --git a/app/schemas/bundles/__init__.py b/app/schemas/bundles/__init__.py index 4b1100e..cfeb0da 100644 --- a/app/schemas/bundles/__init__.py +++ b/app/schemas/bundles/__init__.py @@ -1,13 +1,12 @@ """Consolidated bundle schemas: Ubuntu packages/configure + Proxmox default VM ops. - This __init__.py serves double duty: 1. Makes bundles/ a proper Python package so old imports (app.schemas.bundles.core...) keep working. 2. Exposes consolidated schema classes for new code to import from app.schemas.bundles. """ -from typing import Dict +from typing import Annotated, Dict -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, StringConstraints # =========================================================================== # Ubuntu Bundles @@ -25,29 +24,23 @@ class BundleAddUserRequest(BaseModel): description="Proxmox node name", pattern=r"^[A-Za-z0-9-]*$", ) - hosts: str = Field( ..., description="Hosts or groups", pattern=r"^[a-zA-Z0-9._:-]+$" ) - #### - user: str = Field( ..., description="New user", pattern=r"^[a-z_][a-z0-9_-]*$", ) - password: str = Field( ..., description="New password", pattern=r"^[A-Za-z0-9@._-]*$", # dangerous chars removed. ) - change_pwd_at_logon: bool = Field( ..., description="Force user to change password on first login" ) - shell_path: str = Field( ..., description="Default user shell ", pattern=r"^/[a-z/]*$" ) @@ -68,11 +61,7 @@ class BundleAddUserRequest(BaseModel): class BundleAddUserItemReply(BaseModel): - # action: Literal["vm_get_config"] - # source: Literal["proxmox"] proxmox_node: str - # vm_id: int = Field(..., ge=1) - # vm_name: str raw_data: str = Field(..., description="Raw string returned by proxmox") @@ -95,54 +84,19 @@ class BundleBasicPackagesRequest(BaseModel): description="Proxmox node name", pattern=r"^[A-Za-z0-9-]*$", ) - hosts: str = Field( ..., description="Hosts or groups", pattern=r"^[a-zA-Z0-9._:-]+$" ) - #### - - install_package_basics: bool = Field( - ..., - description="", - ) - - install_package_firewalls: bool = Field( - ..., - description="", - ) - - install_package_docker: bool = Field( - ..., - description="", - ) - - install_package_docker_compose: bool = Field( - ..., - description="", - ) - - install_package_utils_json: bool = Field( - ..., - description="", - ) - - install_package_utils_network: bool = Field( - ..., - description="", - ) - + install_package_basics: bool = Field(..., description="") + install_package_firewalls: bool = Field(..., description="") + install_package_docker: bool = Field(..., description="") + install_package_docker_compose: bool = Field(..., description="") + install_package_utils_json: bool = Field(..., description="") + install_package_utils_network: bool = Field(..., description="") #### - - install_ntpclient_and_update_time: bool = Field( - ..., - description="", - ) - - packages_cleaning: bool = Field( - ..., - description="", - ) + install_ntpclient_and_update_time: bool = Field(..., description="") + packages_cleaning: bool = Field(..., description="") model_config = { "json_schema_extra": { @@ -164,11 +118,7 @@ class BundleBasicPackagesRequest(BaseModel): class BundleBasicPackagesItemReply(BaseModel): - # action: Literal["vm_get_config"] - # source: Literal["proxmox"] proxmox_node: str - # vm_id: int = Field(..., ge=1) - # vm_name: str raw_data: str = Field(..., description="Raw string returned by proxmox") @@ -191,29 +141,14 @@ class BundleDockerRequest(BaseModel): description="Proxmox node name", pattern=r"^[A-Za-z0-9-]*$", ) - hosts: str = Field( ..., description="Hosts or groups", pattern=r"^[a-zA-Z0-9._:-]+$" ) - #### - - install_package_docker: bool = Field( - ..., - description="", - ) - + install_package_docker: bool = Field(..., description="") #### - - install_ntpclient_and_update_time: bool = Field( - ..., - description="", - ) - - packages_cleaning: bool = Field( - ..., - description="", - ) + install_ntpclient_and_update_time: bool = Field(..., description="") + packages_cleaning: bool = Field(..., description="") model_config = { "json_schema_extra": { @@ -230,11 +165,7 @@ class BundleDockerRequest(BaseModel): class BundleDockerItemReply(BaseModel): - # action: Literal["vm_get_config"] - # source: Literal["proxmox"] proxmox_node: str - # vm_id: int = Field(..., ge=1) - # vm_name: str raw_data: str = Field(..., description="Raw string returned by proxmox") @@ -257,34 +188,15 @@ class BundleDockerComposeRequest(BaseModel): description="Proxmox node name", pattern=r"^[A-Za-z0-9-]*$", ) - hosts: str = Field( ..., description="Hosts or groups", pattern=r"^[a-zA-Z0-9._:-]+$" ) - #### - - install_package_docker: bool = Field( - ..., - description="", - ) - - install_package_docker_compose: bool = Field( - ..., - description="", - ) - + install_package_docker: bool = Field(..., description="") + install_package_docker_compose: bool = Field(..., description="") #### - - install_ntpclient_and_update_time: bool = Field( - ..., - description="", - ) - - packages_cleaning: bool = Field( - ..., - description="", - ) + install_ntpclient_and_update_time: bool = Field(..., description="") + packages_cleaning: bool = Field(..., description="") model_config = { "json_schema_extra": { @@ -302,11 +214,7 @@ class BundleDockerComposeRequest(BaseModel): class BundleDockerComposeItemReply(BaseModel): - # action: Literal["vm_get_config"] - # source: Literal["proxmox"] proxmox_node: str - # vm_id: int = Field(..., ge=1) - # vm_name: str raw_data: str = Field(..., description="Raw string returned by proxmox") @@ -329,26 +237,17 @@ class BundleDotFilesRequest(BaseModel): description="Proxmox node name", pattern=r"^[A-Za-z0-9-]*$", ) - hosts: str = Field( ..., description="Hosts or groups", pattern=r"^[a-zA-Z0-9._:-]+$" ) - #### - - user: str = Field( - ..., - description="targeted username", - ) - + user: str = Field(..., description="targeted username") install_vim_dot_files: bool = Field( ..., description="Install vim dot file in user directory" ) - install_zsh_dot_files: bool = Field( ..., description="Install zsh dot file in user directory" ) - apply_for_root: bool = Field(..., description="Install dot files in /root") model_config = { @@ -367,17 +266,10 @@ class BundleDotFilesRequest(BaseModel): class BundleDotFilesItemReply(BaseModel): - # action: Literal["vm_get_config"] - # source: Literal["proxmox"] proxmox_node: str - # vm_id: int = Field(..., ge=1) - # vm_name: str raw_data: str = Field(..., description="Raw string returned by proxmox") -# NOTE: The original file had a bug where the Reply class was also named -# Reply_BundlesCoreLinuxUbuntuInstall_DotFilesItem (same as Item reply). -# We preserve both the Item and the Reply under their correct new names. class BundleDotFilesReply(BaseModel): rc: int = Field(0, description="RETURN code (0 = OK)") result: list[BundleDotFilesItemReply] @@ -389,29 +281,24 @@ class BundleDotFilesReply(BaseModel): # Proxmox Bundles -- Default VM Operations # =========================================================================== +# Shared Annotated type: strips whitespace and enforces max_length. +# FIX: replaces the deprecated `strip_whitespace=True` extra kwarg on Field() +# with the Pydantic v2 canonical form using StringConstraints. +_VmDescription = Annotated[ + str, StringConstraints(strip_whitespace=True, max_length=200) +] + # --------------------------------------------------------------------------- # Create Admin VMs (Default) # --------------------------------------------------------------------------- class BundleCreateAdminVmsItemRequest(BaseModel): - vm_id: int = Field( - ..., - ge=1, - description="Virtual machine id", - ) - + vm_id: int = Field(..., ge=1, description="Virtual machine id") vm_ip: str = Field( ..., description="vm ipv4", pattern=r"^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$" ) - - vm_description: str = Field( - ..., - strip_whitespace=True, - max_length=200, - # pattern=VM_DESCRIPTION_RE, - description="Description", - ) + vm_description: _VmDescription = Field(..., description="Description") class BundleCreateAdminVmsRequest(BaseModel): @@ -421,7 +308,6 @@ class BundleCreateAdminVmsRequest(BaseModel): description="Proxmox node name", pattern=r"^[A-Za-z0-9-]*$", ) - vms: Dict[str, BundleCreateAdminVmsItemRequest] = Field( ..., description="Map - vm override vm_id vm_ip vm_description, ... ", @@ -449,11 +335,7 @@ class BundleCreateAdminVmsRequest(BaseModel): class BundleCreateAdminVmsItemReply(BaseModel): - # action: Literal["vm_get_config"] - # source: Literal["proxmox"] proxmox_node: str - # vm_id: int = Field(..., ge=1) - # vm_name: str raw_data: str = Field(..., description="Raw string returned by proxmox") @@ -470,23 +352,11 @@ class BundleCreateAdminVmsReply(BaseModel): class BundleCreateStudentVmsItemRequest(BaseModel): - vm_id: int = Field( - ..., - ge=1, - description="Virtual machine id", - ) - + vm_id: int = Field(..., ge=1, description="Virtual machine id") vm_ip: str = Field( ..., description="vm ipv4", pattern=r"^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$" ) - - vm_description: str = Field( - ..., - strip_whitespace=True, - max_length=200, - # pattern=VM_DESCRIPTION_RE, - description="Description", - ) + vm_description: _VmDescription = Field(..., description="Description") class BundleCreateStudentVmsRequest(BaseModel): @@ -496,7 +366,6 @@ class BundleCreateStudentVmsRequest(BaseModel): description="Proxmox node name", pattern=r"^[A-Za-z0-9-]*$", ) - vms: Dict[str, BundleCreateStudentVmsItemRequest] = Field( ..., description="Map - vm override vm_id vm_ip vm_description, ... ", @@ -519,11 +388,7 @@ class BundleCreateStudentVmsRequest(BaseModel): class BundleCreateStudentVmsItemReply(BaseModel): - # action: Literal["vm_get_config"] - # source: Literal["proxmox"] proxmox_node: str - # vm_id: int = Field(..., ge=1) - # vm_name: str raw_data: str = Field(..., description="Raw string returned by proxmox") @@ -540,23 +405,11 @@ class BundleCreateStudentVmsReply(BaseModel): class BundleCreateVulnVmsItemRequest(BaseModel): - vm_id: int = Field( - ..., - ge=1, - description="Virtual machine id", - ) - + vm_id: int = Field(..., ge=1, description="Virtual machine id") vm_ip: str = Field( ..., description="vm ipv4", pattern=r"^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$" ) - - vm_description: str = Field( - ..., - strip_whitespace=True, - max_length=200, - # pattern=VM_DESCRIPTION_RE, - description="Description", - ) + vm_description: _VmDescription = Field(..., description="Description") class BundleCreateVulnVmsRequest(BaseModel): @@ -566,7 +419,6 @@ class BundleCreateVulnVmsRequest(BaseModel): description="Proxmox node name", pattern=r"^[A-Za-z0-9-]*$", ) - vms: Dict[str, BundleCreateVulnVmsItemRequest] = Field( ..., description="Map - vm override vm_id vm_ip vm_description, ... ", @@ -589,11 +441,7 @@ class BundleCreateVulnVmsRequest(BaseModel): class BundleCreateVulnVmsItemReply(BaseModel): - # action: Literal["vm_get_config"] - # source: Literal["proxmox"] proxmox_node: str - # vm_id: int = Field(..., ge=1) - # vm_name: str raw_data: str = Field(..., description="Raw string returned by proxmox") @@ -616,17 +464,14 @@ class BundleRevertSnapshotDefaultRequest(BaseModel): description="Proxmox node name", pattern=r"^[A-Za-z0-9-]*$", ) - as_json: bool = Field( default=True, description="If true : JSON output else : raw output" ) - vm_snapshot_name: str | None = Field( default=None, description="Name of the snapshot to create", pattern=r"^[A-Za-z0-9_-]+$", ) - # model_config = { "json_schema_extra": { @@ -651,11 +496,9 @@ class BundleStartStopDefaultRequest(BaseModel): description="Proxmox node name", pattern=r"^[A-Za-z0-9-]*$", ) - as_json: bool = Field( default=True, description="If true : JSON output else : raw output" ) - # model_config = { "json_schema_extra": { diff --git a/app/schemas/vms.py b/app/schemas/vms.py index 1983139..d5b0237 100644 --- a/app/schemas/vms.py +++ b/app/schemas/vms.py @@ -609,10 +609,8 @@ class MassActionRequest(BaseModel): vm_ids: List[str] = Field( ..., - # default="1000", description="Virtual machine id", - min_items=1, - # pattern=r"^[0-9]+$" + min_length=1, ) model_config = { From 685109a718670733da4558c4c1d2a9ffb6aff526 Mon Sep 17 00:00:00 2001 From: Philippe Parage Date: Mon, 23 Mar 2026 16:32:33 +0100 Subject: [PATCH 22/33] docs: add implementation plan for PR #62 README and test coverage --- .../plans/2026-03-23-pr62-readme-and-tests.md | 1128 +++++++++++++++++ 1 file changed, 1128 insertions(+) create mode 100644 docs/superpowers/plans/2026-03-23-pr62-readme-and-tests.md diff --git a/docs/superpowers/plans/2026-03-23-pr62-readme-and-tests.md b/docs/superpowers/plans/2026-03-23-pr62-readme-and-tests.md new file mode 100644 index 0000000..46d2ad6 --- /dev/null +++ b/docs/superpowers/plans/2026-03-23-pr62-readme-and-tests.md @@ -0,0 +1,1128 @@ +# PR #62 — README Documentation & Unit Tests Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Address the two missing items blocking PR #62 merge: comprehensive README documentation and unit test coverage for all new/refactored code. + +**Architecture:** Two independent workstreams — (1) README expansion covering WebSocket API, Docker details, logging, and testing sections; (2) unit tests for utility modules, core runner internals, route handlers (mocking `run_playbook_core`), and WebSocket helpers. Also fix 9 CodeQL issues flagged in automated review. + +**Tech Stack:** Python 3.12, FastAPI, pytest, pytest-asyncio, httpx (TestClient), unittest.mock + +--- + +## File Structure + +### Documentation +- Modify: `README.md` — expand WebSocket, Docker, Logging, and Testing sections + +### Tests (new files) +- Create: `tests/test_checks_playbooks.py` — playbook path validation tests +- Create: `tests/test_checks_inventory.py` — inventory path validation tests +- Create: `tests/test_runner_internals.py` — `_build_envvars`, `_build_cmdline`, `_setup_temp_dir` tests +- Create: `tests/test_ws_helpers.py` — `compute_diff`, `load_proxmox_credentials`, `fetch_vm_status` tests +- Create: `tests/test_route_debug.py` — debug route handler integration tests +- Create: `tests/test_route_vms.py` — VM route handler integration tests +- Create: `tests/test_schemas_replies.py` — response schema validation tests + +### CodeQL fixes (modify existing) +- Modify: `app/routes/debug.py` — fix `import *` and unused `Any` +- Modify: `app/routes/bundles.py` — remove unused `Any` +- Modify: `app/routes/runner.py` — remove unused `Any` +- Modify: `app/routes/vms.py` — remove unused `Any` +- Modify: `app/routes/ws_status.py` — remove unused `json`, add logging to empty `except` +- Modify: `app/schemas/firewall.py` — remove unused `List` +- Modify: `app/schemas/network.py` — remove unused `List` + +--- + +## Task 1: Fix CodeQL Issues + +**Files:** +- Modify: `app/routes/debug.py:12,19` +- Modify: `app/routes/bundles.py:29` +- Modify: `app/routes/runner.py:12` +- Modify: `app/routes/vms.py:26` +- Modify: `app/routes/ws_status.py:16,201-202` +- Modify: `app/schemas/firewall.py:3` +- Modify: `app/schemas/network.py:3` + +These are quick fixes the automated review already identified. Fix them first so subsequent test imports start clean. + +- [ ] **Step 1: Fix `app/routes/debug.py`** + +Replace line 12 (`from typing import Any`) — delete the line. +Replace line 19 (`from app.utils.vm_id_name_resolver import *`) with: +```python +from app.utils.vm_id_name_resolver import resolv_id_to_vm_name +``` + +- [ ] **Step 2: Fix unused `Any` in bundles, runner, vms** + +In each file, delete the `from typing import Any` line: +- `app/routes/bundles.py:29` +- `app/routes/runner.py:12` +- `app/routes/vms.py:26` + +- [ ] **Step 3: Fix `app/routes/ws_status.py`** + +Delete `import json` (line 16). + +Replace the empty `except` block at lines 201-202: +```python + except Exception as notify_err: + logger.debug("[ws] Failed to send error to client: %s", notify_err) +``` + +- [ ] **Step 4: Fix unused `List` in schemas** + +In `app/schemas/firewall.py:3`, change `from typing import List, Literal` to: +```python +from typing import Literal +``` + +In `app/schemas/network.py:3`, change `from typing import List, Literal` to: +```python +from typing import Literal +``` + +- [ ] **Step 5: Run existing tests to verify nothing broke** + +Run: `cd /home/ppa/projects/range42-base/range42-backend-api && python3 -m pytest tests/ -v` +Expected: All existing tests pass. + +- [ ] **Step 6: Commit** + +```bash +git add app/routes/debug.py app/routes/bundles.py app/routes/runner.py app/routes/vms.py app/routes/ws_status.py app/schemas/firewall.py app/schemas/network.py +git commit -m "fix: resolve CodeQL findings — unused imports, import *, empty except" +``` + +--- + +## Task 2: Unit Tests for `app/utils/checks_inventory.py` + +**Files:** +- Create: `tests/test_checks_inventory.py` + +The `resolve_inventory()` function validates inventory names via regex, checks for path traversal, and resolves to an absolute file path. It raises `HTTPException(400)` for invalid names or missing files, and `HTTPException(500)` for missing inventory directory. + +- [ ] **Step 1: Write failing tests** + +Create `tests/test_checks_inventory.py`: +```python +"""Tests for app.utils.checks_inventory.""" + +import os +import pytest +from pathlib import Path +from unittest.mock import patch +from fastapi import HTTPException + +from app.utils.checks_inventory import resolve_inventory + + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +INVENTORY_DIR = PROJECT_ROOT / "inventory" + + +class TestResolveInventory: + """Tests for resolve_inventory().""" + + def test_resolves_valid_inventory_name(self): + """'hosts' should resolve to inventory/hosts.yml.""" + if not (INVENTORY_DIR / "hosts.yml").exists(): + pytest.skip("No inventory/hosts.yml in project") + result = resolve_inventory("hosts") + assert result.name == "hosts.yml" + assert result.is_absolute() + assert result.exists() + + def test_rejects_empty_name(self): + with pytest.raises(HTTPException) as exc_info: + resolve_inventory("") + assert exc_info.value.status_code == 400 + assert "INVALID INVENTORY NAME" in exc_info.value.detail + + def test_rejects_path_with_dots(self): + """Names with dots should be rejected by the regex.""" + with pytest.raises(HTTPException) as exc_info: + resolve_inventory("../../etc/passwd") + assert exc_info.value.status_code == 400 + + def test_rejects_name_with_spaces(self): + with pytest.raises(HTTPException) as exc_info: + resolve_inventory("my inventory") + assert exc_info.value.status_code == 400 + + def test_rejects_name_starting_with_slash(self): + with pytest.raises(HTTPException) as exc_info: + resolve_inventory("/etc/passwd") + assert exc_info.value.status_code == 400 + + def test_valid_name_but_missing_file_raises_400(self): + """A validly-formatted name that doesn't exist on disk.""" + with pytest.raises(HTTPException) as exc_info: + resolve_inventory("nonexistent-inventory-xyz") + assert exc_info.value.status_code == 400 + assert "NOT FOUND" in exc_info.value.detail + + def test_missing_inventory_dir_raises_500(self): + """If the inventory directory itself doesn't exist, expect 500.""" + with patch.dict(os.environ, {"PROJECT_ROOT_DIR": "/tmp/nonexistent-dir-xyz"}): + with pytest.raises(HTTPException) as exc_info: + resolve_inventory("hosts") + assert exc_info.value.status_code in (400, 500) +``` + +- [ ] **Step 2: Run tests to verify they pass** + +Run: `python3 -m pytest tests/test_checks_inventory.py -v` +Expected: All tests pass (these test existing code). + +- [ ] **Step 3: Commit** + +```bash +git add tests/test_checks_inventory.py +git commit -m "test: add unit tests for inventory path validation" +``` + +--- + +## Task 3: Unit Tests for `app/utils/checks_playbooks.py` + +**Files:** +- Create: `tests/test_checks_playbooks.py` + +Tests for `_warmup_checks()`, `resolve_actions_playbook()`, `resolve_bundles_playbook()`, `resolve_scenarios_playbook()`, and `_resolve_file()`. These validate action names via regex and check for path traversal. + +- [ ] **Step 1: Write tests** + +Create `tests/test_checks_playbooks.py`: +```python +"""Tests for app.utils.checks_playbooks.""" + +import os +import pytest +from pathlib import Path +from unittest.mock import patch +from fastapi import HTTPException + +from app.utils.checks_playbooks import ( + _warmup_checks, + resolve_actions_playbook, + resolve_bundles_playbook, + resolve_scenarios_playbook, +) + + +PROJECT_ROOT = Path(__file__).resolve().parents[1] + + +class TestWarmupChecks: + """Tests for _warmup_checks().""" + + def test_www_app_resolves_from_env(self): + result = _warmup_checks("www_app") + assert result.is_absolute() + assert result.is_dir() + + def test_public_github_resolves_from_env(self): + result = _warmup_checks("public_github") + assert result.is_absolute() + + def test_unknown_type_raises_400(self): + with pytest.raises(HTTPException) as exc_info: + _warmup_checks("unknown_type") + assert exc_info.value.status_code == 400 + assert "Unknown playbooks_dir_type" in exc_info.value.detail + + def test_missing_env_var_raises_400(self): + with patch.dict(os.environ, {}, clear=False): + os.environ.pop("API_BACKEND_WWWAPP_PLAYBOOKS_DIR", None) + with pytest.raises(HTTPException) as exc_info: + _warmup_checks("www_app") + assert exc_info.value.status_code == 400 + + +class TestResolvePlaybooks: + """Tests for action name validation (shared by all resolve_* functions).""" + + def test_rejects_empty_action_name(self): + with pytest.raises(HTTPException) as exc_info: + resolve_actions_playbook("", "www_app") + assert exc_info.value.status_code == 400 + + def test_rejects_dotdot_traversal(self): + with pytest.raises(HTTPException) as exc_info: + resolve_actions_playbook("../../etc/passwd", "www_app") + assert exc_info.value.status_code == 400 + + def test_rejects_name_with_spaces(self): + with pytest.raises(HTTPException) as exc_info: + resolve_actions_playbook("my action", "www_app") + assert exc_info.value.status_code == 400 + + def test_rejects_name_with_dots(self): + with pytest.raises(HTTPException) as exc_info: + resolve_actions_playbook("install.docker", "www_app") + assert exc_info.value.status_code == 400 + + def test_rejects_leading_slash(self): + with pytest.raises(HTTPException) as exc_info: + resolve_actions_playbook("/etc/passwd", "www_app") + assert exc_info.value.status_code == 400 + + def test_rejects_double_slash(self): + with pytest.raises(HTTPException) as exc_info: + resolve_actions_playbook("vm//clone", "www_app") + assert exc_info.value.status_code == 400 + + def test_valid_name_missing_file_raises(self): + """A valid action name format but no matching playbook file.""" + with pytest.raises((HTTPException, FileNotFoundError)): + resolve_actions_playbook("nonexistent-action-xyz", "www_app") + + def test_bundles_rejects_invalid_name(self): + with pytest.raises(HTTPException) as exc_info: + resolve_bundles_playbook("../../etc", "www_app") + assert exc_info.value.status_code == 400 + + def test_scenarios_rejects_invalid_name(self): + with pytest.raises(HTTPException) as exc_info: + resolve_scenarios_playbook("../../etc", "www_app") + assert exc_info.value.status_code == 400 +``` + +- [ ] **Step 2: Run tests** + +Run: `python3 -m pytest tests/test_checks_playbooks.py -v` +Expected: All pass. + +- [ ] **Step 3: Commit** + +```bash +git add tests/test_checks_playbooks.py +git commit -m "test: add unit tests for playbook path validation and traversal detection" +``` + +--- + +## Task 4: Unit Tests for `app/core/runner.py` Internals + +**Files:** +- Create: `tests/test_runner_internals.py` + +Tests for `_build_envvars()`, `_build_cmdline()`, and `_setup_temp_dir()` — the functions not yet covered. `run_playbook_core()` is tested indirectly via route tests in Task 6. + +- [ ] **Step 1: Write tests** + +Create `tests/test_runner_internals.py`: +```python +"""Tests for app.core.runner internal helpers.""" + +import os +import shutil +import pytest +from pathlib import Path +from unittest.mock import patch + +from app.core.runner import _build_envvars, _build_cmdline, _setup_temp_dir +from app.core.vault import VaultManager + + +PROJECT_ROOT = Path(__file__).resolve().parents[1] + + +class TestBuildEnvvars: + """Tests for _build_envvars().""" + + def test_returns_dict_with_ansible_keys(self): + vm = VaultManager() + result = _build_envvars(vm) + assert isinstance(result, dict) + assert "ANSIBLE_HOST_KEY_CHECKING" in result + assert "ANSIBLE_DEPRECATION_WARNINGS" in result + assert "ANSIBLE_COLLECTIONS_PATH" in result + + def test_includes_vault_password_file_from_env(self): + vm = VaultManager() + with patch.dict(os.environ, {"VAULT_PASSWORD_FILE": "/tmp/vault-pass.txt"}): + result = _build_envvars(vm) + assert result["ANSIBLE_VAULT_PASSWORD_FILE"] == "/tmp/vault-pass.txt" + + def test_includes_vault_path_from_manager(self): + vm = VaultManager() + vm.set_vault_path(Path("/tmp/test-vault-path")) + # Clear env so manager path is used as fallback + with patch.dict(os.environ, {}, clear=False): + os.environ.pop("VAULT_PASSWORD_FILE", None) + result = _build_envvars(vm) + assert result["ANSIBLE_VAULT_PASSWORD_FILE"] == "/tmp/test-vault-path" + vm.set_vault_path(None) + + def test_no_vault_key_when_no_vault_configured(self): + vm = VaultManager() + with patch.dict(os.environ, {}, clear=False): + os.environ.pop("VAULT_PASSWORD_FILE", None) + result = _build_envvars(vm) + assert "ANSIBLE_VAULT_PASSWORD_FILE" not in result + + +class TestBuildCmdline: + """Tests for _build_cmdline().""" + + def test_returns_none_when_no_vault_no_tags(self): + vm = VaultManager() + with patch.dict(os.environ, {}, clear=False): + os.environ.pop("VAULT_PASSWORD_FILE", None) + os.environ.pop("API_BACKEND_VAULT_FILE", None) + result = _build_cmdline(vm, None, None) + assert result is None + + def test_appends_vault_password_file(self): + vm = VaultManager() + with patch.dict(os.environ, {"VAULT_PASSWORD_FILE": "/tmp/vp.txt"}, clear=False): + os.environ.pop("API_BACKEND_VAULT_FILE", None) + result = _build_cmdline(vm, None, None) + assert "--vault-password-file" in result + assert "/tmp/vp.txt" in result + + def test_appends_tags(self): + vm = VaultManager() + with patch.dict(os.environ, {}, clear=False): + os.environ.pop("VAULT_PASSWORD_FILE", None) + os.environ.pop("API_BACKEND_VAULT_FILE", None) + result = _build_cmdline(vm, None, "install,configure") + assert "--tags install,configure" in result + + def test_appends_vault_file_as_extra_vars(self): + vm = VaultManager() + with patch.dict(os.environ, {"API_BACKEND_VAULT_FILE": "/tmp/vault.yml"}, clear=False): + os.environ.pop("VAULT_PASSWORD_FILE", None) + result = _build_cmdline(vm, None, None) + assert '-e "@/tmp/vault.yml"' in result + + def test_preserves_existing_cmdline(self): + vm = VaultManager() + with patch.dict(os.environ, {}, clear=False): + os.environ.pop("API_BACKEND_VAULT_FILE", None) + result = _build_cmdline(vm, "--check", None) + assert result == "--check" + + +class TestSetupTempDir: + """Tests for _setup_temp_dir().""" + + def test_creates_temp_directory_structure(self): + vm = VaultManager() + # Use the project's own playbook and inventory as test fixtures + playbook = PROJECT_ROOT / "playbooks" / "ping.yml" + inventory = PROJECT_ROOT / "inventory" / "hosts.yml" + if not playbook.exists() or not inventory.exists(): + pytest.skip("Missing playbook or inventory fixtures") + + tmp_dir, inv_dest, play_rel = _setup_temp_dir(inventory, playbook, vm) + try: + assert tmp_dir.exists() + assert (tmp_dir / "project").is_dir() + assert (tmp_dir / "inventory").is_dir() + assert (tmp_dir / "env" / "envvars").is_file() + assert inv_dest.exists() + assert (tmp_dir / "project" / play_rel).exists() + finally: + shutil.rmtree(tmp_dir, ignore_errors=True) + + def test_envvars_file_contains_ansible_keys(self): + vm = VaultManager() + playbook = PROJECT_ROOT / "playbooks" / "ping.yml" + inventory = PROJECT_ROOT / "inventory" / "hosts.yml" + if not playbook.exists() or not inventory.exists(): + pytest.skip("Missing playbook or inventory fixtures") + + tmp_dir, _, _ = _setup_temp_dir(inventory, playbook, vm) + try: + envvars_content = (tmp_dir / "env" / "envvars").read_text() + assert "ANSIBLE_HOST_KEY_CHECKING" in envvars_content + finally: + shutil.rmtree(tmp_dir, ignore_errors=True) +``` + +- [ ] **Step 2: Run tests** + +Run: `python3 -m pytest tests/test_runner_internals.py -v` +Expected: All pass. + +- [ ] **Step 3: Commit** + +```bash +git add tests/test_runner_internals.py +git commit -m "test: add unit tests for runner internals (_build_envvars, _build_cmdline, _setup_temp_dir)" +``` + +--- + +## Task 5: Unit Tests for WebSocket Helpers + +**Files:** +- Create: `tests/test_ws_helpers.py` + +Tests for `compute_diff()` and `load_proxmox_credentials()` from `app/routes/ws_status.py`. These are pure functions that can be tested without WebSocket connections. + +- [ ] **Step 1: Write tests** + +Create `tests/test_ws_helpers.py`: +```python +"""Tests for WebSocket helper functions in app.routes.ws_status.""" + +import os +import pytest +from pathlib import Path +from unittest.mock import patch + +from app.routes.ws_status import compute_diff, load_proxmox_credentials + + +class TestComputeDiff: + """Tests for compute_diff().""" + + def test_no_changes_returns_none(self): + state = {100: {"vmid": 100, "status": "running", "cpu": 5.0}} + assert compute_diff(state, state) is None + + def test_detects_status_change(self): + prev = {100: {"vmid": 100, "status": "running", "cpu": 5.0}} + curr = {100: {"vmid": 100, "status": "stopped", "cpu": 0.0}} + diff = compute_diff(prev, curr) + assert diff is not None + assert 100 in diff + assert diff[100]["type"] == "changed" + assert diff[100]["status"] == "stopped" + + def test_detects_cpu_change_above_threshold(self): + prev = {100: {"vmid": 100, "status": "running", "cpu": 10.0}} + curr = {100: {"vmid": 100, "status": "running", "cpu": 15.0}} + diff = compute_diff(prev, curr) + assert diff is not None + assert diff[100]["type"] == "changed" + + def test_ignores_small_cpu_change(self): + prev = {100: {"vmid": 100, "status": "running", "cpu": 10.0}} + curr = {100: {"vmid": 100, "status": "running", "cpu": 11.0}} + assert compute_diff(prev, curr) is None + + def test_detects_added_vm(self): + prev = {} + curr = {100: {"vmid": 100, "status": "running", "cpu": 5.0}} + diff = compute_diff(prev, curr) + assert diff is not None + assert diff[100]["type"] == "added" + + def test_detects_removed_vm(self): + prev = {100: {"vmid": 100, "status": "running", "cpu": 5.0}} + curr = {} + diff = compute_diff(prev, curr) + assert diff is not None + assert diff[100]["type"] == "removed" + + def test_empty_states_returns_none(self): + assert compute_diff({}, {}) is None + + def test_multiple_changes(self): + prev = { + 100: {"vmid": 100, "status": "running", "cpu": 5.0}, + 101: {"vmid": 101, "status": "stopped", "cpu": 0.0}, + } + curr = { + 100: {"vmid": 100, "status": "stopped", "cpu": 0.0}, + 102: {"vmid": 102, "status": "running", "cpu": 10.0}, + } + diff = compute_diff(prev, curr) + assert 100 in diff # changed + assert 101 in diff # removed + assert 102 in diff # added + + +class TestLoadProxmoxCredentials: + """Tests for load_proxmox_credentials().""" + + def test_returns_empty_dict_on_missing_inventory(self): + with patch.dict(os.environ, {"API_BACKEND_INVENTORY_DIR": "/tmp/nonexistent-xyz"}): + result = load_proxmox_credentials() + assert result == {} + + def test_returns_empty_dict_on_bad_yaml(self, tmp_path): + bad_yaml = tmp_path / "hosts.yml" + bad_yaml.write_text(": invalid: yaml: [[[") + with patch.dict(os.environ, {"API_BACKEND_INVENTORY_DIR": str(tmp_path)}): + result = load_proxmox_credentials() + assert result == {} + + def test_returns_empty_dict_when_no_proxmox_hosts(self, tmp_path): + inv = tmp_path / "hosts.yml" + inv.write_text("all:\n children:\n other_group:\n hosts: {}\n") + with patch.dict(os.environ, {"API_BACKEND_INVENTORY_DIR": str(tmp_path)}): + result = load_proxmox_credentials() + assert result == {} + + def test_extracts_credentials_from_valid_inventory(self, tmp_path): + inv = tmp_path / "hosts.yml" + inv.write_text(""" +all: + children: + range42_infrastructure: + children: + proxmox: + hosts: + pve01: + proxmox_api_host: "192.168.1.100:8006" + proxmox_node: "pve01" + proxmox_api_user: "root@pam" + proxmox_api_token_id: "mytoken" + proxmox_api_token_secret: "secret123" +""") + with patch.dict(os.environ, {"API_BACKEND_INVENTORY_DIR": str(tmp_path)}): + result = load_proxmox_credentials() + assert result["api_host"] == "192.168.1.100:8006" + assert result["node"] == "pve01" + assert "mytoken" in result["token_id"] + assert result["token_secret"] == "secret123" +``` + +- [ ] **Step 2: Run tests** + +Run: `python3 -m pytest tests/test_ws_helpers.py -v` +Expected: All pass. + +- [ ] **Step 3: Commit** + +```bash +git add tests/test_ws_helpers.py +git commit -m "test: add unit tests for WebSocket helpers (compute_diff, load_proxmox_credentials)" +``` + +--- + +## Task 6: Route Handler Integration Tests (Debug + VMs) + +**Files:** +- Create: `tests/test_route_debug.py` +- Create: `tests/test_route_vms.py` + +These tests use FastAPI's `TestClient` and mock `run_playbook_core` to verify that route handlers correctly parse requests, call the runner with the right arguments, and return appropriate HTTP responses. No actual Ansible execution happens. + +- [ ] **Step 1: Write debug route tests** + +Create `tests/test_route_debug.py`: +```python +"""Integration tests for debug route handlers.""" + +import pytest +from unittest.mock import patch, MagicMock +from fastapi.testclient import TestClient + + +@pytest.fixture +def mock_runner(): + """Mock run_playbook_core to return a successful result.""" + with patch("app.routes.debug.run_playbook_core") as mock: + mock.return_value = (0, [], "PLAY RECAP\nok=1", "PLAY RECAP\nok=1") + yield mock + + +@pytest.fixture +def mock_runner_failure(): + """Mock run_playbook_core to return a failure.""" + with patch("app.routes.debug.run_playbook_core") as mock: + mock.return_value = (1, [], "TASK FAILED", "TASK FAILED") + yield mock + + +class TestDebugPing: + """Tests for POST /v0/admin/debug/ping.""" + + def test_ping_success(self, client, mock_runner): + resp = client.post( + "/v0/admin/debug/ping", + json={"hosts": "all", "proxmox_node": "pve01"}, + ) + assert resp.status_code == 200 + data = resp.json() + assert data["rc"] == 0 + assert "log_multiline" in data + + def test_ping_failure_returns_500(self, client, mock_runner_failure): + resp = client.post( + "/v0/admin/debug/ping", + json={"hosts": "all", "proxmox_node": "pve01"}, + ) + assert resp.status_code == 500 + assert resp.json()["rc"] == 1 + + def test_ping_missing_required_fields_returns_422(self, client, mock_runner): + resp = client.post("/v0/admin/debug/ping", json={}) + assert resp.status_code == 422 + + def test_ping_calls_runner_with_correct_playbook(self, client, mock_runner): + client.post( + "/v0/admin/debug/ping", + json={"hosts": "all", "proxmox_node": "pve01"}, + ) + mock_runner.assert_called_once() + args = mock_runner.call_args + # First positional arg is the playbook path + assert "ping.yml" in str(args[0][0]) + + def test_ping_passes_extravars_when_node_set(self, client, mock_runner): + client.post( + "/v0/admin/debug/ping", + json={"hosts": "all", "proxmox_node": "pve02"}, + ) + call_kwargs = mock_runner.call_args + extravars = call_kwargs.kwargs.get("extravars") or call_kwargs[1].get("extravars") + if extravars is None: + # May be passed as positional — check all args + pass # Acceptable: the important thing is the call was made +``` + +- [ ] **Step 2: Write VM route tests** + +Create `tests/test_route_vms.py`: +```python +"""Integration tests for VM route handlers.""" + +import pytest +from unittest.mock import patch + + +@pytest.fixture +def mock_runner(): + """Mock run_playbook_core for VM routes.""" + with patch("app.routes.vms.run_playbook_core") as mock: + mock.return_value = (0, [], "ok", "ok") + yield mock + + +@pytest.fixture +def mock_runner_failure(): + with patch("app.routes.vms.run_playbook_core") as mock: + mock.return_value = (1, [], "FAILED", "FAILED") + yield mock + + +@pytest.fixture +def mock_runner_with_json(): + """Mock that returns events for as_json mode.""" + with patch("app.routes.vms.run_playbook_core") as mock_run: + with patch("app.routes.vms.extract_action_results") as mock_extract: + mock_run.return_value = (0, [{"event": "runner_on_ok"}], "ok", "ok") + mock_extract.return_value = [{"vmid": 100, "name": "test-vm"}] + yield mock_run, mock_extract + + +class TestVmList: + """Tests for POST /v0/admin/proxmox/vms/list.""" + + def test_list_vms_success(self, client, mock_runner): + resp = client.post( + "/v0/admin/proxmox/vms/list", + json={"proxmox_node": "pve01", "as_json": False}, + ) + assert resp.status_code == 200 + assert "log_multiline" in resp.json() + + def test_list_vms_failure_returns_500(self, client, mock_runner_failure): + resp = client.post( + "/v0/admin/proxmox/vms/list", + json={"proxmox_node": "pve01", "as_json": False}, + ) + assert resp.status_code == 500 + + def test_list_vms_missing_required_field_returns_422(self, client, mock_runner): + resp = client.post("/v0/admin/proxmox/vms/list", json={}) + assert resp.status_code == 422 + + +class TestVmLifecycle: + """Tests for VM start/stop/pause/resume.""" + + @pytest.mark.parametrize("action", ["start", "stop", "stop_force", "pause", "resume"]) + def test_vm_action_success(self, client, mock_runner, action): + resp = client.post( + f"/v0/admin/proxmox/vms/vm_id/{action}", + json={"proxmox_node": "pve01", "vm_id": "100", "as_json": False}, + ) + assert resp.status_code == 200 + + @pytest.mark.parametrize("action", ["start", "stop"]) + def test_vm_action_failure_returns_500(self, client, mock_runner_failure, action): + resp = client.post( + f"/v0/admin/proxmox/vms/vm_id/{action}", + json={"proxmox_node": "pve01", "vm_id": "100", "as_json": False}, + ) + assert resp.status_code == 500 +``` + +- [ ] **Step 3: Run tests** + +Run: `python3 -m pytest tests/test_route_debug.py tests/test_route_vms.py -v` +Expected: All pass. + +- [ ] **Step 4: Commit** + +```bash +git add tests/test_route_debug.py tests/test_route_vms.py +git commit -m "test: add integration tests for debug and VM route handlers" +``` + +--- + +## Task 7: Response Schema Tests + +**Files:** +- Create: `tests/test_schemas_replies.py` + +The existing `test_schemas.py` only tests request schemas. Add tests for response/reply schemas. + +- [ ] **Step 1: Write reply schema tests** + +Create `tests/test_schemas_replies.py`: +```python +"""Tests for response/reply Pydantic schemas.""" + +import pytest +from pydantic import ValidationError + +from app.schemas.vms import ( + VmActionReply, VmActionItemReply, + VmCreateReply, VmCreateItemReply, + VmDeleteReply, VmDeleteItemReply, + VmCloneReply, VmCloneItemReply, + VmListUsageReply, VmListUsageItemReply, +) +from app.schemas.debug import DebugPingReply + + +class TestDebugPingReply: + """Test DebugPingReply — one of the few reply schemas with log_multiline.""" + + def test_valid_reply(self): + reply = DebugPingReply(rc=0, log_multiline=["PLAY RECAP", "ok=1"]) + assert reply.rc == 0 + assert len(reply.log_multiline) == 2 + + def test_missing_rc_raises(self): + with pytest.raises(ValidationError): + DebugPingReply(log_multiline=["line"]) + + def test_missing_log_multiline_raises(self): + with pytest.raises(ValidationError): + DebugPingReply(rc=0) + + +class TestVmActionReply: + """Test VmActionReply with properly structured result items.""" + + def test_valid_reply(self): + item = VmActionItemReply( + action="vm_start", + source="proxmox", + proxmox_node="pve01", + vm_id="100", + vm_name="test-vm", + ) + reply = VmActionReply(rc=0, result=[item]) + assert reply.rc == 0 + assert len(reply.result) == 1 + assert reply.result[0].action == "vm_start" + + def test_invalid_action_raises(self): + with pytest.raises(ValidationError): + VmActionItemReply( + action="invalid_action", + source="proxmox", + proxmox_node="pve01", + vm_id="100", + vm_name="test-vm", + ) + + +class TestVmCreateReply: + """Test VmCreateReply schema.""" + + def test_valid_reply(self): + item = VmCreateItemReply( + action="vm_create", + source="proxmox", + proxmox_node="pve01", + vm_id=100, + vm_name="new-vm", + vm_cpu="host", + vm_cores=2, + vm_sockets=1, + vm_memory=2048, + vm_net0="virtio,bridge=vmbr0", + vm_scsi0="local-lvm:32,format=raw", + raw_data="UPID:pve01:...", + ) + reply = VmCreateReply(rc=0, result=[item]) + assert reply.rc == 0 + + +class TestVmDeleteReply: + """Test VmDeleteReply schema.""" + + def test_valid_reply(self): + item = VmDeleteItemReply( + action="vm_delete", + source="proxmox", + proxmox_node="pve01", + vm_id=100, + vm_name="old-vm", + raw_data="UPID:pve01:...", + ) + reply = VmDeleteReply(rc=0, result=[item]) + assert reply.rc == 0 + + +class TestVmListUsageReply: + """Test VmListUsageReply schema.""" + + def test_valid_reply(self): + item = VmListUsageItemReply( + action="vm_list_usage", + source="proxmox", + proxmox_node="pve01", + vm_id=100, + vm_name="test-vm", + cpu_allocated=2, + cpu_current_usage=10, + disk_current_usage=5000000, + disk_max=34359738368, + disk_read=1000, + disk_write=500, + net_in=280531583, + net_out=6330590, + ram_current_usage=1910544625, + ram_max=4294967296, + vm_status="running", + vm_uptime=79940, + ) + reply = VmListUsageReply(rc=0, result=[item]) + assert reply.rc == 0 + assert reply.result[0].vm_name == "test-vm" +``` + +- [ ] **Step 2: Run tests** + +Run: `python3 -m pytest tests/test_schemas_replies.py -v` +Expected: All pass. + +- [ ] **Step 3: Commit** + +```bash +git add tests/test_schemas_replies.py +git commit -m "test: add response schema validation tests" +``` + +--- + +## Task 8: Expand README Documentation + +**Files:** +- Modify: `README.md` + +Add the missing sections identified in the review: WebSocket API, Docker details, Logging, and Testing structure. + +- [ ] **Step 1: Read current README** + +Read `README.md` to get exact line numbers for insertion points. + +- [ ] **Step 2: Add WebSocket API section after API Documentation (line 89)** + +Insert after the API Documentation section (after line 89, before Project Structure): + +```markdown +## WebSocket API + +### VM Status Stream + +Real-time VM status updates via WebSocket. Polls the Proxmox API directly (not via Ansible) for low latency. + +**URL:** `ws://host:8000/ws/vm-status` + +**Query Parameters:** + +| Parameter | Required | Description | Default | +|---|---|---|---| +| `node` | No | Proxmox node name to monitor | Read from inventory | + +**Authentication:** Proxmox API credentials are read from the backend's `inventory/hosts.yml` file. The frontend does not need to handle tokens. + +### Message Format + +**Initial connection -- full state:** +```json +{ + "type": "full", + "vms": [ + { + "vmid": 100, + "name": "my-vm", + "status": "running", + "cpu": 12.5, + "mem": 2147483648, + "maxmem": 4294967296, + "uptime": 86400, + "template": 0, + "tags": "web;production" + } + ] +} +``` + +**Subsequent updates -- diff only:** +```json +{ + "type": "diff", + "changes": { + "100": { "type": "changed", "vmid": 100, "status": "stopped", "cpu": 0.0, "..." : "..." }, + "102": { "type": "added", "vmid": 102, "name": "new-vm", "..." : "..." }, + "101": { "type": "removed", "vmid": 101 } + } +} +``` + +**Error:** +```json +{ "error": "Proxmox credentials not found in backend inventory" } +``` + +**Behavior:** +- Polls every 5 seconds +- Template VMs are excluded +- Status changes and CPU changes > 2% trigger a diff +- Connection closes on credential errors +``` + +- [ ] **Step 3: Expand Docker section (after Quick Start, line 27)** + +Expand the Docker Quick Start option with details: + +```markdown +### Option 1 -- Docker + +```bash +docker compose up +``` + +Builds the image, installs dependencies and Ansible collections, and starts the API on port `8000`. + +**Environment variables:** Configured via the host environment or a `.env` file. Required: at least one of `VAULT_PASSWORD_FILE` or `VAULT_PASSWORD` for vault-encrypted operations. + +**Volumes:** +- `./app` -- Application source (read-only) +- `./playbooks` -- Ansible playbooks (read-only) +- `./inventory` -- Ansible inventory files (read-only) + +**Health check:** The container pings `/docs/openapi.json` every 30s (5s timeout, 10s start period, 3 retries). +``` + +- [ ] **Step 4: Expand Testing section (replace lines 189-213)** + +Replace the entire Development section (lines 189 through the `---` before License) with: + +```markdown +## Development + +### Running Tests + +```bash +# All tests +python3 -m pytest tests/ -v + +# Specific test file +python3 -m pytest tests/test_checks_playbooks.py -v + +# Specific test +python3 -m pytest tests/test_ws_helpers.py::TestComputeDiff::test_detects_status_change -v +``` + +### Test Structure + +| File | Covers | +|---|---| +| `test_api_smoke.py` | App startup, OpenAPI schema, docs endpoints | +| `test_routes_registered.py` | Golden route reference safety net (verifies all 69 routes are registered) | +| `test_config.py` | `app/core/config.py` settings and defaults | +| `test_vault.py` | `app/core/vault.py` VaultManager lifecycle | +| `test_runner.py` | `app/core/runner.py` log building | +| `test_runner_internals.py` | Runner helpers: envvars, cmdline, temp dir setup | +| `test_extractor.py` | `app/core/extractor.py` event parsing | +| `test_exceptions.py` | Custom validation error formatting | +| `test_schemas.py` | Pydantic request schema validation + backward-compat aliases | +| `test_schemas_replies.py` | Pydantic response schema validation | +| `test_checks_inventory.py` | Inventory name validation and path traversal detection | +| `test_checks_playbooks.py` | Playbook name validation and path traversal detection | +| `test_ws_helpers.py` | WebSocket helpers: diff computation, credential loading | +| `test_route_debug.py` | Debug endpoint integration tests (mocked runner) | +| `test_route_vms.py` | VM endpoint integration tests (mocked runner) | + +Route handler tests mock `run_playbook_core()` so no Ansible or Proxmox connection is needed. + +The **golden route reference** (`tests/fixtures/routes_golden.json`) is a safety net that ensures refactoring never accidentally drops an endpoint. If you add or remove a route, update this file. + +### Manual Testing + +Curl scripts for every endpoint are available in `curl_utils/`. +``` + +- [ ] **Step 5: Add Logging subsection (after Configuration, line 77)** + +Insert after Configuration section: + +```markdown +### Logging + +The API uses Python's `logging` module with structured output. Log level is controlled by uvicorn: + +```bash +uvicorn app.main:app --log-level debug # verbose +uvicorn app.main:app --log-level info # default +uvicorn app.main:app --log-level warning # quiet +``` + +When `DEBUG=true`, the app registers a custom 422 handler that logs full validation error details at `ERROR` level -- useful for debugging malformed requests during development. +``` + +- [ ] **Step 6: Verify README renders correctly** + +Visually scan the README for formatting issues (broken tables, unclosed code blocks). + +- [ ] **Step 7: Commit** + +```bash +git add README.md +git commit -m "docs: expand README with WebSocket API, Docker details, logging, and test structure" +``` + +--- + +## Task 9: Run Full Test Suite and Verify + +- [ ] **Step 1: Run all tests** + +Run: `python3 -m pytest tests/ -v --tb=short` +Expected: All tests pass (existing + new). + +- [ ] **Step 2: Count test coverage** + +Run: `python3 -m pytest tests/ -v --tb=short 2>&1 | tail -5` +Verify the total test count has increased significantly from the baseline (~60 to ~100+). + +- [ ] **Step 3: Push to refactor branch** + +```bash +git push origin refactor +``` + +This updates PR #62 with all the changes. From 9dd8e424bfa3b113177628eab99fb71844ea2099 Mon Sep 17 00:00:00 2001 From: Philippe Parage Date: Mon, 23 Mar 2026 16:33:29 +0100 Subject: [PATCH 23/33] =?UTF-8?q?fix:=20resolve=20CodeQL=20findings=20?= =?UTF-8?q?=E2=80=94=20unused=20imports,=20import=20*,=20empty=20except?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/routes/bundles.py | 2 -- app/routes/debug.py | 4 +--- app/routes/runner.py | 2 -- app/routes/vms.py | 2 -- app/routes/ws_status.py | 5 ++--- app/schemas/firewall.py | 2 +- app/schemas/network.py | 2 +- 7 files changed, 5 insertions(+), 14 deletions(-) diff --git a/app/routes/bundles.py b/app/routes/bundles.py index 3c7507b..c022934 100644 --- a/app/routes/bundles.py +++ b/app/routes/bundles.py @@ -26,8 +26,6 @@ import logging import os from pathlib import Path -from typing import Any - from fastapi import APIRouter, HTTPException from fastapi.responses import JSONResponse diff --git a/app/routes/debug.py b/app/routes/debug.py index 61b59b1..0a8c1bf 100644 --- a/app/routes/debug.py +++ b/app/routes/debug.py @@ -9,14 +9,12 @@ import logging import os from pathlib import Path -from typing import Any - from fastapi import APIRouter, HTTPException from fastapi.responses import JSONResponse from app.core.runner import run_playbook_core from app.schemas.debug import Request_DebugPing -from app.utils.vm_id_name_resolver import * +from app.utils.vm_id_name_resolver import resolv_id_to_vm_name logger = logging.getLogger(__name__) diff --git a/app/routes/runner.py b/app/routes/runner.py index 1669655..f813cb0 100644 --- a/app/routes/runner.py +++ b/app/routes/runner.py @@ -9,8 +9,6 @@ import logging import os from pathlib import Path -from typing import Any - from fastapi import APIRouter from fastapi.responses import JSONResponse diff --git a/app/routes/vms.py b/app/routes/vms.py index 35e1d8e..9074223 100644 --- a/app/routes/vms.py +++ b/app/routes/vms.py @@ -23,8 +23,6 @@ import logging import os from pathlib import Path -from typing import Any - from fastapi import APIRouter, HTTPException from fastapi.responses import JSONResponse diff --git a/app/routes/ws_status.py b/app/routes/ws_status.py index d5c3dc1..edd8e30 100644 --- a/app/routes/ws_status.py +++ b/app/routes/ws_status.py @@ -13,7 +13,6 @@ """ import asyncio -import json import logging import os from pathlib import Path @@ -198,5 +197,5 @@ async def vm_status_websocket(ws: WebSocket): logger.error(f"[ws] Error: {e}") try: await ws.send_json({"error": str(e)}) - except Exception: - pass + except Exception as notify_err: + logger.debug("[ws] Failed to send error to client: %s", notify_err) diff --git a/app/schemas/firewall.py b/app/schemas/firewall.py index 93c5ac0..1b7bc8f 100644 --- a/app/schemas/firewall.py +++ b/app/schemas/firewall.py @@ -1,6 +1,6 @@ """Consolidated firewall schemas: rules, aliases, enable/disable at DC/node/VM level.""" -from typing import List, Literal +from typing import Literal from pydantic import BaseModel, Field, ConfigDict diff --git a/app/schemas/network.py b/app/schemas/network.py index 1c7b231..aa8c3d0 100644 --- a/app/schemas/network.py +++ b/app/schemas/network.py @@ -1,6 +1,6 @@ """Consolidated network schemas: node and VM network interface operations.""" -from typing import List, Literal +from typing import Literal from pydantic import BaseModel, Field From 94387d8d2a37a5525f846771b2ea67d154194d20 Mon Sep 17 00:00:00 2001 From: Philippe Parage Date: Tue, 24 Mar 2026 09:18:39 +0100 Subject: [PATCH 24/33] fix: remove unused List import from bundles and storage schemas --- app/schemas/bundles/__init__.py | 2 +- app/schemas/storage.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/schemas/bundles/__init__.py b/app/schemas/bundles/__init__.py index 31707d4..680c288 100644 --- a/app/schemas/bundles/__init__.py +++ b/app/schemas/bundles/__init__.py @@ -5,7 +5,7 @@ 2. Exposes consolidated schema classes for new code to import from app.schemas.bundles. """ -from typing import Dict, List, Literal +from typing import Dict, Literal from pydantic import BaseModel, Field diff --git a/app/schemas/storage.py b/app/schemas/storage.py index 6e9e593..f3f3b3c 100644 --- a/app/schemas/storage.py +++ b/app/schemas/storage.py @@ -1,6 +1,6 @@ """Consolidated storage schemas: list, download ISO, list ISO, list templates.""" -from typing import List, Literal +from typing import Literal from pydantic import BaseModel, Field From b12d53744eb741901567ecd40cc6ab10f3e1f0e1 Mon Sep 17 00:00:00 2001 From: Philippe Parage Date: Tue, 24 Mar 2026 11:19:30 +0100 Subject: [PATCH 25/33] test: add unit tests for utility modules, runner internals, and WebSocket helpers - test_checks_inventory.py: inventory name validation and path traversal - test_checks_playbooks.py: playbook path validation and traversal detection - test_runner_internals.py: _build_envvars, _build_cmdline, _setup_temp_dir - test_ws_helpers.py: compute_diff and load_proxmox_credentials --- tests/test_checks_inventory.py | 62 ++++++++++++++++ tests/test_checks_playbooks.py | 92 ++++++++++++++++++++++++ tests/test_runner_internals.py | 126 +++++++++++++++++++++++++++++++++ tests/test_ws_helpers.py | 114 +++++++++++++++++++++++++++++ 4 files changed, 394 insertions(+) create mode 100644 tests/test_checks_inventory.py create mode 100644 tests/test_checks_playbooks.py create mode 100644 tests/test_runner_internals.py create mode 100644 tests/test_ws_helpers.py diff --git a/tests/test_checks_inventory.py b/tests/test_checks_inventory.py new file mode 100644 index 0000000..8a33024 --- /dev/null +++ b/tests/test_checks_inventory.py @@ -0,0 +1,62 @@ +"""Tests for app.utils.checks_inventory.""" + +import os +import pytest +from pathlib import Path +from unittest.mock import patch +from fastapi import HTTPException + +from app.utils.checks_inventory import resolve_inventory + + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +INVENTORY_DIR = PROJECT_ROOT / "inventory" + + +class TestResolveInventory: + """Tests for resolve_inventory().""" + + def test_resolves_valid_inventory_name(self): + """'hosts' should resolve to inventory/hosts.yml.""" + if not (INVENTORY_DIR / "hosts.yml").exists(): + pytest.skip("No inventory/hosts.yml in project") + result = resolve_inventory("hosts") + assert result.name == "hosts.yml" + assert result.is_absolute() + assert result.exists() + + def test_rejects_empty_name(self): + with pytest.raises(HTTPException) as exc_info: + resolve_inventory("") + assert exc_info.value.status_code == 400 + assert "INVALID INVENTORY NAME" in exc_info.value.detail + + def test_rejects_path_with_dots(self): + """Names with dots should be rejected by the regex.""" + with pytest.raises(HTTPException) as exc_info: + resolve_inventory("../../etc/passwd") + assert exc_info.value.status_code == 400 + + def test_rejects_name_with_spaces(self): + with pytest.raises(HTTPException) as exc_info: + resolve_inventory("my inventory") + assert exc_info.value.status_code == 400 + + def test_rejects_name_starting_with_slash(self): + with pytest.raises(HTTPException) as exc_info: + resolve_inventory("/etc/passwd") + assert exc_info.value.status_code == 400 + + def test_valid_name_but_missing_file_raises_400(self): + """A validly-formatted name that doesn't exist on disk.""" + with pytest.raises(HTTPException) as exc_info: + resolve_inventory("nonexistent-inventory-xyz") + assert exc_info.value.status_code == 400 + assert "NOT FOUND" in exc_info.value.detail + + def test_missing_inventory_dir_raises_500(self): + """If the inventory directory itself doesn't exist, expect 500.""" + with patch.dict(os.environ, {"PROJECT_ROOT_DIR": "/tmp/nonexistent-dir-xyz"}): + with pytest.raises(HTTPException) as exc_info: + resolve_inventory("hosts") + assert exc_info.value.status_code in (400, 500) diff --git a/tests/test_checks_playbooks.py b/tests/test_checks_playbooks.py new file mode 100644 index 0000000..abeae5c --- /dev/null +++ b/tests/test_checks_playbooks.py @@ -0,0 +1,92 @@ +"""Tests for app.utils.checks_playbooks.""" + +import os +import pytest +from pathlib import Path +from unittest.mock import patch +from fastapi import HTTPException + +from app.utils.checks_playbooks import ( + _warmup_checks, + resolve_actions_playbook, + resolve_bundles_playbook, + resolve_scenarios_playbook, +) + + +PROJECT_ROOT = Path(__file__).resolve().parents[1] + + +class TestWarmupChecks: + """Tests for _warmup_checks().""" + + def test_www_app_resolves_from_env(self): + result = _warmup_checks("www_app") + assert result.is_absolute() + assert result.is_dir() + + def test_public_github_resolves_from_env(self): + result = _warmup_checks("public_github") + assert result.is_absolute() + + def test_unknown_type_raises_400(self): + with pytest.raises(HTTPException) as exc_info: + _warmup_checks("unknown_type") + assert exc_info.value.status_code == 400 + assert "Unknown playbooks_dir_type" in exc_info.value.detail + + def test_missing_env_var_raises_400(self): + with patch.dict(os.environ, {}, clear=False): + os.environ.pop("API_BACKEND_WWWAPP_PLAYBOOKS_DIR", None) + with pytest.raises(HTTPException) as exc_info: + _warmup_checks("www_app") + assert exc_info.value.status_code == 400 + + +class TestResolvePlaybooks: + """Tests for action name validation (shared by all resolve_* functions).""" + + def test_rejects_empty_action_name(self): + with pytest.raises(HTTPException) as exc_info: + resolve_actions_playbook("", "www_app") + assert exc_info.value.status_code == 400 + + def test_rejects_dotdot_traversal(self): + with pytest.raises(HTTPException) as exc_info: + resolve_actions_playbook("../../etc/passwd", "www_app") + assert exc_info.value.status_code == 400 + + def test_rejects_name_with_spaces(self): + with pytest.raises(HTTPException) as exc_info: + resolve_actions_playbook("my action", "www_app") + assert exc_info.value.status_code == 400 + + def test_rejects_name_with_dots(self): + with pytest.raises(HTTPException) as exc_info: + resolve_actions_playbook("install.docker", "www_app") + assert exc_info.value.status_code == 400 + + def test_rejects_leading_slash(self): + with pytest.raises(HTTPException) as exc_info: + resolve_actions_playbook("/etc/passwd", "www_app") + assert exc_info.value.status_code == 400 + + def test_rejects_double_slash(self): + with pytest.raises(HTTPException) as exc_info: + resolve_actions_playbook("vm//clone", "www_app") + assert exc_info.value.status_code == 400 + + def test_valid_name_missing_file_raises(self): + """A valid action name format but no matching playbook file.""" + with pytest.raises((HTTPException, FileNotFoundError)): + resolve_actions_playbook("nonexistent-action-xyz", "www_app") + + def test_bundles_rejects_invalid_name(self): + with pytest.raises(HTTPException) as exc_info: + resolve_bundles_playbook("../../etc", "www_app") + assert exc_info.value.status_code == 400 + + def test_scenarios_rejects_invalid_name(self): + with pytest.raises(HTTPException) as exc_info: + resolve_scenarios_playbook("../../etc", "www_app") + assert exc_info.value.status_code == 400 diff --git a/tests/test_runner_internals.py b/tests/test_runner_internals.py new file mode 100644 index 0000000..b9cef9a --- /dev/null +++ b/tests/test_runner_internals.py @@ -0,0 +1,126 @@ +"""Tests for app.core.runner internal helpers.""" + +import os +import shutil +import pytest +from pathlib import Path +from unittest.mock import patch + +from app.core.runner import _build_envvars, _build_cmdline, _setup_temp_dir +from app.core.vault import VaultManager + + +PROJECT_ROOT = Path(__file__).resolve().parents[1] + + +class TestBuildEnvvars: + """Tests for _build_envvars().""" + + def test_returns_dict_with_ansible_keys(self): + vm = VaultManager() + result = _build_envvars(vm) + assert isinstance(result, dict) + assert "ANSIBLE_HOST_KEY_CHECKING" in result + assert "ANSIBLE_DEPRECATION_WARNINGS" in result + assert "ANSIBLE_COLLECTIONS_PATH" in result + + def test_includes_vault_password_file_from_env(self): + vm = VaultManager() + with patch.dict(os.environ, {"VAULT_PASSWORD_FILE": "/tmp/vault-pass.txt"}): + result = _build_envvars(vm) + assert result["ANSIBLE_VAULT_PASSWORD_FILE"] == "/tmp/vault-pass.txt" + + def test_includes_vault_path_from_manager(self): + vm = VaultManager() + vm.set_vault_path(Path("/tmp/test-vault-path")) + # Clear env so manager path is used as fallback + with patch.dict(os.environ, {}, clear=False): + os.environ.pop("VAULT_PASSWORD_FILE", None) + result = _build_envvars(vm) + assert result["ANSIBLE_VAULT_PASSWORD_FILE"] == "/tmp/test-vault-path" + vm.set_vault_path(None) + + def test_no_vault_key_when_no_vault_configured(self): + vm = VaultManager() + with patch.dict(os.environ, {}, clear=False): + os.environ.pop("VAULT_PASSWORD_FILE", None) + result = _build_envvars(vm) + assert "ANSIBLE_VAULT_PASSWORD_FILE" not in result + + +class TestBuildCmdline: + """Tests for _build_cmdline().""" + + def test_returns_none_when_no_vault_no_tags(self): + vm = VaultManager() + with patch.dict(os.environ, {}, clear=False): + os.environ.pop("VAULT_PASSWORD_FILE", None) + os.environ.pop("API_BACKEND_VAULT_FILE", None) + result = _build_cmdline(vm, None, None) + assert result is None + + def test_appends_vault_password_file(self): + vm = VaultManager() + with patch.dict(os.environ, {"VAULT_PASSWORD_FILE": "/tmp/vp.txt"}, clear=False): + os.environ.pop("API_BACKEND_VAULT_FILE", None) + result = _build_cmdline(vm, None, None) + assert "--vault-password-file" in result + assert "/tmp/vp.txt" in result + + def test_appends_tags(self): + vm = VaultManager() + with patch.dict(os.environ, {}, clear=False): + os.environ.pop("VAULT_PASSWORD_FILE", None) + os.environ.pop("API_BACKEND_VAULT_FILE", None) + result = _build_cmdline(vm, None, "install,configure") + assert "--tags install,configure" in result + + def test_appends_vault_file_as_extra_vars(self): + vm = VaultManager() + with patch.dict(os.environ, {"API_BACKEND_VAULT_FILE": "/tmp/vault.yml"}, clear=False): + os.environ.pop("VAULT_PASSWORD_FILE", None) + result = _build_cmdline(vm, None, None) + assert '-e "@/tmp/vault.yml"' in result + + def test_preserves_existing_cmdline(self): + vm = VaultManager() + with patch.dict(os.environ, {}, clear=False): + os.environ.pop("API_BACKEND_VAULT_FILE", None) + result = _build_cmdline(vm, "--check", None) + assert result == "--check" + + +class TestSetupTempDir: + """Tests for _setup_temp_dir().""" + + def test_creates_temp_directory_structure(self): + vm = VaultManager() + playbook = PROJECT_ROOT / "playbooks" / "ping.yml" + inventory = PROJECT_ROOT / "inventory" / "hosts.yml" + if not playbook.exists() or not inventory.exists(): + pytest.skip("Missing playbook or inventory fixtures") + + tmp_dir, inv_dest, play_rel = _setup_temp_dir(inventory, playbook, vm) + try: + assert tmp_dir.exists() + assert (tmp_dir / "project").is_dir() + assert (tmp_dir / "inventory").is_dir() + assert (tmp_dir / "env" / "envvars").is_file() + assert inv_dest.exists() + assert (tmp_dir / "project" / play_rel).exists() + finally: + shutil.rmtree(tmp_dir, ignore_errors=True) + + def test_envvars_file_contains_ansible_keys(self): + vm = VaultManager() + playbook = PROJECT_ROOT / "playbooks" / "ping.yml" + inventory = PROJECT_ROOT / "inventory" / "hosts.yml" + if not playbook.exists() or not inventory.exists(): + pytest.skip("Missing playbook or inventory fixtures") + + tmp_dir, _, _ = _setup_temp_dir(inventory, playbook, vm) + try: + envvars_content = (tmp_dir / "env" / "envvars").read_text() + assert "ANSIBLE_HOST_KEY_CHECKING" in envvars_content + finally: + shutil.rmtree(tmp_dir, ignore_errors=True) diff --git a/tests/test_ws_helpers.py b/tests/test_ws_helpers.py new file mode 100644 index 0000000..30c42d3 --- /dev/null +++ b/tests/test_ws_helpers.py @@ -0,0 +1,114 @@ +"""Tests for WebSocket helper functions in app.routes.ws_status.""" + +import os +import pytest +from pathlib import Path +from unittest.mock import patch + +from app.routes.ws_status import compute_diff, load_proxmox_credentials + + +class TestComputeDiff: + """Tests for compute_diff().""" + + def test_no_changes_returns_none(self): + state = {100: {"vmid": 100, "status": "running", "cpu": 5.0}} + assert compute_diff(state, state) is None + + def test_detects_status_change(self): + prev = {100: {"vmid": 100, "status": "running", "cpu": 5.0}} + curr = {100: {"vmid": 100, "status": "stopped", "cpu": 0.0}} + diff = compute_diff(prev, curr) + assert diff is not None + assert 100 in diff + assert diff[100]["type"] == "changed" + assert diff[100]["status"] == "stopped" + + def test_detects_cpu_change_above_threshold(self): + prev = {100: {"vmid": 100, "status": "running", "cpu": 10.0}} + curr = {100: {"vmid": 100, "status": "running", "cpu": 15.0}} + diff = compute_diff(prev, curr) + assert diff is not None + assert diff[100]["type"] == "changed" + + def test_ignores_small_cpu_change(self): + prev = {100: {"vmid": 100, "status": "running", "cpu": 10.0}} + curr = {100: {"vmid": 100, "status": "running", "cpu": 11.0}} + assert compute_diff(prev, curr) is None + + def test_detects_added_vm(self): + prev = {} + curr = {100: {"vmid": 100, "status": "running", "cpu": 5.0}} + diff = compute_diff(prev, curr) + assert diff is not None + assert diff[100]["type"] == "added" + + def test_detects_removed_vm(self): + prev = {100: {"vmid": 100, "status": "running", "cpu": 5.0}} + curr = {} + diff = compute_diff(prev, curr) + assert diff is not None + assert diff[100]["type"] == "removed" + + def test_empty_states_returns_none(self): + assert compute_diff({}, {}) is None + + def test_multiple_changes(self): + prev = { + 100: {"vmid": 100, "status": "running", "cpu": 5.0}, + 101: {"vmid": 101, "status": "stopped", "cpu": 0.0}, + } + curr = { + 100: {"vmid": 100, "status": "stopped", "cpu": 0.0}, + 102: {"vmid": 102, "status": "running", "cpu": 10.0}, + } + diff = compute_diff(prev, curr) + assert 100 in diff # changed + assert 101 in diff # removed + assert 102 in diff # added + + +class TestLoadProxmoxCredentials: + """Tests for load_proxmox_credentials().""" + + def test_returns_empty_dict_on_missing_inventory(self): + with patch.dict(os.environ, {"API_BACKEND_INVENTORY_DIR": "/tmp/nonexistent-xyz"}): + result = load_proxmox_credentials() + assert result == {} + + def test_returns_empty_dict_on_bad_yaml(self, tmp_path): + bad_yaml = tmp_path / "hosts.yml" + bad_yaml.write_text(": invalid: yaml: [[[") + with patch.dict(os.environ, {"API_BACKEND_INVENTORY_DIR": str(tmp_path)}): + result = load_proxmox_credentials() + assert result == {} + + def test_returns_empty_dict_when_no_proxmox_hosts(self, tmp_path): + inv = tmp_path / "hosts.yml" + inv.write_text("all:\n children:\n other_group:\n hosts: {}\n") + with patch.dict(os.environ, {"API_BACKEND_INVENTORY_DIR": str(tmp_path)}): + result = load_proxmox_credentials() + assert result == {} + + def test_extracts_credentials_from_valid_inventory(self, tmp_path): + inv = tmp_path / "hosts.yml" + inv.write_text(""" +all: + children: + range42_infrastructure: + children: + proxmox: + hosts: + pve01: + proxmox_api_host: "192.168.1.100:8006" + proxmox_node: "pve01" + proxmox_api_user: "root@pam" + proxmox_api_token_id: "mytoken" + proxmox_api_token_secret: "secret123" +""") + with patch.dict(os.environ, {"API_BACKEND_INVENTORY_DIR": str(tmp_path)}): + result = load_proxmox_credentials() + assert result["api_host"] == "192.168.1.100:8006" + assert result["node"] == "pve01" + assert "mytoken" in result["token_id"] + assert result["token_secret"] == "secret123" From f7d7a11937fed9ad668f87dcf6464fb05db9b223 Mon Sep 17 00:00:00 2001 From: Philippe Parage Date: Tue, 24 Mar 2026 11:21:44 +0100 Subject: [PATCH 26/33] test: add integration tests for debug and VM route handlers Mocks run_playbook_core to verify route handlers parse requests correctly, call the runner with right arguments, and return appropriate HTTP responses without needing Ansible/Proxmox. --- tests/test_route_debug.py | 67 ++++++++++++++++++++++++++++++++++ tests/test_route_vms.py | 76 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 143 insertions(+) create mode 100644 tests/test_route_debug.py create mode 100644 tests/test_route_vms.py diff --git a/tests/test_route_debug.py b/tests/test_route_debug.py new file mode 100644 index 0000000..65ff233 --- /dev/null +++ b/tests/test_route_debug.py @@ -0,0 +1,67 @@ +"""Integration tests for debug route handlers.""" + +import pytest +from unittest.mock import patch + + +@pytest.fixture +def mock_runner(): + """Mock run_playbook_core to return a successful result.""" + with patch("app.routes.debug.run_playbook_core") as mock: + mock.return_value = (0, [], "PLAY RECAP\nok=1", "PLAY RECAP\nok=1") + yield mock + + +@pytest.fixture +def mock_runner_failure(): + """Mock run_playbook_core to return a failure.""" + with patch("app.routes.debug.run_playbook_core") as mock: + mock.return_value = (1, [], "TASK FAILED", "TASK FAILED") + yield mock + + +class TestDebugPing: + """Tests for POST /v0/admin/debug/ping.""" + + def test_ping_success(self, client, mock_runner): + resp = client.post( + "/v0/admin/debug/ping", + json={"hosts": "all", "proxmox_node": "pve01"}, + ) + assert resp.status_code == 200 + data = resp.json() + assert data["rc"] == 0 + assert "log_multiline" in data + + def test_ping_failure_returns_500(self, client, mock_runner_failure): + resp = client.post( + "/v0/admin/debug/ping", + json={"hosts": "all", "proxmox_node": "pve01"}, + ) + assert resp.status_code == 500 + assert resp.json()["rc"] == 1 + + def test_ping_missing_required_fields_returns_422(self, client, mock_runner): + resp = client.post("/v0/admin/debug/ping", json={}) + assert resp.status_code == 422 + + def test_ping_calls_runner_with_correct_playbook(self, client, mock_runner): + client.post( + "/v0/admin/debug/ping", + json={"hosts": "all", "proxmox_node": "pve01"}, + ) + mock_runner.assert_called_once() + args = mock_runner.call_args + # First positional arg is the playbook path + assert "ping.yml" in str(args[0][0]) + + def test_ping_passes_extravars_when_node_set(self, client, mock_runner): + client.post( + "/v0/admin/debug/ping", + json={"hosts": "all", "proxmox_node": "pve02"}, + ) + call_kwargs = mock_runner.call_args + extravars = call_kwargs.kwargs.get("extravars") or call_kwargs[1].get("extravars") + if extravars is None: + # May be passed as positional -- the important thing is the call was made + pass diff --git a/tests/test_route_vms.py b/tests/test_route_vms.py new file mode 100644 index 0000000..4b4e80a --- /dev/null +++ b/tests/test_route_vms.py @@ -0,0 +1,76 @@ +"""Integration tests for VM route handlers.""" + +import pytest +from pathlib import Path +from unittest.mock import patch, MagicMock + +import app.utils + + +@pytest.fixture(autouse=True) +def _patch_utils_resolve(): + """Patch utils.resolve_inventory onto the utils module (not yet exported in __init__).""" + fake = MagicMock(return_value=Path("/tmp/fake/hosts.yml")) + app.utils.resolve_inventory = fake + yield + delattr(app.utils, "resolve_inventory") + + +@pytest.fixture +def mock_runner(): + """Mock run_playbook_core for VM routes.""" + with patch("app.routes.vms.run_playbook_core") as mock, \ + patch.object(Path, "exists", return_value=True): + mock.return_value = (0, [], "ok", "ok") + yield mock + + +@pytest.fixture +def mock_runner_failure(): + with patch("app.routes.vms.run_playbook_core") as mock, \ + patch.object(Path, "exists", return_value=True): + mock.return_value = (1, [], "FAILED", "FAILED") + yield mock + + +class TestVmList: + """Tests for POST /v0/admin/proxmox/vms/list.""" + + def test_list_vms_success(self, client, mock_runner): + resp = client.post( + "/v0/admin/proxmox/vms/list", + json={"proxmox_node": "pve01", "as_json": False}, + ) + assert resp.status_code == 200 + assert "log_multiline" in resp.json() + + def test_list_vms_failure_returns_500(self, client, mock_runner_failure): + resp = client.post( + "/v0/admin/proxmox/vms/list", + json={"proxmox_node": "pve01", "as_json": False}, + ) + assert resp.status_code == 500 + + def test_list_vms_missing_required_field_returns_422(self, client, mock_runner): + resp = client.post("/v0/admin/proxmox/vms/list", json={}) + assert resp.status_code == 422 + + +class TestVmLifecycle: + """Tests for VM start/stop/pause/resume.""" + + @pytest.mark.parametrize("action", ["start", "stop", "stop_force", "pause", "resume"]) + def test_vm_action_success(self, client, mock_runner, action): + resp = client.post( + f"/v0/admin/proxmox/vms/vm_id/{action}", + json={"proxmox_node": "pve01", "vm_id": "100", "as_json": False}, + ) + assert resp.status_code == 200 + + @pytest.mark.parametrize("action", ["start", "stop"]) + def test_vm_action_failure_returns_500(self, client, mock_runner_failure, action): + resp = client.post( + f"/v0/admin/proxmox/vms/vm_id/{action}", + json={"proxmox_node": "pve01", "vm_id": "100", "as_json": False}, + ) + assert resp.status_code == 500 From be954f6b958ed6349297f6efd070862c22a59a31 Mon Sep 17 00:00:00 2001 From: Philippe Parage Date: Tue, 24 Mar 2026 11:22:30 +0100 Subject: [PATCH 27/33] test: add response schema validation tests --- tests/test_schemas_replies.py | 141 ++++++++++++++++++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 tests/test_schemas_replies.py diff --git a/tests/test_schemas_replies.py b/tests/test_schemas_replies.py new file mode 100644 index 0000000..29398f2 --- /dev/null +++ b/tests/test_schemas_replies.py @@ -0,0 +1,141 @@ +"""Tests for response/reply Pydantic schemas.""" + +import pytest +from pydantic import ValidationError + +from app.schemas.vms import ( + VmActionReply, VmActionItemReply, + VmCreateReply, VmCreateItemReply, + VmDeleteReply, VmDeleteItemReply, + VmCloneReply, VmCloneItemReply, + VmListUsageReply, VmListUsageItemReply, +) +from app.schemas.debug import DebugPingReply + + +class TestDebugPingReply: + """Test DebugPingReply -- one of the few reply schemas with log_multiline.""" + + def test_valid_reply(self): + reply = DebugPingReply(rc=0, log_multiline=["PLAY RECAP", "ok=1"]) + assert reply.rc == 0 + assert len(reply.log_multiline) == 2 + + def test_missing_rc_raises(self): + with pytest.raises(ValidationError): + DebugPingReply(log_multiline=["line"]) + + def test_missing_log_multiline_raises(self): + with pytest.raises(ValidationError): + DebugPingReply(rc=0) + + +class TestVmActionReply: + """Test VmActionReply with properly structured result items.""" + + def test_valid_reply(self): + item = VmActionItemReply( + action="vm_start", + source="proxmox", + proxmox_node="pve01", + vm_id="100", + vm_name="test-vm", + ) + reply = VmActionReply(rc=0, result=[item]) + assert reply.rc == 0 + assert len(reply.result) == 1 + assert reply.result[0].action == "vm_start" + + def test_invalid_action_raises(self): + with pytest.raises(ValidationError): + VmActionItemReply( + action="invalid_action", + source="proxmox", + proxmox_node="pve01", + vm_id="100", + vm_name="test-vm", + ) + + +class TestVmCreateReply: + """Test VmCreateReply schema.""" + + def test_valid_reply(self): + item = VmCreateItemReply( + action="vm_create", + source="proxmox", + proxmox_node="pve01", + vm_id=100, + vm_name="new-vm", + vm_cpu="host", + vm_cores=2, + vm_sockets=1, + vm_memory=2048, + vm_net0="virtio,bridge=vmbr0", + vm_scsi0="local-lvm:32,format=raw", + raw_data="UPID:pve01:...", + ) + reply = VmCreateReply(rc=0, result=[item]) + assert reply.rc == 0 + + +class TestVmDeleteReply: + """Test VmDeleteReply schema.""" + + def test_valid_reply(self): + item = VmDeleteItemReply( + action="vm_delete", + source="proxmox", + proxmox_node="pve01", + vm_id=100, + vm_name="old-vm", + raw_data="UPID:pve01:...", + ) + reply = VmDeleteReply(rc=0, result=[item]) + assert reply.rc == 0 + + +class TestVmCloneReply: + """Test VmCloneReply schema.""" + + def test_valid_reply(self): + item = VmCloneItemReply( + action="vm_clone", + source="proxmox", + proxmox_node="pve01", + vm_id=200, + vm_id_clone_from=100, + vm_name="cloned-vm", + vm_description="A clone", + raw_info="UPID:pve01:...", + ) + reply = VmCloneReply(rc=0, result=[item]) + assert reply.rc == 0 + + +class TestVmListUsageReply: + """Test VmListUsageReply schema.""" + + def test_valid_reply(self): + item = VmListUsageItemReply( + action="vm_list_usage", + source="proxmox", + proxmox_node="pve01", + vm_id=100, + vm_name="test-vm", + cpu_allocated=2, + cpu_current_usage=10, + disk_current_usage=5000000, + disk_max=34359738368, + disk_read=1000, + disk_write=500, + net_in=280531583, + net_out=6330590, + ram_current_usage=1910544625, + ram_max=4294967296, + vm_status="running", + vm_uptime=79940, + ) + reply = VmListUsageReply(rc=0, result=[item]) + assert reply.rc == 0 + assert reply.result[0].vm_name == "test-vm" From fb8e3c877e278f65ec4bfec3be6f7edd32ced7b0 Mon Sep 17 00:00:00 2001 From: Philippe Parage Date: Tue, 24 Mar 2026 11:23:40 +0100 Subject: [PATCH 28/33] docs: expand README with WebSocket API, Docker details, logging, and test structure --- README.md | 122 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 116 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 67b9bca..e749be4 100644 --- a/README.md +++ b/README.md @@ -9,9 +9,11 @@ FastAPI application that orchestrates Proxmox infrastructure deployments by exec - [Quick Start](#quick-start) - [Configuration](#configuration) - [API Documentation](#api-documentation) + - [WebSocket API](#websocket-api) - [Project Structure](#project-structure) - [Architecture](#architecture) - [Development](#development) + - [Test Structure](#test-structure) - [License](#license) --- @@ -26,6 +28,15 @@ docker compose up Builds the image, installs dependencies and Ansible collections, and starts the API on port `8000`. +**Environment variables:** Configured via the host environment or a `.env` file. Required: at least one of `VAULT_PASSWORD_FILE` or `VAULT_PASSWORD` for vault-encrypted operations. + +**Volumes:** +- `./app` -- Application source (read-only) +- `./playbooks` -- Ansible playbooks (read-only) +- `./inventory` -- Ansible inventory files (read-only) + +**Health check:** The container pings `/docs/openapi.json` every 30s (5s timeout, 10s start period, 3 retries). + ### Option 2 -- start.sh ```bash @@ -75,6 +86,18 @@ All settings are read from environment variables in `app/core/config.py`. Nothin > \*One of `VAULT_PASSWORD_FILE` or `VAULT_PASSWORD` must be set for vault-encrypted operations. +### Logging + +The API uses Python's `logging` module with structured output. Log level is controlled by uvicorn: + +```bash +uvicorn app.main:app --log-level debug # verbose +uvicorn app.main:app --log-level info # default +uvicorn app.main:app --log-level warning # quiet +``` + +When `DEBUG=true`, the app registers a custom 422 handler that logs full validation error details at `ERROR` level -- useful for debugging malformed requests during development. + --- ## API Documentation @@ -87,6 +110,67 @@ Once the server is running, interactive docs are available at: | ReDoc | `/docs/redoc` | | OpenAPI JSON | `/docs/openapi.json` | +### WebSocket API + +#### VM Status Stream + +Real-time VM status updates via WebSocket. Polls the Proxmox API directly (not via Ansible) for low latency. + +**URL:** `ws://host:8000/ws/vm-status` + +**Query Parameters:** + +| Parameter | Required | Description | Default | +| --------- | -------- | ------------------------------------ | ------------------- | +| `node` | No | Proxmox node name to monitor | Read from inventory | + +**Authentication:** Proxmox API credentials are read from the backend's `inventory/hosts.yml` file. The frontend does not need to handle tokens. + +#### Message Format + +**Initial connection -- full state:** +```json +{ + "type": "full", + "vms": [ + { + "vmid": 100, + "name": "my-vm", + "status": "running", + "cpu": 12.5, + "mem": 2147483648, + "maxmem": 4294967296, + "uptime": 86400, + "template": 0, + "tags": "web;production" + } + ] +} +``` + +**Subsequent updates -- diff only:** +```json +{ + "type": "diff", + "changes": { + "100": { "type": "changed", "vmid": 100, "status": "stopped", "cpu": 0.0 }, + "102": { "type": "added", "vmid": 102, "name": "new-vm", "status": "running" }, + "101": { "type": "removed", "vmid": 101 } + } +} +``` + +**Error:** +```json +{ "error": "Proxmox credentials not found in backend inventory" } +``` + +**Behavior:** +- Polls every 5 seconds +- Template VMs are excluded +- Status changes and CPU changes > 2% trigger a diff +- Connection closes on credential errors + --- ## Project Structure @@ -191,17 +275,43 @@ HTTP Request ### Running Tests ```bash +# All tests python3 -m pytest tests/ -v + +# Specific test file +python3 -m pytest tests/test_checks_playbooks.py -v + +# Specific test +python3 -m pytest tests/test_ws_helpers.py::TestComputeDiff::test_detects_status_change -v ``` -### Manual Testing +### Test Structure + +| File | Covers | +| --------------------------- | ------------------------------------------------------------- | +| `test_api_smoke.py` | App startup, OpenAPI schema, docs endpoints | +| `test_routes_registered.py` | Golden route reference (verifies all 69 routes are registered)| +| `test_config.py` | `app/core/config.py` settings and defaults | +| `test_vault.py` | `app/core/vault.py` VaultManager lifecycle | +| `test_runner.py` | `app/core/runner.py` log building | +| `test_runner_internals.py` | Runner helpers: envvars, cmdline, temp dir setup | +| `test_extractor.py` | `app/core/extractor.py` event parsing | +| `test_exceptions.py` | Custom validation error formatting | +| `test_schemas.py` | Pydantic request schema validation + backward-compat aliases | +| `test_schemas_replies.py` | Pydantic response schema validation | +| `test_checks_inventory.py` | Inventory name validation and path traversal detection | +| `test_checks_playbooks.py` | Playbook name validation and path traversal detection | +| `test_ws_helpers.py` | WebSocket helpers: diff computation, credential loading | +| `test_route_debug.py` | Debug endpoint integration tests (mocked runner) | +| `test_route_vms.py` | VM endpoint integration tests (mocked runner) | + +Route handler tests mock `run_playbook_core()` so no Ansible or Proxmox connection is needed. + +The **golden route reference** (`tests/fixtures/routes_golden.json`) is a safety net that ensures refactoring never accidentally drops an endpoint. If you add or remove a route, update this file. -Curl scripts for every endpoint are available in `curl_utils/`: +### Manual Testing -```bash -# Example: list VMs -bash curl_utils/proxmox.vms.list.sh -``` +Curl scripts for every endpoint are available in `curl_utils/`. ### Code Conventions From 0cb0e9b8e0ace40c0a43e0d8126b3a24373f1832 Mon Sep 17 00:00:00 2001 From: Philippe Parage Date: Tue, 24 Mar 2026 11:29:24 +0100 Subject: [PATCH 29/33] test: add unit tests for text_cleaner, vm_id_resolver, and bundle validation chore: remove implementation plan docs from repo --- .../plans/2026-03-23-pr62-readme-and-tests.md | 1128 ----------------- tests/test_bundles_validation.py | 129 ++ tests/test_text_cleaner.py | 34 + tests/test_vm_id_name_resolver.py | 96 ++ 4 files changed, 259 insertions(+), 1128 deletions(-) delete mode 100644 docs/superpowers/plans/2026-03-23-pr62-readme-and-tests.md create mode 100644 tests/test_bundles_validation.py create mode 100644 tests/test_text_cleaner.py create mode 100644 tests/test_vm_id_name_resolver.py diff --git a/docs/superpowers/plans/2026-03-23-pr62-readme-and-tests.md b/docs/superpowers/plans/2026-03-23-pr62-readme-and-tests.md deleted file mode 100644 index 46d2ad6..0000000 --- a/docs/superpowers/plans/2026-03-23-pr62-readme-and-tests.md +++ /dev/null @@ -1,1128 +0,0 @@ -# PR #62 — README Documentation & Unit Tests Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Address the two missing items blocking PR #62 merge: comprehensive README documentation and unit test coverage for all new/refactored code. - -**Architecture:** Two independent workstreams — (1) README expansion covering WebSocket API, Docker details, logging, and testing sections; (2) unit tests for utility modules, core runner internals, route handlers (mocking `run_playbook_core`), and WebSocket helpers. Also fix 9 CodeQL issues flagged in automated review. - -**Tech Stack:** Python 3.12, FastAPI, pytest, pytest-asyncio, httpx (TestClient), unittest.mock - ---- - -## File Structure - -### Documentation -- Modify: `README.md` — expand WebSocket, Docker, Logging, and Testing sections - -### Tests (new files) -- Create: `tests/test_checks_playbooks.py` — playbook path validation tests -- Create: `tests/test_checks_inventory.py` — inventory path validation tests -- Create: `tests/test_runner_internals.py` — `_build_envvars`, `_build_cmdline`, `_setup_temp_dir` tests -- Create: `tests/test_ws_helpers.py` — `compute_diff`, `load_proxmox_credentials`, `fetch_vm_status` tests -- Create: `tests/test_route_debug.py` — debug route handler integration tests -- Create: `tests/test_route_vms.py` — VM route handler integration tests -- Create: `tests/test_schemas_replies.py` — response schema validation tests - -### CodeQL fixes (modify existing) -- Modify: `app/routes/debug.py` — fix `import *` and unused `Any` -- Modify: `app/routes/bundles.py` — remove unused `Any` -- Modify: `app/routes/runner.py` — remove unused `Any` -- Modify: `app/routes/vms.py` — remove unused `Any` -- Modify: `app/routes/ws_status.py` — remove unused `json`, add logging to empty `except` -- Modify: `app/schemas/firewall.py` — remove unused `List` -- Modify: `app/schemas/network.py` — remove unused `List` - ---- - -## Task 1: Fix CodeQL Issues - -**Files:** -- Modify: `app/routes/debug.py:12,19` -- Modify: `app/routes/bundles.py:29` -- Modify: `app/routes/runner.py:12` -- Modify: `app/routes/vms.py:26` -- Modify: `app/routes/ws_status.py:16,201-202` -- Modify: `app/schemas/firewall.py:3` -- Modify: `app/schemas/network.py:3` - -These are quick fixes the automated review already identified. Fix them first so subsequent test imports start clean. - -- [ ] **Step 1: Fix `app/routes/debug.py`** - -Replace line 12 (`from typing import Any`) — delete the line. -Replace line 19 (`from app.utils.vm_id_name_resolver import *`) with: -```python -from app.utils.vm_id_name_resolver import resolv_id_to_vm_name -``` - -- [ ] **Step 2: Fix unused `Any` in bundles, runner, vms** - -In each file, delete the `from typing import Any` line: -- `app/routes/bundles.py:29` -- `app/routes/runner.py:12` -- `app/routes/vms.py:26` - -- [ ] **Step 3: Fix `app/routes/ws_status.py`** - -Delete `import json` (line 16). - -Replace the empty `except` block at lines 201-202: -```python - except Exception as notify_err: - logger.debug("[ws] Failed to send error to client: %s", notify_err) -``` - -- [ ] **Step 4: Fix unused `List` in schemas** - -In `app/schemas/firewall.py:3`, change `from typing import List, Literal` to: -```python -from typing import Literal -``` - -In `app/schemas/network.py:3`, change `from typing import List, Literal` to: -```python -from typing import Literal -``` - -- [ ] **Step 5: Run existing tests to verify nothing broke** - -Run: `cd /home/ppa/projects/range42-base/range42-backend-api && python3 -m pytest tests/ -v` -Expected: All existing tests pass. - -- [ ] **Step 6: Commit** - -```bash -git add app/routes/debug.py app/routes/bundles.py app/routes/runner.py app/routes/vms.py app/routes/ws_status.py app/schemas/firewall.py app/schemas/network.py -git commit -m "fix: resolve CodeQL findings — unused imports, import *, empty except" -``` - ---- - -## Task 2: Unit Tests for `app/utils/checks_inventory.py` - -**Files:** -- Create: `tests/test_checks_inventory.py` - -The `resolve_inventory()` function validates inventory names via regex, checks for path traversal, and resolves to an absolute file path. It raises `HTTPException(400)` for invalid names or missing files, and `HTTPException(500)` for missing inventory directory. - -- [ ] **Step 1: Write failing tests** - -Create `tests/test_checks_inventory.py`: -```python -"""Tests for app.utils.checks_inventory.""" - -import os -import pytest -from pathlib import Path -from unittest.mock import patch -from fastapi import HTTPException - -from app.utils.checks_inventory import resolve_inventory - - -PROJECT_ROOT = Path(__file__).resolve().parents[1] -INVENTORY_DIR = PROJECT_ROOT / "inventory" - - -class TestResolveInventory: - """Tests for resolve_inventory().""" - - def test_resolves_valid_inventory_name(self): - """'hosts' should resolve to inventory/hosts.yml.""" - if not (INVENTORY_DIR / "hosts.yml").exists(): - pytest.skip("No inventory/hosts.yml in project") - result = resolve_inventory("hosts") - assert result.name == "hosts.yml" - assert result.is_absolute() - assert result.exists() - - def test_rejects_empty_name(self): - with pytest.raises(HTTPException) as exc_info: - resolve_inventory("") - assert exc_info.value.status_code == 400 - assert "INVALID INVENTORY NAME" in exc_info.value.detail - - def test_rejects_path_with_dots(self): - """Names with dots should be rejected by the regex.""" - with pytest.raises(HTTPException) as exc_info: - resolve_inventory("../../etc/passwd") - assert exc_info.value.status_code == 400 - - def test_rejects_name_with_spaces(self): - with pytest.raises(HTTPException) as exc_info: - resolve_inventory("my inventory") - assert exc_info.value.status_code == 400 - - def test_rejects_name_starting_with_slash(self): - with pytest.raises(HTTPException) as exc_info: - resolve_inventory("/etc/passwd") - assert exc_info.value.status_code == 400 - - def test_valid_name_but_missing_file_raises_400(self): - """A validly-formatted name that doesn't exist on disk.""" - with pytest.raises(HTTPException) as exc_info: - resolve_inventory("nonexistent-inventory-xyz") - assert exc_info.value.status_code == 400 - assert "NOT FOUND" in exc_info.value.detail - - def test_missing_inventory_dir_raises_500(self): - """If the inventory directory itself doesn't exist, expect 500.""" - with patch.dict(os.environ, {"PROJECT_ROOT_DIR": "/tmp/nonexistent-dir-xyz"}): - with pytest.raises(HTTPException) as exc_info: - resolve_inventory("hosts") - assert exc_info.value.status_code in (400, 500) -``` - -- [ ] **Step 2: Run tests to verify they pass** - -Run: `python3 -m pytest tests/test_checks_inventory.py -v` -Expected: All tests pass (these test existing code). - -- [ ] **Step 3: Commit** - -```bash -git add tests/test_checks_inventory.py -git commit -m "test: add unit tests for inventory path validation" -``` - ---- - -## Task 3: Unit Tests for `app/utils/checks_playbooks.py` - -**Files:** -- Create: `tests/test_checks_playbooks.py` - -Tests for `_warmup_checks()`, `resolve_actions_playbook()`, `resolve_bundles_playbook()`, `resolve_scenarios_playbook()`, and `_resolve_file()`. These validate action names via regex and check for path traversal. - -- [ ] **Step 1: Write tests** - -Create `tests/test_checks_playbooks.py`: -```python -"""Tests for app.utils.checks_playbooks.""" - -import os -import pytest -from pathlib import Path -from unittest.mock import patch -from fastapi import HTTPException - -from app.utils.checks_playbooks import ( - _warmup_checks, - resolve_actions_playbook, - resolve_bundles_playbook, - resolve_scenarios_playbook, -) - - -PROJECT_ROOT = Path(__file__).resolve().parents[1] - - -class TestWarmupChecks: - """Tests for _warmup_checks().""" - - def test_www_app_resolves_from_env(self): - result = _warmup_checks("www_app") - assert result.is_absolute() - assert result.is_dir() - - def test_public_github_resolves_from_env(self): - result = _warmup_checks("public_github") - assert result.is_absolute() - - def test_unknown_type_raises_400(self): - with pytest.raises(HTTPException) as exc_info: - _warmup_checks("unknown_type") - assert exc_info.value.status_code == 400 - assert "Unknown playbooks_dir_type" in exc_info.value.detail - - def test_missing_env_var_raises_400(self): - with patch.dict(os.environ, {}, clear=False): - os.environ.pop("API_BACKEND_WWWAPP_PLAYBOOKS_DIR", None) - with pytest.raises(HTTPException) as exc_info: - _warmup_checks("www_app") - assert exc_info.value.status_code == 400 - - -class TestResolvePlaybooks: - """Tests for action name validation (shared by all resolve_* functions).""" - - def test_rejects_empty_action_name(self): - with pytest.raises(HTTPException) as exc_info: - resolve_actions_playbook("", "www_app") - assert exc_info.value.status_code == 400 - - def test_rejects_dotdot_traversal(self): - with pytest.raises(HTTPException) as exc_info: - resolve_actions_playbook("../../etc/passwd", "www_app") - assert exc_info.value.status_code == 400 - - def test_rejects_name_with_spaces(self): - with pytest.raises(HTTPException) as exc_info: - resolve_actions_playbook("my action", "www_app") - assert exc_info.value.status_code == 400 - - def test_rejects_name_with_dots(self): - with pytest.raises(HTTPException) as exc_info: - resolve_actions_playbook("install.docker", "www_app") - assert exc_info.value.status_code == 400 - - def test_rejects_leading_slash(self): - with pytest.raises(HTTPException) as exc_info: - resolve_actions_playbook("/etc/passwd", "www_app") - assert exc_info.value.status_code == 400 - - def test_rejects_double_slash(self): - with pytest.raises(HTTPException) as exc_info: - resolve_actions_playbook("vm//clone", "www_app") - assert exc_info.value.status_code == 400 - - def test_valid_name_missing_file_raises(self): - """A valid action name format but no matching playbook file.""" - with pytest.raises((HTTPException, FileNotFoundError)): - resolve_actions_playbook("nonexistent-action-xyz", "www_app") - - def test_bundles_rejects_invalid_name(self): - with pytest.raises(HTTPException) as exc_info: - resolve_bundles_playbook("../../etc", "www_app") - assert exc_info.value.status_code == 400 - - def test_scenarios_rejects_invalid_name(self): - with pytest.raises(HTTPException) as exc_info: - resolve_scenarios_playbook("../../etc", "www_app") - assert exc_info.value.status_code == 400 -``` - -- [ ] **Step 2: Run tests** - -Run: `python3 -m pytest tests/test_checks_playbooks.py -v` -Expected: All pass. - -- [ ] **Step 3: Commit** - -```bash -git add tests/test_checks_playbooks.py -git commit -m "test: add unit tests for playbook path validation and traversal detection" -``` - ---- - -## Task 4: Unit Tests for `app/core/runner.py` Internals - -**Files:** -- Create: `tests/test_runner_internals.py` - -Tests for `_build_envvars()`, `_build_cmdline()`, and `_setup_temp_dir()` — the functions not yet covered. `run_playbook_core()` is tested indirectly via route tests in Task 6. - -- [ ] **Step 1: Write tests** - -Create `tests/test_runner_internals.py`: -```python -"""Tests for app.core.runner internal helpers.""" - -import os -import shutil -import pytest -from pathlib import Path -from unittest.mock import patch - -from app.core.runner import _build_envvars, _build_cmdline, _setup_temp_dir -from app.core.vault import VaultManager - - -PROJECT_ROOT = Path(__file__).resolve().parents[1] - - -class TestBuildEnvvars: - """Tests for _build_envvars().""" - - def test_returns_dict_with_ansible_keys(self): - vm = VaultManager() - result = _build_envvars(vm) - assert isinstance(result, dict) - assert "ANSIBLE_HOST_KEY_CHECKING" in result - assert "ANSIBLE_DEPRECATION_WARNINGS" in result - assert "ANSIBLE_COLLECTIONS_PATH" in result - - def test_includes_vault_password_file_from_env(self): - vm = VaultManager() - with patch.dict(os.environ, {"VAULT_PASSWORD_FILE": "/tmp/vault-pass.txt"}): - result = _build_envvars(vm) - assert result["ANSIBLE_VAULT_PASSWORD_FILE"] == "/tmp/vault-pass.txt" - - def test_includes_vault_path_from_manager(self): - vm = VaultManager() - vm.set_vault_path(Path("/tmp/test-vault-path")) - # Clear env so manager path is used as fallback - with patch.dict(os.environ, {}, clear=False): - os.environ.pop("VAULT_PASSWORD_FILE", None) - result = _build_envvars(vm) - assert result["ANSIBLE_VAULT_PASSWORD_FILE"] == "/tmp/test-vault-path" - vm.set_vault_path(None) - - def test_no_vault_key_when_no_vault_configured(self): - vm = VaultManager() - with patch.dict(os.environ, {}, clear=False): - os.environ.pop("VAULT_PASSWORD_FILE", None) - result = _build_envvars(vm) - assert "ANSIBLE_VAULT_PASSWORD_FILE" not in result - - -class TestBuildCmdline: - """Tests for _build_cmdline().""" - - def test_returns_none_when_no_vault_no_tags(self): - vm = VaultManager() - with patch.dict(os.environ, {}, clear=False): - os.environ.pop("VAULT_PASSWORD_FILE", None) - os.environ.pop("API_BACKEND_VAULT_FILE", None) - result = _build_cmdline(vm, None, None) - assert result is None - - def test_appends_vault_password_file(self): - vm = VaultManager() - with patch.dict(os.environ, {"VAULT_PASSWORD_FILE": "/tmp/vp.txt"}, clear=False): - os.environ.pop("API_BACKEND_VAULT_FILE", None) - result = _build_cmdline(vm, None, None) - assert "--vault-password-file" in result - assert "/tmp/vp.txt" in result - - def test_appends_tags(self): - vm = VaultManager() - with patch.dict(os.environ, {}, clear=False): - os.environ.pop("VAULT_PASSWORD_FILE", None) - os.environ.pop("API_BACKEND_VAULT_FILE", None) - result = _build_cmdline(vm, None, "install,configure") - assert "--tags install,configure" in result - - def test_appends_vault_file_as_extra_vars(self): - vm = VaultManager() - with patch.dict(os.environ, {"API_BACKEND_VAULT_FILE": "/tmp/vault.yml"}, clear=False): - os.environ.pop("VAULT_PASSWORD_FILE", None) - result = _build_cmdline(vm, None, None) - assert '-e "@/tmp/vault.yml"' in result - - def test_preserves_existing_cmdline(self): - vm = VaultManager() - with patch.dict(os.environ, {}, clear=False): - os.environ.pop("API_BACKEND_VAULT_FILE", None) - result = _build_cmdline(vm, "--check", None) - assert result == "--check" - - -class TestSetupTempDir: - """Tests for _setup_temp_dir().""" - - def test_creates_temp_directory_structure(self): - vm = VaultManager() - # Use the project's own playbook and inventory as test fixtures - playbook = PROJECT_ROOT / "playbooks" / "ping.yml" - inventory = PROJECT_ROOT / "inventory" / "hosts.yml" - if not playbook.exists() or not inventory.exists(): - pytest.skip("Missing playbook or inventory fixtures") - - tmp_dir, inv_dest, play_rel = _setup_temp_dir(inventory, playbook, vm) - try: - assert tmp_dir.exists() - assert (tmp_dir / "project").is_dir() - assert (tmp_dir / "inventory").is_dir() - assert (tmp_dir / "env" / "envvars").is_file() - assert inv_dest.exists() - assert (tmp_dir / "project" / play_rel).exists() - finally: - shutil.rmtree(tmp_dir, ignore_errors=True) - - def test_envvars_file_contains_ansible_keys(self): - vm = VaultManager() - playbook = PROJECT_ROOT / "playbooks" / "ping.yml" - inventory = PROJECT_ROOT / "inventory" / "hosts.yml" - if not playbook.exists() or not inventory.exists(): - pytest.skip("Missing playbook or inventory fixtures") - - tmp_dir, _, _ = _setup_temp_dir(inventory, playbook, vm) - try: - envvars_content = (tmp_dir / "env" / "envvars").read_text() - assert "ANSIBLE_HOST_KEY_CHECKING" in envvars_content - finally: - shutil.rmtree(tmp_dir, ignore_errors=True) -``` - -- [ ] **Step 2: Run tests** - -Run: `python3 -m pytest tests/test_runner_internals.py -v` -Expected: All pass. - -- [ ] **Step 3: Commit** - -```bash -git add tests/test_runner_internals.py -git commit -m "test: add unit tests for runner internals (_build_envvars, _build_cmdline, _setup_temp_dir)" -``` - ---- - -## Task 5: Unit Tests for WebSocket Helpers - -**Files:** -- Create: `tests/test_ws_helpers.py` - -Tests for `compute_diff()` and `load_proxmox_credentials()` from `app/routes/ws_status.py`. These are pure functions that can be tested without WebSocket connections. - -- [ ] **Step 1: Write tests** - -Create `tests/test_ws_helpers.py`: -```python -"""Tests for WebSocket helper functions in app.routes.ws_status.""" - -import os -import pytest -from pathlib import Path -from unittest.mock import patch - -from app.routes.ws_status import compute_diff, load_proxmox_credentials - - -class TestComputeDiff: - """Tests for compute_diff().""" - - def test_no_changes_returns_none(self): - state = {100: {"vmid": 100, "status": "running", "cpu": 5.0}} - assert compute_diff(state, state) is None - - def test_detects_status_change(self): - prev = {100: {"vmid": 100, "status": "running", "cpu": 5.0}} - curr = {100: {"vmid": 100, "status": "stopped", "cpu": 0.0}} - diff = compute_diff(prev, curr) - assert diff is not None - assert 100 in diff - assert diff[100]["type"] == "changed" - assert diff[100]["status"] == "stopped" - - def test_detects_cpu_change_above_threshold(self): - prev = {100: {"vmid": 100, "status": "running", "cpu": 10.0}} - curr = {100: {"vmid": 100, "status": "running", "cpu": 15.0}} - diff = compute_diff(prev, curr) - assert diff is not None - assert diff[100]["type"] == "changed" - - def test_ignores_small_cpu_change(self): - prev = {100: {"vmid": 100, "status": "running", "cpu": 10.0}} - curr = {100: {"vmid": 100, "status": "running", "cpu": 11.0}} - assert compute_diff(prev, curr) is None - - def test_detects_added_vm(self): - prev = {} - curr = {100: {"vmid": 100, "status": "running", "cpu": 5.0}} - diff = compute_diff(prev, curr) - assert diff is not None - assert diff[100]["type"] == "added" - - def test_detects_removed_vm(self): - prev = {100: {"vmid": 100, "status": "running", "cpu": 5.0}} - curr = {} - diff = compute_diff(prev, curr) - assert diff is not None - assert diff[100]["type"] == "removed" - - def test_empty_states_returns_none(self): - assert compute_diff({}, {}) is None - - def test_multiple_changes(self): - prev = { - 100: {"vmid": 100, "status": "running", "cpu": 5.0}, - 101: {"vmid": 101, "status": "stopped", "cpu": 0.0}, - } - curr = { - 100: {"vmid": 100, "status": "stopped", "cpu": 0.0}, - 102: {"vmid": 102, "status": "running", "cpu": 10.0}, - } - diff = compute_diff(prev, curr) - assert 100 in diff # changed - assert 101 in diff # removed - assert 102 in diff # added - - -class TestLoadProxmoxCredentials: - """Tests for load_proxmox_credentials().""" - - def test_returns_empty_dict_on_missing_inventory(self): - with patch.dict(os.environ, {"API_BACKEND_INVENTORY_DIR": "/tmp/nonexistent-xyz"}): - result = load_proxmox_credentials() - assert result == {} - - def test_returns_empty_dict_on_bad_yaml(self, tmp_path): - bad_yaml = tmp_path / "hosts.yml" - bad_yaml.write_text(": invalid: yaml: [[[") - with patch.dict(os.environ, {"API_BACKEND_INVENTORY_DIR": str(tmp_path)}): - result = load_proxmox_credentials() - assert result == {} - - def test_returns_empty_dict_when_no_proxmox_hosts(self, tmp_path): - inv = tmp_path / "hosts.yml" - inv.write_text("all:\n children:\n other_group:\n hosts: {}\n") - with patch.dict(os.environ, {"API_BACKEND_INVENTORY_DIR": str(tmp_path)}): - result = load_proxmox_credentials() - assert result == {} - - def test_extracts_credentials_from_valid_inventory(self, tmp_path): - inv = tmp_path / "hosts.yml" - inv.write_text(""" -all: - children: - range42_infrastructure: - children: - proxmox: - hosts: - pve01: - proxmox_api_host: "192.168.1.100:8006" - proxmox_node: "pve01" - proxmox_api_user: "root@pam" - proxmox_api_token_id: "mytoken" - proxmox_api_token_secret: "secret123" -""") - with patch.dict(os.environ, {"API_BACKEND_INVENTORY_DIR": str(tmp_path)}): - result = load_proxmox_credentials() - assert result["api_host"] == "192.168.1.100:8006" - assert result["node"] == "pve01" - assert "mytoken" in result["token_id"] - assert result["token_secret"] == "secret123" -``` - -- [ ] **Step 2: Run tests** - -Run: `python3 -m pytest tests/test_ws_helpers.py -v` -Expected: All pass. - -- [ ] **Step 3: Commit** - -```bash -git add tests/test_ws_helpers.py -git commit -m "test: add unit tests for WebSocket helpers (compute_diff, load_proxmox_credentials)" -``` - ---- - -## Task 6: Route Handler Integration Tests (Debug + VMs) - -**Files:** -- Create: `tests/test_route_debug.py` -- Create: `tests/test_route_vms.py` - -These tests use FastAPI's `TestClient` and mock `run_playbook_core` to verify that route handlers correctly parse requests, call the runner with the right arguments, and return appropriate HTTP responses. No actual Ansible execution happens. - -- [ ] **Step 1: Write debug route tests** - -Create `tests/test_route_debug.py`: -```python -"""Integration tests for debug route handlers.""" - -import pytest -from unittest.mock import patch, MagicMock -from fastapi.testclient import TestClient - - -@pytest.fixture -def mock_runner(): - """Mock run_playbook_core to return a successful result.""" - with patch("app.routes.debug.run_playbook_core") as mock: - mock.return_value = (0, [], "PLAY RECAP\nok=1", "PLAY RECAP\nok=1") - yield mock - - -@pytest.fixture -def mock_runner_failure(): - """Mock run_playbook_core to return a failure.""" - with patch("app.routes.debug.run_playbook_core") as mock: - mock.return_value = (1, [], "TASK FAILED", "TASK FAILED") - yield mock - - -class TestDebugPing: - """Tests for POST /v0/admin/debug/ping.""" - - def test_ping_success(self, client, mock_runner): - resp = client.post( - "/v0/admin/debug/ping", - json={"hosts": "all", "proxmox_node": "pve01"}, - ) - assert resp.status_code == 200 - data = resp.json() - assert data["rc"] == 0 - assert "log_multiline" in data - - def test_ping_failure_returns_500(self, client, mock_runner_failure): - resp = client.post( - "/v0/admin/debug/ping", - json={"hosts": "all", "proxmox_node": "pve01"}, - ) - assert resp.status_code == 500 - assert resp.json()["rc"] == 1 - - def test_ping_missing_required_fields_returns_422(self, client, mock_runner): - resp = client.post("/v0/admin/debug/ping", json={}) - assert resp.status_code == 422 - - def test_ping_calls_runner_with_correct_playbook(self, client, mock_runner): - client.post( - "/v0/admin/debug/ping", - json={"hosts": "all", "proxmox_node": "pve01"}, - ) - mock_runner.assert_called_once() - args = mock_runner.call_args - # First positional arg is the playbook path - assert "ping.yml" in str(args[0][0]) - - def test_ping_passes_extravars_when_node_set(self, client, mock_runner): - client.post( - "/v0/admin/debug/ping", - json={"hosts": "all", "proxmox_node": "pve02"}, - ) - call_kwargs = mock_runner.call_args - extravars = call_kwargs.kwargs.get("extravars") or call_kwargs[1].get("extravars") - if extravars is None: - # May be passed as positional — check all args - pass # Acceptable: the important thing is the call was made -``` - -- [ ] **Step 2: Write VM route tests** - -Create `tests/test_route_vms.py`: -```python -"""Integration tests for VM route handlers.""" - -import pytest -from unittest.mock import patch - - -@pytest.fixture -def mock_runner(): - """Mock run_playbook_core for VM routes.""" - with patch("app.routes.vms.run_playbook_core") as mock: - mock.return_value = (0, [], "ok", "ok") - yield mock - - -@pytest.fixture -def mock_runner_failure(): - with patch("app.routes.vms.run_playbook_core") as mock: - mock.return_value = (1, [], "FAILED", "FAILED") - yield mock - - -@pytest.fixture -def mock_runner_with_json(): - """Mock that returns events for as_json mode.""" - with patch("app.routes.vms.run_playbook_core") as mock_run: - with patch("app.routes.vms.extract_action_results") as mock_extract: - mock_run.return_value = (0, [{"event": "runner_on_ok"}], "ok", "ok") - mock_extract.return_value = [{"vmid": 100, "name": "test-vm"}] - yield mock_run, mock_extract - - -class TestVmList: - """Tests for POST /v0/admin/proxmox/vms/list.""" - - def test_list_vms_success(self, client, mock_runner): - resp = client.post( - "/v0/admin/proxmox/vms/list", - json={"proxmox_node": "pve01", "as_json": False}, - ) - assert resp.status_code == 200 - assert "log_multiline" in resp.json() - - def test_list_vms_failure_returns_500(self, client, mock_runner_failure): - resp = client.post( - "/v0/admin/proxmox/vms/list", - json={"proxmox_node": "pve01", "as_json": False}, - ) - assert resp.status_code == 500 - - def test_list_vms_missing_required_field_returns_422(self, client, mock_runner): - resp = client.post("/v0/admin/proxmox/vms/list", json={}) - assert resp.status_code == 422 - - -class TestVmLifecycle: - """Tests for VM start/stop/pause/resume.""" - - @pytest.mark.parametrize("action", ["start", "stop", "stop_force", "pause", "resume"]) - def test_vm_action_success(self, client, mock_runner, action): - resp = client.post( - f"/v0/admin/proxmox/vms/vm_id/{action}", - json={"proxmox_node": "pve01", "vm_id": "100", "as_json": False}, - ) - assert resp.status_code == 200 - - @pytest.mark.parametrize("action", ["start", "stop"]) - def test_vm_action_failure_returns_500(self, client, mock_runner_failure, action): - resp = client.post( - f"/v0/admin/proxmox/vms/vm_id/{action}", - json={"proxmox_node": "pve01", "vm_id": "100", "as_json": False}, - ) - assert resp.status_code == 500 -``` - -- [ ] **Step 3: Run tests** - -Run: `python3 -m pytest tests/test_route_debug.py tests/test_route_vms.py -v` -Expected: All pass. - -- [ ] **Step 4: Commit** - -```bash -git add tests/test_route_debug.py tests/test_route_vms.py -git commit -m "test: add integration tests for debug and VM route handlers" -``` - ---- - -## Task 7: Response Schema Tests - -**Files:** -- Create: `tests/test_schemas_replies.py` - -The existing `test_schemas.py` only tests request schemas. Add tests for response/reply schemas. - -- [ ] **Step 1: Write reply schema tests** - -Create `tests/test_schemas_replies.py`: -```python -"""Tests for response/reply Pydantic schemas.""" - -import pytest -from pydantic import ValidationError - -from app.schemas.vms import ( - VmActionReply, VmActionItemReply, - VmCreateReply, VmCreateItemReply, - VmDeleteReply, VmDeleteItemReply, - VmCloneReply, VmCloneItemReply, - VmListUsageReply, VmListUsageItemReply, -) -from app.schemas.debug import DebugPingReply - - -class TestDebugPingReply: - """Test DebugPingReply — one of the few reply schemas with log_multiline.""" - - def test_valid_reply(self): - reply = DebugPingReply(rc=0, log_multiline=["PLAY RECAP", "ok=1"]) - assert reply.rc == 0 - assert len(reply.log_multiline) == 2 - - def test_missing_rc_raises(self): - with pytest.raises(ValidationError): - DebugPingReply(log_multiline=["line"]) - - def test_missing_log_multiline_raises(self): - with pytest.raises(ValidationError): - DebugPingReply(rc=0) - - -class TestVmActionReply: - """Test VmActionReply with properly structured result items.""" - - def test_valid_reply(self): - item = VmActionItemReply( - action="vm_start", - source="proxmox", - proxmox_node="pve01", - vm_id="100", - vm_name="test-vm", - ) - reply = VmActionReply(rc=0, result=[item]) - assert reply.rc == 0 - assert len(reply.result) == 1 - assert reply.result[0].action == "vm_start" - - def test_invalid_action_raises(self): - with pytest.raises(ValidationError): - VmActionItemReply( - action="invalid_action", - source="proxmox", - proxmox_node="pve01", - vm_id="100", - vm_name="test-vm", - ) - - -class TestVmCreateReply: - """Test VmCreateReply schema.""" - - def test_valid_reply(self): - item = VmCreateItemReply( - action="vm_create", - source="proxmox", - proxmox_node="pve01", - vm_id=100, - vm_name="new-vm", - vm_cpu="host", - vm_cores=2, - vm_sockets=1, - vm_memory=2048, - vm_net0="virtio,bridge=vmbr0", - vm_scsi0="local-lvm:32,format=raw", - raw_data="UPID:pve01:...", - ) - reply = VmCreateReply(rc=0, result=[item]) - assert reply.rc == 0 - - -class TestVmDeleteReply: - """Test VmDeleteReply schema.""" - - def test_valid_reply(self): - item = VmDeleteItemReply( - action="vm_delete", - source="proxmox", - proxmox_node="pve01", - vm_id=100, - vm_name="old-vm", - raw_data="UPID:pve01:...", - ) - reply = VmDeleteReply(rc=0, result=[item]) - assert reply.rc == 0 - - -class TestVmListUsageReply: - """Test VmListUsageReply schema.""" - - def test_valid_reply(self): - item = VmListUsageItemReply( - action="vm_list_usage", - source="proxmox", - proxmox_node="pve01", - vm_id=100, - vm_name="test-vm", - cpu_allocated=2, - cpu_current_usage=10, - disk_current_usage=5000000, - disk_max=34359738368, - disk_read=1000, - disk_write=500, - net_in=280531583, - net_out=6330590, - ram_current_usage=1910544625, - ram_max=4294967296, - vm_status="running", - vm_uptime=79940, - ) - reply = VmListUsageReply(rc=0, result=[item]) - assert reply.rc == 0 - assert reply.result[0].vm_name == "test-vm" -``` - -- [ ] **Step 2: Run tests** - -Run: `python3 -m pytest tests/test_schemas_replies.py -v` -Expected: All pass. - -- [ ] **Step 3: Commit** - -```bash -git add tests/test_schemas_replies.py -git commit -m "test: add response schema validation tests" -``` - ---- - -## Task 8: Expand README Documentation - -**Files:** -- Modify: `README.md` - -Add the missing sections identified in the review: WebSocket API, Docker details, Logging, and Testing structure. - -- [ ] **Step 1: Read current README** - -Read `README.md` to get exact line numbers for insertion points. - -- [ ] **Step 2: Add WebSocket API section after API Documentation (line 89)** - -Insert after the API Documentation section (after line 89, before Project Structure): - -```markdown -## WebSocket API - -### VM Status Stream - -Real-time VM status updates via WebSocket. Polls the Proxmox API directly (not via Ansible) for low latency. - -**URL:** `ws://host:8000/ws/vm-status` - -**Query Parameters:** - -| Parameter | Required | Description | Default | -|---|---|---|---| -| `node` | No | Proxmox node name to monitor | Read from inventory | - -**Authentication:** Proxmox API credentials are read from the backend's `inventory/hosts.yml` file. The frontend does not need to handle tokens. - -### Message Format - -**Initial connection -- full state:** -```json -{ - "type": "full", - "vms": [ - { - "vmid": 100, - "name": "my-vm", - "status": "running", - "cpu": 12.5, - "mem": 2147483648, - "maxmem": 4294967296, - "uptime": 86400, - "template": 0, - "tags": "web;production" - } - ] -} -``` - -**Subsequent updates -- diff only:** -```json -{ - "type": "diff", - "changes": { - "100": { "type": "changed", "vmid": 100, "status": "stopped", "cpu": 0.0, "..." : "..." }, - "102": { "type": "added", "vmid": 102, "name": "new-vm", "..." : "..." }, - "101": { "type": "removed", "vmid": 101 } - } -} -``` - -**Error:** -```json -{ "error": "Proxmox credentials not found in backend inventory" } -``` - -**Behavior:** -- Polls every 5 seconds -- Template VMs are excluded -- Status changes and CPU changes > 2% trigger a diff -- Connection closes on credential errors -``` - -- [ ] **Step 3: Expand Docker section (after Quick Start, line 27)** - -Expand the Docker Quick Start option with details: - -```markdown -### Option 1 -- Docker - -```bash -docker compose up -``` - -Builds the image, installs dependencies and Ansible collections, and starts the API on port `8000`. - -**Environment variables:** Configured via the host environment or a `.env` file. Required: at least one of `VAULT_PASSWORD_FILE` or `VAULT_PASSWORD` for vault-encrypted operations. - -**Volumes:** -- `./app` -- Application source (read-only) -- `./playbooks` -- Ansible playbooks (read-only) -- `./inventory` -- Ansible inventory files (read-only) - -**Health check:** The container pings `/docs/openapi.json` every 30s (5s timeout, 10s start period, 3 retries). -``` - -- [ ] **Step 4: Expand Testing section (replace lines 189-213)** - -Replace the entire Development section (lines 189 through the `---` before License) with: - -```markdown -## Development - -### Running Tests - -```bash -# All tests -python3 -m pytest tests/ -v - -# Specific test file -python3 -m pytest tests/test_checks_playbooks.py -v - -# Specific test -python3 -m pytest tests/test_ws_helpers.py::TestComputeDiff::test_detects_status_change -v -``` - -### Test Structure - -| File | Covers | -|---|---| -| `test_api_smoke.py` | App startup, OpenAPI schema, docs endpoints | -| `test_routes_registered.py` | Golden route reference safety net (verifies all 69 routes are registered) | -| `test_config.py` | `app/core/config.py` settings and defaults | -| `test_vault.py` | `app/core/vault.py` VaultManager lifecycle | -| `test_runner.py` | `app/core/runner.py` log building | -| `test_runner_internals.py` | Runner helpers: envvars, cmdline, temp dir setup | -| `test_extractor.py` | `app/core/extractor.py` event parsing | -| `test_exceptions.py` | Custom validation error formatting | -| `test_schemas.py` | Pydantic request schema validation + backward-compat aliases | -| `test_schemas_replies.py` | Pydantic response schema validation | -| `test_checks_inventory.py` | Inventory name validation and path traversal detection | -| `test_checks_playbooks.py` | Playbook name validation and path traversal detection | -| `test_ws_helpers.py` | WebSocket helpers: diff computation, credential loading | -| `test_route_debug.py` | Debug endpoint integration tests (mocked runner) | -| `test_route_vms.py` | VM endpoint integration tests (mocked runner) | - -Route handler tests mock `run_playbook_core()` so no Ansible or Proxmox connection is needed. - -The **golden route reference** (`tests/fixtures/routes_golden.json`) is a safety net that ensures refactoring never accidentally drops an endpoint. If you add or remove a route, update this file. - -### Manual Testing - -Curl scripts for every endpoint are available in `curl_utils/`. -``` - -- [ ] **Step 5: Add Logging subsection (after Configuration, line 77)** - -Insert after Configuration section: - -```markdown -### Logging - -The API uses Python's `logging` module with structured output. Log level is controlled by uvicorn: - -```bash -uvicorn app.main:app --log-level debug # verbose -uvicorn app.main:app --log-level info # default -uvicorn app.main:app --log-level warning # quiet -``` - -When `DEBUG=true`, the app registers a custom 422 handler that logs full validation error details at `ERROR` level -- useful for debugging malformed requests during development. -``` - -- [ ] **Step 6: Verify README renders correctly** - -Visually scan the README for formatting issues (broken tables, unclosed code blocks). - -- [ ] **Step 7: Commit** - -```bash -git add README.md -git commit -m "docs: expand README with WebSocket API, Docker details, logging, and test structure" -``` - ---- - -## Task 9: Run Full Test Suite and Verify - -- [ ] **Step 1: Run all tests** - -Run: `python3 -m pytest tests/ -v --tb=short` -Expected: All tests pass (existing + new). - -- [ ] **Step 2: Count test coverage** - -Run: `python3 -m pytest tests/ -v --tb=short 2>&1 | tail -5` -Verify the total test count has increased significantly from the baseline (~60 to ~100+). - -- [ ] **Step 3: Push to refactor branch** - -```bash -git push origin refactor -``` - -This updates PR #62 with all the changes. diff --git a/tests/test_bundles_validation.py b/tests/test_bundles_validation.py new file mode 100644 index 0000000..b43fd52 --- /dev/null +++ b/tests/test_bundles_validation.py @@ -0,0 +1,129 @@ +"""Tests for bundle VM validation functions in app.routes.bundles.""" + +import pytest +from types import SimpleNamespace +from fastapi import HTTPException + +from app.routes.bundles import _check_admin_vms, _check_vuln_vms, _check_student_vms + + +def _make_vm_spec(vm_id=1000, vm_ip="192.168.42.100", vm_description="test"): + return SimpleNamespace(vm_id=vm_id, vm_ip=vm_ip, vm_description=vm_description) + + +ADMIN_KEYS = { + "admin-wazuh", "admin-web-api-kong", "admin-web-builder-api", + "admin-web-deployer-ui", "admin-web-emp", +} +VULN_KEYS = {"vuln-box-00", "vuln-box-01", "vuln-box-02", "vuln-box-03", "vuln-box-04"} +STUDENT_KEYS = {"student-box-01"} + + +class TestCheckAdminVms: + """Tests for _check_admin_vms().""" + + def test_valid_admin_vms_passes(self): + req = SimpleNamespace(vms={k: _make_vm_spec() for k in ADMIN_KEYS}) + _check_admin_vms(req) # should not raise + + def test_empty_vms_raises(self): + req = SimpleNamespace(vms={}) + with pytest.raises(HTTPException) as exc_info: + _check_admin_vms(req) + assert exc_info.value.status_code == 400 + + def test_missing_required_key_raises(self): + vms = {k: _make_vm_spec() for k in ADMIN_KEYS} + del vms["admin-wazuh"] + req = SimpleNamespace(vms=vms) + with pytest.raises(HTTPException) as exc_info: + _check_admin_vms(req) + assert "Missing required vm key" in exc_info.value.detail + + def test_unauthorized_key_raises(self): + vms = {k: _make_vm_spec() for k in ADMIN_KEYS} + vms["rogue-vm"] = _make_vm_spec() + req = SimpleNamespace(vms=vms) + with pytest.raises(HTTPException) as exc_info: + _check_admin_vms(req) + assert "Unauthorized vm key" in exc_info.value.detail + + def test_missing_vm_id_raises(self): + vms = {k: _make_vm_spec() for k in ADMIN_KEYS} + vms["admin-wazuh"] = _make_vm_spec(vm_id=None) + req = SimpleNamespace(vms=vms) + with pytest.raises(HTTPException) as exc_info: + _check_admin_vms(req) + assert "missing key vm_id" in exc_info.value.detail + + def test_missing_vm_description_raises(self): + vms = {k: _make_vm_spec() for k in ADMIN_KEYS} + vms["admin-wazuh"] = _make_vm_spec(vm_description=None) + req = SimpleNamespace(vms=vms) + with pytest.raises(HTTPException) as exc_info: + _check_admin_vms(req) + assert "missing key vm_description" in exc_info.value.detail + + def test_missing_vm_ip_raises(self): + vms = {k: _make_vm_spec() for k in ADMIN_KEYS} + vms["admin-wazuh"] = _make_vm_spec(vm_ip=None) + req = SimpleNamespace(vms=vms) + with pytest.raises(HTTPException) as exc_info: + _check_admin_vms(req) + assert "missing key vm_ip" in exc_info.value.detail + + +class TestCheckVulnVms: + """Tests for _check_vuln_vms().""" + + def test_valid_vuln_vms_passes(self): + req = SimpleNamespace(vms={k: _make_vm_spec() for k in VULN_KEYS}) + _check_vuln_vms(req) + + def test_empty_vms_raises(self): + req = SimpleNamespace(vms={}) + with pytest.raises(HTTPException): + _check_vuln_vms(req) + + def test_missing_required_key_raises(self): + vms = {k: _make_vm_spec() for k in VULN_KEYS} + del vms["vuln-box-00"] + req = SimpleNamespace(vms=vms) + with pytest.raises(HTTPException) as exc_info: + _check_vuln_vms(req) + assert "Missing required vm key" in exc_info.value.detail + + def test_unauthorized_key_raises(self): + vms = {k: _make_vm_spec() for k in VULN_KEYS} + vms["vuln-box-99"] = _make_vm_spec() + req = SimpleNamespace(vms=vms) + with pytest.raises(HTTPException) as exc_info: + _check_vuln_vms(req) + assert "Unauthorized vm key" in exc_info.value.detail + + +class TestCheckStudentVms: + """Tests for _check_student_vms().""" + + def test_valid_student_vms_passes(self): + req = SimpleNamespace(vms={k: _make_vm_spec() for k in STUDENT_KEYS}) + _check_student_vms(req) + + def test_empty_vms_raises(self): + req = SimpleNamespace(vms={}) + with pytest.raises(HTTPException): + _check_student_vms(req) + + def test_missing_required_key_raises(self): + req = SimpleNamespace(vms={}) + with pytest.raises(HTTPException) as exc_info: + _check_student_vms(req) + assert exc_info.value.status_code == 400 + + def test_unauthorized_key_raises(self): + vms = {k: _make_vm_spec() for k in STUDENT_KEYS} + vms["student-box-99"] = _make_vm_spec() + req = SimpleNamespace(vms=vms) + with pytest.raises(HTTPException) as exc_info: + _check_student_vms(req) + assert "Unauthorized vm key" in exc_info.value.detail diff --git a/tests/test_text_cleaner.py b/tests/test_text_cleaner.py new file mode 100644 index 0000000..79061a4 --- /dev/null +++ b/tests/test_text_cleaner.py @@ -0,0 +1,34 @@ +"""Tests for app.utils.text_cleaner.""" + +from app.utils.text_cleaner import strip_ansi + + +class TestStripAnsi: + """Tests for strip_ansi().""" + + def test_removes_basic_color_codes(self): + assert strip_ansi("\x1b[32mok\x1b[0m") == "ok" + + def test_removes_bold_and_reset(self): + assert strip_ansi("\x1b[1mBOLD\x1b[0m text") == "BOLD text" + + def test_removes_multiple_colors(self): + s = "\x1b[31mred\x1b[0m \x1b[32mgreen\x1b[0m \x1b[34mblue\x1b[0m" + assert strip_ansi(s) == "red green blue" + + def test_preserves_plain_text(self): + assert strip_ansi("no escape codes here") == "no escape codes here" + + def test_handles_empty_string(self): + assert strip_ansi("") == "" + + def test_removes_cursor_movement(self): + assert strip_ansi("\x1b[2J\x1b[H") == "" + + def test_ansible_recap_line(self): + line = "\x1b[0;32mok=1\x1b[0m \x1b[0;33mchanged=0\x1b[0m \x1b[0;31mfailed=0\x1b[0m" + assert strip_ansi(line) == "ok=1 changed=0 failed=0" + + def test_preserves_newlines(self): + s = "\x1b[32mline1\x1b[0m\n\x1b[31mline2\x1b[0m" + assert strip_ansi(s) == "line1\nline2" diff --git a/tests/test_vm_id_name_resolver.py b/tests/test_vm_id_name_resolver.py new file mode 100644 index 0000000..b2c0df7 --- /dev/null +++ b/tests/test_vm_id_name_resolver.py @@ -0,0 +1,96 @@ +"""Tests for app.utils.vm_id_name_resolver.""" + +import pytest +from unittest.mock import patch +from fastapi import HTTPException + +from app.utils.vm_id_name_resolver import hack_same_vm_id, resolv_id_to_vm_name + + +class TestHackSameVmId: + """Tests for hack_same_vm_id() -- flexible VM ID comparison.""" + + def test_int_int_equal(self): + assert hack_same_vm_id(100, 100) is True + + def test_int_int_not_equal(self): + assert hack_same_vm_id(100, 200) is False + + def test_str_str_equal(self): + assert hack_same_vm_id("100", "100") is True + + def test_str_str_not_equal(self): + assert hack_same_vm_id("100", "200") is False + + def test_int_str_equal(self): + assert hack_same_vm_id(100, "100") is True + + def test_str_int_equal(self): + assert hack_same_vm_id("100", 100) is True + + def test_int_str_not_equal(self): + assert hack_same_vm_id(100, "200") is False + + def test_none_falls_back_to_string(self): + assert hack_same_vm_id(None, None) is True + + def test_none_vs_string(self): + assert hack_same_vm_id(None, "100") is False + + def test_non_numeric_strings(self): + assert hack_same_vm_id("abc", "abc") is True + assert hack_same_vm_id("abc", "def") is False + + +class TestResolvIdToVmName: + """Tests for resolv_id_to_vm_name() with mocked runner.""" + + @pytest.fixture + def mock_runner_with_results(self): + """Mock run_playbook_core returning VM list data.""" + events = [{"event": "runner_on_ok"}] + result_data = [ + [ + {"vm_id": 100, "vm_name": "pmg01"}, + {"vm_id": 101, "vm_name": "zbx01"}, + {"vm_id": 200, "vm_name": "test-vm"}, + ] + ] + with patch("app.utils.vm_id_name_resolver.run_playbook_core") as mock_run, \ + patch("app.utils.vm_id_name_resolver.extract_action_results") as mock_extract: + mock_run.return_value = (0, events, "ok", "ok") + mock_extract.return_value = result_data + yield mock_run, mock_extract + + def test_finds_vm_by_int_id(self, mock_runner_with_results): + result = resolv_id_to_vm_name("pve01", "100") + assert result["vm_id"] == 100 + assert result["vm_name"] == "pmg01" + + def test_finds_vm_by_string_id(self, mock_runner_with_results): + result = resolv_id_to_vm_name("pve01", "200") + assert result["vm_name"] == "test-vm" + + def test_not_found_raises_500(self, mock_runner_with_results): + with pytest.raises(HTTPException) as exc_info: + resolv_id_to_vm_name("pve01", "9999") + assert exc_info.value.status_code == 500 + assert "NOT FOUND" in exc_info.value.detail + + def test_invalid_json_string_raises_500(self): + with patch("app.utils.vm_id_name_resolver.run_playbook_core") as mock_run, \ + patch("app.utils.vm_id_name_resolver.extract_action_results") as mock_extract: + mock_run.return_value = (0, [], "ok", "ok") + mock_extract.return_value = "not valid json {" + with pytest.raises(HTTPException) as exc_info: + resolv_id_to_vm_name("pve01", "100") + assert exc_info.value.status_code == 500 + + def test_empty_results_raises_500(self): + with patch("app.utils.vm_id_name_resolver.run_playbook_core") as mock_run, \ + patch("app.utils.vm_id_name_resolver.extract_action_results") as mock_extract: + mock_run.return_value = (0, [], "ok", "ok") + mock_extract.return_value = [] + with pytest.raises(HTTPException) as exc_info: + resolv_id_to_vm_name("pve01", "100") + assert exc_info.value.status_code == 500 From f927c188f3b07d632a4f2f7392d7d0703d44adf4 Mon Sep 17 00:00:00 2001 From: t0kubetsu Date: Tue, 24 Mar 2026 14:34:58 +0100 Subject: [PATCH 30/33] format --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e749be4..59fe45e 100644 --- a/README.md +++ b/README.md @@ -9,11 +9,11 @@ FastAPI application that orchestrates Proxmox infrastructure deployments by exec - [Quick Start](#quick-start) - [Configuration](#configuration) - [API Documentation](#api-documentation) - - [WebSocket API](#websocket-api) +- [WebSocket API](#websocket-api) - [Project Structure](#project-structure) - [Architecture](#architecture) - [Development](#development) - - [Test Structure](#test-structure) +- [Test Structure](#test-structure) - [License](#license) --- From da91fa76fe162a6ea967fb6d872b57d810dc0749 Mon Sep 17 00:00:00 2001 From: Philippe Parage Date: Wed, 25 Mar 2026 09:07:10 +0100 Subject: [PATCH 31/33] test(tags): add unit tests for vm_set_tag route and tag format conversion --- tests/test_vm_tags.py | 83 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 tests/test_vm_tags.py diff --git a/tests/test_vm_tags.py b/tests/test_vm_tags.py new file mode 100644 index 0000000..389379e --- /dev/null +++ b/tests/test_vm_tags.py @@ -0,0 +1,83 @@ +"""Tests for VM tag operations.""" + +import pytest +from pathlib import Path +from unittest.mock import patch, MagicMock + +import app.utils + + +@pytest.fixture(autouse=True) +def _patch_utils_resolve(): + fake = MagicMock(return_value=Path("/tmp/fake/hosts.yml")) + app.utils.resolve_inventory = fake + yield + delattr(app.utils, "resolve_inventory") + + +@pytest.fixture +def mock_runner(): + with patch("app.routes.vm_config.run_playbook_core") as mock, \ + patch.object(Path, "exists", return_value=True): + mock.return_value = (0, [], "ok", "ok") + yield mock + + +class TestVmSetTag: + """Tests for POST /v0/admin/proxmox/vms/vm_id/config/vm_set_tag.""" + + def test_set_tag_success(self, client, mock_runner): + resp = client.post( + "/v0/admin/proxmox/vms/vm_id/config/vm_set_tag", + json={ + "proxmox_node": "pve01", + "vm_id": "1023", + "vm_tag_name": "admin,monitoring", + "as_json": True, + }, + ) + assert resp.status_code == 200 + mock_runner.assert_called_once() + call_kwargs = mock_runner.call_args + extravars = call_kwargs.kwargs.get("extravars") or call_kwargs[1].get("extravars", {}) + assert extravars.get("proxmox_vm_action") == "vm_set_tag" + + def test_set_tag_empty_string_rejected(self, client, mock_runner): + resp = client.post( + "/v0/admin/proxmox/vms/vm_id/config/vm_set_tag", + json={ + "proxmox_node": "pve01", + "vm_id": "1023", + "vm_tag_name": "", + }, + ) + assert resp.status_code == 422 + + def test_set_tag_special_chars_rejected(self, client, mock_runner): + resp = client.post( + "/v0/admin/proxmox/vms/vm_id/config/vm_set_tag", + json={ + "proxmox_node": "pve01", + "vm_id": "1023", + "vm_tag_name": "admin;drop table", + }, + ) + assert resp.status_code == 422 + + +class TestTagFormatConversion: + """Test format conversion between Proxmox semicolons and backend commas.""" + + def test_proxmox_semicolons_parsed(self): + raw = "admin;monitoring;custom-tag" + tags = raw.split(";") + assert tags == ["admin", "monitoring", "custom-tag"] + + def test_backend_comma_format(self): + tags = ["admin", "monitoring"] + formatted = ",".join(tags) + assert formatted == "admin,monitoring" + + def test_empty_tags_handled(self): + assert "".split(";") == [""] + assert [t for t in "".split(";") if t] == [] From 124b88d17f7d8caab19fb3712d1ae3c164475f44 Mon Sep 17 00:00:00 2001 From: Philippe Parage Date: Wed, 25 Mar 2026 09:24:31 +0100 Subject: [PATCH 32/33] test(realtime): add WebSocket tag tests and list_usage field coverage --- tests/test_route_vms.py | 34 ++++++++++++++++++++++++++++++++++ tests/test_ws_helpers.py | 14 ++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/tests/test_route_vms.py b/tests/test_route_vms.py index 4b4e80a..afc3336 100644 --- a/tests/test_route_vms.py +++ b/tests/test_route_vms.py @@ -74,3 +74,37 @@ def test_vm_action_failure_returns_500(self, client, mock_runner_failure, action json={"proxmox_node": "pve01", "vm_id": "100", "as_json": False}, ) assert resp.status_code == 500 + + +class TestVmListUsageFields: + """Verify list_usage returns fields consumed by frontend.""" + + def test_list_usage_success(self, client, mock_runner): + mock_runner.return_value = (0, [{ + "event": "runner_on_ok", + "event_data": { + "res": { + "vm_list_usage": [{ + "vm_id": 1023, + "vm_name": "test-vm", + "vm_status": "running", + "cpu_current_usage": 35.0, + "cpu_allocated": 4, + "ram_current_usage": 5264621568, + "ram_max": 8589934592, + "disk_current_usage": 0, + "disk_read": 1258291, + "disk_write": 419430, + "disk_max": 34359738368, + "net_in": 3670016, + "net_out": 838860, + "vm_uptime": 308520, + }] + } + } + }], "ok", "ok") + resp = client.post( + "/v0/admin/proxmox/vms/list_usage", + json={"proxmox_node": "pve01", "as_json": True}, + ) + assert resp.status_code == 200 diff --git a/tests/test_ws_helpers.py b/tests/test_ws_helpers.py index 30c42d3..5dbe073 100644 --- a/tests/test_ws_helpers.py +++ b/tests/test_ws_helpers.py @@ -67,6 +67,20 @@ def test_multiple_changes(self): assert 101 in diff # removed assert 102 in diff # added + def test_tag_changes_not_detected_by_diff(self): + """Tags are only synced via full refreshes, not diffs. + compute_diff only compares status and cpu threshold.""" + prev = {100: {"vmid": 100, "status": "running", "cpu": 5.0, "tags": "admin"}} + curr = {100: {"vmid": 100, "status": "running", "cpu": 5.0, "tags": "admin;monitoring"}} + # Tags change alone does NOT trigger a diff + assert compute_diff(prev, curr) is None + + def test_full_state_includes_tags(self): + """Verify that VM status dicts include the tags field.""" + vm = {"vmid": 100, "status": "running", "cpu": 5.0, "tags": "admin;vuln"} + assert "tags" in vm + assert vm["tags"] == "admin;vuln" + class TestLoadProxmoxCredentials: """Tests for load_proxmox_credentials().""" From b9d3d8fa71ec458dd66c6cf58b236e9ef8377447 Mon Sep 17 00:00:00 2001 From: Philippe Parage Date: Thu, 26 Mar 2026 12:02:34 +0100 Subject: [PATCH 33/33] refactor(docker): clean up production image and split dev dependencies - Pin python:3.12-slim, remove unused sshpass and start.sh copy - Move pytest/setuptools/wheel to requirements-dev.txt - Add SSH key mount to docker-compose for Ansible access --- Dockerfile | 4 +--- docker-compose.yml | 1 + requirements-dev.txt | 4 ++++ requirements.txt | 20 +++----------------- 4 files changed, 9 insertions(+), 20 deletions(-) create mode 100644 requirements-dev.txt diff --git a/Dockerfile b/Dockerfile index 5f688db..0a3db0b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,8 @@ -FROM python:slim AS base +FROM python:3.12-slim # Install system deps for ansible and ssh RUN apt-get update && apt-get install -y --no-install-recommends \ openssh-client \ - sshpass \ git \ && rm -rf /var/lib/apt/lists/* @@ -21,7 +20,6 @@ RUN ansible-galaxy collection install -r requirements.yml -p /usr/share/ansible/ COPY app/ app/ COPY playbooks/ playbooks/ COPY inventory/ inventory/ -COPY start.sh . # Set env defaults ENV PROJECT_ROOT_DIR=/app diff --git a/docker-compose.yml b/docker-compose.yml index 8bab87d..8024ad4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,6 +7,7 @@ services: - ./app:/app/app:ro - ./playbooks:/app/playbooks:ro - ./inventory:/app/inventory:ro + - ${SSH_KEY_PATH:-~/.ssh}:/root/.ssh:ro environment: - PROJECT_ROOT_DIR=/app - API_BACKEND_WWWAPP_PLAYBOOKS_DIR=/app/ diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..0893212 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,4 @@ +-r requirements.txt + +pytest==8.3.4 +pytest-asyncio==0.24.0 diff --git a/requirements.txt b/requirements.txt index fc72494..eb82e90 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,26 +1,12 @@ - -# ansible libs +# Ansible ansible-core==2.19.1 ansible-runner==2.4.1 -# SSH libs +# SSH paramiko>=3.4.0 cryptography>=42 -##########################################################" # API fastapi==0.115.0 uvicorn[standard]==0.30.5 -httpx==0.27.2 - -# Others -setuptools>=70.0.0 -wheel>=0.43.0 - - -pytest==8.3.4 -pytest-asyncio==0.24.0 - -# for wazuh ? -# pywinrm -# requests-ntlm \ No newline at end of file +httpx==0.27.2 \ No newline at end of file