Include full contents of all nested repositories
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
74
letsbe-orchestrator/app/playbooks/__init__.py
Normal file
74
letsbe-orchestrator/app/playbooks/__init__.py
Normal file
@@ -0,0 +1,74 @@
|
||||
"""Playbooks module for infrastructure automation tasks.
|
||||
|
||||
Playbooks define reusable sequences of steps (COMPOSITE tasks) for
|
||||
deploying and configuring services on tenant servers.
|
||||
"""
|
||||
|
||||
from app.playbooks.chatwoot import (
|
||||
CompositeStep,
|
||||
build_chatwoot_setup_steps,
|
||||
create_chatwoot_setup_task,
|
||||
)
|
||||
from app.playbooks.nextcloud import (
|
||||
build_nextcloud_set_domain_steps,
|
||||
create_nextcloud_set_domain_task,
|
||||
)
|
||||
from app.playbooks.keycloak import (
|
||||
build_keycloak_setup_steps,
|
||||
create_keycloak_setup_task,
|
||||
)
|
||||
from app.playbooks.n8n import (
|
||||
build_n8n_setup_steps,
|
||||
create_n8n_setup_task,
|
||||
)
|
||||
from app.playbooks.calcom import (
|
||||
build_calcom_setup_steps,
|
||||
create_calcom_setup_task,
|
||||
)
|
||||
from app.playbooks.umami import (
|
||||
build_umami_setup_steps,
|
||||
create_umami_setup_task,
|
||||
)
|
||||
from app.playbooks.uptime_kuma import (
|
||||
build_uptime_kuma_setup_steps,
|
||||
create_uptime_kuma_setup_task,
|
||||
)
|
||||
from app.playbooks.vaultwarden import (
|
||||
build_vaultwarden_setup_steps,
|
||||
create_vaultwarden_setup_task,
|
||||
)
|
||||
from app.playbooks.portainer import (
|
||||
build_portainer_setup_steps,
|
||||
create_portainer_setup_task,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"CompositeStep",
|
||||
# Chatwoot
|
||||
"build_chatwoot_setup_steps",
|
||||
"create_chatwoot_setup_task",
|
||||
# Nextcloud
|
||||
"build_nextcloud_set_domain_steps",
|
||||
"create_nextcloud_set_domain_task",
|
||||
# Keycloak
|
||||
"build_keycloak_setup_steps",
|
||||
"create_keycloak_setup_task",
|
||||
# n8n
|
||||
"build_n8n_setup_steps",
|
||||
"create_n8n_setup_task",
|
||||
# Cal.com
|
||||
"build_calcom_setup_steps",
|
||||
"create_calcom_setup_task",
|
||||
# Umami
|
||||
"build_umami_setup_steps",
|
||||
"create_umami_setup_task",
|
||||
# Uptime Kuma
|
||||
"build_uptime_kuma_setup_steps",
|
||||
"create_uptime_kuma_setup_task",
|
||||
# Vaultwarden
|
||||
"build_vaultwarden_setup_steps",
|
||||
"create_vaultwarden_setup_task",
|
||||
# Portainer
|
||||
"build_portainer_setup_steps",
|
||||
"create_portainer_setup_task",
|
||||
]
|
||||
207
letsbe-orchestrator/app/playbooks/calcom.py
Normal file
207
letsbe-orchestrator/app/playbooks/calcom.py
Normal file
@@ -0,0 +1,207 @@
|
||||
"""Cal.com scheduling deployment playbook.
|
||||
|
||||
Defines the steps required to:
|
||||
1. Set up Cal.com on a tenant server (ENV_UPDATE + DOCKER_RELOAD)
|
||||
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
|
||||
CALCOM_ENV_PATH = "/opt/letsbe/env/calcom.env"
|
||||
CALCOM_STACK_DIR = "/opt/letsbe/stacks/calcom"
|
||||
|
||||
|
||||
def build_calcom_setup_steps(*, domain: str) -> list[CompositeStep]:
|
||||
"""
|
||||
Build the sequence of steps required to set up Cal.com.
|
||||
|
||||
Assumes the env file already exists at /opt/letsbe/env/calcom.env
|
||||
(created by provisioning/env_setup.sh).
|
||||
|
||||
Args:
|
||||
domain: The domain for Cal.com (e.g., "cal.example.com")
|
||||
|
||||
Returns:
|
||||
List of 2 CompositeStep objects:
|
||||
1. ENV_UPDATE - patches NEXT_PUBLIC_WEBAPP_URL, NEXTAUTH_URL
|
||||
2. DOCKER_RELOAD - restarts the calcom stack with pull=True
|
||||
"""
|
||||
steps = [
|
||||
# Step 1: Update environment variables
|
||||
CompositeStep(
|
||||
type="ENV_UPDATE",
|
||||
payload={
|
||||
"path": CALCOM_ENV_PATH,
|
||||
"updates": {
|
||||
"NEXT_PUBLIC_WEBAPP_URL": f"https://{domain}",
|
||||
"NEXTAUTH_URL": f"https://{domain}",
|
||||
},
|
||||
},
|
||||
),
|
||||
# Step 2: Reload Docker stack
|
||||
CompositeStep(
|
||||
type="DOCKER_RELOAD",
|
||||
payload={
|
||||
"compose_dir": CALCOM_STACK_DIR,
|
||||
"pull": True,
|
||||
},
|
||||
),
|
||||
]
|
||||
return steps
|
||||
|
||||
|
||||
async def create_calcom_setup_task(
|
||||
*,
|
||||
db: AsyncSession,
|
||||
tenant_id: uuid.UUID,
|
||||
agent_id: uuid.UUID | None,
|
||||
domain: str,
|
||||
) -> Task:
|
||||
"""
|
||||
Create and persist a COMPOSITE task for Cal.com 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 Cal.com
|
||||
|
||||
Returns:
|
||||
The created Task object with type="COMPOSITE"
|
||||
"""
|
||||
steps = build_calcom_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_calcom_initial_setup_step(
|
||||
*,
|
||||
base_url: str,
|
||||
admin_email: str,
|
||||
admin_password: str | None = None,
|
||||
admin_username: str = "admin",
|
||||
admin_name: str = "Admin",
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Build a PLAYWRIGHT task payload for Cal.com initial setup.
|
||||
|
||||
This creates the admin account on a fresh Cal.com installation.
|
||||
|
||||
Args:
|
||||
base_url: The base URL for Cal.com (e.g., "https://cal.example.com")
|
||||
admin_email: Email address for the admin account
|
||||
admin_password: Password for admin (auto-generated if None)
|
||||
admin_username: Username for the admin account
|
||||
admin_name: Display name for the admin account
|
||||
|
||||
Returns:
|
||||
Task payload dict with type="PLAYWRIGHT"
|
||||
"""
|
||||
parsed = urlparse(base_url)
|
||||
allowed_domain = parsed.netloc
|
||||
|
||||
inputs: dict[str, Any] = {
|
||||
"base_url": base_url,
|
||||
"admin_email": admin_email,
|
||||
"admin_username": admin_username,
|
||||
"admin_name": admin_name,
|
||||
}
|
||||
|
||||
if admin_password:
|
||||
inputs["admin_password"] = admin_password
|
||||
|
||||
return {
|
||||
"scenario": "calcom_initial_setup",
|
||||
"inputs": inputs,
|
||||
"options": {
|
||||
"allowed_domains": [allowed_domain],
|
||||
},
|
||||
"timeout": 120,
|
||||
}
|
||||
|
||||
|
||||
async def create_calcom_initial_setup_task(
|
||||
*,
|
||||
db: AsyncSession,
|
||||
tenant_id: uuid.UUID,
|
||||
agent_id: uuid.UUID,
|
||||
base_url: str,
|
||||
admin_email: str,
|
||||
admin_password: str | None = None,
|
||||
admin_username: str = "admin",
|
||||
admin_name: str = "Admin",
|
||||
) -> Task:
|
||||
"""
|
||||
Create and persist a PLAYWRIGHT task for Cal.com 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 Cal.com
|
||||
admin_email: Email address for the admin account
|
||||
admin_password: Password for admin (auto-generated if None)
|
||||
admin_username: Username for the admin account
|
||||
admin_name: Display name for the admin account
|
||||
|
||||
Returns:
|
||||
The created Task object with type="PLAYWRIGHT"
|
||||
"""
|
||||
payload = build_calcom_initial_setup_step(
|
||||
base_url=base_url,
|
||||
admin_email=admin_email,
|
||||
admin_password=admin_password,
|
||||
admin_username=admin_username,
|
||||
admin_name=admin_name,
|
||||
)
|
||||
|
||||
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
|
||||
207
letsbe-orchestrator/app/playbooks/chatwoot.py
Normal file
207
letsbe-orchestrator/app/playbooks/chatwoot.py
Normal file
@@ -0,0 +1,207 @@
|
||||
"""Chatwoot deployment playbook.
|
||||
|
||||
Defines the steps required to set up Chatwoot on a tenant server
|
||||
that already has stacks and env templates under /opt/letsbe.
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from typing import Any
|
||||
|
||||
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
|
||||
CHATWOOT_ENV_PATH = "/opt/letsbe/env/chatwoot.env"
|
||||
CHATWOOT_STACK_DIR = "/opt/letsbe/stacks/chatwoot"
|
||||
|
||||
|
||||
def build_chatwoot_setup_steps(*, domain: str) -> list[CompositeStep]:
|
||||
"""
|
||||
Build the sequence of steps required to set up Chatwoot.
|
||||
|
||||
Assumes the env file already exists at /opt/letsbe/env/chatwoot.env
|
||||
(created by provisioning/env_setup.sh).
|
||||
|
||||
Args:
|
||||
domain: The domain for Chatwoot (e.g., "support.example.com")
|
||||
|
||||
Returns:
|
||||
List of 2 CompositeStep objects:
|
||||
1. ENV_UPDATE - patches FRONTEND_URL and BACKEND_URL
|
||||
2. DOCKER_RELOAD - restarts the chatwoot stack with pull=True
|
||||
"""
|
||||
steps = [
|
||||
# Step 1: Update environment variables
|
||||
CompositeStep(
|
||||
type="ENV_UPDATE",
|
||||
payload={
|
||||
"path": CHATWOOT_ENV_PATH,
|
||||
"updates": {
|
||||
"FRONTEND_URL": f"https://{domain}",
|
||||
"BACKEND_URL": f"https://{domain}",
|
||||
},
|
||||
},
|
||||
),
|
||||
# Step 2: Reload Docker stack
|
||||
CompositeStep(
|
||||
type="DOCKER_RELOAD",
|
||||
payload={
|
||||
"compose_dir": CHATWOOT_STACK_DIR,
|
||||
"pull": True,
|
||||
},
|
||||
),
|
||||
]
|
||||
return steps
|
||||
|
||||
|
||||
async def create_chatwoot_setup_task(
|
||||
*,
|
||||
db: AsyncSession,
|
||||
tenant_id: uuid.UUID,
|
||||
agent_id: uuid.UUID | None,
|
||||
domain: str,
|
||||
) -> Task:
|
||||
"""
|
||||
Create and persist a COMPOSITE task for Chatwoot 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 Chatwoot
|
||||
|
||||
Returns:
|
||||
The created Task object with type="COMPOSITE"
|
||||
"""
|
||||
steps = build_chatwoot_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_chatwoot_initial_setup_step(
|
||||
*,
|
||||
base_url: str,
|
||||
admin_name: str,
|
||||
company_name: str,
|
||||
admin_email: str,
|
||||
admin_password: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Build a PLAYWRIGHT task payload for Chatwoot initial setup.
|
||||
|
||||
This creates the super admin account on a fresh Chatwoot installation.
|
||||
|
||||
Args:
|
||||
base_url: The base URL for Chatwoot (e.g., "https://chatwoot.example.com")
|
||||
admin_name: Full name for the admin account
|
||||
company_name: Company/organization name
|
||||
admin_email: Email address for the admin account
|
||||
admin_password: Password for admin (auto-generated if None)
|
||||
|
||||
Returns:
|
||||
Task payload dict with type="PLAYWRIGHT"
|
||||
"""
|
||||
from urllib.parse import urlparse
|
||||
|
||||
# Extract domain from URL for allowlist
|
||||
parsed = urlparse(base_url)
|
||||
allowed_domain = parsed.netloc
|
||||
|
||||
inputs: dict[str, Any] = {
|
||||
"base_url": base_url,
|
||||
"admin_name": admin_name,
|
||||
"company_name": company_name,
|
||||
"admin_email": admin_email,
|
||||
}
|
||||
|
||||
# Only include password if provided
|
||||
if admin_password:
|
||||
inputs["admin_password"] = admin_password
|
||||
|
||||
return {
|
||||
"scenario": "chatwoot_initial_setup",
|
||||
"inputs": inputs,
|
||||
"options": {
|
||||
"allowed_domains": [allowed_domain],
|
||||
},
|
||||
"timeout": 120,
|
||||
}
|
||||
|
||||
|
||||
async def create_chatwoot_initial_setup_task(
|
||||
*,
|
||||
db: AsyncSession,
|
||||
tenant_id: uuid.UUID,
|
||||
agent_id: uuid.UUID,
|
||||
base_url: str,
|
||||
admin_name: str,
|
||||
company_name: str,
|
||||
admin_email: str,
|
||||
admin_password: str | None = None,
|
||||
) -> Task:
|
||||
"""
|
||||
Create and persist a PLAYWRIGHT task for Chatwoot 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 Chatwoot
|
||||
admin_name: Full name for the admin account
|
||||
company_name: Company/organization name
|
||||
admin_email: Email address for the admin account
|
||||
admin_password: Password for admin (auto-generated if None)
|
||||
|
||||
Returns:
|
||||
The created Task object with type="PLAYWRIGHT"
|
||||
"""
|
||||
payload = build_chatwoot_initial_setup_step(
|
||||
base_url=base_url,
|
||||
admin_name=admin_name,
|
||||
company_name=company_name,
|
||||
admin_email=admin_email,
|
||||
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
|
||||
214
letsbe-orchestrator/app/playbooks/keycloak.py
Normal file
214
letsbe-orchestrator/app/playbooks/keycloak.py
Normal file
@@ -0,0 +1,214 @@
|
||||
"""Keycloak SSO deployment playbook.
|
||||
|
||||
Defines the steps required to:
|
||||
1. Set up Keycloak on a tenant server (ENV_UPDATE + DOCKER_RELOAD)
|
||||
2. Perform initial setup via Playwright automation (create admin, configure realm)
|
||||
|
||||
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
|
||||
KEYCLOAK_ENV_PATH = "/opt/letsbe/env/keycloak.env"
|
||||
KEYCLOAK_STACK_DIR = "/opt/letsbe/stacks/keycloak"
|
||||
|
||||
|
||||
def build_keycloak_setup_steps(
|
||||
*,
|
||||
domain: str,
|
||||
admin_user: str = "admin",
|
||||
admin_password: str,
|
||||
) -> list[CompositeStep]:
|
||||
"""
|
||||
Build the sequence of steps required to set up Keycloak.
|
||||
|
||||
Assumes the env file already exists at /opt/letsbe/env/keycloak.env
|
||||
(created by provisioning/env_setup.sh).
|
||||
|
||||
Args:
|
||||
domain: The domain for Keycloak (e.g., "auth.example.com")
|
||||
admin_user: Admin username (default: "admin")
|
||||
admin_password: Admin password
|
||||
|
||||
Returns:
|
||||
List of 2 CompositeStep objects:
|
||||
1. ENV_UPDATE - patches KC_HOSTNAME, KEYCLOAK_ADMIN, KEYCLOAK_ADMIN_PASSWORD
|
||||
2. DOCKER_RELOAD - restarts the keycloak stack with pull=True
|
||||
"""
|
||||
steps = [
|
||||
# Step 1: Update environment variables
|
||||
CompositeStep(
|
||||
type="ENV_UPDATE",
|
||||
payload={
|
||||
"path": KEYCLOAK_ENV_PATH,
|
||||
"updates": {
|
||||
"KC_HOSTNAME": domain,
|
||||
"KEYCLOAK_ADMIN": admin_user,
|
||||
"KEYCLOAK_ADMIN_PASSWORD": admin_password,
|
||||
},
|
||||
},
|
||||
),
|
||||
# Step 2: Reload Docker stack
|
||||
CompositeStep(
|
||||
type="DOCKER_RELOAD",
|
||||
payload={
|
||||
"compose_dir": KEYCLOAK_STACK_DIR,
|
||||
"pull": True,
|
||||
},
|
||||
),
|
||||
]
|
||||
return steps
|
||||
|
||||
|
||||
async def create_keycloak_setup_task(
|
||||
*,
|
||||
db: AsyncSession,
|
||||
tenant_id: uuid.UUID,
|
||||
agent_id: uuid.UUID | None,
|
||||
domain: str,
|
||||
admin_user: str = "admin",
|
||||
admin_password: str,
|
||||
) -> Task:
|
||||
"""
|
||||
Create and persist a COMPOSITE task for Keycloak 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 Keycloak
|
||||
admin_user: Admin username
|
||||
admin_password: Admin password
|
||||
|
||||
Returns:
|
||||
The created Task object with type="COMPOSITE"
|
||||
"""
|
||||
steps = build_keycloak_setup_steps(
|
||||
domain=domain,
|
||||
admin_user=admin_user,
|
||||
admin_password=admin_password,
|
||||
)
|
||||
|
||||
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_keycloak_initial_setup_step(
|
||||
*,
|
||||
base_url: str,
|
||||
admin_user: str,
|
||||
admin_password: str,
|
||||
realm_name: str = "letsbe",
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Build a PLAYWRIGHT task payload for Keycloak initial setup.
|
||||
|
||||
This creates the admin account and configures the "letsbe" realm
|
||||
on a fresh Keycloak installation.
|
||||
|
||||
Args:
|
||||
base_url: The base URL for Keycloak (e.g., "https://auth.example.com")
|
||||
admin_user: Username for the admin account
|
||||
admin_password: Password for the admin account
|
||||
realm_name: Name of the realm to create (default: "letsbe")
|
||||
|
||||
Returns:
|
||||
Task payload dict with type="PLAYWRIGHT"
|
||||
"""
|
||||
parsed = urlparse(base_url)
|
||||
allowed_domain = parsed.netloc
|
||||
|
||||
return {
|
||||
"scenario": "keycloak_initial_setup",
|
||||
"inputs": {
|
||||
"base_url": base_url,
|
||||
"admin_user": admin_user,
|
||||
"admin_password": admin_password,
|
||||
"realm_name": realm_name,
|
||||
},
|
||||
"options": {
|
||||
"allowed_domains": [allowed_domain],
|
||||
},
|
||||
"timeout": 120,
|
||||
}
|
||||
|
||||
|
||||
async def create_keycloak_initial_setup_task(
|
||||
*,
|
||||
db: AsyncSession,
|
||||
tenant_id: uuid.UUID,
|
||||
agent_id: uuid.UUID,
|
||||
base_url: str,
|
||||
admin_user: str,
|
||||
admin_password: str,
|
||||
realm_name: str = "letsbe",
|
||||
) -> Task:
|
||||
"""
|
||||
Create and persist a PLAYWRIGHT task for Keycloak 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 Keycloak
|
||||
admin_user: Username for the admin account
|
||||
admin_password: Password for the admin account
|
||||
realm_name: Name of the realm to create
|
||||
|
||||
Returns:
|
||||
The created Task object with type="PLAYWRIGHT"
|
||||
"""
|
||||
payload = build_keycloak_initial_setup_step(
|
||||
base_url=base_url,
|
||||
admin_user=admin_user,
|
||||
admin_password=admin_password,
|
||||
realm_name=realm_name,
|
||||
)
|
||||
|
||||
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
|
||||
208
letsbe-orchestrator/app/playbooks/n8n.py
Normal file
208
letsbe-orchestrator/app/playbooks/n8n.py
Normal file
@@ -0,0 +1,208 @@
|
||||
"""n8n workflow automation deployment playbook.
|
||||
|
||||
Defines the steps required to:
|
||||
1. Set up n8n on a tenant server (ENV_UPDATE + DOCKER_RELOAD)
|
||||
2. Perform initial setup via Playwright automation (create owner 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
|
||||
N8N_ENV_PATH = "/opt/letsbe/env/n8n.env"
|
||||
N8N_STACK_DIR = "/opt/letsbe/stacks/n8n"
|
||||
|
||||
|
||||
def build_n8n_setup_steps(*, domain: str) -> list[CompositeStep]:
|
||||
"""
|
||||
Build the sequence of steps required to set up n8n.
|
||||
|
||||
Assumes the env file already exists at /opt/letsbe/env/n8n.env
|
||||
(created by provisioning/env_setup.sh).
|
||||
|
||||
Args:
|
||||
domain: The domain for n8n (e.g., "n8n.example.com")
|
||||
|
||||
Returns:
|
||||
List of 2 CompositeStep objects:
|
||||
1. ENV_UPDATE - patches N8N_HOST, N8N_PROTOCOL, WEBHOOK_URL
|
||||
2. DOCKER_RELOAD - restarts the n8n stack with pull=True
|
||||
"""
|
||||
steps = [
|
||||
# Step 1: Update environment variables
|
||||
CompositeStep(
|
||||
type="ENV_UPDATE",
|
||||
payload={
|
||||
"path": N8N_ENV_PATH,
|
||||
"updates": {
|
||||
"N8N_HOST": domain,
|
||||
"N8N_PROTOCOL": "https",
|
||||
"WEBHOOK_URL": f"https://{domain}/",
|
||||
},
|
||||
},
|
||||
),
|
||||
# Step 2: Reload Docker stack
|
||||
CompositeStep(
|
||||
type="DOCKER_RELOAD",
|
||||
payload={
|
||||
"compose_dir": N8N_STACK_DIR,
|
||||
"pull": True,
|
||||
},
|
||||
),
|
||||
]
|
||||
return steps
|
||||
|
||||
|
||||
async def create_n8n_setup_task(
|
||||
*,
|
||||
db: AsyncSession,
|
||||
tenant_id: uuid.UUID,
|
||||
agent_id: uuid.UUID | None,
|
||||
domain: str,
|
||||
) -> Task:
|
||||
"""
|
||||
Create and persist a COMPOSITE task for n8n 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 n8n
|
||||
|
||||
Returns:
|
||||
The created Task object with type="COMPOSITE"
|
||||
"""
|
||||
steps = build_n8n_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_n8n_initial_setup_step(
|
||||
*,
|
||||
base_url: str,
|
||||
admin_email: str,
|
||||
admin_password: str | None = None,
|
||||
admin_first_name: str = "Admin",
|
||||
admin_last_name: str = "User",
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Build a PLAYWRIGHT task payload for n8n initial setup.
|
||||
|
||||
This creates the owner account on a fresh n8n installation.
|
||||
|
||||
Args:
|
||||
base_url: The base URL for n8n (e.g., "https://n8n.example.com")
|
||||
admin_email: Email address for the owner account
|
||||
admin_password: Password for owner (auto-generated if None)
|
||||
admin_first_name: First name for the owner account
|
||||
admin_last_name: Last name for the owner account
|
||||
|
||||
Returns:
|
||||
Task payload dict with type="PLAYWRIGHT"
|
||||
"""
|
||||
parsed = urlparse(base_url)
|
||||
allowed_domain = parsed.netloc
|
||||
|
||||
inputs: dict[str, Any] = {
|
||||
"base_url": base_url,
|
||||
"admin_email": admin_email,
|
||||
"admin_first_name": admin_first_name,
|
||||
"admin_last_name": admin_last_name,
|
||||
}
|
||||
|
||||
if admin_password:
|
||||
inputs["admin_password"] = admin_password
|
||||
|
||||
return {
|
||||
"scenario": "n8n_initial_setup",
|
||||
"inputs": inputs,
|
||||
"options": {
|
||||
"allowed_domains": [allowed_domain],
|
||||
},
|
||||
"timeout": 120,
|
||||
}
|
||||
|
||||
|
||||
async def create_n8n_initial_setup_task(
|
||||
*,
|
||||
db: AsyncSession,
|
||||
tenant_id: uuid.UUID,
|
||||
agent_id: uuid.UUID,
|
||||
base_url: str,
|
||||
admin_email: str,
|
||||
admin_password: str | None = None,
|
||||
admin_first_name: str = "Admin",
|
||||
admin_last_name: str = "User",
|
||||
) -> Task:
|
||||
"""
|
||||
Create and persist a PLAYWRIGHT task for n8n 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 n8n
|
||||
admin_email: Email address for the owner account
|
||||
admin_password: Password for owner (auto-generated if None)
|
||||
admin_first_name: First name for the owner account
|
||||
admin_last_name: Last name for the owner account
|
||||
|
||||
Returns:
|
||||
The created Task object with type="PLAYWRIGHT"
|
||||
"""
|
||||
payload = build_n8n_initial_setup_step(
|
||||
base_url=base_url,
|
||||
admin_email=admin_email,
|
||||
admin_password=admin_password,
|
||||
admin_first_name=admin_first_name,
|
||||
admin_last_name=admin_last_name,
|
||||
)
|
||||
|
||||
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
|
||||
192
letsbe-orchestrator/app/playbooks/nextcloud.py
Normal file
192
letsbe-orchestrator/app/playbooks/nextcloud.py
Normal file
@@ -0,0 +1,192 @@
|
||||
"""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
|
||||
105
letsbe-orchestrator/app/playbooks/portainer.py
Normal file
105
letsbe-orchestrator/app/playbooks/portainer.py
Normal file
@@ -0,0 +1,105 @@
|
||||
"""Portainer container management deployment playbook.
|
||||
|
||||
Defines the steps required to set up Portainer on a tenant server
|
||||
(ENV_UPDATE + DOCKER_RELOAD). No Playwright setup needed - Portainer's
|
||||
admin account is created via its first-use web UI which is already
|
||||
handled during provisioning.
|
||||
|
||||
Tenant servers must have stacks and env templates under /opt/letsbe.
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from typing import Any
|
||||
|
||||
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
|
||||
PORTAINER_ENV_PATH = "/opt/letsbe/env/portainer.env"
|
||||
PORTAINER_STACK_DIR = "/opt/letsbe/stacks/portainer"
|
||||
|
||||
|
||||
def build_portainer_setup_steps(*, domain: str) -> list[CompositeStep]:
|
||||
"""
|
||||
Build the sequence of steps required to set up Portainer.
|
||||
|
||||
Assumes the env file already exists at /opt/letsbe/env/portainer.env
|
||||
(created by provisioning/env_setup.sh).
|
||||
|
||||
Args:
|
||||
domain: The domain for Portainer (e.g., "portainer.example.com")
|
||||
|
||||
Returns:
|
||||
List of 2 CompositeStep objects:
|
||||
1. ENV_UPDATE - patches PORTAINER_DOMAIN
|
||||
2. DOCKER_RELOAD - restarts the portainer stack with pull=True
|
||||
"""
|
||||
steps = [
|
||||
# Step 1: Update environment variables
|
||||
CompositeStep(
|
||||
type="ENV_UPDATE",
|
||||
payload={
|
||||
"path": PORTAINER_ENV_PATH,
|
||||
"updates": {
|
||||
"PORTAINER_DOMAIN": domain,
|
||||
},
|
||||
},
|
||||
),
|
||||
# Step 2: Reload Docker stack
|
||||
CompositeStep(
|
||||
type="DOCKER_RELOAD",
|
||||
payload={
|
||||
"compose_dir": PORTAINER_STACK_DIR,
|
||||
"pull": True,
|
||||
},
|
||||
),
|
||||
]
|
||||
return steps
|
||||
|
||||
|
||||
async def create_portainer_setup_task(
|
||||
*,
|
||||
db: AsyncSession,
|
||||
tenant_id: uuid.UUID,
|
||||
agent_id: uuid.UUID | None,
|
||||
domain: str,
|
||||
) -> Task:
|
||||
"""
|
||||
Create and persist a COMPOSITE task for Portainer 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 Portainer
|
||||
|
||||
Returns:
|
||||
The created Task object with type="COMPOSITE"
|
||||
"""
|
||||
steps = build_portainer_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
|
||||
111
letsbe-orchestrator/app/playbooks/poste.py
Normal file
111
letsbe-orchestrator/app/playbooks/poste.py
Normal file
@@ -0,0 +1,111 @@
|
||||
"""Poste.io mail server deployment playbook.
|
||||
|
||||
Defines the steps required to:
|
||||
1. Perform initial setup via Playwright automation (configure hostname, 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 sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.task import Task, TaskStatus
|
||||
|
||||
|
||||
# LetsBe standard paths
|
||||
POSTE_STACK_DIR = "/opt/letsbe/stacks/poste"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Initial Setup via Playwright
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def build_poste_initial_setup_step(
|
||||
*,
|
||||
base_url: str,
|
||||
admin_email: str,
|
||||
admin_password: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Build a PLAYWRIGHT task payload for Poste.io initial setup.
|
||||
|
||||
This configures the mail server hostname and creates the admin account
|
||||
on a fresh Poste.io installation.
|
||||
|
||||
Args:
|
||||
base_url: The base URL for Poste.io (e.g., "https://mail.example.com")
|
||||
admin_email: Email address for the admin account (e.g., admin@example.com)
|
||||
admin_password: Password for the admin account (auto-generated if None)
|
||||
|
||||
Returns:
|
||||
Task payload dict with type="PLAYWRIGHT"
|
||||
"""
|
||||
# Extract domain from URL for allowlist
|
||||
parsed = urlparse(base_url)
|
||||
allowed_domain = parsed.netloc # e.g., "mail.example.com"
|
||||
|
||||
inputs: dict[str, Any] = {
|
||||
"base_url": base_url,
|
||||
"admin_email": admin_email,
|
||||
}
|
||||
|
||||
# Only include password if provided - scenario will auto-generate if missing
|
||||
if admin_password:
|
||||
inputs["admin_password"] = admin_password
|
||||
|
||||
return {
|
||||
"scenario": "poste_initial_setup",
|
||||
"inputs": inputs,
|
||||
"options": {
|
||||
"allowed_domains": [allowed_domain],
|
||||
},
|
||||
"timeout": 120,
|
||||
}
|
||||
|
||||
|
||||
async def create_poste_initial_setup_task(
|
||||
*,
|
||||
db: AsyncSession,
|
||||
tenant_id: uuid.UUID,
|
||||
agent_id: uuid.UUID,
|
||||
base_url: str,
|
||||
admin_email: str,
|
||||
admin_password: str | None = None,
|
||||
) -> Task:
|
||||
"""
|
||||
Create and persist a PLAYWRIGHT task for Poste.io 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 Poste.io
|
||||
admin_email: Email address for the admin account
|
||||
admin_password: Password for admin (auto-generated if None)
|
||||
|
||||
Returns:
|
||||
The created Task object with type="PLAYWRIGHT"
|
||||
"""
|
||||
payload = build_poste_initial_setup_step(
|
||||
base_url=base_url,
|
||||
admin_email=admin_email,
|
||||
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
|
||||
205
letsbe-orchestrator/app/playbooks/umami.py
Normal file
205
letsbe-orchestrator/app/playbooks/umami.py
Normal file
@@ -0,0 +1,205 @@
|
||||
"""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
|
||||
194
letsbe-orchestrator/app/playbooks/uptime_kuma.py
Normal file
194
letsbe-orchestrator/app/playbooks/uptime_kuma.py
Normal file
@@ -0,0 +1,194 @@
|
||||
"""Uptime Kuma monitoring deployment playbook.
|
||||
|
||||
Defines the steps required to:
|
||||
1. Set up Uptime Kuma on a tenant server (ENV_UPDATE + DOCKER_RELOAD)
|
||||
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
|
||||
UPTIME_KUMA_ENV_PATH = "/opt/letsbe/env/uptime-kuma.env"
|
||||
UPTIME_KUMA_STACK_DIR = "/opt/letsbe/stacks/uptime-kuma"
|
||||
|
||||
|
||||
def build_uptime_kuma_setup_steps(*, domain: str) -> list[CompositeStep]:
|
||||
"""
|
||||
Build the sequence of steps required to set up Uptime Kuma.
|
||||
|
||||
Assumes the env file already exists at /opt/letsbe/env/uptime-kuma.env
|
||||
(created by provisioning/env_setup.sh).
|
||||
|
||||
Args:
|
||||
domain: The domain for Uptime Kuma (e.g., "status.example.com")
|
||||
|
||||
Returns:
|
||||
List of 2 CompositeStep objects:
|
||||
1. ENV_UPDATE - patches UPTIME_KUMA_DOMAIN
|
||||
2. DOCKER_RELOAD - restarts the uptime-kuma stack with pull=True
|
||||
"""
|
||||
steps = [
|
||||
# Step 1: Update environment variables
|
||||
CompositeStep(
|
||||
type="ENV_UPDATE",
|
||||
payload={
|
||||
"path": UPTIME_KUMA_ENV_PATH,
|
||||
"updates": {
|
||||
"UPTIME_KUMA_DOMAIN": domain,
|
||||
},
|
||||
},
|
||||
),
|
||||
# Step 2: Reload Docker stack
|
||||
CompositeStep(
|
||||
type="DOCKER_RELOAD",
|
||||
payload={
|
||||
"compose_dir": UPTIME_KUMA_STACK_DIR,
|
||||
"pull": True,
|
||||
},
|
||||
),
|
||||
]
|
||||
return steps
|
||||
|
||||
|
||||
async def create_uptime_kuma_setup_task(
|
||||
*,
|
||||
db: AsyncSession,
|
||||
tenant_id: uuid.UUID,
|
||||
agent_id: uuid.UUID | None,
|
||||
domain: str,
|
||||
) -> Task:
|
||||
"""
|
||||
Create and persist a COMPOSITE task for Uptime Kuma 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 Uptime Kuma
|
||||
|
||||
Returns:
|
||||
The created Task object with type="COMPOSITE"
|
||||
"""
|
||||
steps = build_uptime_kuma_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_uptime_kuma_initial_setup_step(
|
||||
*,
|
||||
base_url: str,
|
||||
admin_username: str = "admin",
|
||||
admin_password: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Build a PLAYWRIGHT task payload for Uptime Kuma initial setup.
|
||||
|
||||
This creates the admin account on a fresh Uptime Kuma installation.
|
||||
|
||||
Args:
|
||||
base_url: The base URL for Uptime Kuma (e.g., "https://status.example.com")
|
||||
admin_username: Username for the admin account
|
||||
admin_password: Password for admin (auto-generated if None)
|
||||
|
||||
Returns:
|
||||
Task payload dict with type="PLAYWRIGHT"
|
||||
"""
|
||||
parsed = urlparse(base_url)
|
||||
allowed_domain = parsed.netloc
|
||||
|
||||
inputs: dict[str, Any] = {
|
||||
"base_url": base_url,
|
||||
"admin_username": admin_username,
|
||||
}
|
||||
|
||||
if admin_password:
|
||||
inputs["admin_password"] = admin_password
|
||||
|
||||
return {
|
||||
"scenario": "uptime_kuma_initial_setup",
|
||||
"inputs": inputs,
|
||||
"options": {
|
||||
"allowed_domains": [allowed_domain],
|
||||
},
|
||||
"timeout": 120,
|
||||
}
|
||||
|
||||
|
||||
async def create_uptime_kuma_initial_setup_task(
|
||||
*,
|
||||
db: AsyncSession,
|
||||
tenant_id: uuid.UUID,
|
||||
agent_id: uuid.UUID,
|
||||
base_url: str,
|
||||
admin_username: str = "admin",
|
||||
admin_password: str | None = None,
|
||||
) -> Task:
|
||||
"""
|
||||
Create and persist a PLAYWRIGHT task for Uptime Kuma 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 Uptime Kuma
|
||||
admin_username: Username for the admin account
|
||||
admin_password: Password for admin (auto-generated if None)
|
||||
|
||||
Returns:
|
||||
The created Task object with type="PLAYWRIGHT"
|
||||
"""
|
||||
payload = build_uptime_kuma_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
|
||||
121
letsbe-orchestrator/app/playbooks/vaultwarden.py
Normal file
121
letsbe-orchestrator/app/playbooks/vaultwarden.py
Normal file
@@ -0,0 +1,121 @@
|
||||
"""Vaultwarden password manager deployment playbook.
|
||||
|
||||
Defines the steps required to set up Vaultwarden on a tenant server
|
||||
(ENV_UPDATE + DOCKER_RELOAD). No Playwright setup needed - Vaultwarden
|
||||
uses a web-based registration flow that doesn't require automation.
|
||||
|
||||
Tenant servers must have stacks and env templates under /opt/letsbe.
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from typing import Any
|
||||
|
||||
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
|
||||
VAULTWARDEN_ENV_PATH = "/opt/letsbe/env/vaultwarden.env"
|
||||
VAULTWARDEN_STACK_DIR = "/opt/letsbe/stacks/vaultwarden"
|
||||
|
||||
|
||||
def build_vaultwarden_setup_steps(
|
||||
*,
|
||||
domain: str,
|
||||
admin_token: str,
|
||||
signups_allowed: bool = True,
|
||||
) -> list[CompositeStep]:
|
||||
"""
|
||||
Build the sequence of steps required to set up Vaultwarden.
|
||||
|
||||
Assumes the env file already exists at /opt/letsbe/env/vaultwarden.env
|
||||
(created by provisioning/env_setup.sh).
|
||||
|
||||
Args:
|
||||
domain: The domain for Vaultwarden (e.g., "vault.example.com")
|
||||
admin_token: Admin panel access token
|
||||
signups_allowed: Whether new user registration is allowed
|
||||
|
||||
Returns:
|
||||
List of 2 CompositeStep objects:
|
||||
1. ENV_UPDATE - patches DOMAIN, ADMIN_TOKEN, SIGNUPS_ALLOWED
|
||||
2. DOCKER_RELOAD - restarts the vaultwarden stack with pull=True
|
||||
"""
|
||||
steps = [
|
||||
# Step 1: Update environment variables
|
||||
CompositeStep(
|
||||
type="ENV_UPDATE",
|
||||
payload={
|
||||
"path": VAULTWARDEN_ENV_PATH,
|
||||
"updates": {
|
||||
"DOMAIN": f"https://{domain}",
|
||||
"ADMIN_TOKEN": admin_token,
|
||||
"SIGNUPS_ALLOWED": str(signups_allowed).lower(),
|
||||
},
|
||||
},
|
||||
),
|
||||
# Step 2: Reload Docker stack
|
||||
CompositeStep(
|
||||
type="DOCKER_RELOAD",
|
||||
payload={
|
||||
"compose_dir": VAULTWARDEN_STACK_DIR,
|
||||
"pull": True,
|
||||
},
|
||||
),
|
||||
]
|
||||
return steps
|
||||
|
||||
|
||||
async def create_vaultwarden_setup_task(
|
||||
*,
|
||||
db: AsyncSession,
|
||||
tenant_id: uuid.UUID,
|
||||
agent_id: uuid.UUID | None,
|
||||
domain: str,
|
||||
admin_token: str,
|
||||
signups_allowed: bool = True,
|
||||
) -> Task:
|
||||
"""
|
||||
Create and persist a COMPOSITE task for Vaultwarden 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 Vaultwarden
|
||||
admin_token: Admin panel access token
|
||||
signups_allowed: Whether new user registration is allowed
|
||||
|
||||
Returns:
|
||||
The created Task object with type="COMPOSITE"
|
||||
"""
|
||||
steps = build_vaultwarden_setup_steps(
|
||||
domain=domain,
|
||||
admin_token=admin_token,
|
||||
signups_allowed=signups_allowed,
|
||||
)
|
||||
|
||||
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
|
||||
Reference in New Issue
Block a user