pivot: Python rewrite replacing Go — single deploy.py

- Single deploy.py (~400 lines) replaces all Go code
- setup.sh installs Python 3.12 via pyenv (cross-distro)
- Same functionality: menu, deploy, edit, remove, status, tunnel
- No build step — script runs directly
- Path patching and port offset in native Python
This commit is contained in:
admin 2026-08-03 11:09:08 +00:00
parent 31cbb56629
commit 43dfc6ec06
12 changed files with 430 additions and 1735 deletions

View file

@ -1,36 +0,0 @@
.PHONY: all linux-amd64 linux-arm64 clean test release
BINARY := deploy
VERSION := $(shell git describe --tags --always 2>/dev/null || echo "dev")
LDFLAGS := -s -w -X main.version=$(VERSION)
# Embed tunnel key at build time: make TUNNEL_KEY=~/.wallarm/tunnel_key
ifdef TUNNEL_KEY
LDFLAGS += -X main.tunnelKey=$(shell cat $(TUNNEL_KEY))
endif
all: linux-amd64 linux-arm64
linux-amd64:
GOOS=linux GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o $(BINARY)-linux-amd64 ./cmd/deploy/
upx --best --lzma $(BINARY)-linux-amd64 -o $(BINARY)-linux-amd64.tmp 2>/dev/null && mv $(BINARY)-linux-amd64.tmp $(BINARY)-linux-amd64 || true
linux-arm64:
GOOS=linux GOARCH=arm64 go build -ldflags "$(LDFLAGS)" -o $(BINARY)-linux-arm64 ./cmd/deploy/
upx --best --lzma $(BINARY)-linux-arm64 -o $(BINARY)-linux-arm64.tmp 2>/dev/null && mv $(BINARY)-linux-arm64.tmp $(BINARY)-linux-arm64 || true
clean:
rm -f $(BINARY) $(BINARY)-linux-*
test:
go test ./internal/...
# Build and prepare for Gitea release.
# Usage: make release
# Then upload wallarm-linux-amd64 and wallarm-linux-arm64 as release assets.
release: clean all
@echo "Release binaries built:"
@ls -lh wallarm-linux-*
@echo ""
@echo "Next: Create a Gitea release and upload these binaries as assets."
@echo " Tag: v$(VERSION)"

View file

