- ExecStartPre=- allows nginx start failure without blocking service - Removed shell constructs (|| true) incompatible with systemd - Removed env.list background goroutine (setup.sh no longer used) - Multi-instance needs per-instance ELF patching (WIP)
266 lines
7.8 KiB
Go
266 lines
7.8 KiB
Go
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 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)
|
|
cmd := exec.Command("curl", "-fsSL", "-o", installerPath, installerURL())
|
|
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)
|
|
|
|
// 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 → /opt/fw/{name}/wallarm
|
|
os.MkdirAll(filepath.Join(BaseDir, node.Name), 0755)
|
|
os.RemoveAll(instanceDir)
|
|
os.Rename(DeployTo, instanceDir)
|
|
|
|
// 9. Create symlink so ELF binaries find their interpreter
|
|
os.Remove(Symlink)
|
|
os.Symlink(instanceDir, Symlink)
|
|
|
|
// 10. Patch paths + systemd
|
|
patchPaths(instanceDir)
|
|
GenerateSystemdTemplate()
|
|
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
|
|
ExecStartPre=-%s/%%i/wallarm/nginx/sbin/nginx -c %s/%%i/wallarm/nginx/conf/nginx.conf -p %s/%%i/wallarm/nginx/
|
|
ExecStartPre=/bin/sleep 2
|
|
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=-%s/%%i/wallarm/nginx/sbin/nginx -s quit -p %s/%%i/wallarm/nginx/
|
|
Restart=on-failure
|
|
RestartSec=5
|
|
User=root
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
`, BaseDir, BaseDir, 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 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 monitoring;
|
|
|
|
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, 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"
|
|
// Patch all text config files (not binaries)
|
|
for _, pattern := range []string{
|
|
"*.sh", "*.list", "*.conf", "*.yaml", "*.yml", "*.json",
|
|
} {
|
|
exec.Command("find", dir, "-name", pattern, "-exec",
|
|
"sed", "-i", fmt.Sprintf("s|%s|%s|g", old, dir), "{}", ";").Run()
|
|
}
|
|
}
|