Compare commits

..

7 Commits

Author SHA1 Message Date
Sven van Ginkel
d71714cbba [Feature] Expand system info bar to include memory, disk, CPU, and OS details (#952)
* collect OS info

* Fix systeminfo

* Fix it

* optimize it

* Add disk info

* add ethernet info

* add ethernet

* remove speed from ethernet

* add cpu info

* chore cleanup data

* chore fix podman

* restruct systeminfo

* use short cpu name

* debug memory

* collect and show memory

* remove os from the table

* truncate nic name

* chore: shorter names in json

* collect memory info

* add debug

* undo memory

* revert package.json

* fix conflicts

* fix conflixts

* Fix MacOs os family

* add ISP data for remote systems

* reorder the system page bar information

* remove OS from the system table

* Update with main

* Fix vulcheck

* Fix systembar

* fix system bar

* fix vulcheck

* update struct with static info

* Adjust collection method to upon agent connection
2025-12-13 16:11:31 -05:00
henrygd
35329abcbd agent install: improve freebsd checksum verification (#1526) 2025-12-12 15:32:55 -05:00
henrygd
ee7741c3ab add --url and --token command line args for agent (#1524) 2025-12-12 13:58:44 -05:00
David Davis
ab0803b2da Upgrade react to address CVE-2025-55182
More info: https://react.dev/blog/2025/12/03/critical-security-vulnerability-in-react-server-components
2025-12-12 07:42:29 -05:00
henrygd
96196a353c smart: fallback to nvme namespace path if base controller path fails (#1504) 2025-12-10 14:09:20 -05:00
henrygd
2a8796c38d update hub install script to support freebsd 2025-12-09 15:08:59 -05:00
Zero2A11
c8d4f7427d fix: When there is no client, LoaderCircle will always transfer (#1511) 2025-12-09 11:01:13 -05:00
19 changed files with 944 additions and 174 deletions

View File

@@ -22,6 +22,14 @@ import (
gossh "golang.org/x/crypto/ssh" gossh "golang.org/x/crypto/ssh"
) )
const (
// StaticInfoIntervalMs defines the cache time threshold for including static system info
// Requests with cache time >= this value will include static info (reduces bandwidth)
// Note: uint16 max is 65535, so we can't use 15 minutes directly. The hub will make
// periodic requests at this interval.
StaticInfoIntervalMs uint16 = 60_001 // Just above the standard 60s interval
)
type Agent struct { type Agent struct {
sync.Mutex // Used to lock agent while collecting data sync.Mutex // Used to lock agent while collecting data
debug bool // true if LOG_LEVEL is set to debug debug bool // true if LOG_LEVEL is set to debug
@@ -37,7 +45,8 @@ type Agent struct {
netInterfaceDeltaTrackers map[uint16]*deltatracker.DeltaTracker[string, uint64] // Per-cache-time NIC delta trackers netInterfaceDeltaTrackers map[uint16]*deltatracker.DeltaTracker[string, uint64] // Per-cache-time NIC delta trackers
dockerManager *dockerManager // Manages Docker API requests dockerManager *dockerManager // Manages Docker API requests
sensorConfig *SensorConfig // Sensors config sensorConfig *SensorConfig // Sensors config
systemInfo system.Info // Host system info systemInfo system.Info // Host system info (dynamic dashboard data)
staticSystemInfo system.StaticInfo // Static system info (collected at longer intervals)
gpuManager *GPUManager // Manages GPU data gpuManager *GPUManager // Manages GPU data
cache *systemDataCache // Cache for system stats based on cache time cache *systemDataCache // Cache for system stats based on cache time
connectionManager *ConnectionManager // Channel to signal connection events connectionManager *ConnectionManager // Channel to signal connection events
@@ -164,6 +173,14 @@ func (a *Agent) gatherStats(cacheTimeMs uint16) *system.CombinedData {
} }
// slog.Info("System data", "data", data, "cacheTimeMs", cacheTimeMs) // slog.Info("System data", "data", data, "cacheTimeMs", cacheTimeMs)
// Include static info for requests with longer intervals (e.g., 15 min)
// This reduces bandwidth by only sending static data occasionally
if cacheTimeMs >= StaticInfoIntervalMs {
staticInfoCopy := a.staticSystemInfo
data.StaticInfo = &staticInfoCopy
slog.Debug("Including static info", "cacheTimeMs", cacheTimeMs)
}
if a.dockerManager != nil { if a.dockerManager != nil {
if containerStats, err := a.dockerManager.getDockerStats(cacheTimeMs); err == nil { if containerStats, err := a.dockerManager.getDockerStats(cacheTimeMs); err == nil {
data.Containers = containerStats data.Containers = containerStats
@@ -225,7 +242,11 @@ func (a *Agent) getFingerprint() string {
// if no fingerprint is found, generate one // if no fingerprint is found, generate one
fingerprint, err := host.HostID() fingerprint, err := host.HostID()
if err != nil || fingerprint == "" { if err != nil || fingerprint == "" {
fingerprint = a.systemInfo.Hostname + a.systemInfo.CpuModel cpuModel := ""
if len(a.staticSystemInfo.Cpus) > 0 {
cpuModel = a.staticSystemInfo.Cpus[0].Model
}
fingerprint = a.staticSystemInfo.Hostname + cpuModel
} }
// hash fingerprint // hash fingerprint

View File

@@ -201,7 +201,7 @@ func (client *WebSocketClient) handleAuthChallenge(msg *common.HubRequest[cbor.R
if authRequest.NeedSysInfo { if authRequest.NeedSysInfo {
response.Name, _ = GetEnv("SYSTEM_NAME") response.Name, _ = GetEnv("SYSTEM_NAME")
response.Hostname = client.agent.systemInfo.Hostname response.Hostname = client.agent.staticSystemInfo.Hostname
serverAddr := client.agent.connectionManager.serverOptions.Addr serverAddr := client.agent.connectionManager.serverOptions.Addr
_, response.Port, _ = net.SplitHostPort(serverAddr) _, response.Port, _ = net.SplitHostPort(serverAddr)
} }

View File

@@ -564,7 +564,7 @@ func newDockerManager(a *Agent) *dockerManager {
// If using podman, return client // If using podman, return client
if strings.Contains(dockerHost, "podman") { if strings.Contains(dockerHost, "podman") {
a.systemInfo.Podman = true a.staticSystemInfo.Podman = true
manager.goodDockerVersion = true manager.goodDockerVersion = true
return manager return manager
} }

View File

@@ -552,11 +552,8 @@ func createTestCombinedData() *system.CombinedData {
}, },
Info: system.Info{ Info: system.Info{
Hostname: "test-host", Hostname: "test-host",
Cores: 8,
CpuModel: "Test CPU Model",
Uptime: 3600, Uptime: 3600,
AgentVersion: "0.12.0", AgentVersion: "0.12.0",
Os: system.Linux,
}, },
Containers: []*container.Stats{ Containers: []*container.Stats{
{ {

View File

@@ -10,6 +10,7 @@ import (
"fmt" "fmt"
"os" "os"
"os/exec" "os/exec"
"path/filepath"
"runtime" "runtime"
"strconv" "strconv"
"strings" "strings"
@@ -454,6 +455,34 @@ func (sm *SmartManager) CollectSmart(deviceInfo *DeviceInfo) error {
hasValidData := sm.parseSmartOutput(deviceInfo, output) hasValidData := sm.parseSmartOutput(deviceInfo, output)
// If NVMe controller path failed, try namespace path as fallback.
// NVMe controllers (/dev/nvme0) don't always support SMART queries. See github.com/henrygd/beszel/issues/1504
if !hasValidData && err != nil && isNvmeControllerPath(deviceInfo.Name) {
controllerPath := deviceInfo.Name
namespacePath := controllerPath + "n1"
if !sm.isExcludedDevice(namespacePath) {
deviceInfo.Name = namespacePath
ctx3, cancel3 := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel3()
args = sm.smartctlArgs(deviceInfo, false)
cmd = exec.CommandContext(ctx3, sm.binPath, args...)
output, err = cmd.CombinedOutput()
hasValidData = sm.parseSmartOutput(deviceInfo, output)
// Auto-exclude the controller path so future scans don't re-add it
if hasValidData {
sm.Lock()
if sm.excludedDevices == nil {
sm.excludedDevices = make(map[string]struct{})
}
sm.excludedDevices[controllerPath] = struct{}{}
sm.Unlock()
slog.Debug("auto-excluded NVMe controller path", "path", controllerPath)
}
}
}
if !hasValidData { if !hasValidData {
if err != nil { if err != nil {
slog.Info("smartctl failed", "device", deviceInfo.Name, "err", err) slog.Info("smartctl failed", "device", deviceInfo.Name, "err", err)
@@ -957,6 +986,27 @@ func (sm *SmartManager) detectSmartctl() (string, error) {
return "", errors.New("smartctl not found") return "", errors.New("smartctl not found")
} }
// isNvmeControllerPath checks if the path matches an NVMe controller pattern
// like /dev/nvme0, /dev/nvme1, etc. (without namespace suffix like n1)
func isNvmeControllerPath(path string) bool {
base := filepath.Base(path)
if !strings.HasPrefix(base, "nvme") {
return false
}
suffix := strings.TrimPrefix(base, "nvme")
if suffix == "" {
return false
}
// Controller paths are just "nvme" + digits (e.g., nvme0, nvme1)
// Namespace paths have "n" after the controller number (e.g., nvme0n1)
for _, c := range suffix {
if c < '0' || c > '9' {
return false
}
}
return true
}
// NewSmartManager creates and initializes a new SmartManager // NewSmartManager creates and initializes a new SmartManager
func NewSmartManager() (*SmartManager, error) { func NewSmartManager() (*SmartManager, error) {
sm := &SmartManager{ sm := &SmartManager{

View File

@@ -780,3 +780,36 @@ func TestFilterExcludedDevices(t *testing.T) {
}) })
} }
} }
func TestIsNvmeControllerPath(t *testing.T) {
tests := []struct {
path string
expected bool
}{
// Controller paths (should return true)
{"/dev/nvme0", true},
{"/dev/nvme1", true},
{"/dev/nvme10", true},
{"nvme0", true},
// Namespace paths (should return false)
{"/dev/nvme0n1", false},
{"/dev/nvme1n1", false},
{"/dev/nvme0n1p1", false},
{"nvme0n1", false},
// Non-NVMe paths (should return false)
{"/dev/sda", false},
{"/dev/sda1", false},
{"/dev/hda", false},
{"", false},
{"/dev/nvme", false},
}
for _, tt := range tests {
t.Run(tt.path, func(t *testing.T) {
result := isNvmeControllerPath(tt.path)
assert.Equal(t, tt.expected, result, "path: %s", tt.path)
})
}
}

View File

@@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"log/slog" "log/slog"
"os" "os"
"runtime"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@@ -12,7 +13,9 @@ import (
"github.com/henrygd/beszel" "github.com/henrygd/beszel"
"github.com/henrygd/beszel/agent/battery" "github.com/henrygd/beszel/agent/battery"
"github.com/henrygd/beszel/internal/entities/system" "github.com/henrygd/beszel/internal/entities/system"
"github.com/jaypipes/ghw/pkg/block"
ghwnet "github.com/jaypipes/ghw/pkg/net"
ghwpci "github.com/jaypipes/ghw/pkg/pci"
"github.com/shirou/gopsutil/v4/cpu" "github.com/shirou/gopsutil/v4/cpu"
"github.com/shirou/gopsutil/v4/host" "github.com/shirou/gopsutil/v4/host"
"github.com/shirou/gopsutil/v4/load" "github.com/shirou/gopsutil/v4/load"
@@ -28,41 +31,76 @@ type prevDisk struct {
// Sets initial / non-changing values about the host system // Sets initial / non-changing values about the host system
func (a *Agent) initializeSystemInfo() { func (a *Agent) initializeSystemInfo() {
a.systemInfo.AgentVersion = beszel.Version hostname, _ := os.Hostname()
a.systemInfo.Hostname, _ = os.Hostname() a.staticSystemInfo.Hostname = hostname
a.staticSystemInfo.AgentVersion = beszel.Version
platform, _, version, _ := host.PlatformInformation() platform, family, version, _ := host.PlatformInformation()
var osFamily, osVersion, osKernel string
var osType system.Os
if platform == "darwin" { if platform == "darwin" {
a.systemInfo.KernelVersion = version osKernel = version
a.systemInfo.Os = system.Darwin osFamily = "macOS" // macOS is the family name for Darwin
osVersion = version
} else if strings.Contains(platform, "indows") { } else if strings.Contains(platform, "indows") {
a.systemInfo.KernelVersion = fmt.Sprintf("%s %s", strings.Replace(platform, "Microsoft ", "", 1), version) osKernel = strings.Replace(platform, "Microsoft ", "", 1) + " " + version
a.systemInfo.Os = system.Windows osFamily = family
osVersion = version
osType = system.Windows
} else if platform == "freebsd" { } else if platform == "freebsd" {
a.systemInfo.Os = system.Freebsd osKernel = version
a.systemInfo.KernelVersion = version osFamily = family
osVersion = version
} else { } else {
a.systemInfo.Os = system.Linux osFamily = family
osVersion = version
osKernel = ""
osRelease := readOsRelease()
if pretty, ok := osRelease["PRETTY_NAME"]; ok {
osFamily = pretty
}
if name, ok := osRelease["NAME"]; ok {
osFamily = name
}
if versionId, ok := osRelease["VERSION_ID"]; ok {
osVersion = versionId
}
} }
if osKernel == "" {
if a.systemInfo.KernelVersion == "" { osKernel, _ = host.KernelVersion()
a.systemInfo.KernelVersion, _ = host.KernelVersion()
} }
a.staticSystemInfo.KernelVersion = osKernel
a.staticSystemInfo.Os = osType
a.staticSystemInfo.Oses = []system.OsInfo{{
Family: osFamily,
Version: osVersion,
Kernel: osKernel,
}}
// cpu model // cpu model
if info, err := cpu.Info(); err == nil && len(info) > 0 { if info, err := cpu.Info(); err == nil && len(info) > 0 {
a.systemInfo.CpuModel = info[0].ModelName arch := runtime.GOARCH
} totalCores := 0
// cores / threads totalThreads := 0
a.systemInfo.Cores, _ = cpu.Counts(false) for _, cpuInfo := range info {
if threads, err := cpu.Counts(true); err == nil { totalCores += int(cpuInfo.Cores)
if threads > 0 && threads < a.systemInfo.Cores { totalThreads++
// in lxc logical cores reflects container limits, so use that as cores if lower
a.systemInfo.Cores = threads
} else {
a.systemInfo.Threads = threads
} }
modelName := info[0].ModelName
if idx := strings.Index(modelName, "@"); idx > 0 {
modelName = strings.TrimSpace(modelName[:idx])
}
cpu := system.CpuInfo{
Model: modelName,
SpeedGHz: fmt.Sprintf("%.2f GHz", info[0].Mhz/1000),
Arch: arch,
Cores: totalCores,
Threads: totalThreads,
}
a.staticSystemInfo.Cpus = []system.CpuInfo{cpu}
a.staticSystemInfo.Threads = totalThreads
slog.Debug("CPU info populated", "cpus", a.staticSystemInfo.Cpus)
} }
// zfs // zfs
@@ -71,6 +109,41 @@ func (a *Agent) initializeSystemInfo() {
} else { } else {
a.zfs = true a.zfs = true
} }
// Collect disk info (model/vendor)
a.staticSystemInfo.Disks = getDiskInfo()
// Collect network interface info
a.staticSystemInfo.Networks = getNetworkInfo()
// Collect total memory and store in staticSystemInfo.Memory
if v, err := mem.VirtualMemory(); err == nil {
total := fmt.Sprintf("%d GB", int((float64(v.Total)/(1024*1024*1024))+0.5))
a.staticSystemInfo.Memory = []system.MemoryInfo{{Total: total}}
slog.Debug("Memory info populated", "memory", a.staticSystemInfo.Memory)
}
}
// readPrettyName reads the PRETTY_NAME from /etc/os-release
func readPrettyName() string {
file, err := os.Open("/etc/os-release")
if err != nil {
return ""
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, "PRETTY_NAME=") {
// Remove the prefix and any surrounding quotes
prettyName := strings.TrimPrefix(line, "PRETTY_NAME=")
prettyName = strings.Trim(prettyName, `"`)
return prettyName
}
}
return ""
} }
// Returns current info, stats about the host system // Returns current info, stats about the host system
@@ -240,3 +313,136 @@ func getARCSize() (uint64, error) {
return 0, fmt.Errorf("failed to parse size field") return 0, fmt.Errorf("failed to parse size field")
} }
func getDiskInfo() []system.DiskInfo {
blockInfo, err := block.New()
if err != nil {
slog.Debug("Failed to get block info with ghw", "err", err)
return nil
}
var disks []system.DiskInfo
for _, disk := range blockInfo.Disks {
disks = append(disks, system.DiskInfo{
Name: disk.Name,
Model: disk.Model,
Vendor: disk.Vendor,
})
}
return disks
}
func getNetworkInfo() []system.NetworkInfo {
netInfo, err := ghwnet.New()
if err != nil {
slog.Debug("Failed to get network info with ghw", "err", err)
return nil
}
pciInfo, err := ghwpci.New()
if err != nil {
slog.Debug("Failed to get PCI info with ghw", "err", err)
}
var networks []system.NetworkInfo
for _, nic := range netInfo.NICs {
if nic.IsVirtual {
continue
}
var vendor, model string
if nic.PCIAddress != nil && pciInfo != nil {
for _, dev := range pciInfo.Devices {
if dev.Address == *nic.PCIAddress {
if dev.Vendor != nil {
vendor = dev.Vendor.Name
}
if dev.Product != nil {
model = dev.Product.Name
}
break
}
}
}
networks = append(networks, system.NetworkInfo{
Name: nic.Name,
Vendor: vendor,
Model: model,
})
}
return networks
}
// getInterfaceCapabilitiesFromGhw uses ghw library to get interface capabilities
func getInterfaceCapabilitiesFromGhw(nic *ghwnet.NIC) string {
// Use the speed information from ghw if available
if nic.Speed != "" {
return nic.Speed
}
// If no speed info from ghw, try to get interface type from name
return getInterfaceTypeFromName(nic.Name)
}
// getInterfaceTypeFromName tries to determine interface type from name
func getInterfaceTypeFromName(ifaceName string) string {
// Common interface naming patterns
switch {
case strings.HasPrefix(ifaceName, "eth"):
return "Ethernet"
case strings.HasPrefix(ifaceName, "en"):
return "Ethernet"
case strings.HasPrefix(ifaceName, "wlan"):
return "WiFi"
case strings.HasPrefix(ifaceName, "wl"):
return "WiFi"
case strings.HasPrefix(ifaceName, "usb"):
return "USB"
case strings.HasPrefix(ifaceName, "tun"):
return "Tunnel"
case strings.HasPrefix(ifaceName, "tap"):
return "TAP"
case strings.HasPrefix(ifaceName, "br"):
return "Bridge"
case strings.HasPrefix(ifaceName, "bond"):
return "Bond"
case strings.HasPrefix(ifaceName, "veth"):
return "Virtual Ethernet"
case strings.HasPrefix(ifaceName, "docker"):
return "Docker"
case strings.HasPrefix(ifaceName, "lo"):
return "Loopback"
default:
return ""
}
}
func readOsRelease() map[string]string {
file, err := os.Open("/etc/os-release")
if err != nil {
return map[string]string{}
}
defer file.Close()
release := make(map[string]string)
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
if i := strings.Index(line, "="); i > 0 {
key := line[:i]
val := strings.Trim(line[i+1:], `"`)
release[key] = val
}
}
return release
}
func getMemoryInfo() []system.MemoryInfo {
var total string
if v, err := mem.VirtualMemory(); err == nil {
total = fmt.Sprintf("%d GB", int((float64(v.Total)/(1024*1024*1024))+0.5))
}
return []system.MemoryInfo{{
Total: total,
}}
}

5
go.mod
View File

@@ -9,6 +9,7 @@ require (
github.com/fxamacker/cbor/v2 v2.9.0 github.com/fxamacker/cbor/v2 v2.9.0
github.com/gliderlabs/ssh v0.3.8 github.com/gliderlabs/ssh v0.3.8
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/jaypipes/ghw v0.17.0
github.com/lxzan/gws v1.8.9 github.com/lxzan/gws v1.8.9
github.com/nicholas-fedor/shoutrrr v0.12.1 github.com/nicholas-fedor/shoutrrr v0.12.1
github.com/pocketbase/dbx v1.11.0 github.com/pocketbase/dbx v1.11.0
@@ -24,6 +25,7 @@ require (
) )
require ( require (
github.com/StackExchange/wmi v1.2.1 // indirect
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
@@ -41,11 +43,14 @@ require (
github.com/godbus/dbus/v5 v5.2.0 // indirect github.com/godbus/dbus/v5 v5.2.0 // indirect
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jaypipes/pcidb v1.0.1 // indirect
github.com/klauspost/compress v1.18.1 // indirect github.com/klauspost/compress v1.18.1 // indirect
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect

11
go.sum
View File

@@ -2,6 +2,8 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA=
github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg= github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg=
@@ -41,6 +43,7 @@ github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
@@ -68,6 +71,10 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jarcoal/httpmock v1.4.1 h1:0Ju+VCFuARfFlhVXFc2HxlcQkfB+Xq12/EotHko+x2A= github.com/jarcoal/httpmock v1.4.1 h1:0Ju+VCFuARfFlhVXFc2HxlcQkfB+Xq12/EotHko+x2A=
github.com/jarcoal/httpmock v1.4.1/go.mod h1:ftW1xULwo+j0R0JJkJIIi7UKigZUXCLLanykgjwBXL0= github.com/jarcoal/httpmock v1.4.1/go.mod h1:ftW1xULwo+j0R0JJkJIIi7UKigZUXCLLanykgjwBXL0=
github.com/jaypipes/ghw v0.17.0 h1:EVLJeNcy5z6GK/Lqby0EhBpynZo+ayl8iJWY0kbEUJA=
github.com/jaypipes/ghw v0.17.0/go.mod h1:In8SsaDqlb1oTyrbmTC14uy+fbBMvp+xdqX51MidlD8=
github.com/jaypipes/pcidb v1.0.1 h1:WB2zh27T3nwg8AE8ei81sNRb9yWBii3JGNJtT7K9Oic=
github.com/jaypipes/pcidb v1.0.1/go.mod h1:6xYUz/yYEyOkIkUt2t2J2folIuZ4Yg6uByCGFXMCeE4=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co= github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0= github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
@@ -83,6 +90,8 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/nicholas-fedor/shoutrrr v0.12.1 h1:8NjY+I3K7cGHy89ncnaPGUA0ex44XbYK3SAFJX9YMI8= github.com/nicholas-fedor/shoutrrr v0.12.1 h1:8NjY+I3K7cGHy89ncnaPGUA0ex44XbYK3SAFJX9YMI8=
@@ -91,6 +100,8 @@ github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns
github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo=
github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A=
github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=

View File

@@ -17,9 +17,8 @@ import (
type cmdOptions struct { type cmdOptions struct {
key string // key is the public key(s) for SSH authentication. key string // key is the public key(s) for SSH authentication.
listen string // listen is the address or port to listen on. listen string // listen is the address or port to listen on.
// TODO: add hubURL and token hubURL string // hubURL is the URL of the Beszel hub.
// hubURL string // hubURL is the URL of the hub to use. token string // token is the token to use for authentication.
// token string // token is the token to use for authentication.
} }
// parse parses the command line flags and populates the config struct. // parse parses the command line flags and populates the config struct.
@@ -47,13 +46,13 @@ func (opts *cmdOptions) parse() bool {
// pflag.CommandLine.ParseErrorsWhitelist.UnknownFlags = true // pflag.CommandLine.ParseErrorsWhitelist.UnknownFlags = true
pflag.StringVarP(&opts.key, "key", "k", "", "Public key(s) for SSH authentication") pflag.StringVarP(&opts.key, "key", "k", "", "Public key(s) for SSH authentication")
pflag.StringVarP(&opts.listen, "listen", "l", "", "Address or port to listen on") pflag.StringVarP(&opts.listen, "listen", "l", "", "Address or port to listen on")
// pflag.StringVarP(&opts.hubURL, "hub-url", "u", "", "URL of the hub to use") pflag.StringVarP(&opts.hubURL, "url", "u", "", "URL of the Beszel hub")
// pflag.StringVarP(&opts.token, "token", "t", "", "Token to use for authentication") pflag.StringVarP(&opts.token, "token", "t", "", "Token to use for authentication")
chinaMirrors := pflag.BoolP("china-mirrors", "c", false, "Use mirror for update (gh.beszel.dev) instead of GitHub") chinaMirrors := pflag.BoolP("china-mirrors", "c", false, "Use mirror for update (gh.beszel.dev) instead of GitHub")
help := pflag.BoolP("help", "h", false, "Show this help message") help := pflag.BoolP("help", "h", false, "Show this help message")
// Convert old single-dash long flags to double-dash for backward compatibility // Convert old single-dash long flags to double-dash for backward compatibility
flagsToConvert := []string{"key", "listen"} flagsToConvert := []string{"key", "listen", "url", "token"}
for i, arg := range os.Args { for i, arg := range os.Args {
for _, flag := range flagsToConvert { for _, flag := range flagsToConvert {
singleDash := "-" + flag singleDash := "-" + flag
@@ -95,6 +94,13 @@ func (opts *cmdOptions) parse() bool {
return true return true
} }
// Set environment variables from CLI flags (if provided)
if opts.hubURL != "" {
os.Setenv("HUB_URL", opts.hubURL)
}
if opts.token != "" {
os.Setenv("TOKEN", opts.token)
}
return false return false
} }

View File

@@ -115,6 +115,37 @@ const (
Freebsd Freebsd
) )
type DiskInfo struct {
Name string `json:"n"`
Model string `json:"m,omitempty"`
Vendor string `json:"v,omitempty"`
}
type NetworkInfo struct {
Name string `json:"n"`
Vendor string `json:"v,omitempty"`
Model string `json:"m,omitempty"`
Speed string `json:"s,omitempty"`
}
type MemoryInfo struct {
Total string `json:"t,omitempty"`
}
type CpuInfo struct {
Model string `json:"m"`
SpeedGHz string `json:"s"`
Arch string `json:"a"`
Cores int `json:"c"`
Threads int `json:"t"`
}
type OsInfo struct {
Family string `json:"f"`
Version string `json:"v"`
Kernel string `json:"k"`
}
type ConnectionType = uint8 type ConnectionType = uint8
const ( const (
@@ -123,26 +154,35 @@ const (
ConnectionTypeWebSocket ConnectionTypeWebSocket
) )
// StaticInfo contains system information that rarely or never changes
// This is collected at a longer interval (e.g., 10-15 minutes) to reduce bandwidth
type StaticInfo struct {
Hostname string `json:"h" cbor:"0,keyasint"`
KernelVersion string `json:"k,omitempty" cbor:"1,keyasint,omitempty"`
Threads int `json:"t,omitempty" cbor:"2,keyasint,omitempty"`
AgentVersion string `json:"v" cbor:"3,keyasint"`
Podman bool `json:"p,omitempty" cbor:"4,keyasint,omitempty"`
Os Os `json:"os" cbor:"5,keyasint"`
Disks []DiskInfo `json:"d,omitempty" cbor:"6,omitempty"`
Networks []NetworkInfo `json:"n,omitempty" cbor:"7,omitempty"`
Memory []MemoryInfo `json:"m" cbor:"8"`
Cpus []CpuInfo `json:"c" cbor:"9"`
Oses []OsInfo `json:"o,omitempty" cbor:"10,omitempty"`
}
// Info contains frequently-changing system snapshot data for the dashboard
type Info struct { type Info struct {
Hostname string `json:"h" cbor:"0,keyasint"` Uptime uint64 `json:"u" cbor:"0,keyasint"`
KernelVersion string `json:"k,omitempty" cbor:"1,keyasint,omitempty"` Cpu float64 `json:"cpu" cbor:"1,keyasint"`
Cores int `json:"c" cbor:"2,keyasint"` MemPct float64 `json:"mp" cbor:"2,keyasint"`
Threads int `json:"t,omitempty" cbor:"3,keyasint,omitempty"` DiskPct float64 `json:"dp" cbor:"3,keyasint"`
CpuModel string `json:"m" cbor:"4,keyasint"` Bandwidth float64 `json:"b" cbor:"4,keyasint"`
Uptime uint64 `json:"u" cbor:"5,keyasint"` GpuPct float64 `json:"g,omitempty" cbor:"5,keyasint,omitempty"`
Cpu float64 `json:"cpu" cbor:"6,keyasint"` DashboardTemp float64 `json:"dt,omitempty" cbor:"6,keyasint,omitempty"`
MemPct float64 `json:"mp" cbor:"7,keyasint"` LoadAvg1 float64 `json:"l1,omitempty" cbor:"7,keyasint,omitempty"`
DiskPct float64 `json:"dp" cbor:"8,keyasint"` LoadAvg5 float64 `json:"l5,omitempty" cbor:"8,keyasint,omitempty"`
Bandwidth float64 `json:"b" cbor:"9,keyasint"` LoadAvg15 float64 `json:"l15,omitempty" cbor:"9,keyasint,omitempty"`
AgentVersion string `json:"v" cbor:"10,keyasint"` BandwidthBytes uint64 `json:"bb" cbor:"10,keyasint"`
Podman bool `json:"p,omitempty" cbor:"11,keyasint,omitempty"`
GpuPct float64 `json:"g,omitempty" cbor:"12,keyasint,omitempty"`
DashboardTemp float64 `json:"dt,omitempty" cbor:"13,keyasint,omitempty"`
Os Os `json:"os" cbor:"14,keyasint"`
LoadAvg1 float64 `json:"l1,omitempty" cbor:"15,keyasint,omitempty"`
LoadAvg5 float64 `json:"l5,omitempty" cbor:"16,keyasint,omitempty"`
LoadAvg15 float64 `json:"l15,omitempty" cbor:"17,keyasint,omitempty"`
BandwidthBytes uint64 `json:"bb" cbor:"18,keyasint"`
// TODO: remove load fields in future release in favor of load avg array // TODO: remove load fields in future release in favor of load avg array
LoadAvg [3]float64 `json:"la,omitempty" cbor:"19,keyasint"` LoadAvg [3]float64 `json:"la,omitempty" cbor:"19,keyasint"`
ConnectionType ConnectionType `json:"ct,omitempty" cbor:"20,keyasint,omitempty,omitzero"` ConnectionType ConnectionType `json:"ct,omitempty" cbor:"20,keyasint,omitempty,omitzero"`
@@ -157,4 +197,5 @@ type CombinedData struct {
Info Info `json:"info" cbor:"1,keyasint"` Info Info `json:"info" cbor:"1,keyasint"`
Containers []*container.Stats `json:"container" cbor:"2,keyasint"` Containers []*container.Stats `json:"container" cbor:"2,keyasint"`
SystemdServices []*systemd.Service `json:"systemd,omitempty" cbor:"3,keyasint,omitempty"` SystemdServices []*systemd.Service `json:"systemd,omitempty" cbor:"3,keyasint,omitempty"`
StaticInfo *StaticInfo `json:"static_info,omitempty" cbor:"4,keyasint,omitempty"` // Collected at longer intervals
} }

View File

@@ -36,6 +36,7 @@ type System struct {
manager *SystemManager // Manager that this system belongs to manager *SystemManager // Manager that this system belongs to
client *ssh.Client // SSH client for fetching data client *ssh.Client // SSH client for fetching data
data *system.CombinedData // system data from agent data *system.CombinedData // system data from agent
staticInfo *system.StaticInfo // cached static system info, fetched once per connection
ctx context.Context // Context for stopping the updater ctx context.Context // Context for stopping the updater
cancel context.CancelFunc // Stops and removes system from updater cancel context.CancelFunc // Stops and removes system from updater
WsConn *ws.WsConn // Handler for agent WebSocket connection WsConn *ws.WsConn // Handler for agent WebSocket connection
@@ -114,8 +115,22 @@ func (sys *System) update() error {
sys.handlePaused() sys.handlePaused()
return nil return nil
} }
data, err := sys.fetchDataFromAgent(common.DataRequestOptions{CacheTimeMs: uint16(interval)})
// Determine which cache time to use based on whether we need static info
cacheTimeMs := uint16(interval)
if sys.staticInfo == nil {
// Request with a cache time that signals the agent to include static info
// We use 60001ms (just above the standard interval) since uint16 max is 65535
cacheTimeMs = 60_001
}
data, err := sys.fetchDataFromAgent(common.DataRequestOptions{CacheTimeMs: cacheTimeMs})
if err == nil { if err == nil {
// If we received static info, cache it
if data.StaticInfo != nil {
sys.staticInfo = data.StaticInfo
sys.manager.hub.Logger().Debug("Cached static system info", "system", sys.Id)
}
_, err = sys.createRecords(data) _, err = sys.createRecords(data)
} }
return err return err
@@ -136,6 +151,11 @@ func (sys *System) handlePaused() {
// createRecords updates the system record and adds system_stats and container_stats records // createRecords updates the system record and adds system_stats and container_stats records
func (sys *System) createRecords(data *system.CombinedData) (*core.Record, error) { func (sys *System) createRecords(data *system.CombinedData) (*core.Record, error) {
// Build complete info combining dynamic and static data
completeInfo := sys.buildCompleteInfo(data)
sys.manager.hub.Logger().Debug("Creating records - complete info", "info", completeInfo)
systemRecord, err := sys.getRecord() systemRecord, err := sys.getRecord()
if err != nil { if err != nil {
return nil, err return nil, err
@@ -186,7 +206,7 @@ func (sys *System) createRecords(data *system.CombinedData) (*core.Record, error
// update system record (do this last because it triggers alerts and we need above records to be inserted first) // update system record (do this last because it triggers alerts and we need above records to be inserted first)
systemRecord.Set("status", up) systemRecord.Set("status", up)
systemRecord.Set("info", data.Info) systemRecord.Set("info", completeInfo)
if err := txApp.SaveNoValidate(systemRecord); err != nil { if err := txApp.SaveNoValidate(systemRecord); err != nil {
return err return err
} }
@@ -203,6 +223,70 @@ func (sys *System) createRecords(data *system.CombinedData) (*core.Record, error
return systemRecord, err return systemRecord, err
} }
// buildCompleteInfo combines the dynamic Info with cached StaticInfo to create a complete system info structure
// This is needed because we've split the original Info structure for bandwidth optimization
func (sys *System) buildCompleteInfo(data *system.CombinedData) map[string]interface{} {
info := make(map[string]interface{})
// Add dynamic fields from data.Info
if data.Info.Uptime > 0 {
info["u"] = data.Info.Uptime
}
info["cpu"] = data.Info.Cpu
info["mp"] = data.Info.MemPct
info["dp"] = data.Info.DiskPct
info["b"] = data.Info.Bandwidth
info["bb"] = data.Info.BandwidthBytes
if data.Info.GpuPct > 0 {
info["g"] = data.Info.GpuPct
}
if data.Info.DashboardTemp > 0 {
info["dt"] = data.Info.DashboardTemp
}
if data.Info.LoadAvg1 > 0 || data.Info.LoadAvg5 > 0 || data.Info.LoadAvg15 > 0 {
info["l1"] = data.Info.LoadAvg1
info["l5"] = data.Info.LoadAvg5
info["l15"] = data.Info.LoadAvg15
info["la"] = data.Info.LoadAvg
}
if data.Info.ConnectionType > 0 {
info["ct"] = data.Info.ConnectionType
}
// Add static fields from cached staticInfo
if sys.staticInfo != nil {
info["h"] = sys.staticInfo.Hostname
if sys.staticInfo.KernelVersion != "" {
info["k"] = sys.staticInfo.KernelVersion
}
if sys.staticInfo.Threads > 0 {
info["t"] = sys.staticInfo.Threads
}
info["v"] = sys.staticInfo.AgentVersion
if sys.staticInfo.Podman {
info["p"] = true
}
info["os"] = sys.staticInfo.Os
if len(sys.staticInfo.Cpus) > 0 {
info["c"] = sys.staticInfo.Cpus
}
if len(sys.staticInfo.Memory) > 0 {
info["m"] = sys.staticInfo.Memory
}
if len(sys.staticInfo.Disks) > 0 {
info["d"] = sys.staticInfo.Disks
}
if len(sys.staticInfo.Networks) > 0 {
info["n"] = sys.staticInfo.Networks
}
if len(sys.staticInfo.Oses) > 0 {
info["o"] = sys.staticInfo.Oses
}
}
return info
}
func createSystemdStatsRecords(app core.App, data []*systemd.Service, systemId string) error { func createSystemdStatsRecords(app core.App, data []*systemd.Service, systemId string) error {
if len(data) == 0 { if len(data) == 0 {
return nil return nil
@@ -602,6 +686,7 @@ func (sys *System) closeSSHConnection() {
sys.client.Close() sys.client.Close()
sys.client = nil sys.client = nil
} }
sys.staticInfo = nil
} }
// closeWebSocketConnection closes the WebSocket connection but keeps the system in the manager // closeWebSocketConnection closes the WebSocket connection but keeps the system in the manager
@@ -611,6 +696,7 @@ func (sys *System) closeWebSocketConnection() {
if sys.WsConn != nil { if sys.WsConn != nil {
sys.WsConn.Close(nil) sys.WsConn.Close(nil)
} }
sys.staticInfo = nil
} }
// extractAgentVersion extracts the beszel version from SSH server version string // extractAgentVersion extracts the beszel version from SSH server version string

View File

@@ -39,8 +39,8 @@
"lucide-react": "^0.452.0", "lucide-react": "^0.452.0",
"nanostores": "^0.11.4", "nanostores": "^0.11.4",
"pocketbase": "^0.26.2", "pocketbase": "^0.26.2",
"react": "^19.1.1", "react": "^19.1.2",
"react-dom": "^19.1.1", "react-dom": "^19.1.2",
"recharts": "^2.15.4", "recharts": "^2.15.4",
"shiki": "^3.13.0", "shiki": "^3.13.0",
"tailwind-merge": "^3.3.1", "tailwind-merge": "^3.3.1",
@@ -5745,9 +5745,9 @@
} }
}, },
"node_modules/react": { "node_modules/react": {
"version": "19.1.1", "version": "19.1.2",
"resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz", "resolved": "https://registry.npmjs.org/react/-/react-19.1.2.tgz",
"integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==", "integrity": "sha512-MdWVitvLbQULD+4DP8GYjZUrepGW7d+GQkNVqJEzNxE+e9WIa4egVFE/RDfVb1u9u/Jw7dNMmPB4IqxzbFYJ0w==",
"license": "MIT", "license": "MIT",
"peer": true, "peer": true,
"engines": { "engines": {
@@ -5755,16 +5755,16 @@
} }
}, },
"node_modules/react-dom": { "node_modules/react-dom": {
"version": "19.1.1", "version": "19.1.2",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.2.tgz",
"integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==", "integrity": "sha512-dEoydsCp50i7kS1xHOmPXq4zQYoGWedUsvqv9H6zdif2r7yLHygyfP9qou71TulRN0d6ng9EbRVsQhSqfUc19g==",
"license": "MIT", "license": "MIT",
"peer": true, "peer": true,
"dependencies": { "dependencies": {
"scheduler": "^0.26.0" "scheduler": "^0.26.0"
}, },
"peerDependencies": { "peerDependencies": {
"react": "^19.1.1" "react": "^19.1.2"
} }
}, },
"node_modules/react-is": { "node_modules/react-is": {

View File

@@ -46,8 +46,8 @@
"lucide-react": "^0.452.0", "lucide-react": "^0.452.0",
"nanostores": "^0.11.4", "nanostores": "^0.11.4",
"pocketbase": "^0.26.2", "pocketbase": "^0.26.2",
"react": "^19.1.1", "react": "^19.1.2",
"react-dom": "^19.1.1", "react-dom": "^19.1.2",
"recharts": "^2.15.4", "recharts": "^2.15.4",
"shiki": "^3.13.0", "shiki": "^3.13.0",
"tailwind-merge": "^3.3.1", "tailwind-merge": "^3.3.1",

View File

@@ -55,8 +55,11 @@ export default function ContainersTable({ systemId }: { systemId?: string }) {
filter: systemId ? pb.filter("system={:system}", { system: systemId }) : undefined, filter: systemId ? pb.filter("system={:system}", { system: systemId }) : undefined,
}) })
.then( .then(
({ items }) => ({ items }) => {
items.length && if (items.length === 0) {
setData([]);
return;
}
setData((curItems) => { setData((curItems) => {
const lastUpdated = Math.max(items[0].updated, items.at(-1)?.updated ?? 0) const lastUpdated = Math.max(items[0].updated, items.at(-1)?.updated ?? 0)
const containerIds = new Set() const containerIds = new Set()
@@ -74,6 +77,7 @@ export default function ContainersTable({ systemId }: { systemId?: string }) {
} }
return newItems return newItems
}) })
}
) )
} }
@@ -333,12 +337,12 @@ function ContainerSheet({
setLogsDisplay("") setLogsDisplay("")
setInfoDisplay("") setInfoDisplay("")
if (!container) return if (!container) return
;(async () => { ;(async () => {
const [logsHtml, infoHtml] = await Promise.all([getLogsHtml(container), getInfoHtml(container)]) const [logsHtml, infoHtml] = await Promise.all([getLogsHtml(container), getInfoHtml(container)])
setLogsDisplay(logsHtml) setLogsDisplay(logsHtml)
setInfoDisplay(infoHtml) setInfoDisplay(infoHtml)
setTimeout(scrollLogsToBottom, 20) setTimeout(scrollLogsToBottom, 20)
})() })()
}, [container]) }, [container])
return ( return (

View File

@@ -8,8 +8,10 @@ import {
ClockArrowUp, ClockArrowUp,
CpuIcon, CpuIcon,
GlobeIcon, GlobeIcon,
HardDriveIcon,
LayoutGridIcon, LayoutGridIcon,
MonitorIcon, MonitorIcon,
ServerIcon,
XIcon, XIcon,
} from "lucide-react" } from "lucide-react"
import { subscribeKeys } from "nanostores" import { subscribeKeys } from "nanostores"
@@ -66,7 +68,7 @@ import { $router, navigate } from "../router"
import Spinner from "../spinner" import Spinner from "../spinner"
import { Button } from "../ui/button" import { Button } from "../ui/button"
import { Card, CardDescription, CardHeader, CardTitle } from "../ui/card" import { Card, CardDescription, CardHeader, CardTitle } from "../ui/card"
import { AppleIcon, ChartAverage, ChartMax, FreeBsdIcon, Rows, TuxIcon, WebSocketIcon, WindowsIcon } from "../ui/icons" import { AppleIcon, ChartAverage, ChartMax, EthernetIcon, FreeBsdIcon, Rows, TuxIcon, WebSocketIcon, WindowsIcon } from "../ui/icons"
import { Input } from "../ui/input" import { Input } from "../ui/input"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select"
import { Separator } from "../ui/separator" import { Separator } from "../ui/separator"
@@ -333,6 +335,20 @@ export default memo(function SystemDetail({ id }: { id: string }) {
}) })
}, [system, chartTime]) }, [system, chartTime])
// Helper to format hardware info (disk/nic) with vendor and model
const formatHardwareInfo = useCallback((item: { n: string; v?: string; m?: string }) => {
const vendor = item.v && item.v.toLowerCase() !== 'unknown' ? item.v : null
const model = item.m && item.m.toLowerCase() !== 'unknown' ? item.m : null
if (vendor && model) {
return `${item.n}: ${vendor} ${model}`
} else if (model) {
return `${item.n}: ${model}`
} else if (vendor) {
return `${item.n}: ${vendor}`
}
return item.n
}, [])
// values for system info bar // values for system info bar
const systemInfo = useMemo(() => { const systemInfo = useMemo(() => {
if (!system.info) { if (!system.info) {
@@ -366,6 +382,11 @@ export default memo(function SystemDetail({ id }: { id: string }) {
} else { } else {
uptime = secondsToString(system.info.u, "day") uptime = secondsToString(system.info.u, "day")
} }
// Extract CPU and Memory info from arrays
const cpuInfo = system.info.c && system.info.c.length > 0 ? system.info.c[0] : undefined
const memoryInfo = system.info.m && system.info.m.length > 0 ? system.info.m[0] : undefined
const osData = system.info.o && system.info.o.length > 0 ? system.info.o[0] : undefined
return [ return [
{ value: getHostDisplayValue(system), Icon: GlobeIcon }, { value: getHostDisplayValue(system), Icon: GlobeIcon },
{ {
@@ -376,19 +397,43 @@ export default memo(function SystemDetail({ id }: { id: string }) {
hide: system.info.h === system.host || system.info.h === system.name, hide: system.info.h === system.host || system.info.h === system.name,
}, },
{ value: uptime, Icon: ClockArrowUp, label: t`Uptime`, hide: !system.info.u }, { value: uptime, Icon: ClockArrowUp, label: t`Uptime`, hide: !system.info.u },
osInfo[system.info.os ?? Os.Linux], osData ? {
{ value: `${osData.f} ${osData.v}`.trim(),
value: `${system.info.m} (${system.info.c}c${system.info.t ? `/${system.info.t}t` : ""})`, Icon: osInfo[system.info.os ?? Os.Linux]?.Icon ?? TuxIcon,
label: osData.k ? `Kernel: ${osData.k}` : undefined,
} : osInfo[system.info.os ?? Os.Linux],
cpuInfo ? {
value: cpuInfo.m,
Icon: CpuIcon, Icon: CpuIcon,
hide: !system.info.m, hide: !cpuInfo.m,
}, label: [
] as { (cpuInfo.c || cpuInfo.t) ? `Cores / Threads: ${cpuInfo.c || '?'} / ${cpuInfo.t || cpuInfo.c || '?'}` : null,
cpuInfo.a ? `Arch: ${cpuInfo.a}` : null,
cpuInfo.s ? `Speed: ${cpuInfo.s}` : null,
].filter(Boolean).join('\n'),
} : undefined,
memoryInfo ? {
value: memoryInfo.t,
Icon: ServerIcon,
label: "Total Memory",
} : undefined,
system.info.d && system.info.d.length > 0 ? {
value: `${system.info.d.length} ${system.info.d.length === 1 ? t`Disk` : t`Disks`}`,
Icon: HardDriveIcon,
label: system.info.d.map(formatHardwareInfo).join('\n'),
} : undefined,
system.info.n && system.info.n.length > 0 ? {
value: `${system.info.n.length} ${system.info.n.length === 1 ? t`NIC` : t`NICs`}`,
Icon: EthernetIcon,
label: system.info.n.map(formatHardwareInfo).join('\n'),
} : undefined,
].filter(Boolean) as {
value: string | number | undefined value: string | number | undefined
label?: string label?: string
Icon: React.ElementType Icon: React.ElementType
hide?: boolean hide?: boolean
}[] }[]
}, [system, t]) }, [system, t, formatHardwareInfo])
/** Space for tooltip if more than 10 sensors and no containers table */ /** Space for tooltip if more than 10 sensors and no containers table */
useEffect(() => { useEffect(() => {

View File

@@ -27,12 +27,32 @@ export interface SystemRecord extends RecordModel {
host: string host: string
status: "up" | "down" | "paused" | "pending" status: "up" | "down" | "paused" | "pending"
port: string port: string
info: SystemInfo info: systemInfo
v: string v: string
updated: string updated: string
} }
export interface SystemInfo { export interface CpuInfo {
m: string
s: string
a: string
c: number
t: number
}
export interface OsInfo {
f: string
v: string
k: string
}
export interface NetworkLocationInfo {
ip?: string
isp?: string
asn?: string
}
export interface systemInfo {
/** hostname */ /** hostname */
h: string h: string
/** kernel **/ /** kernel **/
@@ -75,6 +95,16 @@ export interface SystemInfo {
g?: number g?: number
/** dashboard display temperature */ /** dashboard display temperature */
dt?: number dt?: number
/** disks info (array of block devices with model/vendor/serial) */
d?: { n: string; m?: string; v?: string; serial?: string }[]
/** networks info (array of network interfaces with vendor/model/capabilities) */
n?: { n: string; v?: string; m?: string; s?: string }[]
/** memory info (array with total property) */
m?: { t: string }[]
/** cpu info (array of cpu objects) */
c?: CpuInfo[]
/** os info (array of os objects) */
o?: OsInfo[]
/** operating system */ /** operating system */
os?: Os os?: Os
/** connection type */ /** connection type */

View File

@@ -504,10 +504,11 @@ KEY=$(echo "$KEY" | tr -d '\n')
# Verify checksum # Verify checksum
if command -v sha256sum >/dev/null; then if command -v sha256sum >/dev/null; then
CHECK_CMD="sha256sum" CHECK_CMD="sha256sum"
elif command -v md5 >/dev/null; then elif command -v sha256 >/dev/null; then
CHECK_CMD="md5 -q" # FreeBSD uses 'sha256' instead of 'sha256sum', with different output format
CHECK_CMD="sha256 -q"
else else
echo "No MD5 checksum utility found" echo "No SHA256 checksum utility found"
exit 1 exit 1
fi fi

View File

@@ -1,22 +1,8 @@
#!/bin/bash #!/bin/sh
# Check if running as root is_freebsd() {
if [ "$(id -u)" != "0" ]; then [ "$(uname -s)" = "FreeBSD" ]
if command -v sudo >/dev/null 2>&1; then }
exec sudo "$0" "$@"
else
echo "This script must be run as root. Please either:"
echo "1. Run this script as root (su root)"
echo "2. Install sudo and run with sudo"
exit 1
fi
fi
# Define default values
version=0.0.1
PORT=8090 # Default port
GITHUB_PROXY_URL="https://ghfast.top/" # Default proxy URL
AUTO_UPDATE_FLAG="false" # default to no auto-updates, "true" means enable
# Function to ensure the proxy URL ends with a / # Function to ensure the proxy URL ends with a /
ensure_trailing_slash() { ensure_trailing_slash() {
@@ -30,14 +16,155 @@ ensure_trailing_slash() {
fi fi
} }
# Ensure the proxy URL ends with a / # Generate FreeBSD rc service content
GITHUB_PROXY_URL=$(ensure_trailing_slash "$GITHUB_PROXY_URL") generate_freebsd_rc_service() {
cat <<'EOF'
#!/bin/sh
# PROVIDE: beszel_hub
# REQUIRE: DAEMON NETWORKING
# BEFORE: LOGIN
# KEYWORD: shutdown
# Add the following lines to /etc/rc.conf to configure Beszel Hub:
#
# beszel_hub_enable (bool): Set to YES to enable Beszel Hub
# Default: YES
# beszel_hub_port (str): Port to listen on
# Default: 8090
# beszel_hub_user (str): Beszel Hub daemon user
# Default: beszel
# beszel_hub_bin (str): Path to the beszel binary
# Default: /usr/local/sbin/beszel
# beszel_hub_data (str): Path to the beszel data directory
# Default: /usr/local/etc/beszel/beszel_data
# beszel_hub_flags (str): Extra flags passed to beszel command invocation
# Default:
. /etc/rc.subr
name="beszel_hub"
rcvar=beszel_hub_enable
load_rc_config $name
: ${beszel_hub_enable:="YES"}
: ${beszel_hub_port:="8090"}
: ${beszel_hub_user:="beszel"}
: ${beszel_hub_flags:=""}
: ${beszel_hub_bin:="/usr/local/sbin/beszel"}
: ${beszel_hub_data:="/usr/local/etc/beszel/beszel_data"}
logfile="/var/log/${name}.log"
pidfile="/var/run/${name}.pid"
procname="/usr/sbin/daemon"
start_precmd="${name}_prestart"
start_cmd="${name}_start"
stop_cmd="${name}_stop"
extra_commands="upgrade"
upgrade_cmd="beszel_hub_upgrade"
beszel_hub_prestart()
{
if [ ! -d "${beszel_hub_data}" ]; then
echo "Creating data directory ${beszel_hub_data}"
mkdir -p "${beszel_hub_data}"
chown "${beszel_hub_user}:${beszel_hub_user}" "${beszel_hub_data}"
fi
}
beszel_hub_start()
{
echo "Starting ${name}"
cd "$(dirname "${beszel_hub_data}")" || exit 1
/usr/sbin/daemon -f \
-P "${pidfile}" \
-o "${logfile}" \
-u "${beszel_hub_user}" \
"${beszel_hub_bin}" serve --http "0.0.0.0:${beszel_hub_port}" ${beszel_hub_flags}
}
beszel_hub_stop()
{
pid="$(check_pidfile "${pidfile}" "${procname}")"
if [ -n "${pid}" ]; then
echo "Stopping ${name} (pid=${pid})"
kill -- "-${pid}"
wait_for_pids "${pid}"
else
echo "${name} isn't running"
fi
}
beszel_hub_upgrade()
{
echo "Upgrading ${name}"
if command -v sudo >/dev/null; then
sudo -u "${beszel_hub_user}" -- "${beszel_hub_bin}" update
else
su -m "${beszel_hub_user}" -c "${beszel_hub_bin} update"
fi
}
run_rc_command "$1"
EOF
}
# Detect system architecture
detect_architecture() {
arch=$(uname -m)
case "$arch" in
x86_64)
arch="amd64"
;;
armv7l)
arch="arm"
;;
aarch64)
arch="arm64"
;;
esac
echo "$arch"
}
# Build sudo args by properly quoting everything
build_sudo_args() {
QUOTED_ARGS=""
while [ $# -gt 0 ]; do
if [ -n "$QUOTED_ARGS" ]; then
QUOTED_ARGS="$QUOTED_ARGS "
fi
QUOTED_ARGS="$QUOTED_ARGS'$(echo "$1" | sed "s/'/'\\\\''/g")'"
shift
done
echo "$QUOTED_ARGS"
}
# Check if running as root and re-execute with sudo if needed
if [ "$(id -u)" != "0" ]; then
if command -v sudo >/dev/null 2>&1; then
SUDO_ARGS=$(build_sudo_args "$@")
eval "exec sudo $0 $SUDO_ARGS"
else
echo "This script must be run as root. Please either:"
echo "1. Run this script as root (su root)"
echo "2. Install sudo and run with sudo"
exit 1
fi
fi
# Define default values
PORT=8090
GITHUB_PROXY_URL="https://ghfast.top/"
AUTO_UPDATE_FLAG="false"
UNINSTALL=false
# Parse command line arguments # Parse command line arguments
while [ $# -gt 0 ]; do while [ $# -gt 0 ]; do
case "$1" in case "$1" in
-u) -u)
UNINSTALL="true" UNINSTALL=true
shift shift
;; ;;
-h|--help) -h|--help)
@@ -72,37 +199,75 @@ while [ $# -gt 0 ]; do
esac esac
done done
if [ "$UNINSTALL" = "true" ]; then # Ensure the proxy URL ends with a /
# Stop and disable the Beszel Hub service GITHUB_PROXY_URL=$(ensure_trailing_slash "$GITHUB_PROXY_URL")
echo "Stopping and disabling the Beszel Hub service..."
systemctl stop beszel-hub.service
systemctl disable beszel-hub.service
# Remove the systemd service file # Set paths based on operating system
echo "Removing the systemd service file..." if is_freebsd; then
rm -f /etc/systemd/system/beszel-hub.service HUB_DIR="/usr/local/etc/beszel"
BIN_PATH="/usr/local/sbin/beszel"
else
HUB_DIR="/opt/beszel"
BIN_PATH="/opt/beszel/beszel"
fi
# Remove the update timer and service if they exist # Uninstall process
echo "Removing the daily update service and timer..." if [ "$UNINSTALL" = true ]; then
systemctl stop beszel-hub-update.timer 2>/dev/null if is_freebsd; then
systemctl disable beszel-hub-update.timer 2>/dev/null echo "Stopping and disabling the Beszel Hub service..."
rm -f /etc/systemd/system/beszel-hub-update.service service beszel-hub stop 2>/dev/null
rm -f /etc/systemd/system/beszel-hub-update.timer sysrc beszel_hub_enable="NO" 2>/dev/null
# Reload the systemd daemon echo "Removing the FreeBSD service files..."
echo "Reloading the systemd daemon..." rm -f /usr/local/etc/rc.d/beszel-hub
systemctl daemon-reload
# Remove the Beszel Hub binary and data echo "Removing the daily update cron job..."
echo "Removing the Beszel Hub binary and data..." rm -f /etc/cron.d/beszel-hub
rm -rf /opt/beszel
# Remove the dedicated user echo "Removing log files..."
echo "Removing the dedicated user..." rm -f /var/log/beszel_hub.log
userdel beszel 2>/dev/null
echo "The Beszel Hub has been uninstalled successfully!" echo "Removing the Beszel Hub binary and data..."
exit 0 rm -f "$BIN_PATH"
rm -rf "$HUB_DIR"
echo "Removing the dedicated user..."
pw user del beszel 2>/dev/null
echo "The Beszel Hub has been uninstalled successfully!"
exit 0
else
# Stop and disable the Beszel Hub service
echo "Stopping and disabling the Beszel Hub service..."
systemctl stop beszel-hub.service
systemctl disable beszel-hub.service
# Remove the systemd service file
echo "Removing the systemd service file..."
rm -f /etc/systemd/system/beszel-hub.service
# Remove the update timer and service if they exist
echo "Removing the daily update service and timer..."
systemctl stop beszel-hub-update.timer 2>/dev/null
systemctl disable beszel-hub-update.timer 2>/dev/null
rm -f /etc/systemd/system/beszel-hub-update.service
rm -f /etc/systemd/system/beszel-hub-update.timer
# Reload the systemd daemon
echo "Reloading the systemd daemon..."
systemctl daemon-reload
# Remove the Beszel Hub binary and data
echo "Removing the Beszel Hub binary and data..."
rm -rf "$HUB_DIR"
# Remove the dedicated user
echo "Removing the dedicated user..."
userdel beszel 2>/dev/null
echo "The Beszel Hub has been uninstalled successfully!"
exit 0
fi
fi fi
# Function to check if a package is installed # Function to check if a package is installed
@@ -111,7 +276,12 @@ package_installed() {
} }
# Check for package manager and install necessary packages if not installed # Check for package manager and install necessary packages if not installed
if package_installed apt-get; then if package_installed pkg && is_freebsd; then
if ! package_installed tar || ! package_installed curl; then
pkg update
pkg install -y gtar curl
fi
elif package_installed apt-get; then
if ! package_installed tar || ! package_installed curl; then if ! package_installed tar || ! package_installed curl; then
apt-get update apt-get update
apt-get install -y tar curl apt-get install -y tar curl
@@ -129,28 +299,91 @@ else
fi fi
# Create a dedicated user for the service if it doesn't exist # Create a dedicated user for the service if it doesn't exist
if ! id -u beszel >/dev/null 2>&1; then echo "Creating a dedicated user for the Beszel Hub service..."
echo "Creating a dedicated user for the Beszel Hub service..." if is_freebsd; then
useradd -M -s /bin/false beszel if ! id -u beszel >/dev/null 2>&1; then
pw user add beszel -d /nonexistent -s /usr/sbin/nologin -c "beszel user"
fi
else
if ! id -u beszel >/dev/null 2>&1; then
useradd -M -s /bin/false beszel
fi
fi fi
# Create the directory for the Beszel Hub
echo "Creating the directory for the Beszel Hub..."
mkdir -p "$HUB_DIR/beszel_data"
chown -R beszel:beszel "$HUB_DIR"
chmod 755 "$HUB_DIR"
# Download and install the Beszel Hub # Download and install the Beszel Hub
echo "Downloading and installing the Beszel Hub..." echo "Downloading and installing the Beszel Hub..."
curl -sL "${GITHUB_PROXY_URL}https://github.com/henrygd/beszel/releases/latest/download/beszel_$(uname -s)_$(uname -m | sed 's/x86_64/amd64/' | sed 's/armv7l/arm/' | sed 's/aarch64/arm64/').tar.gz" | tar -xz -O beszel | tee ./beszel >/dev/null && chmod +x beszel
mkdir -p /opt/beszel/beszel_data
mv ./beszel /opt/beszel/beszel
chown -R beszel:beszel /opt/beszel
# Create the systemd service OS=$(uname -s | tr '[:upper:]' '[:lower:]')
printf "Creating the systemd service for the Beszel Hub...\n\n" ARCH=$(detect_architecture)
tee /etc/systemd/system/beszel-hub.service <<EOF FILE_NAME="beszel_${OS}_${ARCH}.tar.gz"
curl -sL "${GITHUB_PROXY_URL}https://github.com/henrygd/beszel/releases/latest/download/$FILE_NAME" | tar -xz -O beszel | tee ./beszel >/dev/null
chmod +x ./beszel
mv ./beszel "$BIN_PATH"
chown beszel:beszel "$BIN_PATH"
if is_freebsd; then
echo "Creating FreeBSD rc service..."
# Create the rc service file
generate_freebsd_rc_service > /usr/local/etc/rc.d/beszel-hub
# Set proper permissions for the rc script
chmod 755 /usr/local/etc/rc.d/beszel-hub
# Configure the port
sysrc beszel_hub_port="$PORT"
# Enable and start the service
echo "Enabling and starting the Beszel Hub service..."
sysrc beszel_hub_enable="YES"
service beszel-hub restart
# Check if service started successfully
sleep 2
if ! service beszel-hub status | grep -q "is running"; then
echo "Error: The Beszel Hub service failed to start. Checking logs..."
tail -n 20 /var/log/beszel_hub.log
exit 1
fi
# Auto-update service for FreeBSD
if [ "$AUTO_UPDATE_FLAG" = "true" ]; then
echo "Setting up daily automatic updates for beszel-hub..."
# Create cron job in /etc/cron.d
cat >/etc/cron.d/beszel-hub <<EOF
# Beszel Hub daily update job
12 8 * * * root $BIN_PATH update >/dev/null 2>&1
EOF
chmod 644 /etc/cron.d/beszel-hub
printf "\nDaily updates have been enabled via /etc/cron.d.\n"
fi
# Check service status
if ! service beszel-hub status >/dev/null 2>&1; then
echo "Error: The Beszel Hub service is not running."
service beszel-hub status
exit 1
fi
else
# Original systemd service installation code
printf "Creating the systemd service for the Beszel Hub...\n\n"
tee /etc/systemd/system/beszel-hub.service <<EOF
[Unit] [Unit]
Description=Beszel Hub Service Description=Beszel Hub Service
After=network.target After=network.target
[Service] [Service]
ExecStart=/opt/beszel/beszel serve --http "0.0.0.0:$PORT" ExecStart=$BIN_PATH serve --http "0.0.0.0:$PORT"
WorkingDirectory=/opt/beszel WorkingDirectory=$HUB_DIR
User=beszel User=beszel
Restart=always Restart=always
RestartSec=5 RestartSec=5
@@ -159,39 +392,39 @@ RestartSec=5
WantedBy=multi-user.target WantedBy=multi-user.target
EOF EOF
# Load and start the service # Load and start the service
printf "\nLoading and starting the Beszel Hub service...\n" printf "\nLoading and starting the Beszel Hub service...\n"
systemctl daemon-reload systemctl daemon-reload
systemctl enable beszel-hub.service systemctl enable beszel-hub.service
systemctl start beszel-hub.service systemctl start beszel-hub.service
# Wait for the service to start or fail # Wait for the service to start or fail
sleep 2 sleep 2
# Check if the service is running # Check if the service is running
if [ "$(systemctl is-active beszel-hub.service)" != "active" ]; then if [ "$(systemctl is-active beszel-hub.service)" != "active" ]; then
echo "Error: The Beszel Hub service is not running." echo "Error: The Beszel Hub service is not running."
echo "$(systemctl status beszel-hub.service)" echo "$(systemctl status beszel-hub.service)"
exit 1 exit 1
fi fi
# Enable auto-update if flag is set to true # Enable auto-update if flag is set to true
if [ "$AUTO_UPDATE_FLAG" = "true" ]; then if [ "$AUTO_UPDATE_FLAG" = "true" ]; then
echo "Setting up daily automatic updates for beszel-hub..." echo "Setting up daily automatic updates for beszel-hub..."
# Create systemd service for the daily update # Create systemd service for the daily update
cat >/etc/systemd/system/beszel-hub-update.service <<EOF cat >/etc/systemd/system/beszel-hub-update.service <<EOF
[Unit] [Unit]
Description=Update beszel-hub if needed Description=Update beszel-hub if needed
Wants=beszel-hub.service Wants=beszel-hub.service
[Service] [Service]
Type=oneshot Type=oneshot
ExecStart=/opt/beszel/beszel update ExecStart=$BIN_PATH update
EOF EOF
# Create systemd timer for the daily update # Create systemd timer for the daily update
cat >/etc/systemd/system/beszel-hub-update.timer <<EOF cat >/etc/systemd/system/beszel-hub-update.timer <<EOF
[Unit] [Unit]
Description=Run beszel-hub update daily Description=Run beszel-hub update daily
@@ -204,10 +437,11 @@ RandomizedDelaySec=4h
WantedBy=timers.target WantedBy=timers.target
EOF EOF
systemctl daemon-reload systemctl daemon-reload
systemctl enable --now beszel-hub-update.timer systemctl enable --now beszel-hub-update.timer
printf "\nDaily updates have been enabled.\n" printf "\nDaily updates have been enabled.\n"
fi
fi fi
echo "The Beszel Hub has been installed and configured successfully! It is now accessible on port $PORT." echo "The Beszel Hub has been installed and configured successfully! It is now accessible on port $PORT."