wallarm/internal/native/native.go

269 lines
7.6 KiB
Go

package native
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
"git.sechpoint.app/customer-engineering/wallarm/internal/state"
)
const (
BaseDir = "/opt/fw"
DeployTo = "/opt/wallarm" // setup.sh hardcodes this, deploy here then move
)
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)
startNginx(DeployTo, node, port)
// 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. Fix: recreate env.list in background while setup.sh runs
done := make(chan struct{})
go func() {
defer close(done)
for {
select {
case <-done:
return
default:
os.WriteFile(filepath.Join(DeployTo, "env.list"),
[]byte("# wallarm env\n"), 0644)
time.Sleep(500 * time.Millisecond)
}
}
}()
// 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()
// 6. Register node — skip if this node was previously deployed
existingState, _ := state.Load()
alreadyRegistered := false
if existingState != nil {
for _, n := range existingState.Nodes {
if n.Name == node.Name {
alreadyRegistered = true
break
}
}
}
if alreadyRegistered {
fmt.Printf("[%s] Already registered, skipping.\n", node.Name)
} else {
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))
}
}
// 7. Move /opt/wallarm → /opt/fw/{name}/wallarm
os.MkdirAll(filepath.Join(BaseDir, node.Name), 0755)
os.RemoveAll(instanceDir)
os.Rename(DeployTo, instanceDir)
// 8. Patch hardcoded /opt/wallarm paths
patchPaths(instanceDir)
// 9. Set up systemd service
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
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)
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")
os.WriteFile(confPath, []byte(fmt.Sprintf(`
worker_processes auto;
pid %s/nginx.pid;
error_log %s/error.log;
events { worker_connections 10240; }
http {
access_log %s/access.log;
server {
listen %s;
server_name _;
location /health { return 200; }
}
}
`, nginxDir, nginxDir, nginxDir, port)), 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()
}
}