Files
adaptive-multi-protocol-gat…/ampg/install_plan.py
T

269 lines
7.7 KiB
Python

from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
from .config import GatewayConfig, ProtocolConfig, SiteConfig
from .platforms import PlatformProvider, detect_platform
from .status import DaemonProbeFunc, TransportStatus, gateway_status
from .transports import fallback_transport_adapter, transport_adapter
@dataclass(frozen=True)
class InstallStep:
site_id: str
protocol: str
platform: str
stage: str
action: str
target: str
status: str
command: str
message: str
def install_plan(
config: GatewayConfig,
*,
platform_provider: PlatformProvider | None = None,
daemon_probe: DaemonProbeFunc | None = None,
) -> list[InstallStep]:
provider = platform_provider or detect_platform()
statuses = gateway_status(
config,
platform_provider=provider,
daemon_probe=daemon_probe,
)
status_by_protocol = {
(status.site_id, status.protocol): status for status in statuses
}
steps: list[InstallStep] = []
for site in config.sites:
for protocol in site.protocols.values():
if not protocol.enabled:
continue
status = status_by_protocol[(site.id, protocol.name)]
steps.extend(_protocol_install_steps(site, protocol, provider, status))
return steps
def blocked_install_steps(steps: list[InstallStep]) -> list[InstallStep]:
return [step for step in steps if step.status == "blocked"]
def _protocol_install_steps(
site: SiteConfig,
protocol: ProtocolConfig,
provider: PlatformProvider,
status: TransportStatus,
) -> list[InstallStep]:
if status.action == "adopt-existing":
return [
_step(
site,
protocol,
provider,
stage="daemon",
action="adopt-existing",
target=protocol.daemon,
status="skipped",
command="-",
message="existing daemon is running; no AMPG-owned install is planned",
)
]
if status.action == "render-only":
return [
_step(
site,
protocol,
provider,
stage="daemon",
action="render-only",
target=protocol.daemon,
status="skipped",
command="-",
message="external policy renders outputs and review artifacts only",
)
]
if status.action != "manage-owned" or status.status == "error":
return [
_step(
site,
protocol,
provider,
stage="daemon",
action=status.action,
target=protocol.daemon,
status="blocked",
command="-",
message=status.message,
)
]
adapter = transport_adapter(protocol.name, protocol.daemon)
adapter = adapter or fallback_transport_adapter(protocol.name, protocol.daemon)
package_steps = [
_package_step(site, protocol, provider, protocol.daemon, installed=status.installed)
]
package_steps.extend(
_package_step(site, protocol, provider, support_daemon, installed=False)
for support_daemon in _support_daemons(protocol)
)
return [
*package_steps,
_step(
site,
protocol,
provider,
stage="state",
action="create-state",
target=str(_state_dir(provider, site, protocol)),
status="planned",
command=_mkdir_command(provider, _state_dir(provider, site, protocol)),
message="create AMPG-owned state directory for daemon config, keys, and logs",
),
_step(
site,
protocol,
provider,
stage="config",
action="write-managed-config",
target=str(_state_dir(provider, site, protocol) / "config"),
status="planned",
command="-",
message=f"write managed {adapter.backend} config after plan review",
),
_step(
site,
protocol,
provider,
stage="supervisor",
action="configure-supervisor",
target=_service_name(site, protocol),
status="planned",
command=_supervisor_command(provider, site, protocol),
message=f"register AMPG-owned service with {provider.process_supervisor}",
),
_step(
site,
protocol,
provider,
stage="health",
action="check-transport",
target=protocol.name,
status="planned",
command="-",
message="verify generated site through the managed transport after start",
),
]
def _package_step(
site: SiteConfig,
protocol: ProtocolConfig,
provider: PlatformProvider,
daemon: str,
*,
installed: bool,
) -> InstallStep:
package = _package_name(provider, daemon)
return _step(
site,
protocol,
provider,
stage="package",
action="ensure-package",
target=package,
status="ready" if installed else "planned",
command="-" if installed else _package_command(provider, package),
message=(
f"{daemon} executable is already present"
if installed
else f"install package providing {daemon}"
),
)
def _support_daemons(protocol: ProtocolConfig) -> tuple[str, ...]:
if protocol.name in {"tor", "i2p"}:
return ("nginx",)
return ()
def _package_name(provider: PlatformProvider, daemon: str) -> str:
if provider.name == "macos-launchd" and daemon == "ipfs":
return "kubo"
return daemon
def _package_command(provider: PlatformProvider, package: str) -> str:
if provider.name == "android-termux":
return f"pkg install {package}"
if provider.name == "linux-systemd":
return f"sudo apt install {package}"
if provider.name == "macos-launchd":
return f"brew install {package}"
if provider.name == "linux-user":
return f"install {package} with the user package manager"
return "-"
def _mkdir_command(provider: PlatformProvider, path: Path) -> str:
prefix = "sudo " if provider.can_write_system_config else ""
return f"{prefix}mkdir -p {path}"
def _supervisor_command(
provider: PlatformProvider,
site: SiteConfig,
protocol: ProtocolConfig,
) -> str:
service = _service_name(site, protocol)
if provider.name == "android-termux":
return f"sv-enable {service}"
if provider.name == "linux-systemd":
return f"sudo systemctl enable --now {service}.service"
if provider.name == "macos-launchd":
return f"launchctl bootstrap gui/$UID ~/Library/LaunchAgents/org.ampg.{service}.plist"
if provider.name == "linux-user":
return f"run {service} under the user supervisor"
return "-"
def _state_dir(
provider: PlatformProvider,
site: SiteConfig,
protocol: ProtocolConfig,
) -> Path:
return provider.state_root / site.id / protocol.name
def _service_name(site: SiteConfig, protocol: ProtocolConfig) -> str:
return f"ampg-{site.id}-{protocol.name}"
def _step(
site: SiteConfig,
protocol: ProtocolConfig,
provider: PlatformProvider,
*,
stage: str,
action: str,
target: str,
status: str,
command: str,
message: str,
) -> InstallStep:
return InstallStep(
site_id=site.id,
protocol=protocol.name,
platform=provider.name,
stage=stage,
action=action,
target=target,
status=status,
command=command,
message=message,
)