mirror of
https://github.com/Such-Software/adaptive-multi-protocol-gateway.git
synced 2026-10-17 07:31:32 +02:00
Add managed daemon install plans
This commit is contained in:
+45
@@ -14,6 +14,7 @@ from .audit import audit_gateway
|
||||
from .build import build_gateway
|
||||
from .config import load_config
|
||||
from .docsgen import generate_docs
|
||||
from .install_plan import InstallStep, blocked_install_steps, install_plan
|
||||
from .manifest import write_fixture_manifests
|
||||
from .plan import plan_gateway, write_plan_artifacts
|
||||
from .platforms import PLATFORM_NAMES, platform_by_name
|
||||
@@ -84,6 +85,16 @@ def main(argv: list[str] | None = None) -> int:
|
||||
choices=PLATFORM_NAMES,
|
||||
help="Override platform detection for dry-run checks.",
|
||||
)
|
||||
install_plan_parser = subcommands.add_parser(
|
||||
"install-plan",
|
||||
help="Print managed daemon package, state, and supervisor steps.",
|
||||
)
|
||||
_add_target_selection(install_plan_parser)
|
||||
install_plan_parser.add_argument(
|
||||
"--platform",
|
||||
choices=PLATFORM_NAMES,
|
||||
help="Override platform detection for install planning.",
|
||||
)
|
||||
build_parser = subcommands.add_parser("build", help="Build enabled protocol outputs.")
|
||||
_add_target_selection(build_parser)
|
||||
fixture_parser = subcommands.add_parser("manifest", help="Write AMPB fixture manifests.")
|
||||
@@ -172,6 +183,8 @@ def main(argv: list[str] | None = None) -> int:
|
||||
return _cmd_doctor(config, args)
|
||||
if args.command == "apply":
|
||||
return _cmd_apply(config, args)
|
||||
if args.command == "install-plan":
|
||||
return _cmd_install_plan(config, args)
|
||||
if args.command == "build":
|
||||
return _cmd_build(config)
|
||||
if args.command == "manifest":
|
||||
@@ -218,6 +231,23 @@ def _cmd_apply(config, args) -> int:
|
||||
return 1 if blocked else 0
|
||||
|
||||
|
||||
def _cmd_install_plan(config, args) -> int:
|
||||
steps = install_plan(config, platform_provider=_platform_override(config, args))
|
||||
for step in steps:
|
||||
_print_install_step(step)
|
||||
blocked = blocked_install_steps(steps)
|
||||
print(
|
||||
"AMPG_INSTALL_SUMMARY "
|
||||
f"sites={len(config.sites)} "
|
||||
f"steps={len(steps)} "
|
||||
f"planned={sum(1 for step in steps if step.status == 'planned')} "
|
||||
f"ready={sum(1 for step in steps if step.status == 'ready')} "
|
||||
f"skipped={sum(1 for step in steps if step.status == 'skipped')} "
|
||||
f"blocked={len(blocked)}"
|
||||
)
|
||||
return 1 if blocked else 0
|
||||
|
||||
|
||||
def _add_target_selection(parser) -> None:
|
||||
parser.add_argument(
|
||||
"--profile",
|
||||
@@ -334,6 +364,21 @@ def _cmd_build(config) -> int:
|
||||
return 0
|
||||
|
||||
|
||||
def _print_install_step(step: InstallStep) -> None:
|
||||
print(
|
||||
"AMPG_INSTALL_STEP "
|
||||
f"site={step.site_id} "
|
||||
f"protocol={step.protocol} "
|
||||
f"platform={step.platform} "
|
||||
f"stage={step.stage} "
|
||||
f"action={step.action} "
|
||||
f"target=\"{_quote(step.target)}\" "
|
||||
f"status={step.status} "
|
||||
f"command=\"{_quote(step.command)}\" "
|
||||
f"message=\"{_quote(step.message)}\""
|
||||
)
|
||||
|
||||
|
||||
def _print_activation_step(step: ActivationStep) -> None:
|
||||
print(
|
||||
"AMPG_APPLY_STEP "
|
||||
|
||||
@@ -0,0 +1,268 @@
|
||||
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,
|
||||
)
|
||||
Reference in New Issue
Block a user