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, kill temp nginx exec.Command("pkill", "-f", filepath.Join(DeployTo, "nginx")).Run() os.MkdirAll(filepath.Join(BaseDir, node.Name), 0755) os.RemoveAll(instanceDir) os.Rename(DeployTo, instanceDir) // 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 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 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 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" // 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.