Files
apex.ermnvldmr.com/actions/tiers/link.py
T
deytenit 6aaef35780 fix: resolve 17 confirmed bash-parity drifts from port review
Adversarial parity review of every ported action against its bash source
confirmed 17 real behavioral drifts (of 22 claimed); all fixed here:

- tiers/chown: defaults were compositions/apex/tierN (one level too deep);
  now the tier roots. shared/ base reset is non-recursive again.
- compose: --dry-run was swallowed by argparse.REMAINDER; now stripped.
- host.write_file: rendered files land 0644 (tempfile's 0600 made fresh
  systemd units/crowdsec configs owner-only).
- systemd.deploy_units: top-level non-hidden files only.
- ufw: missing docker/host config dir exits 66; missing /etc/ufw target
  aborts; empty rules dir warns; dotfile .rules excluded.
- sync/packages: Telegram error-trap parity and failed notify exits 1.
  Same notify-failure exit for backup/run and sync/repository.
  repository regains the node-dir guard.
- tiers/useradd: no more secret-bearing .env.bak per run.
- tiers/link: EOF on confirm prompt declines and continues.
- extract-traefik-certs: sudo mkdir inside the notify envelope.
- generate-happ-subscriptions: realpath parent-exists guards + docker check.
- configure/crowdsec: bouncer install goes through install_packages.
- lint-docker-compose: malformed/unreadable anchor-IP inputs are lint errors.
- core compose: APEX_RESTIC_REPOSITORY now ?error; RESTIC_COMPRESSION
  per-node via APEX_RESTIC_COMPRESSION (default auto).
- sys.service_exists line-anchored; tpl docstring corrected.
2026-07-05 10:50:40 +03:00

70 lines
3.0 KiB
Python

# actions/tiers/link.py
"""Port of tiers/link — @tier symlink structure for the node + each composition project."""
import os
from engine.descriptor import Meta, Arg
METADATA = Meta(summary="Create @tier symlink structure for the node + each composition project.",
args=[Arg("tier1_path", "Path to tier 1 storage root"),
Arg("tier2_path", "Path to tier 2 storage root"),
Arg("tier3_path", "Path to tier 3 storage root")])
def run(ctx, args):
log, s = ctx.log, ctx.sys
comp = ctx.paths.compositions
tiers = [os.path.realpath(args.tier1_path),
os.path.realpath(args.tier2_path),
os.path.realpath(args.tier3_path)]
def confirm(msg):
try:
return input(f"{msg} [y/N]: ").strip().lower() in ("y", "yes")
except EOFError: # non-interactive stdin = decline, like bash `read` at EOF
return False
def safe_remove(link, desc):
if os.path.islink(link):
s.sudo(["rm", link]); return True
if os.path.exists(link):
log.warn(f"Path exists but is not a symlink: {link}")
if confirm(f"Remove existing {desc} (NOT a symlink)?"):
s.sudo(["rm", "-rf", link]); return True
return False
return True
def mklink(target, link, desc):
s.sudo(["ln", "-snf", target, link])
log.success(f"Created {desc} symlink: {link} -> {target}")
log.info("Starting @tier setup...")
for t in tiers:
s.sudo(["mkdir", "-p", t])
# node-level @tierN -> tier roots
for i, n in enumerate((1, 2, 3)):
link = os.path.join(comp, f"@tier{n}")
if safe_remove(link, f"node-@tier{n}"):
mklink(tiers[i], link, f"node-@tier{n}")
# per-project (every composition subdir except dot/@)
for proj in sorted(os.listdir(comp)):
full = os.path.join(comp, proj)
if not os.path.isdir(full) or proj.startswith(".") or proj.startswith("@"):
continue
log.info(f"Processing project: {proj}")
for i, n in enumerate((1, 2, 3)):
base = tiers[i]
s.sudo(["mkdir", "-p", os.path.join(base, "shared")])
s.sudo(["mkdir", "-p", os.path.join(base, "shared", proj)])
s.sudo(["mkdir", "-p", os.path.join(base, proj)])
shared_link = os.path.join(base, proj, "shared")
if safe_remove(shared_link, "shared-internal"):
s.sudo(["ln", "-snf", "../shared", shared_link])
link = os.path.join(full, f"@tier{n}")
if safe_remove(link, f"@tier{n}"):
mklink(os.path.join(base, proj), link, f"@tier{n}")
env_link = os.path.join(full, ".env")
env_target = os.path.join(tiers[0], proj, ".env")
if safe_remove(env_link, ".env"):
if not os.path.isfile(env_target):
s.sudo(["touch", env_target])
mklink(env_target, env_link, ".env")
log.success("@tier setup completed.")