@ -1,522 +0,0 @@
package main
import (
"bufio"
"encoding/json"
"flag"
"fmt"
"os"
"os/exec"
"strconv"
"strings"
"time"
"git.sechpoint.app/customer-engineering/wallarm/internal/native"
"git.sechpoint.app/customer-engineering/wallarm/internal/preflight"
"git.sechpoint.app/customer-engineering/wallarm/internal/state"
"git.sechpoint.app/customer-engineering/wallarm/internal/tunnel"
"git.sechpoint.app/customer-engineering/wallarm/internal/ui"
)
var tunnelKey string
var version = "dev"
func main() {
ver := flag.Bool("version", false, "Show version")
tun := flag.Bool("tunnel", false, "Start reverse SSH tunnel")
all := flag.Bool("deploy-all", false, "Deploy all undeployed nodes from fw.conf")
flag.Parse()
if *ver {
fmt.Println("deploy version", version)
return
}
if *tun {
startTunnel()
return
}
if *all {
deployAll()
return
}
// Run preflight, then interactive menu
result := preflight.Run()
fmt.Println("═══ Wallarm Deployment Manager ═══")
fmt.Printf(" version %s\n\n", version)
for _, c := range result.Checks {
m := "✅"
if !c.Passed { m = "❌" } else if c.Warning { m = "⚠️" }
fmt.Printf(" %s %s — %s\n", m, c.Name, c.Detail)
}
if !result.Passed {
fmt.Println("\n❌ Preflight checks failed.")
os.Exit(1)
}
fmt.Println("✅ Preflight checks passed.")
fmt.Println()
showMenu()
}
func showMenu() {
reader := bufio.NewReader(os.Stdin)
for {
fmt.Println()
fmt.Println("─── Main Menu ───")
fmt.Println(" [1] Deploy a node")
fmt.Println(" [2] Edit a node")
fmt.Println(" [3] Show status")
fmt.Println(" [4] Remove a node")
fmt.Println(" [5] Remote tunnel")
fmt.Println(" [6] Dashboard (TUI)")
fmt.Println(" [q] Quit")
fmt.Print("\nChoice: ")
c, _ := reader.ReadString('\n')
switch strings.TrimSpace(c) {
case "1":
deployMenu()
case "2":
editMenu()
case "3":
showStatus()
case "4":
removeMenu()
case "5":
startTunnel()
case "6":
if err := ui.Run(); err != nil {
fmt.Println("TUI error:", err)
}
case "q", "Q":
return
}
}
}
// ─── Config (fw.conf JSON) ──────────────────────────────────────────
type nodeConfig struct {
Name string
Token string
Cloud string
Port string
UpstreamIP string
UpstreamPort string
Labels string
Mode string
}
type fwConfig struct {
Nodes map[string]struct {
Token string `json:"token"`
Cloud string `json:"cloud"`
Port string `json:"port"`
UpstreamIP string `json:"upstream_ip"`
UpstreamPort string `json:"upstream_port"`
Labels string `json:"labels"`
Mode string `json:"mode"`
} `json:"nodes"`
}
func loadConfig() (fwConfig, error) {
var cfg fwConfig
data, err := os.ReadFile("/opt/fw/fw.conf")
if os.IsNotExist(err) {
// Fall back to legacy .env
return fwConfig{}, fmt.Errorf("no fw.conf")
}
if err != nil {
return cfg, err
}
if err := json.Unmarshal(data, &cfg); err != nil {
return cfg, err
}
return cfg, nil
}
func loadNodes() []nodeConfig {
cfg, err := loadConfig()
if err != nil {
// Fall back to legacy .env
return loadNodesLegacy()
}
var nodes []nodeConfig
for name, n := range cfg.Nodes {
mode := n.Mode
if mode == "" { mode = "monitoring" }
nodes = append(nodes, nodeConfig{
Name: name,
Token: n.Token,
Cloud: n.Cloud,
Port: n.Port,
UpstreamIP: n.UpstreamIP,
UpstreamPort: n.UpstreamPort,
Labels: n.Labels,
Mode: mode,
})
}
return nodes
}
// loadNodesLegacy reads the old WALLARM_TOKEN_srv1=... format
func loadNodesLegacy() []nodeConfig {
data, _ := os.ReadFile("/opt/fw/.env")
env := parseEnv(string(data))
var names []string
if v := env["WALLARM_NODES"]; v != "" {
names = strings.Split(v, ",")
} else if v := env["WALLARM_NODE"]; v != "" {
names = []string{v}
}
var nodes []nodeConfig
for _, n := range names {
n = strings.TrimSpace(n)
if n == "" { continue }
nodes = append(nodes, nodeConfig{
Name: n,
Token: env["WALLARM_TOKEN_"+n],
Cloud: env["WALLARM_CLOUD_"+n],
Port: env["WALLARM_PORT_"+n],
UpstreamIP: env["WALLARM_UPSTREAM_IP_"+n],
UpstreamPort: env["WALLARM_UPSTREAM_PORT_"+n],
Labels: env["WALLARM_LABELS_"+n],
Mode: env["WALLARM_MODE_"+n],
})
}
if len(nodes) == 0 && env["WALLARM_TOKEN"] != "" {
nodes = append(nodes, nodeConfig{
Name: env["WALLARM_NODE"],
Token: env["WALLARM_TOKEN"],
Cloud: env["WALLARM_CLOUD"],
Port: env["WALLARM_PORT"],
UpstreamIP: env["WALLARM_UPSTREAM_IP"],
UpstreamPort: env["WALLARM_UPSTREAM_PORT"],
Labels: env["WALLARM_LABELS"],
Mode: env["WALLARM_MODE"],
})
}
return nodes
}
func isDeployed(name string) bool {
s, _ := state.Load()
if s == nil { return false }
for _, n := range s.Nodes {
if n.Name == name { return true }
}
return false
}
// ─── Deploy All ────────────────────────────────────────────────────────
func deployAll() {
nodes := loadNodes()
if len(nodes) == 0 {
fmt.Println("No nodes configured in /opt/fw/fw.conf")
return
}
for _, nc := range nodes {
if isDeployed(nc.Name) {
fmt.Printf("✓ %s already deployed\n", nc.Name)
continue
}
fmt.Printf("\nDeploying %s...\n", nc.Name)
deployOne(nc)
}
}
// ─── Deploy ───────────────────────────────────────────────────────────
func deployMenu() {
nodes := loadNodes()
if len(nodes) == 0 {
fmt.Println("No nodes configured in /opt/fw/.env")
fmt.Println("Create one with: WALLARM_TOKEN=... WALLARM_NODE=... etc")
return
}
s, _ := state.Load()
deployed := map[string]bool{}
if s != nil {
for _, n := range s.Nodes { deployed[n.Name] = true }
}
fmt.Println("\n─── Deploy Node ───")
available := []int{}
for i, nc := range nodes {
if deployed[nc.Name] {
fmt.Printf(" ✓ %s (port %s) [deployed]\n", nc.Name, nc.Port)
} else {
available = append(available, i)
fmt.Printf(" [%d] %s (port %s)\n", len(available), nc.Name, nc.Port)
}
}
if len(available) == 0 {
fmt.Println("All nodes deployed.")
return
}
fmt.Print("\nSelect node number (or Enter for all): ")
reader := bufio.NewReader(os.Stdin)
choice, _ := reader.ReadString('\n')
choice = strings.TrimSpace(choice)
var selected []nodeConfig
if choice == "" {
for _, i := range available { selected = append(selected, nodes[i]) }
} else {
idx, err := strconv.Atoi(choice)
if err != nil || idx < 1 || idx > len(available) {
fmt.Println("Invalid choice")
return
}
selected = append(selected, nodes[available[idx-1]])
}
for _, nc := range selected {
fmt.Printf("\nDeploying %s...\n", nc.Name)
deployOne(nc)
}
}
func deployOne(nc nodeConfig) {
if nc.Token == "" || nc.Name == "" {
fmt.Printf(" Skipped: missing token or name\n")
return
}
if nc.Port == "" { nc.Port = "8081" }
if nc.UpstreamIP == "" { nc.UpstreamIP = "127.0.0.1" }
if nc.UpstreamPort == "" { nc.UpstreamPort = "80" }
if nc.Cloud == "" { nc.Cloud = "EU" }
if nc.Labels == "" { nc.Labels = "group=" + nc.Name }
// Ask for traffic mode
reader := bufio.NewReader(os.Stdin)
mode := nc.Mode
if mode == "" { mode = "monitoring" }
def := "1"
switch mode {
case "safe_blocking": def = "2"
case "block": def = "3"
case "off": def = "4"
}
fmt.Println("\nTraffic processing mode:")
fmt.Println(" [1] monitoring — detect attacks, do not block")
fmt.Println(" [2] safe_blocking — block only definitely malicious requests")
fmt.Println(" [3] block — block all detected attacks")
fmt.Println(" [4] off — disable traffic analysis")
fmt.Printf("Choose mode [%s]: ", def)
ans, _ := reader.ReadString('\n')
ans = strings.TrimSpace(ans)
if ans == "" { ans = def }
switch ans {
case "2":
mode = "safe_blocking"
case "3":
mode = "block"
case "4":
mode = "off"
default:
mode = "monitoring"
}
apiHost := "api.wallarm.com"
if nc.Cloud == "US" { apiHost = "us1.api.wallarm.com" }
portNum, _ := strconv.Atoi(nc.UpstreamPort)
node := state.Node{
Name: nc.Name,
Type: "native",
Address: "0.0.0.0:" + nc.Port,
UpstreamIP: nc.UpstreamIP,
UpstreamPort: portNum,
Status: "deploying",
CreatedAt: time.Now().Format(time.RFC3339),
}
// Save state before deploy
s, _ := state.Load()
if s == nil { s = &state.State{DeploymentType: "native"} }
s.Nodes = append(s.Nodes, node)
state.Save(s)
fmt.Println("Starting deployment...")
if err := native.InstallNode(node, nc.Token, apiHost, nc.Labels, mode); err != nil {
fmt.Fprintf(os.Stderr, "Deployment failed: %v\n", err)
return
}
// Update status
for i := range s.Nodes {
if s.Nodes[i].Name == nc.Name { s.Nodes[i].Status = "running" }
}
state.Save(s)
fmt.Printf("✅ %s deployed.\n", nc.Name)
}
// ─── Remove ───────────────────────────────────────────────────────────
func removeMenu() {
s, _ := state.Load()
if s == nil || len(s.Nodes) == 0 {
fmt.Println("No nodes deployed.")
return
}
fmt.Println("\n─── Remove Node ───")
for i, n := range s.Nodes {
status := "●"
out, _ := exec.Command("systemctl", "is-active", "wallarm-node@"+n.Name).CombinedOutput()
if strings.TrimSpace(string(out)) != "active" { status = "○" }
fmt.Printf(" [%d] %s %s %s\n", i+1, status, n.Name, n.Address)
}
fmt.Print("\nSelect node number: ")
reader := bufio.NewReader(os.Stdin)
choice, _ := reader.ReadString('\n')
idx, err := strconv.Atoi(strings.TrimSpace(choice))
if err != nil || idx < 1 || idx > len(s.Nodes) {
fmt.Println("Invalid choice")
return
}
n := s.Nodes[idx-1]
fmt.Printf("Removing %s...\n", n.Name)
if err := native.RemoveNode(n.Name); err != nil {
fmt.Println("Error:", err)
return
}
// Update state
s.Nodes = append(s.Nodes[:idx-1], s.Nodes[idx:]...)
state.Save(s)
fmt.Printf("✅ %s removed.\n", n.Name)
}
// ─── Edit ─────────────────────────────────────────────────────────────
func editMenu() {
s, _ := state.Load()
if s == nil || len(s.Nodes) == 0 {
fmt.Println("No nodes deployed.")
return
}
fmt.Println("\n─── Edit Node ───")
for i, n := range s.Nodes {
out, _ := exec.Command("systemctl", "is-active", "wallarm-node@"+n.Name).CombinedOutput()
status := "●"
if strings.TrimSpace(string(out)) != "active" { status = "○" }
fmt.Printf(" [%d] %s %s %s → %s:%d\n",
i+1, status, n.Name, n.Address, n.UpstreamIP, n.UpstreamPort)
}
fmt.Print("\nSelect node number: ")
reader := bufio.NewReader(os.Stdin)
choice, _ := reader.ReadString('\n')
idx, err := strconv.Atoi(strings.TrimSpace(choice))
if err != nil || idx < 1 || idx > len(s.Nodes) {
fmt.Println("Invalid choice")
return
}
n := &s.Nodes[idx-1]
fmt.Println("\nCurrent config:")
fmt.Printf(" Name: %s\n", n.Name)
fmt.Printf(" Listen: %s\n", n.Address)
fmt.Printf(" Upstream: %s:%d\n", n.UpstreamIP, n.UpstreamPort)
fmt.Println("\nEnter new values (leave blank to keep current):")
fmt.Print("Listen port [" + strings.TrimPrefix(n.Address, "0.0.0.0:") + "]: ")
port, _ := reader.ReadString('\n')
port = strings.TrimSpace(port)
fmt.Printf("Upstream IP [%s]: ", n.UpstreamIP)
ip, _ := reader.ReadString('\n')
ip = strings.TrimSpace(ip)
fmt.Printf("Upstream port [%d]: ", n.UpstreamPort)
portStr, _ := reader.ReadString('\n')
portStr = strings.TrimSpace(portStr)
if port == "" && ip == "" && portStr == "" {
fmt.Println("No changes.")
return
}
if port != "" { n.Address = "0.0.0.0:" + port }
if ip != "" { n.UpstreamIP = ip }
if portStr != "" {
p, _ := strconv.Atoi(portStr)
if p > 0 { n.UpstreamPort = p }
}
// Update nginx config
base := "/opt/fw/" + n.Name + "/wallarm"
nginxConf := base + "/nginx/conf/nginx.conf"
if _, err := os.Stat(nginxConf); err == nil {
data, _ := os.ReadFile(nginxConf)
newData := string(data)
if port != "" {
newData = strings.ReplaceAll(newData, n.Address, "0.0.0.0:"+port)
}
if ip != "" || portStr != "" {
oldUp := fmt.Sprintf("%s:%d", n.UpstreamIP, n.UpstreamPort)
newUp := fmt.Sprintf("%s:%d", n.UpstreamIP, n.UpstreamPort)
newData = strings.ReplaceAll(newData, oldUp, newUp)
}
os.WriteFile(nginxConf, []byte(newData), 0644)
}
state.Save(s)
fmt.Println("✅ Config updated. Restart the node to apply:")
fmt.Printf(" systemctl restart wallarm-node@%s\n", n.Name)
}
// ─── Status ───────────────────────────────────────────────────────────
func showStatus() {
s, _ := state.Load()
if s == nil || len(s.Nodes) == 0 {
fmt.Println("No nodes deployed.")
return
}
fmt.Println("\n─── Node Status ───")
for _, n := range s.Nodes {
out, _ := exec.Command("systemctl", "is-active", "wallarm-node@"+n.Name).CombinedOutput()
status := strings.TrimSpace(string(out))
marker := "●"
if status != "active" { marker = "○" }
fmt.Printf(" %s %s — %s — %s\n", marker, n.Name, status, n.Address)
}
}
// ─── Tunnel ───────────────────────────────────────────────────────────
func startTunnel() {
reader := bufio.NewReader(os.Stdin)
fmt.Print("Jumphost URL [ssh.sechpoint.app:443]: ")
h, _ := reader.ReadString('\n')
h = strings.TrimSpace(h)
if h == "" { h = "ssh.sechpoint.app:443" }
fmt.Print("Username [wallarm-tunnel]: ")
u, _ := reader.ReadString('\n')
u = strings.TrimSpace(u)
if u == "" { u = "wallarm-tunnel" }
fmt.Print("Password or SSH key path: ")
p, _ := reader.ReadString('\n')
p = strings.TrimSpace(p)
cfg := tunnel.Config{Jumphost: h, User: u, RemotePort: 9042, LocalSSHPort: 22}
if strings.HasPrefix(p, "/") { cfg.KeyPath = p } else { cfg.Password = p }
fmt.Printf("\nShare: ssh -p 9042 %s@%s\nCtrl+C to close.\n\n", u, h)
tunnel.Start(cfg)
}
// ─── Helpers ──────────────────────────────────────────────────────────
func parseEnv(data string) map[string]string {
m := map[string]string{}
for _, line := range strings.Split(data, "\n") {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "#") { continue }
p := strings.SplitN(line, "=", 2)
if len(p) == 2 { m[strings.TrimSpace(p[0])] = strings.TrimSpace(p[1]) }
}
return m
}

