mirror of
https://github.com/trailofbits/algo.git
synced 2026-09-22 17:45:00 +02:00
Implements issue #196 - a local web application that allows users to deploy Algo VPN servers through a browser-based GUI. Features: - Single-step launch: `./algo-web` opens browser automatically - Cloud provider support: DigitalOcean, Hetzner, Linode, Vultr (MVP) - Real-time credential validation via provider APIs - Dynamic region loading after validation - Live deployment progress streaming via SSE - Terminal-style output with color-coded log lines - Config file downloads (individual + ZIP archive) Technical stack: - Starlette + uvicorn (ASGI web framework) - htmx for frontend interactivity (no build step) - Server-Sent Events for streaming Ansible output - Credentials passed via --extra-vars (never persisted) Design: - "Luxury Terminal" dark theme with electric teal accents - Algo VPN and Trail of Bits branding - Responsive layout (desktop, tablet, mobile) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
110 lines
3.1 KiB
Python
110 lines
3.1 KiB
Python
"""In-memory session management for deployments."""
|
|
|
|
import asyncio
|
|
import uuid
|
|
from dataclasses import dataclass, field
|
|
from datetime import datetime, timedelta
|
|
from enum import Enum
|
|
from typing import Any
|
|
|
|
|
|
class DeploymentStatus(Enum):
|
|
"""Status of a deployment session."""
|
|
|
|
PENDING = "pending"
|
|
VALIDATING = "validating"
|
|
RUNNING = "running"
|
|
SUCCESS = "success"
|
|
FAILED = "failed"
|
|
CANCELLED = "cancelled"
|
|
|
|
|
|
@dataclass
|
|
class Session:
|
|
"""A deployment session."""
|
|
|
|
id: str
|
|
provider: str
|
|
credentials: dict[str, Any]
|
|
config: dict[str, Any]
|
|
status: DeploymentStatus = DeploymentStatus.PENDING
|
|
output_lines: list[str] = field(default_factory=list)
|
|
config_path: str | None = None
|
|
error: str | None = None
|
|
exit_code: int | None = None
|
|
created_at: datetime = field(default_factory=datetime.now)
|
|
process: asyncio.subprocess.Process | None = None
|
|
|
|
def is_expired(self) -> bool:
|
|
"""Check if this session has expired (1 hour TTL)."""
|
|
return datetime.now() - self.created_at > timedelta(hours=1)
|
|
|
|
def add_output(self, line: str) -> None:
|
|
"""Add a line to the output buffer."""
|
|
self.output_lines.append(line)
|
|
|
|
def clear_credentials(self) -> None:
|
|
"""Clear credentials from memory after deployment starts."""
|
|
self.credentials = {}
|
|
|
|
|
|
class SessionStore:
|
|
"""Thread-safe in-memory session store."""
|
|
|
|
def __init__(self) -> None:
|
|
self._sessions: dict[str, Session] = {}
|
|
self._lock = asyncio.Lock()
|
|
|
|
async def create(
|
|
self,
|
|
provider: str,
|
|
credentials: dict[str, Any],
|
|
config: dict[str, Any],
|
|
) -> Session:
|
|
"""Create a new session."""
|
|
session_id = str(uuid.uuid4())
|
|
session = Session(
|
|
id=session_id,
|
|
provider=provider,
|
|
credentials=credentials,
|
|
config=config,
|
|
)
|
|
|
|
async with self._lock:
|
|
self._sessions[session_id] = session
|
|
# Cleanup expired sessions
|
|
await self._cleanup_expired()
|
|
|
|
return session
|
|
|
|
async def get(self, session_id: str) -> Session | None:
|
|
"""Get a session by ID."""
|
|
async with self._lock:
|
|
session = self._sessions.get(session_id)
|
|
if session and session.is_expired():
|
|
del self._sessions[session_id]
|
|
return None
|
|
return session
|
|
|
|
async def update(self, session: Session) -> None:
|
|
"""Update a session."""
|
|
async with self._lock:
|
|
if session.id in self._sessions:
|
|
self._sessions[session.id] = session
|
|
|
|
async def delete(self, session_id: str) -> None:
|
|
"""Delete a session."""
|
|
async with self._lock:
|
|
if session_id in self._sessions:
|
|
del self._sessions[session_id]
|
|
|
|
async def _cleanup_expired(self) -> None:
|
|
"""Remove expired sessions (called during create)."""
|
|
expired = [sid for sid, session in self._sessions.items() if session.is_expired()]
|
|
for sid in expired:
|
|
del self._sessions[sid]
|
|
|
|
|
|
# Global session store instance
|
|
sessions = SessionStore()
|