mirror of
https://github.com/Such-Software/adaptive-multi-protocol-gateway.git
synced 2026-10-17 07:31:32 +02:00
Add state stage deploy apply
This commit is contained in:
@@ -56,15 +56,18 @@ python3 -m ampg --config gateway.toml build
|
||||
python3 -m ampg --config gateway.toml doctor
|
||||
python3 -m ampg --config gateway.toml install-plan --profile vps-full --write-artifacts
|
||||
python3 -m ampg --config gateway.toml approvals list --profile vps-full
|
||||
python3 -m ampg --config gateway.toml approvals approve --profile vps-full --all
|
||||
python3 -m ampg --config gateway.toml apply --dry-run --profile vps-full
|
||||
python3 -m ampg --config gateway.toml deploy apply --stage state --dry-run --profile vps-full
|
||||
```
|
||||
|
||||
`init site` writes a readable `gateway.toml` with selected transports enabled and common
|
||||
transports left as one-line toggles. `deploy plan` condenses the lower-level checks into
|
||||
clear next steps. `dns plan` covers static DNS, Dynamic DNS, and behind-router
|
||||
reachability hints. The current `apply --dry-run` prints the activation sequence and a
|
||||
preflight verdict without changing services. Live apply support will perform only
|
||||
approved changes.
|
||||
reachability hints. `apply --dry-run` prints the activation sequence and a preflight
|
||||
verdict without changing services. `deploy apply --stage state` copies only approved
|
||||
managed-daemon artifacts into AMPG-owned state; later live stages will install and start
|
||||
services after the same review gate.
|
||||
|
||||
## Interaction tiers
|
||||
|
||||
@@ -126,6 +129,8 @@ python3 -m ampg --config examples/wownero.gateway.toml apply --dry-run --protoco
|
||||
python3 -m ampg --config examples/wownero.gateway.toml apply --dry-run --write-artifacts
|
||||
python3 -m ampg --config examples/wownero.gateway.toml approvals list --profile mobile-i2p
|
||||
python3 -m ampg --config examples/wownero.gateway.toml approvals approve --profile mobile-i2p --all
|
||||
python3 -m ampg --config examples/wownero.gateway.toml deploy apply --stage state --dry-run --profile mobile-i2p
|
||||
python3 -m ampg --config examples/wownero.gateway.toml deploy apply --stage state --profile mobile-i2p --yes
|
||||
python3 -m ampg --config examples/wownero.gateway.toml build
|
||||
python3 -m ampg --config examples/wownero.gateway.toml build --profile tor-i2p
|
||||
python3 -m ampg --config examples/wownero.gateway.toml build --protocol tor --protocol i2p
|
||||
@@ -173,6 +178,11 @@ would be copied into managed state and the supervisor services that would be reg
|
||||
or started. The preflight gate reports `blocked`, `review`, or `ready` across activation
|
||||
steps, managed-state copies, and supervisor actions.
|
||||
|
||||
`deploy apply --stage state --dry-run` shows approved managed-daemon artifacts that are
|
||||
ready to copy into `gateway.state_dir`. The live form requires `--yes`; it creates only
|
||||
AMPG-owned state directories and copies approved artifact contents. It does not install
|
||||
packages, change adopted daemons, start services, or remove keys.
|
||||
|
||||
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
|
||||
unselected transports blocking the run.
|
||||
@@ -193,4 +203,5 @@ Explicit `--protocol` and `--platform` flags override the profile when present.
|
||||
- [ ] Run `ampg health-plan`; review post-start transport checks.
|
||||
- [ ] Run `ampg approvals approve --all` after reviewing generated artifacts.
|
||||
- [ ] Run `ampg apply --dry-run`; review the activation sequence and preflight gate.
|
||||
- [ ] Run `ampg deploy apply --stage state --dry-run`; confirm approved state copies.
|
||||
- [ ] Run live apply after the preflight gate is ready.
|
||||
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
|
||||
from .config import GatewayConfig
|
||||
from .install_plan import InstallStateCopy, install_state_copies
|
||||
from .platforms import PlatformProvider
|
||||
from .state_contract import gateway_state_root
|
||||
from .status import DaemonProbeFunc
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class StateApplyResult:
|
||||
site_id: str
|
||||
protocol: str
|
||||
platform: str
|
||||
kind: str
|
||||
source: Path
|
||||
target: Path
|
||||
status: str
|
||||
mode: str
|
||||
message: str
|
||||
|
||||
|
||||
def apply_state(
|
||||
config: GatewayConfig,
|
||||
*,
|
||||
dry_run: bool,
|
||||
platform_provider: PlatformProvider | None = None,
|
||||
daemon_probe: DaemonProbeFunc | None = None,
|
||||
) -> list[StateApplyResult]:
|
||||
copies = install_state_copies(
|
||||
config,
|
||||
platform_provider=platform_provider,
|
||||
daemon_probe=daemon_probe,
|
||||
)
|
||||
results: list[StateApplyResult] = []
|
||||
for copy in copies:
|
||||
validation = _validate_state_copy(config, copy, dry_run=dry_run)
|
||||
if validation.status == "blocked":
|
||||
results.append(validation)
|
||||
continue
|
||||
if not dry_run:
|
||||
copy.target.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(copy.source, copy.target)
|
||||
results.append(
|
||||
StateApplyResult(
|
||||
site_id=copy.site_id,
|
||||
protocol=copy.protocol,
|
||||
platform=copy.platform,
|
||||
kind=copy.kind,
|
||||
source=copy.source,
|
||||
target=copy.target,
|
||||
status="planned" if dry_run else "written",
|
||||
mode="dry-run" if dry_run else "live",
|
||||
message=(
|
||||
"would copy approved artifact into AMPG-owned state"
|
||||
if dry_run
|
||||
else "copied approved artifact into AMPG-owned state"
|
||||
),
|
||||
)
|
||||
)
|
||||
return results
|
||||
|
||||
|
||||
def _validate_state_copy(
|
||||
config: GatewayConfig,
|
||||
copy: InstallStateCopy,
|
||||
*,
|
||||
dry_run: bool,
|
||||
) -> StateApplyResult:
|
||||
if copy.status != "planned":
|
||||
return _result(
|
||||
copy,
|
||||
status="blocked",
|
||||
mode="dry-run" if dry_run else "live",
|
||||
message=copy.message,
|
||||
)
|
||||
state_root = gateway_state_root(config).resolve()
|
||||
target = copy.target.resolve()
|
||||
if not _is_relative_to(target, state_root):
|
||||
return _result(
|
||||
copy,
|
||||
status="blocked",
|
||||
mode="dry-run" if dry_run else "live",
|
||||
message=f"target is outside AMPG state root: {target}",
|
||||
)
|
||||
if not copy.source.exists():
|
||||
return _result(
|
||||
copy,
|
||||
status="blocked",
|
||||
mode="dry-run" if dry_run else "live",
|
||||
message="approved source artifact is missing",
|
||||
)
|
||||
if copy.source.is_dir():
|
||||
return _result(
|
||||
copy,
|
||||
status="blocked",
|
||||
mode="dry-run" if dry_run else "live",
|
||||
message="approved source artifact is a directory",
|
||||
)
|
||||
return _result(
|
||||
copy,
|
||||
status="ready",
|
||||
mode="dry-run" if dry_run else "live",
|
||||
message="state copy passed safety checks",
|
||||
)
|
||||
|
||||
|
||||
def _result(
|
||||
copy: InstallStateCopy,
|
||||
*,
|
||||
status: str,
|
||||
mode: str,
|
||||
message: str,
|
||||
) -> StateApplyResult:
|
||||
return StateApplyResult(
|
||||
site_id=copy.site_id,
|
||||
protocol=copy.protocol,
|
||||
platform=copy.platform,
|
||||
kind=copy.kind,
|
||||
source=copy.source,
|
||||
target=copy.target,
|
||||
status=status,
|
||||
mode=mode,
|
||||
message=message,
|
||||
)
|
||||
|
||||
|
||||
def _is_relative_to(path: Path, parent: Path) -> bool:
|
||||
try:
|
||||
path.relative_to(parent)
|
||||
except ValueError:
|
||||
return False
|
||||
return True
|
||||
+71
@@ -21,6 +21,7 @@ from .addresses import (
|
||||
effective_address_records,
|
||||
set_address,
|
||||
)
|
||||
from .apply import StateApplyResult, apply_state
|
||||
from .approvals import (
|
||||
ACTIVATION_ARTIFACT_KIND,
|
||||
GENERIC_PLATFORM,
|
||||
@@ -143,6 +144,32 @@ def main(argv: list[str] | None = None) -> int:
|
||||
choices=PLATFORM_NAMES,
|
||||
help="Override platform detection for deploy planning.",
|
||||
)
|
||||
deploy_apply_parser = deploy_subcommands.add_parser(
|
||||
"apply",
|
||||
help="Apply an approved deployment stage.",
|
||||
)
|
||||
_add_target_selection(deploy_apply_parser)
|
||||
deploy_apply_parser.add_argument(
|
||||
"--stage",
|
||||
choices=("state",),
|
||||
required=True,
|
||||
help="Deployment stage to apply.",
|
||||
)
|
||||
deploy_apply_parser.add_argument(
|
||||
"--dry-run",
|
||||
action="store_true",
|
||||
help="Show what would change without writing files.",
|
||||
)
|
||||
deploy_apply_parser.add_argument(
|
||||
"--yes",
|
||||
action="store_true",
|
||||
help="Required for live stage application.",
|
||||
)
|
||||
deploy_apply_parser.add_argument(
|
||||
"--platform",
|
||||
choices=PLATFORM_NAMES,
|
||||
help="Override platform detection for deploy apply.",
|
||||
)
|
||||
dns_parser = subcommands.add_parser(
|
||||
"dns",
|
||||
help="Plan and check clearnet DNS records.",
|
||||
@@ -530,6 +557,35 @@ def _cmd_deploy(config, args) -> int:
|
||||
_print_deploy_next(index, next_step)
|
||||
_print_deploy_summary(config, plan)
|
||||
return 1 if plan.status == "blocked" else 0
|
||||
if args.deploy_command == "apply":
|
||||
if not args.dry_run and not args.yes:
|
||||
print(
|
||||
'AMPG_DEPLOY_APPLY status=error message="live deploy apply requires --yes or rerun with --dry-run"',
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
platform_provider = _platform_override(config, args)
|
||||
if args.stage == "state":
|
||||
results = apply_state(
|
||||
config,
|
||||
dry_run=args.dry_run,
|
||||
platform_provider=platform_provider,
|
||||
)
|
||||
for result in results:
|
||||
_print_state_apply_result(result)
|
||||
blocked = [result for result in results if result.status == "blocked"]
|
||||
written = [result for result in results if result.status == "written"]
|
||||
planned = [result for result in results if result.status == "planned"]
|
||||
print(
|
||||
"AMPG_DEPLOY_APPLY_SUMMARY "
|
||||
f"stage=state "
|
||||
f"mode={'dry-run' if args.dry_run else 'live'} "
|
||||
f"results={len(results)} "
|
||||
f"planned={len(planned)} "
|
||||
f"written={len(written)} "
|
||||
f"blocked={len(blocked)}"
|
||||
)
|
||||
return 1 if blocked else 0
|
||||
return 1
|
||||
|
||||
|
||||
@@ -1000,6 +1056,21 @@ def _print_deploy_summary(config, plan: DeployPlan) -> None:
|
||||
)
|
||||
|
||||
|
||||
def _print_state_apply_result(result: StateApplyResult) -> None:
|
||||
print(
|
||||
"AMPG_DEPLOY_STATE "
|
||||
f"site={result.site_id} "
|
||||
f"protocol={result.protocol} "
|
||||
f"platform={result.platform} "
|
||||
f"kind={result.kind} "
|
||||
f"mode={result.mode} "
|
||||
f"source=\"{_quote(str(result.source))}\" "
|
||||
f"target=\"{_quote(str(result.target))}\" "
|
||||
f"status={result.status} "
|
||||
f"message=\"{_quote(result.message)}\""
|
||||
)
|
||||
|
||||
|
||||
def _print_dns_record(record: DNSRecordPlan) -> None:
|
||||
print(
|
||||
"AMPG_DNS_RECORD "
|
||||
|
||||
@@ -131,6 +131,12 @@ managed state and planned supervisor start/reload actions. The command emits an
|
||||
Blocked or unapproved preflight items stop mutation before any live apply path can touch
|
||||
services.
|
||||
|
||||
`ampg deploy apply --stage state` is the narrow live stage for managed state. It reads
|
||||
the same approved artifact plan, refuses missing, stale, or unapproved inputs, and copies
|
||||
only approved generated daemon config into AMPG-owned state. Service installation,
|
||||
supervisor registration, daemon start, address capture, and health verification remain
|
||||
separate stages.
|
||||
|
||||
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
|
||||
root.
|
||||
|
||||
@@ -55,6 +55,8 @@ python3 -m ampg --config gateway.toml apply --dry-run --protocol i2p
|
||||
python3 -m ampg --config gateway.toml apply --dry-run --write-artifacts
|
||||
python3 -m ampg --config gateway.toml approvals list --profile mobile-i2p
|
||||
python3 -m ampg --config gateway.toml approvals approve --profile mobile-i2p --all
|
||||
python3 -m ampg --config gateway.toml deploy apply --stage state --dry-run --profile mobile-i2p
|
||||
python3 -m ampg --config gateway.toml deploy apply --stage state --profile mobile-i2p --yes
|
||||
python3 -m ampg --config gateway.toml doctor --platform android-termux
|
||||
```
|
||||
|
||||
@@ -112,6 +114,12 @@ reviewable config snippets to the configured plan root, then prints the copies f
|
||||
approved artifacts into `gateway.state_dir` and the supervisor services that would be
|
||||
registered or started. It still does not install or reload services.
|
||||
|
||||
`deploy apply --stage state` is the first live apply stage. Dry-run mode prints
|
||||
`AMPG_DEPLOY_STATE` rows. Live mode requires `--yes`, refuses unapproved or stale
|
||||
artifacts, creates AMPG-owned state directories, and copies approved managed-daemon
|
||||
config into `gateway.state_dir`. It does not install packages, edit adopted daemon
|
||||
configs, start services, or delete identity material.
|
||||
|
||||
`--protocol` scopes operational commands to selected enabled protocols. A clearnet
|
||||
`adopt` failure will block a full activation run, but it will not block
|
||||
`apply --dry-run --protocol i2p` because clearnet is outside that selected run.
|
||||
|
||||
@@ -46,5 +46,9 @@ AMPG must never delete sensitive contract paths without an explicit destructive
|
||||
approved artifacts would be copied into managed state and `AMPG_APPLY_SUPERVISOR` rows
|
||||
for the services that would be registered or started. The accompanying
|
||||
`AMPG_APPLY_PREFLIGHT` verdict includes those rows so live apply can refuse mutation when
|
||||
state or supervisor inputs still need review. Live apply should perform approved copies,
|
||||
start daemons, capture public addresses, and then run published health checks.
|
||||
state or supervisor inputs still need review.
|
||||
|
||||
`deploy apply --stage state` performs the approved copy portion only. It requires
|
||||
`--yes` for live writes, refuses unapproved or stale artifacts, writes only under
|
||||
`gateway.state_dir`, and leaves daemon start, address capture, and published health
|
||||
checks for later stages.
|
||||
|
||||
@@ -141,6 +141,8 @@ daemon_policy = "auto"
|
||||
- `ampg build` writes a fixture manifest that AMPB can check.
|
||||
- `ampg preview manifest` writes loopback fixture URLs that AMPB can check before live
|
||||
transport daemons are installed or adopted.
|
||||
- `ampg deploy apply --stage state --dry-run` shows approved managed-daemon config copies
|
||||
before any Wownero transport service is started.
|
||||
- Tor output contains no `<script>` tags or inline event handlers.
|
||||
- Generated Gemini output has readable headings, paragraphs, and links.
|
||||
- Generated Micron output fits terminal-first browsing.
|
||||
@@ -158,6 +160,10 @@ python3 -m ampg --config examples/wownero.gateway.toml manifest
|
||||
python3 -m ampg --config examples/wownero.gateway.toml addresses list
|
||||
python3 -m ampg --config examples/wownero.gateway.toml preview endpoints
|
||||
python3 -m ampg --config examples/wownero.gateway.toml preview manifest
|
||||
python3 -m ampg --config examples/wownero.gateway.toml install-plan --profile mobile-i2p --write-artifacts
|
||||
python3 -m ampg --config examples/wownero.gateway.toml approvals list --profile mobile-i2p
|
||||
python3 -m ampg --config examples/wownero.gateway.toml approvals approve --profile mobile-i2p --all
|
||||
python3 -m ampg --config examples/wownero.gateway.toml deploy apply --stage state --dry-run --profile mobile-i2p
|
||||
python3 -m ampg --config examples/i2p-only.gateway.toml plan
|
||||
python3 -m ampg --config examples/i2p-only.gateway.toml build
|
||||
```
|
||||
@@ -170,3 +176,4 @@ python3 -m ampg --config examples/i2p-only.gateway.toml build
|
||||
- [ ] Add Gemini conversion fixtures.
|
||||
- [ ] Add Reticulum/Micron conversion fixtures.
|
||||
- [ ] Enable I2P and Reticulum daemon management only after static outputs are stable.
|
||||
- [ ] Copy only reviewed Wownero daemon artifacts into managed state.
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
import contextlib
|
||||
import io
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from ampg.cli import main
|
||||
|
||||
|
||||
def _source(root: Path) -> Path:
|
||||
source = root / "site"
|
||||
source.mkdir()
|
||||
(source / "index.html").write_text("<h1>Hello</h1>", encoding="utf-8")
|
||||
return source
|
||||
|
||||
|
||||
def _run_cli(args: list[str]) -> tuple[int, str, str]:
|
||||
stdout = io.StringIO()
|
||||
stderr = io.StringIO()
|
||||
with contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr):
|
||||
status = main(args)
|
||||
return status, stdout.getvalue(), stderr.getvalue()
|
||||
|
||||
|
||||
def _init_i2p_config(root: Path) -> Path:
|
||||
config_path = root / "gateway.toml"
|
||||
status, _, error = _run_cli(
|
||||
[
|
||||
"--config",
|
||||
str(config_path),
|
||||
"init",
|
||||
"site",
|
||||
"example",
|
||||
"--domain",
|
||||
"example.test",
|
||||
"--source",
|
||||
str(_source(root)),
|
||||
"--protocol",
|
||||
"i2p",
|
||||
]
|
||||
)
|
||||
if status != 0:
|
||||
raise AssertionError(error)
|
||||
output = root / "dist/example/i2p"
|
||||
output.mkdir(parents=True)
|
||||
(output / ".ampg-output").write_text("generated by AMPG\n", encoding="utf-8")
|
||||
return config_path
|
||||
|
||||
|
||||
def _write_and_approve_artifacts(config_path: Path) -> None:
|
||||
status, _, error = _run_cli(
|
||||
[
|
||||
"--config",
|
||||
str(config_path),
|
||||
"apply",
|
||||
"--dry-run",
|
||||
"--write-artifacts",
|
||||
"--profile",
|
||||
"mobile-i2p",
|
||||
]
|
||||
)
|
||||
if status != 0:
|
||||
raise AssertionError(error)
|
||||
status, _, error = _run_cli(
|
||||
[
|
||||
"--config",
|
||||
str(config_path),
|
||||
"approvals",
|
||||
"approve",
|
||||
"--profile",
|
||||
"mobile-i2p",
|
||||
"--all",
|
||||
]
|
||||
)
|
||||
if status != 0:
|
||||
raise AssertionError(error)
|
||||
|
||||
|
||||
class DeployApplyTest(unittest.TestCase):
|
||||
def test_deploy_apply_state_blocks_unapproved_artifacts(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp).resolve()
|
||||
config_path = _init_i2p_config(root)
|
||||
|
||||
status, output, error = _run_cli(
|
||||
[
|
||||
"--config",
|
||||
str(config_path),
|
||||
"deploy",
|
||||
"apply",
|
||||
"--stage",
|
||||
"state",
|
||||
"--dry-run",
|
||||
"--profile",
|
||||
"mobile-i2p",
|
||||
]
|
||||
)
|
||||
|
||||
self.assertEqual(1, status)
|
||||
self.assertEqual("", error)
|
||||
self.assertIn("AMPG_DEPLOY_STATE", output)
|
||||
self.assertIn("status=blocked", output)
|
||||
self.assertIn("artifact is missing", output)
|
||||
|
||||
def test_deploy_apply_state_dry_run_after_approval(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp).resolve()
|
||||
config_path = _init_i2p_config(root)
|
||||
_write_and_approve_artifacts(config_path)
|
||||
|
||||
status, output, error = _run_cli(
|
||||
[
|
||||
"--config",
|
||||
str(config_path),
|
||||
"deploy",
|
||||
"apply",
|
||||
"--stage",
|
||||
"state",
|
||||
"--dry-run",
|
||||
"--profile",
|
||||
"mobile-i2p",
|
||||
]
|
||||
)
|
||||
|
||||
self.assertEqual(0, status, error)
|
||||
self.assertIn("AMPG_DEPLOY_STATE", output)
|
||||
self.assertIn("status=planned", output)
|
||||
self.assertIn("AMPG_DEPLOY_APPLY_SUMMARY stage=state mode=dry-run", output)
|
||||
self.assertIn("blocked=0", output)
|
||||
|
||||
def test_deploy_apply_state_live_requires_yes(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp).resolve()
|
||||
config_path = _init_i2p_config(root)
|
||||
_write_and_approve_artifacts(config_path)
|
||||
|
||||
status, _, error = _run_cli(
|
||||
[
|
||||
"--config",
|
||||
str(config_path),
|
||||
"deploy",
|
||||
"apply",
|
||||
"--stage",
|
||||
"state",
|
||||
"--profile",
|
||||
"mobile-i2p",
|
||||
]
|
||||
)
|
||||
|
||||
self.assertEqual(1, status)
|
||||
self.assertIn("requires --yes", error)
|
||||
|
||||
def test_deploy_apply_state_blocks_stale_approved_artifacts(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp).resolve()
|
||||
config_path = _init_i2p_config(root)
|
||||
_write_and_approve_artifacts(config_path)
|
||||
artifact = (
|
||||
root / "dist/ampg-plan/example/i2p/managed/android-termux/i2pd-tunnels.conf"
|
||||
)
|
||||
artifact.write_text(
|
||||
artifact.read_text(encoding="utf-8") + "# edited\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
status, output, error = _run_cli(
|
||||
[
|
||||
"--config",
|
||||
str(config_path),
|
||||
"deploy",
|
||||
"apply",
|
||||
"--stage",
|
||||
"state",
|
||||
"--dry-run",
|
||||
"--profile",
|
||||
"mobile-i2p",
|
||||
]
|
||||
)
|
||||
|
||||
self.assertEqual(1, status)
|
||||
self.assertEqual("", error)
|
||||
self.assertIn("status=blocked", output)
|
||||
self.assertIn("artifact differs from current generator output", output)
|
||||
|
||||
def test_deploy_apply_state_live_copies_approved_artifacts(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp).resolve()
|
||||
config_path = _init_i2p_config(root)
|
||||
_write_and_approve_artifacts(config_path)
|
||||
|
||||
status, output, error = _run_cli(
|
||||
[
|
||||
"--config",
|
||||
str(config_path),
|
||||
"deploy",
|
||||
"apply",
|
||||
"--stage",
|
||||
"state",
|
||||
"--profile",
|
||||
"mobile-i2p",
|
||||
"--yes",
|
||||
]
|
||||
)
|
||||
|
||||
daemon_config = root / ".ampg/state/example/i2p/i2pd-tunnels.conf"
|
||||
loopback_config = root / ".ampg/state/example/i2p/nginx-loopback.conf"
|
||||
daemon_config_exists = daemon_config.exists()
|
||||
loopback_config_exists = loopback_config.exists()
|
||||
|
||||
self.assertEqual(0, status, error)
|
||||
self.assertIn("status=written", output)
|
||||
self.assertTrue(daemon_config_exists)
|
||||
self.assertTrue(loopback_config_exists)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user