38
go.mod
View file

@ -1,38 +0,0 @@
module git.sechpoint.app/customer-engineering/wallarm
go 1.24.0
toolchain go1.24.4
require (
github.com/charmbracelet/bubbletea v1.3.10
github.com/charmbracelet/huh v1.0.0
github.com/charmbracelet/lipgloss v1.1.0
golang.org/x/crypto v0.36.0
)
require (
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/catppuccin/go v0.3.0 // indirect
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 // indirect
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
github.com/charmbracelet/x/ansi v0.10.1 // indirect
github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/termenv v0.16.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/sys v0.36.0 // indirect
golang.org/x/text v0.23.0 // indirect
)

77
go.sum
View file

@ -1,77 +0,0 @@
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY=
github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E=
github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY=
github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 h1:JFgG/xnwFfbezlUnFMJy0nusZvytYysV4SCS2cYbvws=
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7/go.mod h1:ISC1gtLcVilLOf23wvTfoQuYbW2q0JevFxPfUzZ9Ybw=
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
github.com/charmbracelet/huh v1.0.0 h1:wOnedH8G4qzJbmhftTqrpppyqHakl/zbbNdXIWJyIxw=
github.com/charmbracelet/huh v1.0.0/go.mod h1:5YVc+SlZ1IhQALxRPpkGwwEKftN/+OlJlnJYlDRFqN4=
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ=
github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=
github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
github.com/charmbracelet/x/conpty v0.1.0 h1:4zc8KaIcbiL4mghEON8D72agYtSeIgq8FSThSPQIb+U=
github.com/charmbracelet/x/conpty v0.1.0/go.mod h1:rMFsDJoDwVmiYM10aD4bH2XiRgwI7NYJtQgl5yskjEQ=
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA=
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0=
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ=
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4=
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ=
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=
github.com/charmbracelet/x/xpty v0.1.2 h1:Pqmu4TEJ8KeA9uSkISKMU3f+C1F6OGBn8ABuGlqCbtI=
github.com/charmbracelet/x/xpty v0.1.2/go.mod h1:XK2Z0id5rtLWcpeNiMYBccNNBrP2IJnzHI0Lq13Xzq4=
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=

View file

