172 lines
4.6 KiB
Go
172 lines
4.6 KiB
Go
// 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, ""
|
|
}
|