Single-binary wallarm deployment manager: - shared/ — system detection, validation, connectivity (port of wallarm-lib.sh) - preflight/ — mandatory checks on every start (OS, arch, disk, memory, cloud) - state/ — ~/.wallarm/state.json persistence (nodes, deployment type) - tunnel/ — reverse SSH tunnel over TLS:443 via Zoraxy edge proxy - cmd/wallarm/main.go — auto-detect state, route to wizard or dashboard wallarm-docker.sh wrapper delegates to existing ct-* scripts for now.
174 lines
4.9 KiB
Go
174 lines
4.9 KiB
Go
// 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
|
|
}
|