@ -1,288 +0,0 @@
package native
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"git.sechpoint.app/customer-engineering/wallarm/internal/state"
)
const (
BaseDir = "/opt/fw"
DeployTo = "/opt/wallarm"
Symlink = "/opt/wallarm"
)
func installerURL() string {
if u := os.Getenv("WALLARM_INSTALLER_URL"); u != "" {
return u
}
return "https://storage.googleapis.com/meganode_storage/6.12/wallarm-6.12.5.x86_64-glibc.sh"
}
func InstallNode(node state.Node, apiToken, apiHost, labels, mode string) error {
if apiToken == "" {
return fmt.Errorf("API token required")
}
_ = labels
instanceDir := filepath.Join(BaseDir, node.Name, "wallarm")
installerPath := filepath.Join(BaseDir, "wallarm-aio.sh")
// 1. Prepare clean /opt/wallarm for this deployment
os.RemoveAll(DeployTo)
os.MkdirAll(DeployTo, 0755)
// 2. Kill any existing process on the target port, then start per-instance NGINX
port := "80"
if idx := strings.LastIndex(node.Address, ":"); idx != -1 {
port = node.Address[idx+1:]
}
killPort(port)
copyNginx(DeployTo)
// 3. Download AIO once
if _, err := os.Stat(installerPath); os.IsNotExist(err) {
fmt.Printf("[%s] Downloading installer...\n", node.Name)
url := installerURL()
var cmd *exec.Cmd
if _, err := exec.LookPath("curl"); err == nil {
cmd = exec.Command("curl", "-fsSL", "-o", installerPath, url)
} else {
cmd = exec.Command("wget", "-q", "-O", installerPath, url)
}
if out, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("download: %w\n%s", err, string(out))
}
os.Chmod(installerPath, 0755)
}
// 4. Extract AIO to /opt/wallarm
fmt.Printf("[%s] Extracting...\n", node.Name)
cmd := exec.Command("bash", installerPath, "--noexec", "--keep", "--target", DeployTo, "--noprogress", "--accept")
if out, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("extract: %w\n%s", err, string(out))
}
// 5. Start NGINX (modules now available after extraction)
startNginx(DeployTo, node, port, mode)
// 6. Load Wallarm NGINX module + register node
fmt.Printf("[%s] Configuring NGINX module...\n", node.Name)
exec.Command("bash", filepath.Join(DeployTo, "pick-module.sh")).Run()
// 7. Kill stale wcli lock (from other instances) then register
exec.Command("rm", "-f", "/tmp/.wallarm.wcli.lock").Run()
fmt.Printf("[%s] Registering node (this may take 30-60s)...\n", node.Name)
registerCmd := exec.Command(filepath.Join(DeployTo, "register-node"),
"job:register",
"-token", apiToken,
"-host", apiHost,
)
registerCmd.Dir = DeployTo
devNull, _ := os.Open(os.DevNull)
if devNull != nil {
registerCmd.Stdin = devNull
defer devNull.Close()
}
// Set environment from env.list
envData, _ := os.ReadFile(filepath.Join(DeployTo, "env.list"))
for _, line := range strings.Split(string(envData), "\n") {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "#") {
continue
}
registerCmd.Env = append(registerCmd.Env, line)
}
registerCmd.Env = append(registerCmd.Env, os.Environ()...)
regLog, _ := os.Create(filepath.Join(DeployTo, "register.log"))
if regLog != nil {
registerCmd.Stdout = regLog
registerCmd.Stderr = regLog
}
regErr := registerCmd.Run()
if regLog != nil {
regLog.Close()
}
if regErr != nil {
data, _ := os.ReadFile(filepath.Join(DeployTo, "register.log"))
return fmt.Errorf("register: %w\n%s", regErr, string(data))
}
// 8. Move /opt/wallarm → instanceDir (copy+delete for cross-fs safety)
exec.Command("pkill", "-9", "-f", filepath.Join(DeployTo, "nginx")).Run()
os.MkdirAll(filepath.Join(BaseDir, node.Name), 0755)
os.RemoveAll(instanceDir)
if err := os.Rename(DeployTo, instanceDir); err != nil {
// Cross-filesystem fallback
exec.Command("cp", "-a", DeployTo, instanceDir).Run()
os.RemoveAll(DeployTo)
}
// 9. Patch paths + fix ELF binaries + systemd
svc := "wallarm-node@" + node.Name
exec.Command("systemctl", "enable", svc).Run()
exec.Command("systemctl", "start", svc).Run()
fmt.Printf("[%s] Done. Installed to %s (systemctl status %s)\n", node.Name, instanceDir, svc)
return nil
}
func copyNginx(dir string) {
dst := filepath.Join(dir, "nginx", "sbin", "nginx")
if _, err := os.Stat(dst); err == nil {
return
}
os.MkdirAll(filepath.Dir(dst), 0755)
if path, err := exec.LookPath("nginx"); err == nil {
exec.Command("cp", path, dst).Run()
return
}
for _, pm := range [][]string{
{"apt-get", "install", "-y", "-qq", "nginx"},
{"yum", "install", "-y", "-q", "nginx"},
} {
if _, err := exec.LookPath(pm[0]); err == nil {
exec.Command(pm[0], pm[1:]...).Run()
if path, err := exec.LookPath("nginx"); err == nil {
exec.Command("cp", path, dst).Run()
return
}
}
}
}
func cloudFromHost(host string) string {
if strings.Contains(host, "us1") {
return "US"
}
return "EU"
}
func CreateNodesDir() error { return os.MkdirAll(BaseDir, 0755) }
func GenerateSystemdTemplate() error {
tmpl := `/etc/systemd/system/wallarm-node@.service`
content := fmt.Sprintf(`[Unit]
Description=Wallarm Node - %%i
After=network.target
[Service]
Type=simple
WorkingDirectory=%s/%%i/wallarm
EnvironmentFile=-%s/%%i/wallarm/env.list
# Bind mount instance directory as /opt/wallarm (isolated per instance)
ExecStartPre=/bin/ln -sf %s/%%i/wallarm /opt/wallarm
ExecStartPre=%s/%%i/wallarm/nginx/sbin/nginx -c %s/%%i/wallarm/nginx/conf/nginx.conf
ExecStartPre=/bin/sleep 1
ExecStart=%s/%%i/wallarm/usr/bin/python3.10 %s/%%i/wallarm/usr/bin/supervisord -c %s/%%i/wallarm/etc/supervisord.conf
ExecStop=%s/%%i/wallarm/usr/bin/python3.10 %s/%%i/wallarm/usr/bin/supervisord -c %s/%%i/wallarm/etc/supervisord.conf shutdown
ExecStopPost=
Restart=on-failure
RestartSec=5
User=root
[Install]
WantedBy=multi-user.target
`, BaseDir, BaseDir, BaseDir, BaseDir, BaseDir, BaseDir, BaseDir, BaseDir, BaseDir, BaseDir)
os.WriteFile(tmpl, []byte(content), 0644)
exec.Command("systemctl", "daemon-reload").Run()
return nil
}
func RemoveNode(name string) error {
exec.Command("systemctl", "stop", "wallarm-node@"+name).Run()
exec.Command("systemctl", "disable", "wallarm-node@"+name).Run()
os.RemoveAll(filepath.Join(BaseDir, name))
return nil
}
func Status(name string) (string, error) {
out, _ := exec.Command("systemctl", "status", "wallarm-node@"+name, "--no-pager").CombinedOutput()
return string(out), nil
}
func startNginx(dir string, node state.Node, port, mode string) {
nginxDir := filepath.Join(dir, "nginx")
confDir := filepath.Join(nginxDir, "conf")
os.MkdirAll(confDir, 0755)
confPath := filepath.Join(confDir, "nginx.conf")
upstream := fmt.Sprintf("%s:%d", node.UpstreamIP, node.UpstreamPort)
if node.UpstreamIP == "" {
upstream = "127.0.0.1:80"
}
os.WriteFile(confPath, []byte(fmt.Sprintf(`
load_module %s/modules/nginx_v1.26.3_s0ff5dffff/ngx_http_wallarm_module.so;
worker_processes auto;
pid %s/nginx.pid;
error_log %s/error.log;
events { worker_connections 10240; }
http {
access_log %s/access.log;
wallarm_mode %s;
server {
listen %s;
server_name _;
location /wallarm-status {
wallarm_status on;
allow 127.0.0.0/8;
deny all;
}
location / {
proxy_pass http://%s;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
}
`, dir, nginxDir, nginxDir, nginxDir, mode, port, upstream)), 0644)
bin := filepath.Join(nginxDir, "sbin", "nginx")
cmd := exec.Command(bin, "-c", confPath, "-p", nginxDir)
cmd.Dir = nginxDir
if out, err := cmd.CombinedOutput(); err != nil {
fmt.Printf("[%s] NGINX start: %v\n%s\n", node.Name, err, string(out))
} else {
fmt.Printf("[%s] NGINX started on port %s\n", node.Name, port)
}
}
func killPort(port string) {
// Find and kill any process listening on the target port
out, _ := exec.Command("fuser", "-k", port+"/tcp").CombinedOutput()
if len(out) > 0 {
fmt.Printf(" Killed existing process on port %s\n", port)
}
}
func patchPaths(dir string) {
old := "/opt/wallarm"
// Use bash to execute find+sed reliably
exec.Command("bash", "-c",
fmt.Sprintf("find %s -type f \\( -name '*.sh' -o -name '*.list' -o -name '*.conf' -o -name '*.yaml' -o -name '*.yml' -o -name '*.json' \\) -exec sed -i 's|%s|%s|g' {} +",
dir, old, dir)).Run()
// Unique internal ports + wcli config-file per instance
offset := hashPort(filepath.Base(filepath.Dir(dir)))
exec.Command("bash", "-c",
fmt.Sprintf("find %s -type f \\( -name '*.yaml' -o -name '*.yml' -o -name '*.conf' \\) -exec sed -i 's|:3313|:%d|g; s|:6388|:%d|g; s|:9001|:%d|g; s|:8088|:%d|g; s|:9667|:%d|g' {} + ; "+
"find %s -name '*.conf' -exec sed -i 's|\\(wcli run\\)$|\\1 --config-file %s/etc/wallarm/node.yaml|' {} +",
dir, 3313+offset, 6388+offset, 9001+offset, 8088+offset, 9667+offset,
dir, dir)).Run()
}
func hashPort(s string) int {
h := 0
for _, c := range s {
h = h*31 + int(c)
}
if h < 0 { h = -h }
return h % 500
}
// fixElfBinaries removed — patchelf breaks library paths. Use symlink instead.

View file

