mirror of
https://github.com/Such-Software/adaptive-multi-protocol-gateway.git
synced 2026-10-17 07:31:32 +02:00
269 lines
7.7 KiB
Python
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,
|
|
)
|