mirror of
https://github.com/Such-Software/adaptive-multi-protocol-gateway.git
synced 2026-10-17 07:31:32 +02:00
Add address capture to activation plan
This commit is contained in:
@@ -140,12 +140,16 @@ passed. They are reviewable snippets, not installed daemon config.
|
|||||||
|
|
||||||
Managed install artifacts are also written under `dist/ampg-plan/` when
|
Managed install artifacts are also written under `dist/ampg-plan/` when
|
||||||
`install-plan --write-artifacts` is passed. They include reviewable daemon configs and
|
`install-plan --write-artifacts` is passed. They include reviewable daemon configs and
|
||||||
platform supervisor files, but are not installed or started.
|
platform supervisor files, but are not installed or started. Managed daemon configs point
|
||||||
|
runtime state at `gateway.state_dir`.
|
||||||
|
|
||||||
Captured transport addresses are written under `gateway.state_dir` by
|
Captured transport addresses are written under `gateway.state_dir` by
|
||||||
`addresses capture` or `addresses set`. Fixture manifests and health plans use explicit
|
`addresses capture` or `addresses set`. Fixture manifests and health plans use explicit
|
||||||
config first, then captured addresses, then deterministic placeholders.
|
config first, then captured addresses, then deterministic placeholders.
|
||||||
|
|
||||||
|
`apply --dry-run` includes an address stage. Placeholder addresses remain review items
|
||||||
|
until the generated transport identity is captured or configured.
|
||||||
|
|
||||||
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.
|
||||||
|
|||||||
+80
-8
@@ -2,7 +2,9 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import shlex
|
||||||
|
|
||||||
|
from .addresses import AddressRecord, address_registry_path, effective_address_records
|
||||||
from .config import GatewayConfig, ProtocolConfig, SiteConfig
|
from .config import GatewayConfig, ProtocolConfig, SiteConfig
|
||||||
from .plan import plan_artifacts, write_plan_artifacts
|
from .plan import plan_artifacts, write_plan_artifacts
|
||||||
from .platforms import PlatformProvider
|
from .platforms import PlatformProvider
|
||||||
@@ -23,6 +25,7 @@ class ActivationStep:
|
|||||||
target: str
|
target: str
|
||||||
status: str
|
status: str
|
||||||
message: str
|
message: str
|
||||||
|
command: str = "-"
|
||||||
|
|
||||||
|
|
||||||
def activation_steps(
|
def activation_steps(
|
||||||
@@ -39,6 +42,10 @@ def activation_steps(
|
|||||||
status_by_protocol = {
|
status_by_protocol = {
|
||||||
(status.site_id, status.protocol): status for status in statuses
|
(status.site_id, status.protocol): status for status in statuses
|
||||||
}
|
}
|
||||||
|
address_by_protocol = {
|
||||||
|
(record.site_id, record.protocol): record
|
||||||
|
for record in effective_address_records(config)
|
||||||
|
}
|
||||||
steps: list[ActivationStep] = []
|
steps: list[ActivationStep] = []
|
||||||
|
|
||||||
for issue in doctor_gateway(
|
for issue in doctor_gateway(
|
||||||
@@ -69,9 +76,11 @@ def activation_steps(
|
|||||||
transport_status = status_by_protocol[(site.id, protocol.name)]
|
transport_status = status_by_protocol[(site.id, protocol.name)]
|
||||||
steps.extend(
|
steps.extend(
|
||||||
_protocol_steps(
|
_protocol_steps(
|
||||||
|
config,
|
||||||
site,
|
site,
|
||||||
protocol,
|
protocol,
|
||||||
transport_status=transport_status,
|
transport_status=transport_status,
|
||||||
|
address_record=address_by_protocol[(site.id, protocol.name)],
|
||||||
output_ready=output_ready,
|
output_ready=output_ready,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -84,10 +93,12 @@ def blocked_steps(steps: list[ActivationStep]) -> list[ActivationStep]:
|
|||||||
|
|
||||||
|
|
||||||
def _protocol_steps(
|
def _protocol_steps(
|
||||||
|
config: GatewayConfig,
|
||||||
site: SiteConfig,
|
site: SiteConfig,
|
||||||
protocol: ProtocolConfig,
|
protocol: ProtocolConfig,
|
||||||
*,
|
*,
|
||||||
transport_status: TransportStatus,
|
transport_status: TransportStatus,
|
||||||
|
address_record: AddressRecord,
|
||||||
output_ready: bool,
|
output_ready: bool,
|
||||||
) -> list[ActivationStep]:
|
) -> list[ActivationStep]:
|
||||||
output_root = site.outputs.root / protocol.name
|
output_root = site.outputs.root / protocol.name
|
||||||
@@ -145,6 +156,7 @@ def _protocol_steps(
|
|||||||
message=transport_status.message,
|
message=transport_status.message,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
steps.append(_address_step(config, site, protocol, address_record))
|
||||||
steps.append(
|
steps.append(
|
||||||
ActivationStep(
|
ActivationStep(
|
||||||
site_id=site.id,
|
site_id=site.id,
|
||||||
@@ -152,21 +164,81 @@ def _protocol_steps(
|
|||||||
stage="health",
|
stage="health",
|
||||||
action="check-transport",
|
action="check-transport",
|
||||||
target=protocol.name,
|
target=protocol.name,
|
||||||
status=(
|
status=_health_activation_status(
|
||||||
"planned"
|
output_ready=output_ready,
|
||||||
if output_ready and transport_status.status != "error"
|
transport_status=transport_status,
|
||||||
else "blocked"
|
address_record=address_record,
|
||||||
),
|
),
|
||||||
message=(
|
message=_health_activation_message(
|
||||||
"verify generated site through this transport after apply"
|
output_ready=output_ready,
|
||||||
if output_ready and transport_status.status != "error"
|
transport_status=transport_status,
|
||||||
else "health check is blocked until output and daemon steps are ready"
|
address_record=address_record,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return steps
|
return steps
|
||||||
|
|
||||||
|
|
||||||
|
def _address_step(
|
||||||
|
config: GatewayConfig,
|
||||||
|
site: SiteConfig,
|
||||||
|
protocol: ProtocolConfig,
|
||||||
|
address_record: AddressRecord,
|
||||||
|
) -> ActivationStep:
|
||||||
|
if address_record.address_status == "placeholder":
|
||||||
|
return ActivationStep(
|
||||||
|
site_id=site.id,
|
||||||
|
protocol=protocol.name,
|
||||||
|
stage="address",
|
||||||
|
action="capture-address",
|
||||||
|
target=str(address_registry_path(config)),
|
||||||
|
status="review",
|
||||||
|
message="generated transport address is not known yet",
|
||||||
|
command=_address_capture_command(config, protocol),
|
||||||
|
)
|
||||||
|
return ActivationStep(
|
||||||
|
site_id=site.id,
|
||||||
|
protocol=protocol.name,
|
||||||
|
stage="address",
|
||||||
|
action="use-address",
|
||||||
|
target=address_record.url,
|
||||||
|
status="ready",
|
||||||
|
message=f"using {address_record.address_status} transport address",
|
||||||
|
command="-",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _address_capture_command(config: GatewayConfig, protocol: ProtocolConfig) -> str:
|
||||||
|
config_path = shlex.quote(str(config.config_path))
|
||||||
|
return f"python3 -m ampg --config {config_path} addresses capture --protocol {protocol.name}"
|
||||||
|
|
||||||
|
|
||||||
|
def _health_activation_status(
|
||||||
|
*,
|
||||||
|
output_ready: bool,
|
||||||
|
transport_status: TransportStatus,
|
||||||
|
address_record: AddressRecord,
|
||||||
|
) -> str:
|
||||||
|
if not output_ready or transport_status.status == "error":
|
||||||
|
return "blocked"
|
||||||
|
if address_record.address_status == "placeholder":
|
||||||
|
return "review"
|
||||||
|
return "planned"
|
||||||
|
|
||||||
|
|
||||||
|
def _health_activation_message(
|
||||||
|
*,
|
||||||
|
output_ready: bool,
|
||||||
|
transport_status: TransportStatus,
|
||||||
|
address_record: AddressRecord,
|
||||||
|
) -> str:
|
||||||
|
if not output_ready or transport_status.status == "error":
|
||||||
|
return "health check is blocked until output and daemon steps are ready"
|
||||||
|
if address_record.address_status == "placeholder":
|
||||||
|
return "capture or configure the generated transport address before published checks"
|
||||||
|
return "verify generated site through this transport after apply"
|
||||||
|
|
||||||
|
|
||||||
def _output_ready(site: SiteConfig, protocol: ProtocolConfig) -> bool:
|
def _output_ready(site: SiteConfig, protocol: ProtocolConfig) -> bool:
|
||||||
return ((site.outputs.root / protocol.name) / ".ampg-output").exists()
|
return ((site.outputs.root / protocol.name) / ".ampg-output").exists()
|
||||||
|
|
||||||
|
|||||||
@@ -524,6 +524,7 @@ def _print_activation_step(step: ActivationStep) -> None:
|
|||||||
f"action={step.action} "
|
f"action={step.action} "
|
||||||
f"target={step.target} "
|
f"target={step.target} "
|
||||||
f"status={step.status} "
|
f"status={step.status} "
|
||||||
|
f"command=\"{_quote(step.command)}\" "
|
||||||
f"message=\"{_quote(step.message)}\""
|
f"message=\"{_quote(step.message)}\""
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
+30
-18
@@ -53,7 +53,7 @@ def install_plan(
|
|||||||
if not protocol.enabled:
|
if not protocol.enabled:
|
||||||
continue
|
continue
|
||||||
status = status_by_protocol[(site.id, protocol.name)]
|
status = status_by_protocol[(site.id, protocol.name)]
|
||||||
steps.extend(_protocol_install_steps(site, protocol, provider, status))
|
steps.extend(_protocol_install_steps(config, site, protocol, provider, status))
|
||||||
return steps
|
return steps
|
||||||
|
|
||||||
|
|
||||||
@@ -82,7 +82,7 @@ def install_artifacts(
|
|||||||
if not protocol.enabled:
|
if not protocol.enabled:
|
||||||
continue
|
continue
|
||||||
status = status_by_protocol[(site.id, protocol.name)]
|
status = status_by_protocol[(site.id, protocol.name)]
|
||||||
artifacts.extend(_protocol_install_artifacts(site, protocol, provider, status))
|
artifacts.extend(_protocol_install_artifacts(config, site, protocol, provider, status))
|
||||||
return artifacts
|
return artifacts
|
||||||
|
|
||||||
|
|
||||||
@@ -104,6 +104,7 @@ def write_install_artifacts(
|
|||||||
|
|
||||||
|
|
||||||
def _protocol_install_steps(
|
def _protocol_install_steps(
|
||||||
|
config: GatewayConfig,
|
||||||
site: SiteConfig,
|
site: SiteConfig,
|
||||||
protocol: ProtocolConfig,
|
protocol: ProtocolConfig,
|
||||||
provider: PlatformProvider,
|
provider: PlatformProvider,
|
||||||
@@ -170,9 +171,9 @@ def _protocol_install_steps(
|
|||||||
provider,
|
provider,
|
||||||
stage="state",
|
stage="state",
|
||||||
action="create-state",
|
action="create-state",
|
||||||
target=str(_state_dir(provider, site, protocol)),
|
target=str(_state_dir(config, provider, site, protocol)),
|
||||||
status="planned",
|
status="planned",
|
||||||
command=_mkdir_command(provider, _state_dir(provider, site, protocol)),
|
command=_mkdir_command(provider, _state_dir(config, provider, site, protocol)),
|
||||||
message="create AMPG-owned state directory for daemon config, keys, and logs",
|
message="create AMPG-owned state directory for daemon config, keys, and logs",
|
||||||
),
|
),
|
||||||
_step(
|
_step(
|
||||||
@@ -181,7 +182,7 @@ def _protocol_install_steps(
|
|||||||
provider,
|
provider,
|
||||||
stage="config",
|
stage="config",
|
||||||
action="write-managed-config",
|
action="write-managed-config",
|
||||||
target=str(_state_dir(provider, site, protocol) / "config"),
|
target=str(_state_dir(config, provider, site, protocol) / "config"),
|
||||||
status="planned",
|
status="planned",
|
||||||
command="-",
|
command="-",
|
||||||
message=f"write managed {adapter.backend} config after plan review",
|
message=f"write managed {adapter.backend} config after plan review",
|
||||||
@@ -212,6 +213,7 @@ def _protocol_install_steps(
|
|||||||
|
|
||||||
|
|
||||||
def _protocol_install_artifacts(
|
def _protocol_install_artifacts(
|
||||||
|
config: GatewayConfig,
|
||||||
site: SiteConfig,
|
site: SiteConfig,
|
||||||
protocol: ProtocolConfig,
|
protocol: ProtocolConfig,
|
||||||
provider: PlatformProvider,
|
provider: PlatformProvider,
|
||||||
@@ -227,7 +229,7 @@ def _protocol_install_artifacts(
|
|||||||
provider,
|
provider,
|
||||||
kind="daemon-config",
|
kind="daemon-config",
|
||||||
filename=_daemon_config_filename(protocol),
|
filename=_daemon_config_filename(protocol),
|
||||||
content=_daemon_config_content(site, protocol, provider),
|
content=_daemon_config_content(config, site, protocol, provider),
|
||||||
),
|
),
|
||||||
_artifact(
|
_artifact(
|
||||||
site,
|
site,
|
||||||
@@ -236,11 +238,12 @@ def _protocol_install_artifacts(
|
|||||||
kind="daemon-supervisor",
|
kind="daemon-supervisor",
|
||||||
filename=_supervisor_filename(provider, protocol.daemon),
|
filename=_supervisor_filename(provider, protocol.daemon),
|
||||||
content=_supervisor_content(
|
content=_supervisor_content(
|
||||||
|
config,
|
||||||
site,
|
site,
|
||||||
protocol,
|
protocol,
|
||||||
provider,
|
provider,
|
||||||
service_suffix=protocol.daemon,
|
service_suffix=protocol.daemon,
|
||||||
command=_daemon_start_command(site, protocol, provider),
|
command=_daemon_start_command(config, site, protocol, provider),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
@@ -253,7 +256,7 @@ def _protocol_install_artifacts(
|
|||||||
provider,
|
provider,
|
||||||
kind="loopback-config",
|
kind="loopback-config",
|
||||||
filename="nginx-loopback.conf",
|
filename="nginx-loopback.conf",
|
||||||
content=_managed_nginx_loopback_content(site, protocol, provider),
|
content=_managed_nginx_loopback_content(config, site, protocol, provider),
|
||||||
),
|
),
|
||||||
_artifact(
|
_artifact(
|
||||||
site,
|
site,
|
||||||
@@ -262,11 +265,12 @@ def _protocol_install_artifacts(
|
|||||||
kind="loopback-supervisor",
|
kind="loopback-supervisor",
|
||||||
filename=_supervisor_filename(provider, "nginx-loopback"),
|
filename=_supervisor_filename(provider, "nginx-loopback"),
|
||||||
content=_supervisor_content(
|
content=_supervisor_content(
|
||||||
|
config,
|
||||||
site,
|
site,
|
||||||
protocol,
|
protocol,
|
||||||
provider,
|
provider,
|
||||||
service_suffix="nginx-loopback",
|
service_suffix="nginx-loopback",
|
||||||
command=_loopback_start_command(site, protocol, provider),
|
command=_loopback_start_command(config, site, protocol, provider),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -347,11 +351,13 @@ def _supervisor_command(
|
|||||||
|
|
||||||
|
|
||||||
def _state_dir(
|
def _state_dir(
|
||||||
|
config: GatewayConfig,
|
||||||
provider: PlatformProvider,
|
provider: PlatformProvider,
|
||||||
site: SiteConfig,
|
site: SiteConfig,
|
||||||
protocol: ProtocolConfig,
|
protocol: ProtocolConfig,
|
||||||
) -> Path:
|
) -> Path:
|
||||||
return provider.state_root / site.id / protocol.name
|
root = config.paths.state_dir if config.paths else provider.state_root
|
||||||
|
return root / site.id / protocol.name
|
||||||
|
|
||||||
|
|
||||||
def _artifact_dir(
|
def _artifact_dir(
|
||||||
@@ -387,11 +393,12 @@ def _daemon_config_filename(protocol: ProtocolConfig) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def _daemon_config_content(
|
def _daemon_config_content(
|
||||||
|
config: GatewayConfig,
|
||||||
site: SiteConfig,
|
site: SiteConfig,
|
||||||
protocol: ProtocolConfig,
|
protocol: ProtocolConfig,
|
||||||
provider: PlatformProvider,
|
provider: PlatformProvider,
|
||||||
) -> str:
|
) -> str:
|
||||||
state_dir = _state_dir(provider, site, protocol)
|
state_dir = _state_dir(config, provider, site, protocol)
|
||||||
if protocol.name == "tor":
|
if protocol.name == "tor":
|
||||||
port = int(protocol.options.get("local_port", 18080))
|
port = int(protocol.options.get("local_port", 18080))
|
||||||
hidden_service_dir = state_dir / "hidden-service"
|
hidden_service_dir = state_dir / "hidden-service"
|
||||||
@@ -423,7 +430,7 @@ certificate = {state_dir / f"{site.id}.crt"}
|
|||||||
private_key = {state_dir / f"{site.id}.key"}
|
private_key = {state_dir / f"{site.id}.key"}
|
||||||
"""
|
"""
|
||||||
if protocol.name == "clearnet":
|
if protocol.name == "clearnet":
|
||||||
return _managed_nginx_server_content(site, protocol, provider)
|
return _managed_nginx_server_content(config, site, protocol, provider)
|
||||||
return f"""# Generated by AMPG. Review before installing.
|
return f"""# Generated by AMPG. Review before installing.
|
||||||
daemon = {protocol.daemon}
|
daemon = {protocol.daemon}
|
||||||
protocol = {protocol.name}
|
protocol = {protocol.name}
|
||||||
@@ -433,11 +440,12 @@ state_dir = {state_dir}
|
|||||||
|
|
||||||
|
|
||||||
def _managed_nginx_server_content(
|
def _managed_nginx_server_content(
|
||||||
|
config: GatewayConfig,
|
||||||
site: SiteConfig,
|
site: SiteConfig,
|
||||||
protocol: ProtocolConfig,
|
protocol: ProtocolConfig,
|
||||||
provider: PlatformProvider,
|
provider: PlatformProvider,
|
||||||
) -> str:
|
) -> str:
|
||||||
state_dir = _state_dir(provider, site, protocol)
|
state_dir = _state_dir(config, provider, site, protocol)
|
||||||
return f"""# Generated by AMPG. Review before installing.
|
return f"""# Generated by AMPG. Review before installing.
|
||||||
worker_processes 1;
|
worker_processes 1;
|
||||||
error_log {state_dir / "nginx-error.log"} notice;
|
error_log {state_dir / "nginx-error.log"} notice;
|
||||||
@@ -465,11 +473,12 @@ http {{
|
|||||||
|
|
||||||
|
|
||||||
def _managed_nginx_loopback_content(
|
def _managed_nginx_loopback_content(
|
||||||
|
config: GatewayConfig,
|
||||||
site: SiteConfig,
|
site: SiteConfig,
|
||||||
protocol: ProtocolConfig,
|
protocol: ProtocolConfig,
|
||||||
provider: PlatformProvider,
|
provider: PlatformProvider,
|
||||||
) -> str:
|
) -> str:
|
||||||
state_dir = _state_dir(provider, site, protocol)
|
state_dir = _state_dir(config, provider, site, protocol)
|
||||||
port = int(protocol.options.get("local_port", 18080 if protocol.name == "tor" else 18081))
|
port = int(protocol.options.get("local_port", 18080 if protocol.name == "tor" else 18081))
|
||||||
return f"""# Generated by AMPG. Review before installing.
|
return f"""# Generated by AMPG. Review before installing.
|
||||||
worker_processes 1;
|
worker_processes 1;
|
||||||
@@ -505,6 +514,7 @@ def _supervisor_filename(provider: PlatformProvider, service_suffix: str) -> str
|
|||||||
|
|
||||||
|
|
||||||
def _supervisor_content(
|
def _supervisor_content(
|
||||||
|
config: GatewayConfig,
|
||||||
site: SiteConfig,
|
site: SiteConfig,
|
||||||
protocol: ProtocolConfig,
|
protocol: ProtocolConfig,
|
||||||
provider: PlatformProvider,
|
provider: PlatformProvider,
|
||||||
@@ -522,7 +532,7 @@ Wants=network-online.target
|
|||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
Type=simple
|
||||||
WorkingDirectory={_state_dir(provider, site, protocol)}
|
WorkingDirectory={_state_dir(config, provider, site, protocol)}
|
||||||
ExecStart=/bin/sh -lc '{_shell_single_quote(command)}'
|
ExecStart=/bin/sh -lc '{_shell_single_quote(command)}'
|
||||||
Restart=on-failure
|
Restart=on-failure
|
||||||
RestartSec=5
|
RestartSec=5
|
||||||
@@ -555,17 +565,18 @@ WantedBy=multi-user.target
|
|||||||
return f"""#!{shell}
|
return f"""#!{shell}
|
||||||
# Generated by AMPG. Review before installing.
|
# Generated by AMPG. Review before installing.
|
||||||
set -eu
|
set -eu
|
||||||
mkdir -p {_state_dir(provider, site, protocol)}
|
mkdir -p {_state_dir(config, provider, site, protocol)}
|
||||||
{command}
|
{command}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def _daemon_start_command(
|
def _daemon_start_command(
|
||||||
|
config: GatewayConfig,
|
||||||
site: SiteConfig,
|
site: SiteConfig,
|
||||||
protocol: ProtocolConfig,
|
protocol: ProtocolConfig,
|
||||||
provider: PlatformProvider,
|
provider: PlatformProvider,
|
||||||
) -> str:
|
) -> str:
|
||||||
state_dir = _state_dir(provider, site, protocol)
|
state_dir = _state_dir(config, provider, site, protocol)
|
||||||
if protocol.name == "tor":
|
if protocol.name == "tor":
|
||||||
return f"exec tor -f {state_dir / 'torrc'}"
|
return f"exec tor -f {state_dir / 'torrc'}"
|
||||||
if protocol.name == "i2p":
|
if protocol.name == "i2p":
|
||||||
@@ -585,11 +596,12 @@ def _daemon_start_command(
|
|||||||
|
|
||||||
|
|
||||||
def _loopback_start_command(
|
def _loopback_start_command(
|
||||||
|
config: GatewayConfig,
|
||||||
site: SiteConfig,
|
site: SiteConfig,
|
||||||
protocol: ProtocolConfig,
|
protocol: ProtocolConfig,
|
||||||
provider: PlatformProvider,
|
provider: PlatformProvider,
|
||||||
) -> str:
|
) -> str:
|
||||||
state_dir = _state_dir(provider, site, protocol)
|
state_dir = _state_dir(config, provider, site, protocol)
|
||||||
return f"exec nginx -c {state_dir / 'nginx-loopback.conf'} -g 'daemon off;'"
|
return f"exec nginx -c {state_dir / 'nginx-loopback.conf'} -g 'daemon off;'"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -111,7 +111,8 @@ render-only environment while making daemon ownership decisions explicit.
|
|||||||
|
|
||||||
`ampg apply --dry-run` turns daemon status and generated config artifacts into an ordered
|
`ampg apply --dry-run` turns daemon status and generated config artifacts into an ordered
|
||||||
activation sequence. Each enabled protocol gets output, artifact, daemon, and health
|
activation sequence. Each enabled protocol gets output, artifact, daemon, and health
|
||||||
steps. Blocked steps stop the command before any live apply path can touch services.
|
steps, plus an address step when AMPG must capture or confirm a generated transport
|
||||||
|
identity. Blocked steps stop the command before any live apply path can touch services.
|
||||||
|
|
||||||
The dry-run command is intentionally stricter than `doctor`: missing generated output is
|
The dry-run command is intentionally stricter than `doctor`: missing generated output is
|
||||||
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
|
||||||
@@ -126,7 +127,7 @@ future live apply support.
|
|||||||
|
|
||||||
With `--write-artifacts`, install-plan writes managed config and supervisor files under
|
With `--write-artifacts`, install-plan writes managed config and supervisor files under
|
||||||
the configured `plan_root`; those files are review artifacts, not installed service
|
the configured `plan_root`; those files are review artifacts, not installed service
|
||||||
state.
|
state. Generated managed-daemon configs point runtime state at `gateway.state_dir`.
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
@@ -112,8 +112,8 @@ workflow.
|
|||||||
`[gateway]` paths are resolved relative to `gateway.toml` when they are not absolute.
|
`[gateway]` paths are resolved relative to `gateway.toml` when they are not absolute.
|
||||||
|
|
||||||
`state_dir`
|
`state_dir`
|
||||||
: AMPG-owned durable state. Managed daemon identities, generated transport state, and
|
: AMPG-owned durable state. Managed daemon identities, generated transport state, daemon
|
||||||
the captured address registry live here.
|
logs, daemon-written addresses, and the captured address registry live here.
|
||||||
|
|
||||||
`cache_dir`
|
`cache_dir`
|
||||||
: Rebuildable intermediate data.
|
: Rebuildable intermediate data.
|
||||||
|
|||||||
@@ -66,7 +66,8 @@ blocked.
|
|||||||
`install-plan --write-artifacts` writes managed daemon configs and platform supervisor
|
`install-plan --write-artifacts` writes managed daemon configs and platform supervisor
|
||||||
files under the configured `plan_root`, grouped by site, protocol, and platform. For
|
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
|
Tor/I2P HTTP publishing, AMPG writes both transport-daemon artifacts and loopback nginx
|
||||||
artifacts.
|
artifacts. The generated daemon configs store runtime state, identity keys, logs, and
|
||||||
|
daemon-written addresses under `gateway.state_dir`.
|
||||||
|
|
||||||
`addresses list` prints the effective public address for each enabled protocol:
|
`addresses list` prints the effective public address for each enabled protocol:
|
||||||
configured, captured, derived, or placeholder. `addresses capture` reads daemon-written
|
configured, captured, derived, or placeholder. `addresses capture` reads daemon-written
|
||||||
@@ -78,10 +79,10 @@ target real transport URLs; preview checks target local loopback preview endpoin
|
|||||||
Missing output blocks the plan, while placeholder transport addresses are review items.
|
Missing output blocks the plan, while placeholder transport addresses are review items.
|
||||||
|
|
||||||
`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, address capture or reuse,
|
||||||
checks. It exits nonzero when any step is blocked. `--write-artifacts` writes reviewable
|
and post-apply health checks. It exits nonzero when any step is blocked. `--write-artifacts`
|
||||||
config snippets to the configured plan root but still does not install or reload
|
writes reviewable config snippets to the configured plan root but still does not install
|
||||||
services.
|
or reload services.
|
||||||
|
|
||||||
`--protocol` scopes operational commands to selected enabled protocols. A clearnet
|
`--protocol` scopes operational commands to selected enabled protocols. A clearnet
|
||||||
`adopt` failure will block a full activation run, but it will not block
|
`adopt` failure will block a full activation run, but it will not block
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ plan_root = "./plan"
|
|||||||
|
|
||||||
|
|
||||||
class ActivationTest(unittest.TestCase):
|
class ActivationTest(unittest.TestCase):
|
||||||
def test_activation_plan_is_ready_when_output_and_daemon_policy_are_ready(self):
|
def test_activation_plan_reviews_placeholder_address_before_health(self):
|
||||||
with tempfile.TemporaryDirectory() as tmp:
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
root = Path(tmp)
|
root = Path(tmp)
|
||||||
config_path = _write_config(
|
config_path = _write_config(
|
||||||
@@ -79,9 +79,42 @@ local_port = 19081
|
|||||||
by_stage = {step.stage: step for step in steps if step.stage != "artifact"}
|
by_stage = {step.stage: step for step in steps if step.stage != "artifact"}
|
||||||
self.assertEqual("ready", by_stage["output"].status)
|
self.assertEqual("ready", by_stage["output"].status)
|
||||||
self.assertEqual("manage-owned", by_stage["daemon"].action)
|
self.assertEqual("manage-owned", by_stage["daemon"].action)
|
||||||
self.assertEqual("planned", by_stage["health"].status)
|
self.assertEqual("capture-address", by_stage["address"].action)
|
||||||
|
self.assertEqual("review", by_stage["address"].status)
|
||||||
|
self.assertIn("addresses capture --protocol i2p", by_stage["address"].command)
|
||||||
|
self.assertEqual("review", by_stage["health"].status)
|
||||||
self.assertTrue(any(step.stage == "artifact" for step in steps))
|
self.assertTrue(any(step.stage == "artifact" for step in steps))
|
||||||
|
|
||||||
|
def test_configured_address_allows_planned_activation_health(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"
|
||||||
|
i2p_url = "ready.b32.i2p"
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
output = root / "out/i2p"
|
||||||
|
output.mkdir(parents=True)
|
||||||
|
(output / ".ampg-output").write_text("generated by AMPG\n", encoding="utf-8")
|
||||||
|
config = load_config(config_path)
|
||||||
|
|
||||||
|
steps = activation_steps(
|
||||||
|
config,
|
||||||
|
platform_provider=platform_by_name("android-termux"),
|
||||||
|
daemon_probe=_probe(installed=False, running=False),
|
||||||
|
)
|
||||||
|
|
||||||
|
by_stage = {step.stage: step for step in steps if step.stage != "artifact"}
|
||||||
|
self.assertEqual("use-address", by_stage["address"].action)
|
||||||
|
self.assertEqual("ready", by_stage["address"].status)
|
||||||
|
self.assertEqual("planned", by_stage["health"].status)
|
||||||
|
|
||||||
def test_missing_output_blocks_activation(self):
|
def test_missing_output_blocks_activation(self):
|
||||||
with tempfile.TemporaryDirectory() as tmp:
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
config_path = _write_config(
|
config_path = _write_config(
|
||||||
@@ -162,6 +195,7 @@ port = 1965
|
|||||||
output_text = stdout.getvalue()
|
output_text = stdout.getvalue()
|
||||||
self.assertIn("AMPG_APPLY_ARTIFACT", output_text)
|
self.assertIn("AMPG_APPLY_ARTIFACT", output_text)
|
||||||
self.assertIn("AMPG_APPLY_STEP", output_text)
|
self.assertIn("AMPG_APPLY_STEP", output_text)
|
||||||
|
self.assertIn('command="-"', output_text)
|
||||||
self.assertIn("blocked=0", output_text)
|
self.assertIn("blocked=0", output_text)
|
||||||
artifact_line = next(
|
artifact_line = next(
|
||||||
line for line in output_text.splitlines() if line.startswith("AMPG_APPLY_ARTIFACT")
|
line for line in output_text.splitlines() if line.startswith("AMPG_APPLY_ARTIFACT")
|
||||||
|
|||||||
@@ -189,7 +189,7 @@ daemon_policy = "auto"
|
|||||||
|
|
||||||
def test_managed_i2p_artifacts_include_daemon_loopback_and_supervisors(self):
|
def test_managed_i2p_artifacts_include_daemon_loopback_and_supervisors(self):
|
||||||
with tempfile.TemporaryDirectory() as tmp:
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
root = Path(tmp)
|
root = Path(tmp).resolve()
|
||||||
config = load_config(
|
config = load_config(
|
||||||
_write_config(
|
_write_config(
|
||||||
root,
|
root,
|
||||||
@@ -223,8 +223,12 @@ tunnel_name = "example-web"
|
|||||||
set(by_name),
|
set(by_name),
|
||||||
)
|
)
|
||||||
self.assertIn("[example-web]", by_name["i2pd-tunnels.conf"].content)
|
self.assertIn("[example-web]", by_name["i2pd-tunnels.conf"].content)
|
||||||
self.assertIn("keys =", by_name["i2pd-tunnels.conf"].content)
|
self.assertIn(
|
||||||
|
f"keys = {root / '.ampg/state/example/i2p/example-web.dat'}",
|
||||||
|
by_name["i2pd-tunnels.conf"].content,
|
||||||
|
)
|
||||||
self.assertIn("listen 127.0.0.1:19081", by_name["nginx-loopback.conf"].content)
|
self.assertIn("listen 127.0.0.1:19081", by_name["nginx-loopback.conf"].content)
|
||||||
|
self.assertIn(f"mkdir -p {root / '.ampg/state/example/i2p'}", by_name["i2pd.run"].content)
|
||||||
self.assertIn("exec i2pd", by_name["i2pd.run"].content)
|
self.assertIn("exec i2pd", by_name["i2pd.run"].content)
|
||||||
self.assertIn("exec nginx", by_name["nginx-loopback.run"].content)
|
self.assertIn("exec nginx", by_name["nginx-loopback.run"].content)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user