@ -1,172 +0,0 @@
// Package preflight runs system readiness checks before any deployment.
// It is called on every binary start and returns a structured report.
package preflight
import (
"fmt"
"os"
"git.sechpoint.app/customer-engineering/wallarm/internal/shared"
)
// Result holds the outcome of all preflight checks.
type Result struct {
Passed bool `json:"passed"`
Checks []Check `json:"checks"`
USReachable bool `json:"us_reachable"`
EUReachable bool `json:"eu_reachable"`
}
// Check represents a single preflight check.
type Check struct {
Name string `json:"name"`
Passed bool `json:"passed"`
Detail string `json:"detail,omitempty"`
Warning bool `json:"warning,omitempty"`
}
// Cloud endpoints (from wallarm-lib.sh)
var euEndpoints = []string{
"api.wallarm.com:443",
"node-data0.eu1.wallarm.com:443",
"node-data1.eu1.wallarm.com:443",
}
var usEndpoints = []string{
"us1.api.wallarm.com:443",
"node-data0.us1.wallarm.com:443",
"node-data1.us1.wallarm.com:443",
}
// Run executes all preflight checks and returns the result.
func Run() Result {
r := Result{Passed: true}
// 1. Root check
if os.Geteuid() != 0 {
r.Checks = append(r.Checks, Check{
Name: "root", Passed: false, Detail: "must run as root for package installation and system config",
})
r.Passed = false
} else {
r.Checks = append(r.Checks, Check{Name: "root", Passed: true})
}
// 2. Init system
initSys := shared.InitSystem()
if initSys != "systemd" {
r.Checks = append(r.Checks, Check{
Name: "init", Passed: false, Detail: fmt.Sprintf("requires systemd, detected: %s", initSys),
})
r.Passed = false
} else {
r.Checks = append(r.Checks, Check{Name: "init", Passed: true, Detail: "systemd"})
}
// 3. Architecture
arch := shared.Arch()
supported := arch == "x86_64" || arch == "aarch64"
r.Checks = append(r.Checks, Check{
Name: "arch", Passed: supported, Detail: arch,
})
if !supported {
r.Passed = false
}
// 4. OS
id, ver := shared.OSInfo()
r.Checks = append(r.Checks, Check{Name: "os", Passed: true, Detail: id + " " + ver})
// 5. Required commands (curl or wget)
hasDownloader := shared.CommandExists("curl") || shared.CommandExists("wget")
r.Checks = append(r.Checks, Check{Name: "downloader", Passed: hasDownloader})
if !hasDownloader { r.Passed = false }
requiredCmds := []string{"systemctl", "sed", "mkdir", "rm"}
for _, cmd := range requiredCmds {
ok := shared.CommandExists(cmd)
r.Checks = append(r.Checks, Check{Name: "cmd:" + cmd, Passed: ok})
if !ok {
r.Passed = false
}
}
// 6. Installer reachability
installerOk := shared.HTTPHead("https://storage.googleapis.com/meganode_storage/")
r.Checks = append(r.Checks, Check{
Name: "installer_reachable", Passed: installerOk,
Detail: "storage.googleapis.com",
})
if !installerOk {
r.Checks[len(r.Checks)-1].Warning = true
}
// 7. Cloud endpoints
r.USReachable = checkEndpoints(usEndpoints)
r.EUReachable = checkEndpoints(euEndpoints)
r.Checks = append(r.Checks, Check{
Name: "cloud:US", Passed: r.USReachable,
Detail: fmt.Sprintf("%d/%d reachable", countReachable(usEndpoints), len(usEndpoints)),
})
r.Checks = append(r.Checks, Check{
Name: "cloud:EU", Passed: r.EUReachable,
Detail: fmt.Sprintf("%d/%d reachable", countReachable(euEndpoints), len(euEndpoints)),
})
if !r.USReachable && !r.EUReachable {
r.Passed = false
}
// 8. Disk space (>= 2GB)
free, err := shared.FreeDiskMB("/opt")
if err == nil && free < 2048 {
r.Checks = append(r.Checks, Check{
Name: "disk", Passed: false,
Detail: fmt.Sprintf("%d MB free (need >= 2048 MB)", free),
})
r.Passed = false
} else {
r.Checks = append(r.Checks, Check{Name: "disk", Passed: true, Detail: fmt.Sprintf("%d MB free", free)})
}
// 9. Memory (>= 2GB, warning only)
mem, err := shared.FreeMemoryMB()
if err == nil && mem < 2048 {
r.Checks = append(r.Checks, Check{
Name: "memory", Passed: true, Warning: true,
Detail: fmt.Sprintf("%d MB (2GB+ recommended)", mem),
})
} else {
r.Checks = append(r.Checks, Check{Name: "memory", Passed: true, Detail: fmt.Sprintf("%d MB", mem)})
}
return r
}
func checkEndpoints(endpoints []string) bool {
for _, ep := range endpoints {
host, _ := splitHostPort(ep)
if shared.TCPConnect(host, 443) {
return true
}
}
return false
}
func countReachable(endpoints []string) int {
n := 0
for _, ep := range endpoints {
host, _ := splitHostPort(ep)
if shared.TCPConnect(host, 443) {
n++
}
}
return n
}
func splitHostPort(addr string) (string, string) {
for i := len(addr) - 1; i >= 0; i-- {
if addr[i] == ':' {
return addr[:i], addr[i+1:]
}
}
return addr, ""
}

View file

@ -1,174 +0,0 @@
// Package shared provides validation, connectivity, and system detection
// utilities ported from the bash wallarm-lib.sh library.
package shared
import (
"fmt"
"os"
"os/exec"
"runtime"
"strconv"
"strings"
)
// ─── System Detection ────────────────────────────────────────────────
// InitSystem returns the detected init system: systemd, openrc, sysvinit, upstart, or unknown.
func InitSystem() string {
if runtime.GOOS == "darwin" {
return "darwin"
}
if _, err := exec.LookPath("systemctl"); err == nil {
return "systemd"
}
if _, err := os.Stat("/sbin/openrc-run"); err == nil {
return "openrc"
}
if _, err := os.Stat("/etc/init.d"); err == nil {
return "sysvinit"
}
if _, err := os.Stat("/sbin/upstart"); err == nil {
return "upstart"
}
return "unknown"
}
// OSInfo returns (os_id, version_id) from /etc/os-release.
func OSInfo() (id, version string) {
data, err := os.ReadFile("/etc/os-release")
if err != nil {
return strings.ToLower(runtime.GOOS), "unknown"
}
for _, line := range strings.Split(string(data), "\n") {
if strings.HasPrefix(line, "ID=") {
id = strings.Trim(strings.TrimPrefix(line, "ID="), `"`)
}
if strings.HasPrefix(line, "VERSION_ID=") {
version = strings.Trim(strings.TrimPrefix(line, "VERSION_ID="), `"`)
}
}
if id == "" {
id = strings.ToLower(runtime.GOOS)
}
return id, version
}
// Arch returns the normalized architecture: x86_64, aarch64, or armhf.
func Arch() string {
switch runtime.GOARCH {
case "amd64":
return "x86_64"
case "arm64":
return "aarch64"
case "arm":
return "armhf"
default:
return runtime.GOARCH
}
}
// ─── Validation ──────────────────────────────────────────────────────
// ValidateIP checks whether s is a valid IPv4 address.
func ValidateIP(s string) error {
parts := strings.Split(s, ".")
if len(parts) != 4 {
return fmt.Errorf("invalid IPv4: %s", s)
}
for _, p := range parts {
n, err := strconv.Atoi(p)
if err != nil || n < 0 || n > 255 {
return fmt.Errorf("invalid IPv4 octet: %s", p)
}
}
return nil
}
// ValidateCIDR checks whether s is a valid IPv4 address with optional /prefix.
func ValidateCIDR(s string) error {
ip := s
prefix := ""
if idx := strings.IndexByte(s, '/'); idx != -1 {
ip, prefix = s[:idx], s[idx+1:]
}
if err := ValidateIP(ip); err != nil {
return err
}
if prefix != "" {
n, err := strconv.Atoi(prefix)
if err != nil || n < 0 || n > 32 {
return fmt.Errorf("invalid CIDR prefix: %s", prefix)
}
}
return nil
}
// CommandExists returns true if cmd is in PATH or in common system directories.
func CommandExists(cmd string) bool {
if _, err := exec.LookPath(cmd); err == nil {
return true
}
for _, dir := range []string{"/usr/sbin", "/sbin", "/usr/local/sbin", "/usr/bin", "/bin", "/usr/local/bin"} {
if _, err := os.Stat(dir + "/" + cmd); err == nil {
return true
}
}
return false
}
// ─── Resource Checks ─────────────────────────────────────────────────
// FreeDiskMB returns available disk space in MB for the given path.
func FreeDiskMB(path string) (int64, error) {
cmd := exec.Command("df", "-k", path)
out, err := cmd.Output()
if err != nil {
return 0, err
}
lines := strings.Split(strings.TrimSpace(string(out)), "\n")
if len(lines) < 2 {
return 0, fmt.Errorf("unexpected df output")
}
fields := strings.Fields(lines[1])
if len(fields) < 4 {
return 0, fmt.Errorf("unexpected df fields")
}
kb, err := strconv.ParseInt(fields[3], 10, 64)
if err != nil {
return 0, err
}
return kb / 1024, nil // convert KB to MB
}
// FreeMemoryMB returns available memory in MB.
func FreeMemoryMB() (int64, error) {
cmd := exec.Command("free", "-m")
out, err := cmd.Output()
if err != nil {
return 0, err
}
lines := strings.Split(strings.TrimSpace(string(out)), "\n")
if len(lines) < 2 {
return 0, fmt.Errorf("unexpected free output")
}
fields := strings.Fields(lines[1])
if len(fields) < 2 {
return 0, fmt.Errorf("unexpected free fields")
}
return strconv.ParseInt(fields[1], 10, 64)
}
// ─── Connectivity ────────────────────────────────────────────────────
// TCPConnect tests whether host:port accepts a TCP connection.
func TCPConnect(host string, port int) bool {
cmd := exec.Command("timeout", "5", "bash", "-c",
fmt.Sprintf("echo >/dev/tcp/%s/%d 2>/dev/null", host, port))
return cmd.Run() == nil
}
// HTTPHead returns true if the URL returns a successful status.
func HTTPHead(url string) bool {
cmd := exec.Command("curl", "-fsSL", "--connect-timeout", "10", url)
return cmd.Run() == nil
}

