"""Nextcloud deployment playbook. Defines the steps required to: 1. Set Nextcloud domain on a tenant server (v2: via NEXTCLOUD_SET_DOMAIN task) 2. Perform initial setup via Playwright automation (create admin account) Tenant servers must have stacks and env templates under /opt/letsbe. """ import uuid from typing import Any from urllib.parse import urlparse from pydantic import BaseModel, Field from sqlalchemy.ext.asyncio import AsyncSession from app.models.task import Task, TaskStatus class CompositeStep(BaseModel): """A single step in a composite playbook.""" type: str = Field(..., description="Task type (e.g., ENV_UPDATE, DOCKER_RELOAD)") payload: dict[str, Any] = Field( default_factory=dict, description="Payload for this step" ) # LetsBe standard paths NEXTCLOUD_STACK_DIR = "/opt/letsbe/stacks/nextcloud" # ============================================================================= # Initial Setup via Playwright # ============================================================================= def build_nextcloud_initial_setup_step( *, base_url: str, admin_username: str, admin_password: str, ) -> dict[str, Any]: """ Build a PLAYWRIGHT task payload for Nextcloud initial setup. This creates the admin account on a fresh Nextcloud installation. Args: base_url: The base URL for Nextcloud (e.g., "https://cloud.example.com") admin_username: Username for the admin account admin_password: Password for the admin account Returns: Task payload dict with type="PLAYWRIGHT" """ # Extract domain from URL for allowlist parsed = urlparse(base_url) allowed_domain = parsed.netloc # e.g., "cloud.example.com" return { "scenario": "nextcloud_initial_setup", "inputs": { "base_url": base_url, "admin_username": admin_username, "admin_password": admin_password, }, "options": { "allowed_domains": [allowed_domain], }, "timeout": 120, } async def create_nextcloud_initial_setup_task( *, db: AsyncSession, tenant_id: uuid.UUID, agent_id: uuid.UUID, base_url: str, admin_username: str, admin_password: str, ) -> Task: """ Create and persist a PLAYWRIGHT task for Nextcloud initial setup. Args: db: Async database session tenant_id: UUID of the tenant agent_id: UUID of the agent to assign the task to base_url: The base URL for Nextcloud admin_username: Username for the admin account admin_password: Password for the admin account Returns: The created Task object with type="PLAYWRIGHT" """ payload = build_nextcloud_initial_setup_step( base_url=base_url, admin_username=admin_username, admin_password=admin_password, ) task = Task( tenant_id=tenant_id, agent_id=agent_id, type="PLAYWRIGHT", payload=payload, status=TaskStatus.PENDING.value, ) db.add(task) await db.commit() await db.refresh(task) return task # ============================================================================= # Set Domain via NEXTCLOUD_SET_DOMAIN # ============================================================================= def build_nextcloud_set_domain_steps(*, public_url: str, pull: bool) -> list[CompositeStep]: """ Build the sequence of steps required to set Nextcloud domain (v2). Args: public_url: The public URL for Nextcloud (e.g., "https://cloud.example.com") pull: Whether to pull images before reloading the stack Returns: List of 2 CompositeStep objects: 1. NEXTCLOUD_SET_DOMAIN - configures Nextcloud via occ commands 2. DOCKER_RELOAD - restarts the Nextcloud stack """ steps = [ # Step 1: Configure Nextcloud domain via occ CompositeStep( type="NEXTCLOUD_SET_DOMAIN", payload={ "public_url": public_url, }, ), # Step 2: Reload Docker stack CompositeStep( type="DOCKER_RELOAD", payload={ "compose_dir": NEXTCLOUD_STACK_DIR, "pull": pull, }, ), ] return steps async def create_nextcloud_set_domain_task( *, db: AsyncSession, tenant_id: uuid.UUID, agent_id: uuid.UUID, public_url: str, pull: bool, ) -> Task: """ Create and persist a COMPOSITE task for Nextcloud set-domain. Args: db: Async database session tenant_id: UUID of the tenant agent_id: UUID of the agent to assign the task to public_url: The public URL for Nextcloud pull: Whether to pull images before reloading Returns: The created Task object with type="COMPOSITE" """ steps = build_nextcloud_set_domain_steps(public_url=public_url, pull=pull) task = Task( tenant_id=tenant_id, agent_id=agent_id, type="COMPOSITE", payload={"steps": [step.model_dump() for step in steps]}, status=TaskStatus.PENDING.value, ) db.add(task) await db.commit() await db.refresh(task) return task