diff --git a/app/routes/__init__.py b/app/routes/__init__.py index 7bcc7ac..d0fdce4 100644 --- a/app/routes/__init__.py +++ b/app/routes/__init__.py @@ -8,7 +8,7 @@ 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 +from app.routes.vms import vm_id_router, vms_router router = APIRouter() @@ -48,9 +48,6 @@ # /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") diff --git a/app/routes/bundles.py b/app/routes/bundles.py index a0baa15..e2115dd 100644 --- a/app/routes/bundles.py +++ b/app/routes/bundles.py @@ -2,7 +2,7 @@ Endpoints --------- -Ubuntu install bundles: +Ubuntu install/configure bundles: - ``POST .../core/linux/ubuntu/install/docker`` -- Install Docker. - ``POST .../core/linux/ubuntu/install/docker-compose`` -- Install Docker Compose. @@ -10,27 +10,21 @@ - ``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): +All prefixed under ``/v0/admin/run/bundles``. The HTTP paths above are kept for +API compatibility; the resolved bundle paths follow the generic/ tier grammar +(range42-playbooks#133). -- ``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``. +The former Proxmox default-VM bundle routes were removed: their bundles are +retired to ``decom/`` in range42-playbooks#133 and had no live caller. """ import os from pathlib import Path -from fastapi import APIRouter, HTTPException +from fastapi import APIRouter from fastapi.responses import JSONResponse from app import utils -from app.core.extractor import extract_action_results from app.core.logging import get_logger from app.core.runner import run_playbook_core @@ -41,26 +35,12 @@ 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, - 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 logger = get_logger(__name__) @@ -91,131 +71,6 @@ def _run_bundle_simple(req, action_name: str, extravars: dict) -> JSONResponse: 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 # =========================================================================== @@ -248,7 +103,7 @@ def bundles_core_linux_ubuntu_install_docker( 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 + req, "generic/software.install.docker", extravars or None ) @@ -283,7 +138,7 @@ def bundles_core_linux_ubuntu_install_docker_compose( 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 + req, "generic/software.install.docker_compose", extravars or None ) @@ -319,7 +174,7 @@ def bundles_core_linux_ubuntu_install_basic_packages( if val is not None: extravars[key] = val return _run_bundle_simple( - req, "core/linux/ubuntu/install/basic-packages", extravars or None + req, "generic/software.install.basic_packages", extravars or None ) @@ -352,7 +207,7 @@ def bundles_core_linux_ubuntu_install_dotfiles( 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 + req, "generic/software.install.dotfiles", extravars or None ) @@ -388,296 +243,5 @@ def bundles_core_linux_ubuntu_configure_add_user( 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, -): - """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 - ) - - -# =========================================================================== -# 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, + req, "generic/credentials.create.user", extravars or None ) diff --git a/app/routes/runner.py b/app/routes/runner.py index 9077bbe..816734f 100644 --- a/app/routes/runner.py +++ b/app/routes/runner.py @@ -55,7 +55,7 @@ def _run_generic(req, name: str, resolver_fn) -> JSONResponse: 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 bundles_name: Bundle path (e.g. ``"generic/software.install.docker"``). :param req: Request body with ``hosts`` and optional ``proxmox_node``. :returns: JSON with ``rc`` and ``log_multiline``. """ diff --git a/app/routes/vms.py b/app/routes/vms.py index 6890964..963cae7 100644 --- a/app/routes/vms.py +++ b/app/routes/vms.py @@ -38,15 +38,12 @@ 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, ) from app.utils.vm_id_name_resolver import resolv_id_to_vm_name @@ -336,169 +333,3 @@ def proxmox_vms_vm_id_clone(req: Request_ProxmoxVmsVMID_Clone): # =========================================================================== # 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): - """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") - - -@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, -): - """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") - - -@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, -): - """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") - - -@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, -): - """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") - - -@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, -): - """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") - - -@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): - """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" - ) - - 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/app/schemas/bundles/__init__.py b/app/schemas/bundles/__init__.py index cfeb0da..c761b99 100644 --- a/app/schemas/bundles/__init__.py +++ b/app/schemas/bundles/__init__.py @@ -1,12 +1,10 @@ -"""Consolidated bundle schemas: Ubuntu packages/configure + Proxmox default VM ops. +"""Consolidated bundle schemas: Ubuntu packages/configure. 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 Annotated, Dict - -from pydantic import BaseModel, Field, StringConstraints +from pydantic import BaseModel, Field # =========================================================================== # Ubuntu Bundles @@ -277,239 +275,6 @@ class BundleDotFilesReply(BaseModel): model_config = {"json_schema_extra": {"example": {"rc": 0, "result": [{}]}}} -# =========================================================================== -# 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_ip: str = Field( - ..., description="vm ipv4", pattern=r"^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$" - ) - vm_description: _VmDescription = Field(..., 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): - proxmox_node: 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: _VmDescription = Field(..., 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): - proxmox_node: 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: _VmDescription = Field(..., 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): - proxmox_node: 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 # --------------------------------------------------------------------------- @@ -539,47 +304,3 @@ class BundleStartStopDefaultRequest(BaseModel): 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/utils/checks_playbooks.py b/app/utils/checks_playbooks.py index 0ef1f73..75b722c 100644 --- a/app/utils/checks_playbooks.py +++ b/app/utils/checks_playbooks.py @@ -16,6 +16,13 @@ logger = get_logger(__name__) +# Playbook name grammar. Segments are slash-separated and may contain the +# dotted ``..`` form used by the bundle naming grammar +# (range42-playbooks#133), e.g. ``generic/software.install.docker``. A "." or +# ".." segment is rejected here (and traversal is caught again downstream by the +# is_relative_to check in _resolve_file). +_PLAYBOOK_NAME_REGEX = re.compile(r"^[A-Za-z0-9_.-]+(?:/[A-Za-z0-9_.-]+)*$") + def _warmup_checks(playbooks_dir_type: str) -> Path: """Validate and resolve the playbooks base directory. @@ -81,12 +88,13 @@ def resolve_actions_playbook(action_name: str, playbooks_dir_type: str) -> Path: # - /vm/clone-template # start with / # - vm/clone-template / # ending with / # - vm//clone-template # double slash - # - linux/ubuntu/install.dot # dot not allowed # - ubuntu/ins tall # space + # - ../../etc/passwd # "." / ".." segments (traversal) + # + # Dots WITHIN a segment are allowed (e.g. software.install.docker). # - 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) + main_filepath = _resolve_file(actions_dir, _PLAYBOOK_NAME_REGEX, action_name) return main_filepath @@ -97,7 +105,7 @@ def resolve_bundles_playbook(action_name: str, playbooks_dir_type: str) -> 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"``). + :param action_name: Slash-separated bundle path (e.g. ``"generic/software.install.docker"``). :type action_name: str :param playbooks_dir_type: Either ``"www_app"`` or ``"public_github"``. :type playbooks_dir_type: str @@ -110,10 +118,7 @@ def resolve_bundles_playbook(action_name: str, playbooks_dir_type: str) -> Path: playbooks_dir = _warmup_checks(playbooks_dir_type) actions_dir = (playbooks_dir / "bundles").resolve() - # 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) + main_filepath = _resolve_file(actions_dir, _PLAYBOOK_NAME_REGEX, action_name) return main_filepath @@ -140,11 +145,8 @@ def resolve_bundles_playbook_init_file( playbooks_dir = _warmup_checks(playbooks_dir_type) actions_dir = (playbooks_dir / "bundles").resolve() - # 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 + actions_dir, _PLAYBOOK_NAME_REGEX, action_name, is_init_yaml=True ) return main_filepath @@ -169,8 +171,7 @@ def resolve_scenarios_playbook(action_name: str, playbooks_dir_type: str) -> Pat playbooks_dir = _warmup_checks(playbooks_dir_type) scenarios_dir = (playbooks_dir / "scenarios").resolve() - scenarios_regex_pattern = re.compile(r"^[A-Za-z0-9_-]+(?:/[A-Za-z0-9_-]+)*$") - main_filepath = _resolve_file(scenarios_dir, scenarios_regex_pattern, action_name) + main_filepath = _resolve_file(scenarios_dir, _PLAYBOOK_NAME_REGEX, action_name) return main_filepath @@ -194,7 +195,7 @@ def _resolve_file( :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"``). + :param action_name: The action name to resolve (e.g. ``"generic/software.install.docker"``). :type action_name: str :param is_init_yaml: If ``True``, resolve ``init.yml`` instead of ``main.yml``. :type is_init_yaml: bool @@ -212,6 +213,13 @@ def _resolve_file( logger.error(err) raise HTTPException(status_code=400, detail=err) + # The regex allows dots inside a segment, so a "." / ".." segment slips + # through the format check -- reject it explicitly before resolving. + if any(segment in (".", "..") for segment in action_name.split("/")): + err = f":: err - INVALID ACTION NAME SEGMENT {action_name!r}" + logger.error(err) + raise HTTPException(status_code=400, detail=err) + # # init|main.yaml must exists. # diff --git a/tests/fixtures/routes_golden.json b/tests/fixtures/routes_golden.json index fef0a2e..d6aa24b 100644 --- a/tests/fixtures/routes_golden.json +++ b/tests/fixtures/routes_golden.json @@ -128,24 +128,6 @@ "/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" ], @@ -161,82 +143,10 @@ "/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_bundles_validation.py b/tests/test_bundles_validation.py deleted file mode 100644 index b43fd52..0000000 --- a/tests/test_bundles_validation.py +++ /dev/null @@ -1,129 +0,0 @@ -"""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_checks_playbooks.py b/tests/test_checks_playbooks.py index abeae5c..68afbb3 100644 --- a/tests/test_checks_playbooks.py +++ b/tests/test_checks_playbooks.py @@ -61,9 +61,15 @@ def test_rejects_name_with_spaces(self): resolve_actions_playbook("my action", "www_app") assert exc_info.value.status_code == 400 - def test_rejects_name_with_dots(self): + def test_accepts_dotted_segment_format(self): + """Dotted .. names pass format validation + (range42-playbooks#133) and fail only on the missing file.""" + with pytest.raises((HTTPException, FileNotFoundError)): + resolve_bundles_playbook("generic/software.install.docker", "www_app") + + def test_rejects_dot_segment(self): with pytest.raises(HTTPException) as exc_info: - resolve_actions_playbook("install.docker", "www_app") + resolve_actions_playbook("foo/../bar", "www_app") assert exc_info.value.status_code == 400 def test_rejects_leading_slash(self): diff --git a/tests/test_schemas.py b/tests/test_schemas.py index 61fd3eb..3b5e2cb 100644 --- a/tests/test_schemas.py +++ b/tests/test_schemas.py @@ -275,26 +275,6 @@ def test_bundle_add_user_request(): assert req.user == "elliot" -def test_bundle_create_admin_vms_request(): - from app.schemas.bundles import ( - BundleCreateAdminVmsItemRequest, - BundleCreateAdminVmsRequest, - ) - - 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 # =========================================================================== @@ -441,16 +421,6 @@ def test_backward_compat_aliases_bundles(): assert Request_BundlesCoreLinuxUbuntuConfigure_AddUser is BundleAddUserRequest - from app.schemas.bundles import ( - BundleCreateAdminVmsRequest, - Request_BundlesCoreProxmoxConfigureDefaultVms_CreateAdminVms, - ) - - assert ( - Request_BundlesCoreProxmoxConfigureDefaultVms_CreateAdminVms - is BundleCreateAdminVmsRequest - ) - def test_backward_compat_aliases_debug(): from app.schemas.debug import DebugPingRequest, Request_DebugPing