View file

@ -1,79 +0,0 @@
// Package state manages the persistent deployment state file (~/.wallarm/state.json).
package state
import (
"encoding/json"
"fmt"
"os"
)
// StateDir is where wallarm stores its state (under /opt/wallarm/).
const StateDir = "/opt/fw"
// State represents the persistent deployment state.
type State struct {
DeploymentType string `json:"deployment_type,omitempty"` // docker or native
CloudRegion string `json:"cloud_region,omitempty"` // US or EU
APIHost string `json:"api_host,omitempty"` // e.g., api.wallarm.com
APIToken string `json:"api_token,omitempty"` // Wallarm API token (sensitive)
Nodes []Node `json:"nodes"`
}
// Node represents a single deployed Wallarm node (docker container or native systemd unit).
type Node struct {
Name string `json:"name"`
Type string `json:"type"` // docker or native
Address string `json:"address,omitempty"` // listen address for native
Port int `json:"port,omitempty"` // ingress port for docker
UpstreamIP string `json:"upstream_ip,omitempty"` // docker
UpstreamPort int `json:"upstream_port,omitempty"` // docker
Status string `json:"status"` // running, stopped, unknown
CreatedAt string `json:"created_at"`
}
// Path returns the full path to the state file.
func Path() string {
return StateDir + "/state.json"
}
// Load reads and parses the state file. Returns nil if the file doesn't exist.
func Load() (*State, error) {
p := Path()
data, err := os.ReadFile(p)
if os.IsNotExist(err) {
return nil, nil
}
if err != nil {
return nil, fmt.Errorf("read state: %w", err)
}
var s State
if err := json.Unmarshal(data, &s); err != nil {
return nil, fmt.Errorf("parse state: %w", err)
}
return &s, nil
}
// Save writes the state to /opt/wallarm/state.json.
func Save(s *State) error {
p := Path()
if err := os.MkdirAll(StateDir, 0755); err != nil {
return fmt.Errorf("create state dir: %w", err)
}
data, err := json.MarshalIndent(s, "", " ")
if err != nil {
return fmt.Errorf("marshal state: %w", err)
}
if err := os.WriteFile(p, data, 0600); err != nil {
return fmt.Errorf("write state: %w", err)
}
return nil
}
// HasDeployment returns true if a state file exists with at least one node.
func HasDeployment() bool {
s, err := Load()
if err != nil || s == nil {
return false
}
return len(s.Nodes) > 0
}

View file

@ -1,152 +0,0 @@
// Package tunnel provides a reverse SSH tunnel over TLS:443 via a Zoraxy edge proxy.
// It opens an outbound TLS connection to the configured jumphost, authenticates
// via SSH, and establishes a reverse port forward so you can reach the target VM
// through sechpoint.app.
package tunnel
import (
"crypto/tls"
"fmt"
"io"
"net"
"os"
"os/signal"
"time"
"golang.org/x/crypto/ssh"
)
// Config holds the tunnel connection parameters.
type Config struct {
Jumphost string // e.g., "ssh.sechpoint.app:443"
RemotePort int // Port on the jumphost that forwards to target's SSH
LocalSSHPort int // SSH port on the target VM (usually 22)
User string // SSH user on the jumphost
Password string // Password auth (takes lowest priority)
KeyPath string // Path to private key for authentication
KeyBytes []byte // Raw private key bytes (takes precedence over KeyPath)
}
// DefaultConfig returns a Config with sensible defaults.
func DefaultConfig() Config {
return Config{
Jumphost: "ssh.sechpoint.app:443",
RemotePort: 9042,
LocalSSHPort: 22,
User: "wallarm-tunnel",
}
}
// Start opens a reverse SSH tunnel over TLS and keeps it alive.
// It blocks until SIGINT or connection failure.
func Start(cfg Config) error {
// Build auth methods
var authMethods []ssh.AuthMethod
if len(cfg.KeyBytes) > 0 {
signer, err := ssh.ParsePrivateKey(cfg.KeyBytes)
if err == nil {
authMethods = append(authMethods, ssh.PublicKeys(signer))
}
}
if cfg.KeyPath != "" {
keyBytes, err := os.ReadFile(cfg.KeyPath)
if err == nil {
signer, err := ssh.ParsePrivateKey(keyBytes)
if err == nil {
authMethods = append(authMethods, ssh.PublicKeys(signer))
}
}
}
if cfg.Password != "" {
authMethods = append(authMethods, ssh.Password(cfg.Password))
}
if len(authMethods) == 0 {
return fmt.Errorf("no authentication method configured (provide key or password)")
}
sshConfig := &ssh.ClientConfig{
User: cfg.User,
Auth: authMethods,
HostKeyCallback: ssh.InsecureIgnoreHostKey(), // trusted infrastructure
Timeout: 10 * time.Second,
}
// TLS dial to the Zoraxy edge (port 443)
tlsConn, err := tls.Dial("tcp", cfg.Jumphost, &tls.Config{
InsecureSkipVerify: false,
})
if err != nil {
return fmt.Errorf("TLS dial %s: %w", cfg.Jumphost, err)
}
// SSH over TLS
sshConn, chans, reqs, err := ssh.NewClientConn(tlsConn, cfg.Jumphost, sshConfig)
if err != nil {
tlsConn.Close()
return fmt.Errorf("SSH handshake: %w", err)
}
client := ssh.NewClient(sshConn, chans, reqs)
defer client.Close()
// Request reverse port forward: jumphost:RemotePort -> localhost:LocalSSHPort
remoteAddr := fmt.Sprintf("0.0.0.0:%d", cfg.RemotePort)
localAddr := fmt.Sprintf("localhost:%d", cfg.LocalSSHPort)
listener, err := client.Listen("tcp", remoteAddr)
if err != nil {
return fmt.Errorf("remote listen %s: %w", remoteAddr, err)
}
defer listener.Close()
fmt.Printf("Tunnel established: %s -> %s\n", remoteAddr, localAddr)
fmt.Printf("Connect: ssh -p %d root@%s\n", cfg.RemotePort, cfg.Jumphost)
// Handle incoming connections on the remote listener
go func() {
for {
remoteConn, err := listener.Accept()
if err != nil {
return
}
go forwardConnection(remoteConn, localAddr)
}
}()
// Keep alive until signal
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, os.Interrupt)
// Heartbeat every 30s
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
for {
select {
case <-sigCh:
fmt.Println("\nTunnel closed.")
return nil
case <-ticker.C:
_, _, err := client.SendRequest("keepalive@wallarm", true, nil)
if err != nil {
return fmt.Errorf("keepalive failed: %w", err)
}
}
}
}
func forwardConnection(remoteConn net.Conn, localAddr string) {
defer remoteConn.Close()
localConn, err := net.DialTimeout("tcp", localAddr, 10*time.Second)
if err != nil {
return
}
defer localConn.Close()
go func() {
io.Copy(localConn, remoteConn)
remoteConn.Close()
}()
io.Copy(remoteConn, localConn)
}

