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:
parent
31cbb56629
commit
43dfc6ec06
12 changed files with 430 additions and 1735 deletions
36
Makefile
36
Makefile
|
|
@ -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)"
|
|
||||||
|
|
||||||
|
|
@ -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
38
go.mod
|
|
@ -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
77
go.sum
|
|
@ -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=
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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, ""
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
@ -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)
|
|
||||||
}
|
|
||||||
|
|
@ -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
390
python/deploy.py
Normal 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()
|
||||||
88
setup.sh
88
setup.sh
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
||||||
if ! command -v curl >/dev/null 2>&1 || ! command -v git >/dev/null 2>&1 || ! command -v go >/dev/null 2>&1; then
|
|
||||||
echo -e "${YELLOW}Installing dependencies...${NC}"
|
|
||||||
install_deps
|
|
||||||
fi
|
fi
|
||||||
echo -e "${GREEN}git $(git --version | cut -d' ' -f3) go $(go version | cut -d' ' -f3)${NC}"
|
|
||||||
|
|
||||||
# ── Build ─────────────────────────────────────────────────────────────
|
# ── Install pyenv + Python 3.12 ───────────────────────────────────────
|
||||||
echo -e "${YELLOW}Cloning...${NC}"
|
PYTHON_BIN=""
|
||||||
git clone --depth 1 "$REPO_URL" "$BUILD_DIR" 2>/dev/null
|
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
|
||||||
|
|
||||||
echo -e "${YELLOW}Building (~30s)...${NC}"
|
echo -e "${GREEN}Python: $($PYTHON_BIN --version)${NC}"
|
||||||
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 ───────────────────────────────────────────────────────────
|
# ── Download deploy.py ────────────────────────────────────────────────
|
||||||
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"
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue