wallarm/internal/shared/shared.go
2026-08-01 15:33:35 +00:00

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
}