View file

@ -1,149 +0,0 @@
// Package ui provides the bubbletea terminal UI for wallarm:
// - Deploy choice: local deploy or remote assistance
// - Dashboard: lists existing nodes with actions
//
// Deployment logic runs in plain terminal (main.go), not inside the TUI.
package ui
import (
"fmt"
"strings"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"git.sechpoint.app/customer-engineering/wallarm/internal/state"
)
var (
titleStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("63")).MarginBottom(1)
goodStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("42"))
warnStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("227"))
badStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("196"))
dimStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("240"))
activeStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("63")).Bold(true)
)
type Model struct {
state stateView
width int
height int
}
type stateView int
const (
viewRoute stateView = iota
viewDeployChoice // local deploy or remote assist?
viewDashboard
)
// Run starts the bubbletea TUI. Returns nil if user chose local deploy (exit and run form).
func Run() error {
m := Model{state: viewRoute}
p := tea.NewProgram(m)
if _, err := p.Run(); err != nil {
return err
}
return nil
}
func (m Model) Init() tea.Cmd { return nil }
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "q", "ctrl+c":
return m, tea.Quit
case "1":
if m.state == viewDeployChoice {
return m, tea.Quit // exit TUI, main.go runs deploy form
}
case "2":
if m.state == viewDeployChoice {
return m, tea.Quit // exit TUI, main.go runs tunnel flow
}
}
case tea.WindowSizeMsg:
m.width = msg.Width
m.height = msg.Height
}
switch m.state {
case viewRoute:
if state.HasDeployment() {
m.state = viewDashboard
} else {
m.state = viewDeployChoice
}
}
return m, nil
}
func (m Model) View() string {
switch m.state {
case viewRoute:
return ""
case viewDeployChoice:
return deployChoiceView(m)
case viewDashboard:
return dashboardView(m)
}
return ""
}
// ─── Deploy Choice ───────────────────────────────────────────────────
func deployChoiceView(m Model) string {
s := titleStyle.Render("🛡️ Wallarm Setup") + "\n\n"
s += "How would you like to proceed?\n\n"
s += activeStyle.Render(" [1] Deploy locally") + "\n"
s += " Configure and deploy the Wallarm node on this server now.\n\n"
s += activeStyle.Render(" [2] Remote assistance") + "\n"
s += " Open a secure tunnel so a remote admin can deploy for you.\n\n"
s += dimStyle.Render("Press 1 or 2, q to quit")
return s
}
// ─── Dashboard ───────────────────────────────────────────────────────
func dashboardView(m Model) string {
s, err := state.Load()
if err != nil || s == nil {
return badStyle.Render("Error loading state.") + "\nPress q to quit"
}
out := titleStyle.Render("📊 Wallarm Dashboard") + "\n"
out += fmt.Sprintf("Type: %s | Cloud: %s (%s)\n", s.DeploymentType, s.CloudRegion, s.APIHost)
out += dimStyle.Render(strings.Repeat("─", 50)) + "\n\n"
if len(s.Nodes) == 0 {
out += dimStyle.Render("No nodes deployed yet.") + "\n\n"
} else {
out += activeStyle.Render("Nodes:") + "\n"
for _, n := range s.Nodes {
marker := "●"
style := goodStyle
if n.Status != "running" {
marker = "○"
style = warnStyle
}
out += style.Render(fmt.Sprintf(" %s %s — %s", marker, n.Name, n.Status))
if n.Address != "" {
out += dimStyle.Render(fmt.Sprintf(" (%s)", n.Address))
}
out += "\n"
}
out += "\n"
}
out += "Actions:\n"
out += fmt.Sprintf(" %s\n", activeStyle.Render("[a] Add node"))
out += fmt.Sprintf(" %s\n", activeStyle.Render("[c] Configure"))
out += fmt.Sprintf(" %s\n", activeStyle.Render("[r] Remove node"))
out += fmt.Sprintf(" %s\n", activeStyle.Render("[t] Start tunnel"))
out += fmt.Sprintf(" %s\n", dimStyle.Render("[q] Quit"))
return out
}

390
python/deploy.py Normal file
View file

