mirror of
https://github.com/Such-Software/adaptive-multi-protocol-gateway.git
synced 2026-10-17 15:41:41 +02:00
Write managed install artifacts
This commit is contained in:
@@ -105,6 +105,7 @@ 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 install-plan --profile mobile-i2p
|
||||||
|
python3 -m ampg --config examples/wownero.gateway.toml install-plan --profile mobile-i2p --write-artifacts
|
||||||
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
|
||||||
@@ -132,6 +133,10 @@ that marker is present.
|
|||||||
Plan artifacts are written under `dist/ampg-plan/` only when `--write-artifacts` is
|
Plan artifacts are written under `dist/ampg-plan/` only when `--write-artifacts` is
|
||||||
passed. They are reviewable snippets, not installed daemon config.
|
passed. They are reviewable snippets, not installed daemon config.
|
||||||
|
|
||||||
|
Managed install artifacts are also written under `dist/ampg-plan/` when
|
||||||
|
`install-plan --write-artifacts` is passed. They include reviewable daemon configs and
|
||||||
|
platform supervisor files, but are not installed or started.
|
||||||
|
|
||||||
Use `--protocol` to scope operational commands to one or more enabled protocols. This
|
Use `--protocol` to scope operational commands to one or more enabled protocols. This
|
||||||
lets a full site config build or activate only Tor, only I2P, or a selected subset without
|
lets a full site config build or activate only Tor, only I2P, or a selected subset without
|
||||||
unselected transports blocking the run.
|
unselected transports blocking the run.
|
||||||
|
|||||||
+26
-2
@@ -14,7 +14,12 @@ 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 .install_plan import (
|
||||||
|
InstallStep,
|
||||||
|
blocked_install_steps,
|
||||||
|
install_plan,
|
||||||
|
write_install_artifacts,
|
||||||
|
)
|
||||||
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
|
||||||
@@ -95,6 +100,11 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
choices=PLATFORM_NAMES,
|
choices=PLATFORM_NAMES,
|
||||||
help="Override platform detection for install planning.",
|
help="Override platform detection for install planning.",
|
||||||
)
|
)
|
||||||
|
install_plan_parser.add_argument(
|
||||||
|
"--write-artifacts",
|
||||||
|
action="store_true",
|
||||||
|
help="Write managed daemon config and supervisor artifacts to the plan root.",
|
||||||
|
)
|
||||||
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.")
|
||||||
@@ -232,7 +242,21 @@ def _cmd_apply(config, args) -> int:
|
|||||||
|
|
||||||
|
|
||||||
def _cmd_install_plan(config, args) -> int:
|
def _cmd_install_plan(config, args) -> int:
|
||||||
steps = install_plan(config, platform_provider=_platform_override(config, args))
|
platform_provider = _platform_override(config, args)
|
||||||
|
if _write_artifacts_enabled(config, args):
|
||||||
|
for artifact in write_install_artifacts(
|
||||||
|
config,
|
||||||
|
platform_provider=platform_provider,
|
||||||
|
):
|
||||||
|
print(
|
||||||
|
"AMPG_INSTALL_ARTIFACT "
|
||||||
|
f"site={artifact.site_id} "
|
||||||
|
f"protocol={artifact.protocol} "
|
||||||
|
f"platform={artifact.platform} "
|
||||||
|
f"kind={artifact.kind} "
|
||||||
|
f"path={artifact.path}"
|
||||||
|
)
|
||||||
|
steps = install_plan(config, platform_provider=platform_provider)
|
||||||
for step in steps:
|
for step in steps:
|
||||||
_print_install_step(step)
|
_print_install_step(step)
|
||||||
blocked = blocked_install_steps(steps)
|
blocked = blocked_install_steps(steps)
|
||||||
|
|||||||
@@ -22,6 +22,16 @@ class InstallStep:
|
|||||||
message: str
|
message: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class InstallArtifact:
|
||||||
|
site_id: str
|
||||||
|
protocol: str
|
||||||
|
platform: str
|
||||||
|
kind: str
|
||||||
|
path: Path
|
||||||
|
content: str
|
||||||
|
|
||||||
|
|
||||||
def install_plan(
|
def install_plan(
|
||||||
config: GatewayConfig,
|
config: GatewayConfig,
|
||||||
*,
|
*,
|
||||||
@@ -51,6 +61,48 @@ def blocked_install_steps(steps: list[InstallStep]) -> list[InstallStep]:
|
|||||||
return [step for step in steps if step.status == "blocked"]
|
return [step for step in steps if step.status == "blocked"]
|
||||||
|
|
||||||
|
|
||||||
|
def install_artifacts(
|
||||||
|
config: GatewayConfig,
|
||||||
|
*,
|
||||||
|
platform_provider: PlatformProvider | None = None,
|
||||||
|
daemon_probe: DaemonProbeFunc | None = None,
|
||||||
|
) -> list[InstallArtifact]:
|
||||||
|
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
|
||||||
|
}
|
||||||
|
artifacts: list[InstallArtifact] = []
|
||||||
|
for site in config.sites:
|
||||||
|
for protocol in site.protocols.values():
|
||||||
|
if not protocol.enabled:
|
||||||
|
continue
|
||||||
|
status = status_by_protocol[(site.id, protocol.name)]
|
||||||
|
artifacts.extend(_protocol_install_artifacts(site, protocol, provider, status))
|
||||||
|
return artifacts
|
||||||
|
|
||||||
|
|
||||||
|
def write_install_artifacts(
|
||||||
|
config: GatewayConfig,
|
||||||
|
*,
|
||||||
|
platform_provider: PlatformProvider | None = None,
|
||||||
|
daemon_probe: DaemonProbeFunc | None = None,
|
||||||
|
) -> list[InstallArtifact]:
|
||||||
|
artifacts = install_artifacts(
|
||||||
|
config,
|
||||||
|
platform_provider=platform_provider,
|
||||||
|
daemon_probe=daemon_probe,
|
||||||
|
)
|
||||||
|
for artifact in artifacts:
|
||||||
|
artifact.path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
artifact.path.write_text(artifact.content, encoding="utf-8")
|
||||||
|
return artifacts
|
||||||
|
|
||||||
|
|
||||||
def _protocol_install_steps(
|
def _protocol_install_steps(
|
||||||
site: SiteConfig,
|
site: SiteConfig,
|
||||||
protocol: ProtocolConfig,
|
protocol: ProtocolConfig,
|
||||||
@@ -159,6 +211,69 @@ def _protocol_install_steps(
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _protocol_install_artifacts(
|
||||||
|
site: SiteConfig,
|
||||||
|
protocol: ProtocolConfig,
|
||||||
|
provider: PlatformProvider,
|
||||||
|
status: TransportStatus,
|
||||||
|
) -> list[InstallArtifact]:
|
||||||
|
if status.action != "manage-owned" or status.status == "error":
|
||||||
|
return []
|
||||||
|
|
||||||
|
artifacts = [
|
||||||
|
_artifact(
|
||||||
|
site,
|
||||||
|
protocol,
|
||||||
|
provider,
|
||||||
|
kind="daemon-config",
|
||||||
|
filename=_daemon_config_filename(protocol),
|
||||||
|
content=_daemon_config_content(site, protocol, provider),
|
||||||
|
),
|
||||||
|
_artifact(
|
||||||
|
site,
|
||||||
|
protocol,
|
||||||
|
provider,
|
||||||
|
kind="daemon-supervisor",
|
||||||
|
filename=_supervisor_filename(provider, protocol.daemon),
|
||||||
|
content=_supervisor_content(
|
||||||
|
site,
|
||||||
|
protocol,
|
||||||
|
provider,
|
||||||
|
service_suffix=protocol.daemon,
|
||||||
|
command=_daemon_start_command(site, protocol, provider),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
if protocol.name in {"tor", "i2p"}:
|
||||||
|
artifacts.extend(
|
||||||
|
(
|
||||||
|
_artifact(
|
||||||
|
site,
|
||||||
|
protocol,
|
||||||
|
provider,
|
||||||
|
kind="loopback-config",
|
||||||
|
filename="nginx-loopback.conf",
|
||||||
|
content=_managed_nginx_loopback_content(site, protocol, provider),
|
||||||
|
),
|
||||||
|
_artifact(
|
||||||
|
site,
|
||||||
|
protocol,
|
||||||
|
provider,
|
||||||
|
kind="loopback-supervisor",
|
||||||
|
filename=_supervisor_filename(provider, "nginx-loopback"),
|
||||||
|
content=_supervisor_content(
|
||||||
|
site,
|
||||||
|
protocol,
|
||||||
|
provider,
|
||||||
|
service_suffix="nginx-loopback",
|
||||||
|
command=_loopback_start_command(site, protocol, provider),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return artifacts
|
||||||
|
|
||||||
|
|
||||||
def _package_step(
|
def _package_step(
|
||||||
site: SiteConfig,
|
site: SiteConfig,
|
||||||
protocol: ProtocolConfig,
|
protocol: ProtocolConfig,
|
||||||
@@ -239,10 +354,277 @@ def _state_dir(
|
|||||||
return provider.state_root / site.id / protocol.name
|
return provider.state_root / site.id / protocol.name
|
||||||
|
|
||||||
|
|
||||||
|
def _artifact_dir(
|
||||||
|
site: SiteConfig,
|
||||||
|
protocol: ProtocolConfig,
|
||||||
|
provider: PlatformProvider,
|
||||||
|
) -> Path:
|
||||||
|
return site.outputs.plan_root / site.id / protocol.name / "managed" / provider.name
|
||||||
|
|
||||||
|
|
||||||
def _service_name(site: SiteConfig, protocol: ProtocolConfig) -> str:
|
def _service_name(site: SiteConfig, protocol: ProtocolConfig) -> str:
|
||||||
return f"ampg-{site.id}-{protocol.name}"
|
return f"ampg-{site.id}-{protocol.name}"
|
||||||
|
|
||||||
|
|
||||||
|
def _artifact_service_name(
|
||||||
|
site: SiteConfig,
|
||||||
|
protocol: ProtocolConfig,
|
||||||
|
service_suffix: str,
|
||||||
|
) -> str:
|
||||||
|
return f"{_service_name(site, protocol)}-{service_suffix}"
|
||||||
|
|
||||||
|
|
||||||
|
def _daemon_config_filename(protocol: ProtocolConfig) -> str:
|
||||||
|
if protocol.name == "tor":
|
||||||
|
return "torrc"
|
||||||
|
if protocol.name == "i2p":
|
||||||
|
return "i2pd-tunnels.conf"
|
||||||
|
if protocol.name == "gemini":
|
||||||
|
return "agate-plan.txt"
|
||||||
|
if protocol.name == "clearnet":
|
||||||
|
return "nginx-server.conf"
|
||||||
|
return f"{protocol.daemon}-plan.txt"
|
||||||
|
|
||||||
|
|
||||||
|
def _daemon_config_content(
|
||||||
|
site: SiteConfig,
|
||||||
|
protocol: ProtocolConfig,
|
||||||
|
provider: PlatformProvider,
|
||||||
|
) -> str:
|
||||||
|
state_dir = _state_dir(provider, site, protocol)
|
||||||
|
if protocol.name == "tor":
|
||||||
|
port = int(protocol.options.get("local_port", 18080))
|
||||||
|
hidden_service_dir = state_dir / "hidden-service"
|
||||||
|
return f"""# Generated by AMPG. Review before installing.
|
||||||
|
DataDirectory {state_dir / "data"}
|
||||||
|
HiddenServiceDir {hidden_service_dir}/
|
||||||
|
HiddenServicePort 80 127.0.0.1:{port}
|
||||||
|
Log notice file {state_dir / "tor.log"}
|
||||||
|
"""
|
||||||
|
if protocol.name == "i2p":
|
||||||
|
port = int(protocol.options.get("local_port", 18081))
|
||||||
|
key_file = protocol.options.get("keys_file", f"{site.id}-web.dat")
|
||||||
|
tunnel_name = protocol.options.get("tunnel_name", f"{site.id}-web")
|
||||||
|
return f"""# Generated by AMPG. Review before installing.
|
||||||
|
[{tunnel_name}]
|
||||||
|
type = server
|
||||||
|
host = 127.0.0.1
|
||||||
|
port = {port}
|
||||||
|
keys = {state_dir / str(key_file)}
|
||||||
|
"""
|
||||||
|
if protocol.name == "gemini":
|
||||||
|
port = int(protocol.options.get("port", 1965))
|
||||||
|
return f"""# Generated by AMPG. Review before installing.
|
||||||
|
daemon = agate
|
||||||
|
hostname = {site.domain}
|
||||||
|
listen = [::]:{port}
|
||||||
|
content_root = {site.outputs.root / protocol.name}
|
||||||
|
certificate = {state_dir / f"{site.id}.crt"}
|
||||||
|
private_key = {state_dir / f"{site.id}.key"}
|
||||||
|
"""
|
||||||
|
if protocol.name == "clearnet":
|
||||||
|
return _managed_nginx_server_content(site, protocol, provider)
|
||||||
|
return f"""# Generated by AMPG. Review before installing.
|
||||||
|
daemon = {protocol.daemon}
|
||||||
|
protocol = {protocol.name}
|
||||||
|
content_root = {site.outputs.root / protocol.name}
|
||||||
|
state_dir = {state_dir}
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def _managed_nginx_server_content(
|
||||||
|
site: SiteConfig,
|
||||||
|
protocol: ProtocolConfig,
|
||||||
|
provider: PlatformProvider,
|
||||||
|
) -> str:
|
||||||
|
state_dir = _state_dir(provider, site, protocol)
|
||||||
|
return f"""# Generated by AMPG. Review before installing.
|
||||||
|
worker_processes 1;
|
||||||
|
error_log {state_dir / "nginx-error.log"} notice;
|
||||||
|
pid {state_dir / "nginx.pid"};
|
||||||
|
|
||||||
|
events {{
|
||||||
|
worker_connections 128;
|
||||||
|
}}
|
||||||
|
|
||||||
|
http {{
|
||||||
|
access_log {state_dir / "nginx-access.log"};
|
||||||
|
server {{
|
||||||
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
server_name {site.domain} www.{site.domain};
|
||||||
|
root {site.outputs.root / protocol.name};
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
location / {{
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def _managed_nginx_loopback_content(
|
||||||
|
site: SiteConfig,
|
||||||
|
protocol: ProtocolConfig,
|
||||||
|
provider: PlatformProvider,
|
||||||
|
) -> str:
|
||||||
|
state_dir = _state_dir(provider, site, protocol)
|
||||||
|
port = int(protocol.options.get("local_port", 18080 if protocol.name == "tor" else 18081))
|
||||||
|
return f"""# Generated by AMPG. Review before installing.
|
||||||
|
worker_processes 1;
|
||||||
|
error_log {state_dir / "nginx-loopback-error.log"} notice;
|
||||||
|
pid {state_dir / "nginx-loopback.pid"};
|
||||||
|
|
||||||
|
events {{
|
||||||
|
worker_connections 128;
|
||||||
|
}}
|
||||||
|
|
||||||
|
http {{
|
||||||
|
access_log {state_dir / "nginx-loopback-access.log"};
|
||||||
|
server {{
|
||||||
|
listen 127.0.0.1:{port};
|
||||||
|
server_name _;
|
||||||
|
root {site.outputs.root / protocol.name};
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
location / {{
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def _supervisor_filename(provider: PlatformProvider, service_suffix: str) -> str:
|
||||||
|
if provider.name == "linux-systemd":
|
||||||
|
return f"{service_suffix}.service"
|
||||||
|
if provider.name == "macos-launchd":
|
||||||
|
return f"org.ampg.{service_suffix}.plist"
|
||||||
|
return f"{service_suffix}.run"
|
||||||
|
|
||||||
|
|
||||||
|
def _supervisor_content(
|
||||||
|
site: SiteConfig,
|
||||||
|
protocol: ProtocolConfig,
|
||||||
|
provider: PlatformProvider,
|
||||||
|
*,
|
||||||
|
service_suffix: str,
|
||||||
|
command: str,
|
||||||
|
) -> str:
|
||||||
|
service_name = _artifact_service_name(site, protocol, service_suffix)
|
||||||
|
if provider.name == "linux-systemd":
|
||||||
|
return f"""# Generated by AMPG. Review before installing.
|
||||||
|
[Unit]
|
||||||
|
Description=AMPG {protocol.name} {service_suffix} for {site.id}
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
WorkingDirectory={_state_dir(provider, site, protocol)}
|
||||||
|
ExecStart=/bin/sh -lc '{_shell_single_quote(command)}'
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
"""
|
||||||
|
if provider.name == "macos-launchd":
|
||||||
|
return f"""<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
|
||||||
|
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>Label</key>
|
||||||
|
<string>org.ampg.{service_name}</string>
|
||||||
|
<key>ProgramArguments</key>
|
||||||
|
<array>
|
||||||
|
<string>/bin/sh</string>
|
||||||
|
<string>-lc</string>
|
||||||
|
<string>{_xml_escape(command)}</string>
|
||||||
|
</array>
|
||||||
|
<key>RunAtLoad</key>
|
||||||
|
<true/>
|
||||||
|
<key>KeepAlive</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
|
"""
|
||||||
|
shell = "/data/data/com.termux/files/usr/bin/sh" if provider.name == "android-termux" else "/bin/sh"
|
||||||
|
return f"""#!{shell}
|
||||||
|
# Generated by AMPG. Review before installing.
|
||||||
|
set -eu
|
||||||
|
mkdir -p {_state_dir(provider, site, protocol)}
|
||||||
|
{command}
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def _daemon_start_command(
|
||||||
|
site: SiteConfig,
|
||||||
|
protocol: ProtocolConfig,
|
||||||
|
provider: PlatformProvider,
|
||||||
|
) -> str:
|
||||||
|
state_dir = _state_dir(provider, site, protocol)
|
||||||
|
if protocol.name == "tor":
|
||||||
|
return f"exec tor -f {state_dir / 'torrc'}"
|
||||||
|
if protocol.name == "i2p":
|
||||||
|
return f"exec i2pd --datadir={state_dir / 'data'} --tunconf={state_dir / 'i2pd-tunnels.conf'}"
|
||||||
|
if protocol.name == "gemini":
|
||||||
|
port = int(protocol.options.get("port", 1965))
|
||||||
|
return (
|
||||||
|
"exec agate "
|
||||||
|
f"--content {site.outputs.root / protocol.name} "
|
||||||
|
f"--addr [::]:{port} "
|
||||||
|
f"--hostname {site.domain} "
|
||||||
|
f"--certs {state_dir}"
|
||||||
|
)
|
||||||
|
if protocol.name == "clearnet":
|
||||||
|
return f"exec nginx -c {state_dir / 'nginx-server.conf'} -g 'daemon off;'"
|
||||||
|
return f"exec {protocol.daemon}"
|
||||||
|
|
||||||
|
|
||||||
|
def _loopback_start_command(
|
||||||
|
site: SiteConfig,
|
||||||
|
protocol: ProtocolConfig,
|
||||||
|
provider: PlatformProvider,
|
||||||
|
) -> str:
|
||||||
|
state_dir = _state_dir(provider, site, protocol)
|
||||||
|
return f"exec nginx -c {state_dir / 'nginx-loopback.conf'} -g 'daemon off;'"
|
||||||
|
|
||||||
|
|
||||||
|
def _artifact(
|
||||||
|
site: SiteConfig,
|
||||||
|
protocol: ProtocolConfig,
|
||||||
|
provider: PlatformProvider,
|
||||||
|
*,
|
||||||
|
kind: str,
|
||||||
|
filename: str,
|
||||||
|
content: str,
|
||||||
|
) -> InstallArtifact:
|
||||||
|
return InstallArtifact(
|
||||||
|
site_id=site.id,
|
||||||
|
protocol=protocol.name,
|
||||||
|
platform=provider.name,
|
||||||
|
kind=kind,
|
||||||
|
path=_artifact_dir(site, protocol, provider) / filename,
|
||||||
|
content=content,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _xml_escape(value: str) -> str:
|
||||||
|
return (
|
||||||
|
value.replace("&", "&")
|
||||||
|
.replace("<", "<")
|
||||||
|
.replace(">", ">")
|
||||||
|
.replace('"', """)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _shell_single_quote(value: str) -> str:
|
||||||
|
return value.replace("'", "'\\''")
|
||||||
|
|
||||||
|
|
||||||
def _step(
|
def _step(
|
||||||
site: SiteConfig,
|
site: SiteConfig,
|
||||||
protocol: ProtocolConfig,
|
protocol: ProtocolConfig,
|
||||||
|
|||||||
@@ -124,6 +124,10 @@ supervisor, and health-check steps. It does not install packages, create directo
|
|||||||
start services. The output is a reviewable bridge between daemon ownership decisions and
|
start services. The output is a reviewable bridge between daemon ownership decisions and
|
||||||
future live apply support.
|
future live apply support.
|
||||||
|
|
||||||
|
With `--write-artifacts`, install-plan writes managed config and supervisor files under
|
||||||
|
the configured `plan_root`; those files are review artifacts, not installed service
|
||||||
|
state.
|
||||||
|
|
||||||
Tor and I2P HTTP publishing include the transport daemon and the loopback web-serving
|
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
|
layer in the install plan, because the hidden service or tunnel must have a local HTTP
|
||||||
target.
|
target.
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ config artifacts.
|
|||||||
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 install-plan --profile mobile-i2p
|
||||||
|
python3 -m ampg --config gateway.toml install-plan --profile mobile-i2p --write-artifacts
|
||||||
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
|
||||||
@@ -59,6 +60,11 @@ health-check steps needed for AMPG-owned daemons. It is dry-run only. Adopted da
|
|||||||
external-policy targets are shown as skipped, while unsupported platform/daemon pairs are
|
external-policy targets are shown as skipped, while unsupported platform/daemon pairs are
|
||||||
blocked.
|
blocked.
|
||||||
|
|
||||||
|
`install-plan --write-artifacts` writes managed daemon configs and platform supervisor
|
||||||
|
files under the configured `plan_root`, grouped by site, protocol, and platform. For
|
||||||
|
Tor/I2P HTTP publishing, AMPG writes both transport-daemon artifacts and loopback nginx
|
||||||
|
artifacts.
|
||||||
|
|
||||||
`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
|
||||||
|
|||||||
+140
-1
@@ -6,7 +6,12 @@ from pathlib import Path
|
|||||||
|
|
||||||
from ampg.cli import main
|
from ampg.cli import main
|
||||||
from ampg.config import load_config
|
from ampg.config import load_config
|
||||||
from ampg.install_plan import blocked_install_steps, install_plan
|
from ampg.install_plan import (
|
||||||
|
blocked_install_steps,
|
||||||
|
install_artifacts,
|
||||||
|
install_plan,
|
||||||
|
write_install_artifacts,
|
||||||
|
)
|
||||||
from ampg.platforms import platform_by_name
|
from ampg.platforms import platform_by_name
|
||||||
from ampg.status import DaemonProbe
|
from ampg.status import DaemonProbe
|
||||||
|
|
||||||
@@ -182,6 +187,140 @@ daemon_policy = "auto"
|
|||||||
self.assertIn("blocked=0", output)
|
self.assertIn("blocked=0", output)
|
||||||
self.assertNotIn("protocol=clearnet", output)
|
self.assertNotIn("protocol=clearnet", output)
|
||||||
|
|
||||||
|
def test_managed_i2p_artifacts_include_daemon_loopback_and_supervisors(self):
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
root = Path(tmp)
|
||||||
|
config = load_config(
|
||||||
|
_write_config(
|
||||||
|
root,
|
||||||
|
"""
|
||||||
|
[site.protocols.i2p]
|
||||||
|
enabled = true
|
||||||
|
renderer = "privacy-html"
|
||||||
|
daemon = "i2pd"
|
||||||
|
daemon_policy = "auto"
|
||||||
|
local_port = 19081
|
||||||
|
keys_file = "example-web.dat"
|
||||||
|
tunnel_name = "example-web"
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
artifacts = install_artifacts(
|
||||||
|
config,
|
||||||
|
platform_provider=platform_by_name("android-termux"),
|
||||||
|
daemon_probe=_probe(installed=False, running=False),
|
||||||
|
)
|
||||||
|
|
||||||
|
by_name = {artifact.path.name: artifact for artifact in artifacts}
|
||||||
|
self.assertEqual(
|
||||||
|
{
|
||||||
|
"i2pd-tunnels.conf",
|
||||||
|
"i2pd.run",
|
||||||
|
"nginx-loopback.conf",
|
||||||
|
"nginx-loopback.run",
|
||||||
|
},
|
||||||
|
set(by_name),
|
||||||
|
)
|
||||||
|
self.assertIn("[example-web]", by_name["i2pd-tunnels.conf"].content)
|
||||||
|
self.assertIn("keys =", by_name["i2pd-tunnels.conf"].content)
|
||||||
|
self.assertIn("listen 127.0.0.1:19081", by_name["nginx-loopback.conf"].content)
|
||||||
|
self.assertIn("exec i2pd", by_name["i2pd.run"].content)
|
||||||
|
self.assertIn("exec nginx", by_name["nginx-loopback.run"].content)
|
||||||
|
|
||||||
|
def test_write_install_artifacts_creates_review_files(self):
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
root = Path(tmp)
|
||||||
|
config = load_config(
|
||||||
|
_write_config(
|
||||||
|
root,
|
||||||
|
"""
|
||||||
|
[site.protocols.i2p]
|
||||||
|
enabled = true
|
||||||
|
renderer = "privacy-html"
|
||||||
|
daemon = "i2pd"
|
||||||
|
daemon_policy = "auto"
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
artifacts = write_install_artifacts(
|
||||||
|
config,
|
||||||
|
platform_provider=platform_by_name("android-termux"),
|
||||||
|
daemon_probe=_probe(installed=False, running=False),
|
||||||
|
)
|
||||||
|
|
||||||
|
for artifact in artifacts:
|
||||||
|
self.assertTrue(artifact.path.exists())
|
||||||
|
self.assertTrue(
|
||||||
|
(
|
||||||
|
root
|
||||||
|
/ "plan/example/i2p/managed/android-termux/i2pd-tunnels.conf"
|
||||||
|
).exists()
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_install_plan_cli_can_write_managed_artifacts(self):
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
root = Path(tmp)
|
||||||
|
config_path = _write_config(
|
||||||
|
root,
|
||||||
|
"""
|
||||||
|
[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",
|
||||||
|
"--write-artifacts",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
output = stdout.getvalue()
|
||||||
|
self.assertEqual(0, status)
|
||||||
|
self.assertIn("AMPG_INSTALL_ARTIFACT", output)
|
||||||
|
self.assertIn("kind=daemon-config", output)
|
||||||
|
self.assertTrue(
|
||||||
|
(
|
||||||
|
root
|
||||||
|
/ "plan/example/i2p/managed/android-termux/i2pd-tunnels.conf"
|
||||||
|
).exists()
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_systemd_supervisor_wraps_start_command_in_shell(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"
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
artifacts = install_artifacts(
|
||||||
|
config,
|
||||||
|
platform_provider=platform_by_name("linux-systemd"),
|
||||||
|
daemon_probe=_probe(installed=False, running=False),
|
||||||
|
)
|
||||||
|
|
||||||
|
service = next(artifact for artifact in artifacts if artifact.path.name == "tor.service")
|
||||||
|
self.assertIn("ExecStart=/bin/sh -lc 'exec tor", service.content)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
Reference in New Issue
Block a user