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:
@@ -104,6 +104,7 @@ python3 -m ampg --config examples/wownero.gateway.toml plan
|
|||||||
python3 -m ampg --config examples/wownero.gateway.toml plan --write-artifacts
|
python3 -m ampg --config examples/wownero.gateway.toml plan --write-artifacts
|
||||||
python3 -m ampg --config examples/wownero.gateway.toml status
|
python3 -m ampg --config examples/wownero.gateway.toml status
|
||||||
python3 -m ampg --config examples/wownero.gateway.toml doctor
|
python3 -m ampg --config examples/wownero.gateway.toml doctor
|
||||||
|
python3 -m ampg --config examples/wownero.gateway.toml install-plan --profile mobile-i2p
|
||||||
python3 -m ampg --config examples/wownero.gateway.toml apply --dry-run
|
python3 -m ampg --config examples/wownero.gateway.toml apply --dry-run
|
||||||
python3 -m ampg --config examples/wownero.gateway.toml apply --dry-run --profile mobile-i2p
|
python3 -m ampg --config examples/wownero.gateway.toml apply --dry-run --profile mobile-i2p
|
||||||
python3 -m ampg --config examples/wownero.gateway.toml apply --dry-run --protocol i2p
|
python3 -m ampg --config examples/wownero.gateway.toml apply --dry-run --protocol i2p
|
||||||
@@ -145,6 +146,7 @@ Explicit `--protocol` and `--platform` flags override the profile when present.
|
|||||||
- [ ] Select protocols and daemon policy.
|
- [ ] Select protocols and daemon policy.
|
||||||
- [ ] Run `ampg plan`; review output roots, ports, and config changes.
|
- [ ] Run `ampg plan`; review output roots, ports, and config changes.
|
||||||
- [ ] Run `ampg status` or `ampg doctor`; review daemon ownership decisions.
|
- [ ] Run `ampg status` or `ampg doctor`; review daemon ownership decisions.
|
||||||
|
- [ ] Run `ampg install-plan`; review managed daemon package, state, and supervisor steps.
|
||||||
- [ ] Run `ampg build`; inspect generated variants.
|
- [ ] Run `ampg build`; inspect generated variants.
|
||||||
- [ ] Run AMPB fixture checks against generated manifests.
|
- [ ] Run AMPB fixture checks against generated manifests.
|
||||||
- [ ] Run `ampg apply --dry-run`; review the activation sequence.
|
- [ ] Run `ampg apply --dry-run`; review the activation sequence.
|
||||||
|
|||||||
+45
@@ -14,6 +14,7 @@ from .audit import audit_gateway
|
|||||||
from .build import build_gateway
|
from .build import build_gateway
|
||||||
from .config import load_config
|
from .config import load_config
|
||||||
from .docsgen import generate_docs
|
from .docsgen import generate_docs
|
||||||
|
from .install_plan import InstallStep, blocked_install_steps, install_plan
|
||||||
from .manifest import write_fixture_manifests
|
from .manifest import write_fixture_manifests
|
||||||
from .plan import plan_gateway, write_plan_artifacts
|
from .plan import plan_gateway, write_plan_artifacts
|
||||||
from .platforms import PLATFORM_NAMES, platform_by_name
|
from .platforms import PLATFORM_NAMES, platform_by_name
|
||||||
@@ -84,6 +85,16 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
choices=PLATFORM_NAMES,
|
choices=PLATFORM_NAMES,
|
||||||
help="Override platform detection for dry-run checks.",
|
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.")
|
build_parser = subcommands.add_parser("build", help="Build enabled protocol outputs.")
|
||||||
_add_target_selection(build_parser)
|
_add_target_selection(build_parser)
|
||||||
fixture_parser = subcommands.add_parser("manifest", help="Write AMPB fixture manifests.")
|
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)
|
return _cmd_doctor(config, args)
|
||||||
if args.command == "apply":
|
if args.command == "apply":
|
||||||
return _cmd_apply(config, args)
|
return _cmd_apply(config, args)
|
||||||
|
if args.command == "install-plan":
|
||||||
|
return _cmd_install_plan(config, args)
|
||||||
if args.command == "build":
|
if args.command == "build":
|
||||||
return _cmd_build(config)
|
return _cmd_build(config)
|
||||||
if args.command == "manifest":
|
if args.command == "manifest":
|
||||||
@@ -218,6 +231,23 @@ def _cmd_apply(config, args) -> int:
|
|||||||
return 1 if blocked else 0
|
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:
|
def _add_target_selection(parser) -> None:
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--profile",
|
"--profile",
|
||||||
@@ -334,6 +364,21 @@ def _cmd_build(config) -> int:
|
|||||||
return 0
|
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:
|
def _print_activation_step(step: ActivationStep) -> None:
|
||||||
print(
|
print(
|
||||||
"AMPG_APPLY_STEP "
|
"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,
|
||||||
|
)
|
||||||
@@ -117,6 +117,17 @@ The dry-run command is intentionally stricter than `doctor`: missing generated o
|
|||||||
blocked during activation, because AMPG must not point a transport at an unbuilt output
|
blocked during activation, because AMPG must not point a transport at an unbuilt output
|
||||||
root.
|
root.
|
||||||
|
|
||||||
|
## Install plan
|
||||||
|
|
||||||
|
`ampg install-plan` translates managed daemon decisions into package, state, config,
|
||||||
|
supervisor, and health-check steps. It does not install packages, create directories, or
|
||||||
|
start services. The output is a reviewable bridge between daemon ownership decisions and
|
||||||
|
future live apply support.
|
||||||
|
|
||||||
|
Tor and I2P HTTP publishing include the transport daemon and the loopback web-serving
|
||||||
|
layer in the install plan, because the hidden service or tunnel must have a local HTTP
|
||||||
|
target.
|
||||||
|
|
||||||
## Interaction boundary
|
## Interaction boundary
|
||||||
|
|
||||||
v1 is static-first, but the architecture reserves a path for interactive applications.
|
v1 is static-first, but the architecture reserves a path for interactive applications.
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ config artifacts.
|
|||||||
```sh
|
```sh
|
||||||
python3 -m ampg --config gateway.toml status
|
python3 -m ampg --config gateway.toml status
|
||||||
python3 -m ampg --config gateway.toml doctor
|
python3 -m ampg --config gateway.toml doctor
|
||||||
|
python3 -m ampg --config gateway.toml install-plan --profile mobile-i2p
|
||||||
python3 -m ampg --config gateway.toml apply --dry-run
|
python3 -m ampg --config gateway.toml apply --dry-run
|
||||||
python3 -m ampg --config gateway.toml apply --dry-run --profile mobile-i2p
|
python3 -m ampg --config gateway.toml apply --dry-run --profile mobile-i2p
|
||||||
python3 -m ampg --config gateway.toml apply --dry-run --protocol i2p
|
python3 -m ampg --config gateway.toml apply --dry-run --protocol i2p
|
||||||
@@ -53,6 +54,11 @@ adapter, installed/running daemon probe, action, and policy result.
|
|||||||
daemon policy feasibility. It exits nonzero only for errors; missing build output is a
|
daemon policy feasibility. It exits nonzero only for errors; missing build output is a
|
||||||
warning so operators can run it before the first build.
|
warning so operators can run it before the first build.
|
||||||
|
|
||||||
|
`install-plan` prints the package, state-directory, managed-config, supervisor, and
|
||||||
|
health-check steps needed for AMPG-owned daemons. It is dry-run only. Adopted daemons and
|
||||||
|
external-policy targets are shown as skipped, while unsupported platform/daemon pairs are
|
||||||
|
blocked.
|
||||||
|
|
||||||
`apply --dry-run` prints the activation sequence for each enabled protocol: generated
|
`apply --dry-run` prints the activation sequence for each enabled protocol: generated
|
||||||
output readiness, config artifacts to review, daemon action, and post-apply health
|
output readiness, config artifacts to review, daemon action, and post-apply health
|
||||||
checks. It exits nonzero when any step is blocked. `--write-artifacts` writes reviewable
|
checks. It exits nonzero when any step is blocked. `--write-artifacts` writes reviewable
|
||||||
@@ -75,6 +81,10 @@ Platform providers describe how AMPG may supervise managed daemons:
|
|||||||
- `android-termux`: Termux-style user-space daemons for mobile/server experiments.
|
- `android-termux`: Termux-style user-space daemons for mobile/server experiments.
|
||||||
- `unknown`: render and plan only; daemon management is disabled.
|
- `unknown`: render and plan only; daemon management is disabled.
|
||||||
|
|
||||||
|
Package commands in `install-plan` are review hints, not executed actions. Systemd hosts
|
||||||
|
currently emit `apt`-style package hints, Termux emits `pkg`, macOS emits `brew`, and
|
||||||
|
user-space Linux asks the operator to use the local user package manager.
|
||||||
|
|
||||||
## Adapter notes
|
## Adapter notes
|
||||||
|
|
||||||
### Clearnet
|
### Clearnet
|
||||||
|
|||||||
@@ -0,0 +1,187 @@
|
|||||||
|
import contextlib
|
||||||
|
import io
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from ampg.cli import main
|
||||||
|
from ampg.config import load_config
|
||||||
|
from ampg.install_plan import blocked_install_steps, install_plan
|
||||||
|
from ampg.platforms import platform_by_name
|
||||||
|
from ampg.status import DaemonProbe
|
||||||
|
|
||||||
|
|
||||||
|
def _probe(*, installed: bool = False, running: bool = False):
|
||||||
|
def probe(adapter):
|
||||||
|
executable_path = f"/usr/bin/{adapter.executable}" if installed else None
|
||||||
|
return DaemonProbe(
|
||||||
|
installed=installed,
|
||||||
|
running=running,
|
||||||
|
executable_path=executable_path,
|
||||||
|
)
|
||||||
|
|
||||||
|
return probe
|
||||||
|
|
||||||
|
|
||||||
|
def _write_config(root: Path, protocol_block: str) -> Path:
|
||||||
|
source = root / "site"
|
||||||
|
source.mkdir()
|
||||||
|
(source / "index.html").write_text("<h1>Hello</h1>", encoding="utf-8")
|
||||||
|
config_path = root / "gateway.toml"
|
||||||
|
config_path.write_text(
|
||||||
|
f"""
|
||||||
|
[profiles.mobile-i2p]
|
||||||
|
protocols = ["i2p"]
|
||||||
|
platform = "android-termux"
|
||||||
|
|
||||||
|
[[site]]
|
||||||
|
id = "example"
|
||||||
|
domain = "example.test"
|
||||||
|
|
||||||
|
[site.source]
|
||||||
|
kind = "static-html"
|
||||||
|
path = "./site"
|
||||||
|
|
||||||
|
[site.outputs]
|
||||||
|
root = "./out"
|
||||||
|
plan_root = "./plan"
|
||||||
|
|
||||||
|
{protocol_block}
|
||||||
|
""",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
return config_path
|
||||||
|
|
||||||
|
|
||||||
|
class InstallPlanTest(unittest.TestCase):
|
||||||
|
def test_android_i2p_plan_includes_transport_and_loopback_packages(self):
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
config = load_config(
|
||||||
|
_write_config(
|
||||||
|
Path(tmp),
|
||||||
|
"""
|
||||||
|
[site.protocols.i2p]
|
||||||
|
enabled = true
|
||||||
|
renderer = "privacy-html"
|
||||||
|
daemon = "i2pd"
|
||||||
|
daemon_policy = "auto"
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
steps = install_plan(
|
||||||
|
config,
|
||||||
|
platform_provider=platform_by_name("android-termux"),
|
||||||
|
daemon_probe=_probe(installed=False, running=False),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual([], blocked_install_steps(steps))
|
||||||
|
package_commands = {
|
||||||
|
(step.target, step.command)
|
||||||
|
for step in steps
|
||||||
|
if step.stage == "package"
|
||||||
|
}
|
||||||
|
self.assertIn(("i2pd", "pkg install i2pd"), package_commands)
|
||||||
|
self.assertIn(("nginx", "pkg install nginx"), package_commands)
|
||||||
|
self.assertTrue(any(step.stage == "state" for step in steps))
|
||||||
|
self.assertTrue(
|
||||||
|
any(
|
||||||
|
step.stage == "supervisor"
|
||||||
|
and step.command == "sv-enable ampg-example-i2p"
|
||||||
|
for step in steps
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_adopted_daemon_has_no_managed_install_steps(self):
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
config = load_config(
|
||||||
|
_write_config(
|
||||||
|
Path(tmp),
|
||||||
|
"""
|
||||||
|
[site.protocols.tor]
|
||||||
|
enabled = true
|
||||||
|
renderer = "privacy-html"
|
||||||
|
daemon = "tor"
|
||||||
|
daemon_policy = "auto"
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
steps = install_plan(
|
||||||
|
config,
|
||||||
|
platform_provider=platform_by_name("linux-systemd"),
|
||||||
|
daemon_probe=_probe(installed=True, running=True),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(1, len(steps))
|
||||||
|
self.assertEqual("adopt-existing", steps[0].action)
|
||||||
|
self.assertEqual("skipped", steps[0].status)
|
||||||
|
|
||||||
|
def test_unknown_platform_blocks_managed_install(self):
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
config = load_config(
|
||||||
|
_write_config(
|
||||||
|
Path(tmp),
|
||||||
|
"""
|
||||||
|
[site.protocols.i2p]
|
||||||
|
enabled = true
|
||||||
|
renderer = "privacy-html"
|
||||||
|
daemon = "i2pd"
|
||||||
|
daemon_policy = "auto"
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
steps = install_plan(
|
||||||
|
config,
|
||||||
|
platform_provider=platform_by_name("unknown"),
|
||||||
|
daemon_probe=_probe(installed=False, running=False),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(1, len(steps))
|
||||||
|
self.assertEqual("blocked", steps[0].status)
|
||||||
|
self.assertIn("cannot manage", steps[0].message)
|
||||||
|
|
||||||
|
def test_install_plan_cli_uses_profile_defaults(self):
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
config_path = _write_config(
|
||||||
|
Path(tmp),
|
||||||
|
"""
|
||||||
|
[site.protocols.clearnet]
|
||||||
|
enabled = true
|
||||||
|
renderer = "clearnet"
|
||||||
|
daemon = "nginx"
|
||||||
|
daemon_policy = "adopt"
|
||||||
|
|
||||||
|
[site.protocols.i2p]
|
||||||
|
enabled = true
|
||||||
|
renderer = "privacy-html"
|
||||||
|
daemon = "i2pd"
|
||||||
|
daemon_policy = "auto"
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
stdout = io.StringIO()
|
||||||
|
with contextlib.redirect_stdout(stdout):
|
||||||
|
status = main(
|
||||||
|
[
|
||||||
|
"--config",
|
||||||
|
str(config_path),
|
||||||
|
"install-plan",
|
||||||
|
"--profile",
|
||||||
|
"mobile-i2p",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
output = stdout.getvalue()
|
||||||
|
|
||||||
|
self.assertEqual(0, status)
|
||||||
|
self.assertIn("protocol=i2p", output)
|
||||||
|
self.assertIn("platform=android-termux", output)
|
||||||
|
self.assertIn('command="pkg install i2pd"', output)
|
||||||
|
self.assertIn("blocked=0", output)
|
||||||
|
self.assertNotIn("protocol=clearnet", output)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user