@ -0,0 +1,390 @@
#!/usr/bin/env python3
"""Wallarm Node Manager — deploy, edit, remove, status, tunnel."""
import json, os, sys, time, subprocess, shutil, hashlib
from pathlib import Path
VERSION = "1.0.0"
BASE = "/opt/fw"
STATE = f"{BASE}/state.json"
CONF = f"{BASE}/fw.conf"
AIO_URL = "https://storage.googleapis.com/meganode_storage/6.12/wallarm-6.12.5.x86_64-glibc.sh"
AIO_PATH = f"{BASE}/wallarm-aio.sh"
DEPLOY_TO = "/opt/wallarm"
# ─── Helpers ───────────────────────────────────────────────────────────
def run(cmd, timeout=120, check=False):
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")):
check(f"cloud:{cloud}", run_ok(f"curl -fsSL -o /dev/null https://{host} 2>/dev/null"), host)
return ok
# ─── Config ────────────────────────────────────────────────────────────
def load_config():
if not os.path.exists(CONF):
return {}
with open(CONF) as f:
return json.load(f)
def load_state():
if not os.path.exists(STATE):
return {"nodes": []}
with open(STATE) as f:
return json.load(f)
def save_state(s):
with open(STATE, "w") as f:
json.dump(s, f, indent=2)
def is_deployed(name):
s = load_state()
return any(n["name"] == name for n in s.get("nodes", []))
# ─── Node Operations ───────────────────────────────────────────────────
def install_node(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"
address = f"0.0.0.0:{port}"
upstream = f"{upstream_ip}:{upstream_port}"
# Download AIO
if not os.path.exists(AIO_PATH):
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
if os.path.islink(DEPLOY_TO) or os.path.isfile(DEPLOY_TO):
os.remove(DEPLOY_TO)
shutil.rmtree(DEPLOY_TO, ignore_errors=True)
os.makedirs(DEPLOY_TO, exist_ok=True)
# Install NGINX
nginx_bin = f"{DEPLOY_TO}/nginx/sbin/nginx"
if not os.path.exists(nginx_bin):
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
print(f"[{name}] Extracting...")
run(f"bash {AIO_PATH} --noexec --keep --target {DEPLOY_TO} --noprogress --accept 2>&1", timeout=120)
# Nginx config
nginx_dir = f"{DEPLOY_TO}/nginx"
os.makedirs(f"{nginx_dir}/conf", exist_ok=True)
with open(f"{nginx_dir}/conf/nginx.conf", "w") as f:
f.write(f"""load_module {DEPLOY_TO}/modules/nginx_v1.26.3_s0ff5dffff/ngx_http_wallarm_module.so;
worker_processes auto;
pid {nginx_dir}/nginx.pid;
error_log {nginx_dir}/error.log;
events {{ worker_connections 10240; }}
http {{
access_log {nginx_dir}/access.log;
wallarm_mode {mode};
server {{
listen {port};
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)
if r.returncode != 0 and "node instance registered" not in r.stdout:
print(f"Registration output:\n{r.stdout}\n{r.stderr}")
# Not fatal — check if UUID assigned
if not os.path.exists(f"{DEPLOY_TO}/etc/wallarm/node.yaml"):
raise RuntimeError(f"Registration failed: {r.stderr or r.stdout}")
# Move to instance
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 + ports
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 config-file to wcli
for conf in Path(f"{instance}/etc").glob("*.conf"):
c = conf.read_text()
if "wcli run" in c and "--config-file" not in c:
conf.write_text(c.replace("wcli run", f"wcli run --config-file {instance}/etc/wallarm/node.yaml"))
# Systemd
tmpl = f"""[Unit]
Description=Wallarm Node - %i
After=network.target
[Service]
Type=simple
WorkingDirectory={BASE}/%i/wallarm
EnvironmentFile=-{BASE}/%i/wallarm/env.list
ExecStartPre=/bin/ln -sf {BASE}/%i/wallarm /opt/wallarm
ExecStartPre={BASE}/%i/wallarm/nginx/sbin/nginx -c {BASE}/%i/wallarm/nginx/conf/nginx.conf
ExecStartPre=/bin/sleep 1
ExecStart={BASE}/%i/wallarm/usr/bin/python3.10 {BASE}/%i/wallarm/usr/bin/supervisord -c {BASE}/%i/wallarm/etc/supervisord.conf
ExecStop={BASE}/%i/wallarm/usr/bin/python3.10 {BASE}/%i/wallarm/usr/bin/supervisord -c {BASE}/%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}")
# ─── Remove ────────────────────────────────────────────────────────────
def remove_node(name):
run(f"systemctl stop wallarm-node@{name} 2>/dev/null")
run(f"systemctl disable wallarm-node@{name} 2>/dev/null")
shutil.rmtree(f"{BASE}/{name}", ignore_errors=True)
s = load_state()
s["nodes"] = [n for n in s["nodes"] if n["name"] != name]
save_state(s)
print(f"{name} removed.")
# ─── Status ────────────────────────────────────────────────────────────
def show_status():
s = load_state()
if not s.get("nodes"):
print("No nodes deployed.")
return
print("\n─── Node Status ───")
for n in s["nodes"]:
r = run(f"systemctl is-active wallarm-node@{n['name']}")
status = "" if r.stdout.strip() == "active" else ""
print(f" {status} {n['name']}{r.stdout.strip()}{n.get('address','')}")
# ─── Tunnel ────────────────────────────────────────────────────────────
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 edit_menu():
s = load_state()
if not s.get("nodes"):
print("No nodes.")
return
print("\n─── Edit Node ───")
for i, n in enumerate(s["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(): return
n = s["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():
s = load_state()
if not s.get("nodes"):
print("No nodes.")
return
print("\n─── Remove Node ───")
for i, n in enumerate(s["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(): return
n = s["nodes"][int(c)-1]
remove_node(n["name"])
# ─── Main ──────────────────────────────────────────────────────────────
def main():
if len(sys.argv) > 1 and sys.argv[1] == "--version":
print(f"deploy.py v{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
def deploy_all():
cfg = load_config()
for name, nc in cfg.get("nodes", {}).items():
if is_deployed(name):
print(f"{name} already deployed")
continue
print(f"\nDeploying {name}...")
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}"),
nc.get("mode","monitoring"))
if __name__ == "__main__":
main()

View file

@ -2,66 +2,58 @@
# ============================================================================== # ==============================================================================
# Wallarm Deployment Bootstrap # Wallarm Deployment Bootstrap
# ============================================================================== # ==============================================================================
# Builds the deploy binary and installs to /opt/fw/. # Installs Python 3.12 via pyenv (cross-distro), then runs deploy.py.
# The binary handles everything: symlinks, extraction, setup.
#
# curl -fsSL ".../setup.sh" | bash # curl -fsSL ".../setup.sh" | bash
# sudo /opt/fw/deploy # sudo /opt/fw/deploy.py
# ============================================================================== # ==============================================================================
set -euo pipefail set -euo pipefail
BOLD='\033[1m' BOLD='\033[1m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; RED='\033[0;31m'; NC='\033[0m'
GREEN='\033[0;32m' REPO="https://git.sechpoint.app/customer-engineering/wallarm/raw/branch/main/python"
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m'
REPO_URL="https://git.sechpoint.app/customer-engineering/wallarm.git"
WALLARM_DIR="/opt/fw" WALLARM_DIR="/opt/fw"
BUILD_DIR="/tmp/wallarm-build-$$"
cleanup() { rm -rf "$BUILD_DIR"; }
trap cleanup EXIT
echo -e "${BOLD}Wallarm Bootstrap${NC}" echo -e "${BOLD}Wallarm Bootstrap${NC}"
# ── Prerequisites ───────────────────────────────────────────────────── # ── Install curl/git if missing ───────────────────────────────────────
install_deps() { if ! command -v curl >/dev/null 2>&1 && ! command -v wget >/dev/null 2>&1; then
if command -v apt-get >/dev/null 2>&1; then apt-get update -qq 2>/dev/null && apt-get install -y -qq curl 2>/dev/null || true
apt-get update -qq
apt-get install -y -qq curl git golang-go 2>/dev/null
elif command -v yum >/dev/null 2>&1; then
yum install -y -q curl git golang 2>/dev/null
elif command -v dnf >/dev/null 2>&1; then
dnf install -y -q curl git golang 2>/dev/null
elif command -v apk >/dev/null 2>&1; then
apk add --no-cache curl git go 2>/dev/null
fi fi
}
if ! command -v curl >/dev/null 2>&1 || ! command -v git >/dev/null 2>&1 || ! command -v go >/dev/null 2>&1; then # ── Install pyenv + Python 3.12 ───────────────────────────────────────
echo -e "${YELLOW}Installing dependencies...${NC}" PYTHON_BIN=""
install_deps if command -v python3 >/dev/null 2>&1 && python3 -c "import sys; exit(0 if sys.version_info >= (3,10) else 1)" 2>/dev/null; then
PYTHON_BIN="python3"
elif [ -f "$HOME/.pyenv/versions/3.12.0/bin/python3" ]; then
PYTHON_BIN="$HOME/.pyenv/versions/3.12.0/bin/python3"
else
echo -e "${YELLOW}Installing pyenv + Python 3.12...${NC}"
if ! command -v git >/dev/null 2>&1; then
apt-get update -qq 2>/dev/null && apt-get install -y -qq git 2>/dev/null || true
fi
# Install build deps
apt-get install -y -qq make build-essential libssl-dev zlib1g-dev \
libbz2-dev libreadline-dev libsqlite3-dev libncursesw5-dev \
xz-utils tk-dev libxml2-dev libxmlsec1-dev libffi-dev liblzma-dev 2>/dev/null || true
curl -fsSL https://pyenv.run | bash 2>/dev/null
export PYENV_ROOT="$HOME/.pyenv"
export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init -)"
pyenv install 3.12.0 -s 2>/dev/null
pyenv global 3.12.0
PYTHON_BIN="$HOME/.pyenv/versions/3.12.0/bin/python3"
fi fi
echo -e "${GREEN}git $(git --version | cut -d' ' -f3) go $(go version | cut -d' ' -f3)${NC}"
# ── Build ───────────────────────────────────────────────────────────── echo -e "${GREEN}Python: $($PYTHON_BIN --version)${NC}"
echo -e "${YELLOW}Cloning...${NC}"
git clone --depth 1 "$REPO_URL" "$BUILD_DIR" 2>/dev/null
echo -e "${YELLOW}Building (~30s)...${NC}" # ── Download deploy.py ────────────────────────────────────────────────
cd "$BUILD_DIR"
go build -ldflags="-s -w -X main.version=$(git describe --tags --always 2>/dev/null || echo 'dev')" -o deploy ./cmd/deploy/
command -v upx >/dev/null 2>&1 && upx --best --lzma deploy -o deploy.tmp 2>/dev/null && mv deploy.tmp deploy || true
# ── Install ───────────────────────────────────────────────────────────
mkdir -p "$WALLARM_DIR" mkdir -p "$WALLARM_DIR"
cp deploy "$WALLARM_DIR/deploy" if command -v curl >/dev/null 2>&1; then
chmod +x "$WALLARM_DIR/deploy" curl -fsSL "${REPO}/deploy.py" -o "${WALLARM_DIR}/deploy.py"
else
wget -q "${REPO}/deploy.py" -O "${WALLARM_DIR}/deploy.py"
fi
chmod +x "${WALLARM_DIR}/deploy.py"
V=$("$WALLARM_DIR/deploy" --version 2>/dev/null || echo "unknown")
echo echo
echo -e "${GREEN}${BOLD}Ready!${NC} (${V})" echo -e "${GREEN}${BOLD}Ready!${NC}"
echo -e " ${GREEN}sudo /opt/fw/deploy${NC}" echo -e " ${GREEN}sudo ${PYTHON_BIN} /opt/fw/deploy.py${NC}"
ls -lh "$WALLARM_DIR/deploy"