pivot: Docker-based deployment — one container per node
- Clean Docker approach: pull wallarm/node image, run containers - No symlinks, no path patching, no filesystem tricks - Works on any Linux with Docker (VMs, LXC, bare metal) - Multi-node: each container gets unique port + volume - Simplified to one Python file
This commit is contained in:
parent
10fa528e77
commit
350434e690
1 changed files with 96 additions and 406 deletions
502
python/deploy.py
502
python/deploy.py
|
|
@ -1,449 +1,139 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Wallarm Node Manager — deploy, edit, remove, status, tunnel."""
|
"""Wallarm Docker Node Manager"""
|
||||||
|
|
||||||
import json, os, sys, time, subprocess, shutil, hashlib
|
import json, os, sys, time, subprocess, shutil
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
VERSION = "1.0.0"
|
VERSION = "2.0.0"
|
||||||
BASE = "/opt/fw"
|
BASE = "/opt/fw"
|
||||||
APP = f"{BASE}/app"
|
APP = f"{BASE}/app"
|
||||||
STATE = f"{APP}/state.json"
|
|
||||||
CONF = f"{APP}/fw.conf"
|
CONF = f"{APP}/fw.conf"
|
||||||
AIO_URL = "https://storage.googleapis.com/meganode_storage/6.12/wallarm-6.12.5.x86_64-glibc.sh"
|
STATE = f"{APP}/state.json"
|
||||||
AIO_PATH = f"{BASE}/wallarm-aio.sh"
|
WALLARM_IMAGE = "wallarm/node:6.13.0"
|
||||||
DEPLOY_TO = "/opt/wallarm"
|
DOCKER_BIN_URL = "https://git.sechpoint.app/customer-engineering/wallarm/raw/branch/main/docker/binaries/docker-29.2.1.tgz"
|
||||||
|
|
||||||
# ─── Helpers ───────────────────────────────────────────────────────────
|
def run(cmd, timeout=120):
|
||||||
|
|
||||||
def run(cmd, timeout=120, check=False):
|
|
||||||
return subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=timeout)
|
return subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=timeout)
|
||||||
|
|
||||||
def run_ok(cmd):
|
|
||||||
return subprocess.run(cmd, shell=True, capture_output=True).returncode == 0
|
|
||||||
|
|
||||||
def hash_port(s):
|
|
||||||
return abs(hash(s)) % 500
|
|
||||||
|
|
||||||
def is_root():
|
|
||||||
return os.geteuid() == 0
|
|
||||||
|
|
||||||
# ─── Preflight ─────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
def preflight():
|
|
||||||
checks = []
|
|
||||||
ok = True
|
|
||||||
def check(name, passed, detail=""):
|
|
||||||
nonlocal ok
|
|
||||||
if not passed: ok = False
|
|
||||||
m = "✅" if passed else "❌"
|
|
||||||
print(f" {m} {name} — {detail}")
|
|
||||||
checks.append((name, passed))
|
|
||||||
|
|
||||||
check("root", is_root())
|
|
||||||
check("systemd", run_ok("systemctl --version"))
|
|
||||||
arch = run("uname -m").stdout.strip()
|
|
||||||
check("arch", arch in ("x86_64", "aarch64"), arch)
|
|
||||||
check("curl/wget", run_ok("which curl") or run_ok("which wget"), "downloader")
|
|
||||||
for c in ("systemctl", "sed", "mkdir", "rm"):
|
|
||||||
check(f"cmd:{c}", run_ok(f"which {c}"))
|
|
||||||
check("installer", run_ok(f"curl -fsSL -o /dev/null {AIO_URL} 2>/dev/null"), "meganode")
|
|
||||||
for cloud, host in (("US", "us1.api.wallarm.com"), ("EU", "api.wallarm.com")):
|
|
||||||
reachable = run_ok(f"curl -sL --connect-timeout 5 -o /dev/null https://{host} 2>/dev/null")
|
|
||||||
m = "✅" if reachable else "⚠️ "
|
|
||||||
print(f" {m} cloud:{cloud} — {host}{' (unreachable, will retry on deploy)' if not reachable else ''}")
|
|
||||||
return ok
|
|
||||||
|
|
||||||
# ─── Config ────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
def load_config():
|
def load_config():
|
||||||
if not os.path.exists(CONF):
|
if not os.path.exists(CONF): return {}
|
||||||
return {}
|
with open(CONF) as f: return json.load(f)
|
||||||
with open(CONF) as f:
|
|
||||||
return json.load(f)
|
|
||||||
|
|
||||||
def load_state():
|
def load_state():
|
||||||
if not os.path.exists(STATE):
|
if not os.path.exists(STATE): return {"nodes": []}
|
||||||
return {"nodes": []}
|
with open(STATE) as f: return json.load(f)
|
||||||
with open(STATE) as f:
|
|
||||||
return json.load(f)
|
|
||||||
|
|
||||||
def save_state(s):
|
def save_state(s):
|
||||||
with open(STATE, "w") as f:
|
os.makedirs(APP, exist_ok=True)
|
||||||
json.dump(s, f, indent=2)
|
with open(STATE, "w") as f: json.dump(s, f, indent=2)
|
||||||
|
|
||||||
def is_deployed(name):
|
# ─── Docker ────────────────────────────────────────────────────────────
|
||||||
s = load_state()
|
|
||||||
return any(n["name"] == name for n in s.get("nodes", []))
|
|
||||||
|
|
||||||
# ─── Node Operations ───────────────────────────────────────────────────
|
def install_docker():
|
||||||
|
if shutil.which("docker"):
|
||||||
|
return
|
||||||
|
print("Installing Docker...")
|
||||||
|
run("apt-get update -qq && apt-get install -y -qq docker.io 2>/dev/null || yum install -y -q docker 2>/dev/null", timeout=120)
|
||||||
|
run("systemctl enable docker && systemctl start docker")
|
||||||
|
|
||||||
def install_node(name, token, cloud, port, upstream_ip, upstream_port, labels, mode):
|
def deploy_container(name, token, cloud, port, upstream_ip, upstream_port, labels, mode):
|
||||||
instance = f"{BASE}/{name}/wallarm"
|
|
||||||
api_host = "us1.api.wallarm.com" if cloud == "US" else "api.wallarm.com"
|
api_host = "us1.api.wallarm.com" if cloud == "US" else "api.wallarm.com"
|
||||||
address = f"0.0.0.0:{port}"
|
|
||||||
upstream = f"{upstream_ip}:{upstream_port}"
|
|
||||||
|
|
||||||
# Download AIO
|
# Ensure Docker is running
|
||||||
if not os.path.exists(AIO_PATH):
|
install_docker()
|
||||||
print(f"[{name}] Downloading installer...")
|
|
||||||
dl = "curl -fsSL" if run_ok("which curl") else "wget -q"
|
|
||||||
run(f"{dl} -o {AIO_PATH} {AIO_URL}")
|
|
||||||
os.chmod(AIO_PATH, 0o755)
|
|
||||||
|
|
||||||
# Prepare /opt/wallarm
|
# Pull image if needed
|
||||||
if os.path.islink(DEPLOY_TO) or os.path.isfile(DEPLOY_TO):
|
if run(f"docker images -q {WALLARM_IMAGE}").stdout.strip() == "":
|
||||||
os.remove(DEPLOY_TO)
|
print(f"[{name}] Pulling Wallarm image...")
|
||||||
shutil.rmtree(DEPLOY_TO, ignore_errors=True)
|
run(f"docker pull {WALLARM_IMAGE}", timeout=300)
|
||||||
os.makedirs(DEPLOY_TO, exist_ok=True)
|
|
||||||
|
|
||||||
# Install NGINX
|
# Remove old container if exists
|
||||||
nginx_bin = f"{DEPLOY_TO}/nginx/sbin/nginx"
|
container = f"wallarm-{name}"
|
||||||
if not os.path.exists(nginx_bin):
|
run(f"docker rm -f {container} 2>/dev/null", timeout=10)
|
||||||
os.makedirs(os.path.dirname(nginx_bin), exist_ok=True)
|
|
||||||
sys_nginx = shutil.which("nginx")
|
|
||||||
if sys_nginx:
|
|
||||||
shutil.copy(sys_nginx, nginx_bin)
|
|
||||||
else:
|
|
||||||
run("apt-get install -y -qq nginx 2>/dev/null || yum install -y -q nginx 2>/dev/null", timeout=60)
|
|
||||||
if os.path.exists("/usr/sbin/nginx"):
|
|
||||||
shutil.copy("/usr/sbin/nginx", nginx_bin)
|
|
||||||
|
|
||||||
# Extract AIO
|
# Run container
|
||||||
print(f"[{name}] Extracting...")
|
monitoring_port = int(port) + 10
|
||||||
run(f"bash {AIO_PATH} --noexec --keep --target {DEPLOY_TO} --noprogress --accept 2>&1", timeout=120)
|
print(f"[{name}] Starting container on port {port}...")
|
||||||
|
cmd = f"""docker run -d \
|
||||||
|
--name {container} \
|
||||||
|
--restart unless-stopped \
|
||||||
|
-p {port}:{port} \
|
||||||
|
-p {monitoring_port}:{monitoring_port} \
|
||||||
|
-v /opt/wallarm-{name}:/opt/wallarm \
|
||||||
|
-e WALLARM_API_TOKEN={token} \
|
||||||
|
-e WALLARM_API_HOST={api_host} \
|
||||||
|
-e WALLARM_MODE={mode} \
|
||||||
|
-e NGINX_PORT={port}"""
|
||||||
|
if upstream_ip:
|
||||||
|
cmd += f" -e WALLARM_UPSTREAM=http://{upstream_ip}:{upstream_port}"
|
||||||
|
cmd += f" {WALLARM_IMAGE}"
|
||||||
|
|
||||||
# Nginx config
|
r = run(cmd, timeout=30)
|
||||||
nginx_dir = f"{DEPLOY_TO}/nginx"
|
if r.returncode != 0:
|
||||||
os.makedirs(f"{nginx_dir}/conf", exist_ok=True)
|
print(f"❌ {name}: {r.stderr}")
|
||||||
with open(f"{nginx_dir}/conf/nginx.conf", "w") as f:
|
return False
|
||||||
f.write(f"""load_module {DEPLOY_TO}/modules/nginx_v1.26.3_s0ff5dffff/ngx_http_wallarm_module.so;
|
|
||||||
|
|
||||||
worker_processes auto;
|
# Save state
|
||||||
pid {nginx_dir}/nginx.pid;
|
s = load_state()
|
||||||
error_log {nginx_dir}/error.log;
|
s.setdefault("nodes", []).append({
|
||||||
events {{ worker_connections 10240; }}
|
"name": name, "type": "docker", "port": int(port),
|
||||||
http {{
|
"upstream_ip": upstream_ip, "upstream_port": int(upstream_port),
|
||||||
access_log {nginx_dir}/access.log;
|
"status": "running", "created_at": time.strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||||
wallarm_mode {mode};
|
})
|
||||||
server {{
|
save_state(s)
|
||||||
listen {port};
|
print(f"✅ {name} running on port {port} (docker ps --filter name={container})")
|
||||||
server_name _;
|
|
||||||
location /wallarm-status {{ wallarm_status on; allow 127.0.0.0/8; deny all; }}
|
|
||||||
location / {{ proxy_pass http://{upstream}; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }}
|
|
||||||
}}
|
|
||||||
}}
|
|
||||||
""")
|
|
||||||
run(f"{nginx_bin} -c {nginx_dir}/conf/nginx.conf", timeout=5)
|
|
||||||
|
|
||||||
# Pick module + register
|
|
||||||
run(f"bash {DEPLOY_TO}/pick-module.sh 2>&1", timeout=10)
|
|
||||||
print(f"[{name}] Registering node...")
|
|
||||||
r = run(f"bash -c 'source {DEPLOY_TO}/env.list 2>/dev/null; {DEPLOY_TO}/register-node job:register -token {token} -host {api_host}'",
|
|
||||||
timeout=120)
|
|
||||||
# Check for success indicators
|
|
||||||
registered = "node instance registered" in r.stdout or "init done" in r.stdout
|
|
||||||
access_denied = "access denied" in r.stdout + r.stderr
|
|
||||||
invalid_token = "invalid token" in (r.stdout + r.stderr).lower()
|
|
||||||
|
|
||||||
if access_denied or invalid_token:
|
|
||||||
raise RuntimeError("Token rejected by Wallarm. Check token permissions and cloud region.")
|
|
||||||
if not registered:
|
|
||||||
raise RuntimeError(f"Registration failed:\n{r.stdout[-500:]}\n{r.stderr[-500:]}")
|
|
||||||
|
|
||||||
# Kill deploy nginx + move to instance
|
|
||||||
run(f"pkill -9 -f '{DEPLOY_TO}/nginx' 2>/dev/null; true", timeout=5)
|
|
||||||
print(f"[{name}] Installing...")
|
|
||||||
shutil.rmtree(instance, ignore_errors=True)
|
|
||||||
os.makedirs(os.path.dirname(instance), exist_ok=True)
|
|
||||||
try:
|
|
||||||
os.rename(DEPLOY_TO, instance)
|
|
||||||
except OSError:
|
|
||||||
run(f"cp -a {DEPLOY_TO} {instance}", timeout=30)
|
|
||||||
shutil.rmtree(DEPLOY_TO, ignore_errors=True)
|
|
||||||
|
|
||||||
# Patch paths + port offsets for multi-instance
|
|
||||||
offset = hash_port(instance)
|
|
||||||
for pattern in ("*.sh", "*.list", "*.conf", "*.yaml", "*.yml"):
|
|
||||||
for f in Path(instance).rglob(pattern):
|
|
||||||
content = f.read_text()
|
|
||||||
content = content.replace("/opt/wallarm", instance)
|
|
||||||
for port_num in (3313, 6388, 9001, 8088, 9667, 6060, 5005):
|
|
||||||
content = content.replace(f":{port_num}", f":{port_num + offset}")
|
|
||||||
f.write_text(content)
|
|
||||||
|
|
||||||
# Add -cfg flag to wcli so it reads its own node.yaml
|
|
||||||
for conf in Path(f"{instance}/etc").glob("*.conf"):
|
|
||||||
c = conf.read_text()
|
|
||||||
if "wcli run" in c and "-cfg" not in c:
|
|
||||||
conf.write_text(c.replace("wcli run", f"wcli run -cfg {instance}/etc/wallarm/node.yaml"))
|
|
||||||
|
|
||||||
# Systemd
|
|
||||||
# Systemd template with private /opt/wallarm per instance
|
|
||||||
tmpl = f"""[Unit]
|
|
||||||
Description=Wallarm Node - %i
|
|
||||||
After=network.target
|
|
||||||
[Service]
|
|
||||||
Type=simple
|
|
||||||
# unshare -m creates private mount namespace for /opt/wallarm isolation
|
|
||||||
# while keeping /opt/fw and everything else accessible
|
|
||||||
ExecStart=/bin/unshare -m /bin/sh -c '\
|
|
||||||
mount --bind {BASE}/%i/wallarm /opt/wallarm && \
|
|
||||||
{BASE}/%i/wallarm/nginx/sbin/nginx -c {BASE}/%i/wallarm/nginx/conf/nginx.conf && \
|
|
||||||
sleep 1 && \
|
|
||||||
exec {BASE}/%i/wallarm/usr/bin/python3.10 {BASE}/%i/wallarm/usr/bin/supervisord -c {BASE}/%i/wallarm/etc/supervisord.conf'
|
|
||||||
ExecStop=/opt/fw/%i/wallarm/usr/bin/python3.10 /opt/fw/%i/wallarm/usr/bin/supervisord -c /opt/fw/%i/wallarm/etc/supervisord.conf shutdown
|
|
||||||
Restart=on-failure
|
|
||||||
RestartSec=5
|
|
||||||
User=root
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
"""
|
|
||||||
with open("/etc/systemd/system/wallarm-node@.service", "w") as f:
|
|
||||||
f.write(tmpl)
|
|
||||||
run("systemctl daemon-reload")
|
|
||||||
run(f"systemctl enable wallarm-node@{name}")
|
|
||||||
run(f"systemctl start wallarm-node@{name}")
|
|
||||||
print(f"✅ {name} deployed. systemctl status wallarm-node@{name}")
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# ─── Remove ────────────────────────────────────────────────────────────
|
def remove_container(name):
|
||||||
|
container = f"wallarm-{name}"
|
||||||
def remove_node(name):
|
run(f"docker rm -f {container} 2>/dev/null", timeout=10)
|
||||||
run(f"systemctl stop wallarm-node@{name} 2>/dev/null")
|
shutil.rmtree(f"/opt/wallarm-{name}", ignore_errors=True)
|
||||||
run(f"systemctl disable wallarm-node@{name} 2>/dev/null")
|
|
||||||
shutil.rmtree(f"{BASE}/{name}", ignore_errors=True)
|
|
||||||
s = load_state()
|
s = load_state()
|
||||||
s["nodes"] = [n for n in s["nodes"] if n["name"] != name]
|
s["nodes"] = [n for n in s.get("nodes", []) if n["name"] != name]
|
||||||
save_state(s)
|
save_state(s)
|
||||||
print(f"✅ {name} removed.")
|
print(f"✅ {name} removed")
|
||||||
|
|
||||||
# ─── Status ────────────────────────────────────────────────────────────
|
def container_status():
|
||||||
|
r = run("docker ps --filter 'name=wallarm-' --format '{{.Names}}\t{{.Status}}\t{{.Ports}}'")
|
||||||
def show_status():
|
|
||||||
r = run("systemctl list-units 'wallarm-node@*' --no-legend 2>/dev/null")
|
|
||||||
if not r.stdout.strip():
|
if not r.stdout.strip():
|
||||||
print("No nodes running.")
|
print("No containers running.")
|
||||||
return
|
return
|
||||||
print("\n─── Node Status ───")
|
print("\n─── Containers ───")
|
||||||
for line in r.stdout.strip().split("\n"):
|
for line in r.stdout.strip().split("\n"):
|
||||||
# Format: "● wallarm-node@srv1.service loaded active ..."
|
print(f" {line}")
|
||||||
# or: " wallarm-node@srv1.service loaded active ..."
|
|
||||||
line = line.lstrip('●○● ○')
|
|
||||||
parts = line.split()
|
|
||||||
if len(parts) >= 3 and '@' in parts[0]:
|
|
||||||
name = parts[0].split('@')[1].replace(".service", "")
|
|
||||||
active = parts[2] if parts[1] == "active" else parts[1]
|
|
||||||
status = "●" if parts[1] == "active" else "○"
|
|
||||||
print(f" {status} {name} — {parts[1]} {parts[2] if len(parts)>2 else ''}")
|
|
||||||
|
|
||||||
# ─── Tunnel ────────────────────────────────────────────────────────────
|
# ─── Deploy All ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
def start_tunnel():
|
|
||||||
host = input("Jumphost URL [ssh.sechpoint.app:443]: ") or "ssh.sechpoint.app:443"
|
|
||||||
user = input("Username [wallarm-tunnel]: ") or "wallarm-tunnel"
|
|
||||||
pw = input("Password or SSH key path: ")
|
|
||||||
if not pw:
|
|
||||||
print("No credentials.")
|
|
||||||
return
|
|
||||||
print(f"\nShare: ssh -p 9042 {user}@{host}\nCtrl+C to close.\n")
|
|
||||||
# Import tunnel module on demand
|
|
||||||
try:
|
|
||||||
from tunnel import start_tunnel_ssh
|
|
||||||
start_tunnel_ssh(host, user, pw)
|
|
||||||
except ImportError:
|
|
||||||
print("Tunnel module not available. Install paramiko: pip install paramiko")
|
|
||||||
|
|
||||||
# ─── Deploy Menu ───────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
def deploy_menu():
|
|
||||||
cfg = load_config()
|
|
||||||
nodes = cfg.get("nodes", {})
|
|
||||||
if not nodes:
|
|
||||||
print("No nodes in /opt/fw/fw.conf")
|
|
||||||
return
|
|
||||||
|
|
||||||
deployed = {n["name"] for n in load_state().get("nodes", [])}
|
|
||||||
available = []
|
|
||||||
print("\n─── Deploy Node ───")
|
|
||||||
for name, nc in nodes.items():
|
|
||||||
if name in deployed:
|
|
||||||
print(f" ✓ {name} (port {nc.get('port','?')}) [deployed]")
|
|
||||||
else:
|
|
||||||
available.append(name)
|
|
||||||
print(f" [{len(available)}] {name} (port {nc.get('port','?')})")
|
|
||||||
|
|
||||||
if not available:
|
|
||||||
print("All nodes deployed.")
|
|
||||||
return
|
|
||||||
|
|
||||||
c = input("\nSelect number (Enter=all): ").strip()
|
|
||||||
selected = available if not c else [available[int(c)-1]] if c.isdigit() and 1 <= int(c) <= len(available) else []
|
|
||||||
|
|
||||||
if not selected:
|
|
||||||
print("Invalid.")
|
|
||||||
return
|
|
||||||
|
|
||||||
for name in selected:
|
|
||||||
nc = nodes[name]
|
|
||||||
print(f"\nDeploying {name}...")
|
|
||||||
mode = nc.get("mode", "monitoring")
|
|
||||||
print("\nTraffic mode: [1] monitoring [2] safe_blocking [3] block [4] off")
|
|
||||||
m = input(f"Choose [{['','1','2','3','4'][['monitoring','safe_blocking','block','off'].index(mode)] if mode in ['monitoring','safe_blocking','block','off'] else '1'}]: ").strip()
|
|
||||||
modes = {"2":"safe_blocking","3":"block","4":"off"}
|
|
||||||
mode = modes.get(m, mode)
|
|
||||||
|
|
||||||
try:
|
|
||||||
install_node(name, nc["token"], nc.get("cloud","EU"),
|
|
||||||
str(nc.get("port","8081")), nc.get("upstream_ip","127.0.0.1"),
|
|
||||||
str(nc.get("upstream_port","80")), nc.get("labels",f"group={name}"), mode)
|
|
||||||
s = load_state()
|
|
||||||
s.setdefault("nodes", []).append({
|
|
||||||
"name": name, "type": "native", "address": f"0.0.0.0:{nc.get('port','8081')}",
|
|
||||||
"upstream_ip": nc.get("upstream_ip",""), "upstream_port": int(nc.get("upstream_port",80)),
|
|
||||||
"status": "running", "created_at": time.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
||||||
})
|
|
||||||
save_state(s)
|
|
||||||
print(f"✅ {name} deployed.")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ {name} failed: {e}")
|
|
||||||
|
|
||||||
# ─── Edit Menu ─────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
def list_nodes():
|
|
||||||
"""Discover nodes from systemd if state is empty."""
|
|
||||||
s = load_state()
|
|
||||||
if s.get("nodes"):
|
|
||||||
return s["nodes"]
|
|
||||||
# Fall back to systemd discovery
|
|
||||||
r = run("systemctl list-units 'wallarm-node@*' --no-legend 2>/dev/null")
|
|
||||||
nodes = []
|
|
||||||
for line in r.stdout.strip().split("\n"):
|
|
||||||
parts = line.split()
|
|
||||||
if len(parts) >= 4:
|
|
||||||
name = parts[0].replace("wallarm-node@", "").replace(".service", "")
|
|
||||||
nodes.append({"name": name, "address": "", "upstream_ip": "", "upstream_port": 0})
|
|
||||||
return nodes
|
|
||||||
|
|
||||||
def edit_menu():
|
|
||||||
nodes = list_nodes()
|
|
||||||
if not nodes:
|
|
||||||
print("No nodes.")
|
|
||||||
return
|
|
||||||
print("\n─── Edit Node ───")
|
|
||||||
for i, n in enumerate(nodes):
|
|
||||||
r = run(f"systemctl is-active wallarm-node@{n['name']}")
|
|
||||||
st = "●" if r.stdout.strip() == "active" else "○"
|
|
||||||
print(f" [{i+1}] {st} {n['name']} {n.get('address','')} → {n.get('upstream_ip','')}:{n.get('upstream_port','')}")
|
|
||||||
c = input("\nSelect: ").strip()
|
|
||||||
if not c.isdigit() or int(c) < 1 or int(c) > len(nodes): return
|
|
||||||
n = nodes[int(c)-1]
|
|
||||||
|
|
||||||
port = input(f"Port [{n['address'].split(':')[-1]}]: ").strip()
|
|
||||||
ip = input(f"Upstream IP [{n.get('upstream_ip','')}]: ").strip()
|
|
||||||
up = input(f"Upstream port [{n.get('upstream_port','')}]: ").strip()
|
|
||||||
if not any([port, ip, up]):
|
|
||||||
print("No changes.")
|
|
||||||
return
|
|
||||||
if port: n["address"] = f"0.0.0.0:{port}"
|
|
||||||
if ip: n["upstream_ip"] = ip
|
|
||||||
if up: n["upstream_port"] = int(up)
|
|
||||||
save_state(s)
|
|
||||||
|
|
||||||
# Update nginx config
|
|
||||||
nginx_conf = f"{BASE}/{n['name']}/wallarm/nginx/conf/nginx.conf"
|
|
||||||
if os.path.exists(nginx_conf):
|
|
||||||
c = Path(nginx_conf).read_text()
|
|
||||||
if port: c = c.replace(f"listen {n['address'].split(':')[-1]};", f"listen {port};")
|
|
||||||
if ip or up:
|
|
||||||
old_up = f"{n.get('upstream_ip','')}:{n.get('upstream_port','')}"
|
|
||||||
new_up = f"{n.get('upstream_ip','')}:{n.get('upstream_port','')}"
|
|
||||||
c = c.replace(f"http://{old_up}", f"http://{new_up}")
|
|
||||||
Path(nginx_conf).write_text(c)
|
|
||||||
print(f"✅ Updated. Restart: systemctl restart wallarm-node@{n['name']}")
|
|
||||||
|
|
||||||
# ─── Remove Menu ───────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
def remove_menu():
|
|
||||||
nodes = list_nodes()
|
|
||||||
if not nodes:
|
|
||||||
print("No nodes.")
|
|
||||||
return
|
|
||||||
print("\n─── Remove Node ───")
|
|
||||||
for i, n in enumerate(nodes):
|
|
||||||
r = run(f"systemctl is-active wallarm-node@{n['name']}")
|
|
||||||
st = "●" if r.stdout.strip() == "active" else "○"
|
|
||||||
print(f" [{i+1}] {st} {n['name']} {n.get('address','')}")
|
|
||||||
c = input("\nSelect: ").strip()
|
|
||||||
if not c.isdigit() or int(c) < 1 or int(c) > len(nodes): return
|
|
||||||
n = nodes[int(c)-1]
|
|
||||||
remove_node(n["name"])
|
|
||||||
|
|
||||||
# ─── Main ──────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
def main():
|
|
||||||
if len(sys.argv) > 1 and sys.argv[1] in ("--version", "-v"):
|
|
||||||
print(f"deploy.py v{VERSION}")
|
|
||||||
return
|
|
||||||
if len(sys.argv) > 1 and sys.argv[1] in ("--help", "-h", "help"):
|
|
||||||
print(f"deploy.py v{VERSION} — Wallarm Node Manager")
|
|
||||||
print(" deploy.py Interactive menu")
|
|
||||||
print(" deploy.py --deploy-all Deploy all nodes from fw.conf")
|
|
||||||
print(" deploy.py --version Show version")
|
|
||||||
return
|
|
||||||
if len(sys.argv) > 1 and sys.argv[1] == "--deploy-all":
|
|
||||||
deploy_all()
|
|
||||||
return
|
|
||||||
|
|
||||||
print(f"═══ Wallarm Deployment Manager ═══")
|
|
||||||
print(f" v{VERSION}\n")
|
|
||||||
if not preflight():
|
|
||||||
print("\n❌ Preflight failed.")
|
|
||||||
sys.exit(1)
|
|
||||||
print("✅ Preflight passed.\n")
|
|
||||||
|
|
||||||
while True:
|
|
||||||
print("\n─── Menu ───")
|
|
||||||
print(" [1] Deploy [2] Edit [3] Status [4] Remove [5] Tunnel [q] Quit")
|
|
||||||
c = input("\nChoice: ").strip()
|
|
||||||
if c == "1": deploy_menu()
|
|
||||||
elif c == "2": edit_menu()
|
|
||||||
elif c == "3": show_status()
|
|
||||||
elif c == "4": remove_menu()
|
|
||||||
elif c == "5": start_tunnel()
|
|
||||||
elif c.lower() == "q": break
|
|
||||||
# Clean up stray symlinks
|
|
||||||
if os.path.islink("/opt/wallarm"):
|
|
||||||
os.remove("/opt/wallarm")
|
|
||||||
|
|
||||||
def deploy_all():
|
def deploy_all():
|
||||||
print("Deploying all from fw.conf...")
|
|
||||||
cfg = load_config()
|
cfg = load_config()
|
||||||
if not cfg.get("nodes"):
|
if not cfg.get("nodes"):
|
||||||
print("No nodes found in /opt/fw/fw.conf")
|
print("No nodes in fw.conf")
|
||||||
return
|
return
|
||||||
for name, nc in cfg.get("nodes", {}).items():
|
for name, nc in cfg.items():
|
||||||
if is_deployed(name):
|
deploy_container(name, nc["token"], nc.get("cloud","EU"),
|
||||||
print(f"✓ {name} already deployed")
|
|
||||||
continue
|
|
||||||
print(f"\nDeploying {name}...")
|
|
||||||
try:
|
|
||||||
install_node(name, nc["token"], nc.get("cloud","EU"),
|
|
||||||
str(nc.get("port","8081")), nc.get("upstream_ip","127.0.0.1"),
|
str(nc.get("port","8081")), nc.get("upstream_ip","127.0.0.1"),
|
||||||
str(nc.get("upstream_port","80")), nc.get("labels",f"group={name}"),
|
str(nc.get("upstream_port","80")), nc.get("labels",""),
|
||||||
nc.get("mode","monitoring"))
|
nc.get("mode","monitoring"))
|
||||||
s = load_state()
|
|
||||||
s.setdefault("nodes", []).append({
|
# ─── Menu ─────────────────────────────────────────────────────────────
|
||||||
"name": name, "type": "native", "address": f"0.0.0.0:{nc.get('port','8081')}",
|
|
||||||
"upstream_ip": nc.get("upstream_ip",""), "upstream_port": int(nc.get("upstream_port",80)),
|
def main():
|
||||||
"status": "running", "created_at": time.strftime("%Y-%m-%dT%H:%M:%SZ")
|
if len(sys.argv) > 1 and sys.argv[1] == "--deploy-all":
|
||||||
})
|
return deploy_all()
|
||||||
save_state(s)
|
|
||||||
except Exception as e:
|
print(f"═══ Wallarm Docker Manager v{VERSION} ═══\n")
|
||||||
print(f"❌ {name}: {e}")
|
while True:
|
||||||
# Clean up
|
print("\n─── Menu ───")
|
||||||
if os.path.islink("/opt/wallarm"):
|
print(" [1] Deploy all [2] Status [3] Remove [q] Quit")
|
||||||
os.remove("/opt/wallarm")
|
c = input("\nChoice: ").strip()
|
||||||
print("Done.")
|
if c == "1": deploy_all()
|
||||||
|
elif c == "2": container_status()
|
||||||
|
elif c == "3":
|
||||||
|
print("\nContainers:")
|
||||||
|
for n in load_state().get("nodes", []):
|
||||||
|
print(f" [{n['name']}]")
|
||||||
|
name = input("Name to remove: ").strip()
|
||||||
|
if name: remove_container(name)
|
||||||
|
elif c.lower() == "q": break
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue