206 lines
5.4 KiB
Python
206 lines
5.4 KiB
Python
|
|
"""Umami analytics deployment playbook.
|
||
|
|
|
||
|
|
Defines the steps required to:
|
||
|
|
1. Set up Umami on a tenant server (ENV_UPDATE + DOCKER_RELOAD)
|
||
|
|
2. Perform initial setup via Playwright automation (create admin, add first website)
|
||
|
|
|
||
|
|
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
|
||
|
|
UMAMI_ENV_PATH = "/opt/letsbe/env/umami.env"
|
||
|
|
UMAMI_STACK_DIR = "/opt/letsbe/stacks/umami"
|
||
|
|
|
||
|
|
|
||
|
|
def build_umami_setup_steps(*, domain: str) -> list[CompositeStep]:
|
||
|
|
"""
|
||
|
|
Build the sequence of steps required to set up Umami.
|
||
|
|
|
||
|
|
Assumes the env file already exists at /opt/letsbe/env/umami.env
|
||
|
|
(created by provisioning/env_setup.sh).
|
||
|
|
|
||
|
|
Args:
|
||
|
|
domain: The domain for Umami (e.g., "analytics.example.com")
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
List of 2 CompositeStep objects:
|
||
|
|
1. ENV_UPDATE - patches APP_URL
|
||
|
|
2. DOCKER_RELOAD - restarts the umami stack with pull=True
|
||
|
|
"""
|
||
|
|
steps = [
|
||
|
|
# Step 1: Update environment variables
|
||
|
|
CompositeStep(
|
||
|
|
type="ENV_UPDATE",
|
||
|
|
payload={
|
||
|
|
"path": UMAMI_ENV_PATH,
|
||
|
|
"updates": {
|
||
|
|
"APP_URL": f"https://{domain}",
|
||
|
|
},
|
||
|
|
},
|
||
|
|
),
|
||
|
|
# Step 2: Reload Docker stack
|
||
|
|
CompositeStep(
|
||
|
|
type="DOCKER_RELOAD",
|
||
|
|
payload={
|
||
|
|
"compose_dir": UMAMI_STACK_DIR,
|
||
|
|
"pull": True,
|
||
|
|
},
|
||
|
|
),
|
||
|
|
]
|
||
|
|
return steps
|
||
|
|
|
||
|
|
|
||
|
|
async def create_umami_setup_task(
|
||
|
|
*,
|
||
|
|
db: AsyncSession,
|
||
|
|
tenant_id: uuid.UUID,
|
||
|
|
agent_id: uuid.UUID | None,
|
||
|
|
domain: str,
|
||
|
|
) -> Task:
|
||
|
|
"""
|
||
|
|
Create and persist a COMPOSITE task for Umami setup.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
db: Async database session
|
||
|
|
tenant_id: UUID of the tenant
|
||
|
|
agent_id: Optional UUID of the agent to assign the task to
|
||
|
|
domain: The domain for Umami
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
The created Task object with type="COMPOSITE"
|
||
|
|
"""
|
||
|
|
steps = build_umami_setup_steps(domain=domain)
|
||
|
|
|
||
|
|
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
|
||
|
|
|
||
|
|
|
||
|
|
# =============================================================================
|
||
|
|
# Initial Setup via Playwright
|
||
|
|
# =============================================================================
|
||
|
|
|
||
|
|
|
||
|
|
def build_umami_initial_setup_step(
|
||
|
|
*,
|
||
|
|
base_url: str,
|
||
|
|
admin_password: str | None = None,
|
||
|
|
website_name: str | None = None,
|
||
|
|
website_url: str | None = None,
|
||
|
|
) -> dict[str, Any]:
|
||
|
|
"""
|
||
|
|
Build a PLAYWRIGHT task payload for Umami initial setup.
|
||
|
|
|
||
|
|
This logs in with default credentials, changes the admin password,
|
||
|
|
and optionally adds the first website to track.
|
||
|
|
|
||
|
|
Umami ships with default credentials: admin / umami
|
||
|
|
|
||
|
|
Args:
|
||
|
|
base_url: The base URL for Umami (e.g., "https://analytics.example.com")
|
||
|
|
admin_password: New password for admin (auto-generated if None)
|
||
|
|
website_name: Optional name of the first website to add
|
||
|
|
website_url: Optional URL of the first website to track
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
Task payload dict with type="PLAYWRIGHT"
|
||
|
|
"""
|
||
|
|
parsed = urlparse(base_url)
|
||
|
|
allowed_domain = parsed.netloc
|
||
|
|
|
||
|
|
inputs: dict[str, Any] = {
|
||
|
|
"base_url": base_url,
|
||
|
|
}
|
||
|
|
|
||
|
|
if admin_password:
|
||
|
|
inputs["admin_password"] = admin_password
|
||
|
|
if website_name:
|
||
|
|
inputs["website_name"] = website_name
|
||
|
|
if website_url:
|
||
|
|
inputs["website_url"] = website_url
|
||
|
|
|
||
|
|
return {
|
||
|
|
"scenario": "umami_initial_setup",
|
||
|
|
"inputs": inputs,
|
||
|
|
"options": {
|
||
|
|
"allowed_domains": [allowed_domain],
|
||
|
|
},
|
||
|
|
"timeout": 120,
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
async def create_umami_initial_setup_task(
|
||
|
|
*,
|
||
|
|
db: AsyncSession,
|
||
|
|
tenant_id: uuid.UUID,
|
||
|
|
agent_id: uuid.UUID,
|
||
|
|
base_url: str,
|
||
|
|
admin_password: str | None = None,
|
||
|
|
website_name: str | None = None,
|
||
|
|
website_url: str | None = None,
|
||
|
|
) -> Task:
|
||
|
|
"""
|
||
|
|
Create and persist a PLAYWRIGHT task for Umami 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 Umami
|
||
|
|
admin_password: New password for admin (auto-generated if None)
|
||
|
|
website_name: Optional name of the first website to add
|
||
|
|
website_url: Optional URL of the first website to track
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
The created Task object with type="PLAYWRIGHT"
|
||
|
|
"""
|
||
|
|
payload = build_umami_initial_setup_step(
|
||
|
|
base_url=base_url,
|
||
|
|
admin_password=admin_password,
|
||
|
|
website_name=website_name,
|
||
|
|
website_url=website_url,
|
||
|
|
)
|
||
|
|
|
||
|
|
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
|