mirror of
https://github.com/henrygd/beszel.git
synced 2026-09-21 08:57:48 +02:00
Compare commits
8 Commits
v0.18.8
...
946f2e6be1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
946f2e6be1 | ||
|
|
ba90daf4d6 | ||
|
|
aa1d67a122 | ||
|
|
68a3f8962a | ||
|
|
0eb3426619 | ||
|
|
96beadc8c9 | ||
|
|
65a6f60304 | ||
|
|
54dae08631 |
@@ -33,7 +33,10 @@ var errNoBatteries = errors.New("no readable batteries")
|
||||
func normalizeBatteries(batteries []Battery) []Battery {
|
||||
nameCounts := make(map[string]int, len(batteries))
|
||||
for i := range batteries {
|
||||
name := strings.TrimSpace(batteries[i].Name)
|
||||
// Names come from firmware (e.g. sysfs model_name) and are not guaranteed to
|
||||
// be valid UTF-8. Invalid bytes are rejected when the hub decodes the CBOR
|
||||
// payload, which drops every metric for the system, so strip them here.
|
||||
name := strings.TrimSpace(strings.ToValidUTF8(batteries[i].Name, ""))
|
||||
if name == "" {
|
||||
name = "Battery " + strconv.Itoa(i+1)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package battery
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -33,3 +34,15 @@ func TestNormalizeBatteriesFallbackNames(t *testing.T) {
|
||||
bats := normalizeBatteries([]Battery{{}, {}, {Name: "Mouse"}, {Name: "Mouse"}})
|
||||
assert.Equal(t, []string{"Battery 1", "Battery 2", "Mouse", "Mouse (2)"}, []string{bats[0].Name, bats[1].Name, bats[2].Name, bats[3].Name})
|
||||
}
|
||||
|
||||
func TestNormalizeBatteriesStripsInvalidUTF8(t *testing.T) {
|
||||
// Firmware occasionally reports names that are not valid UTF-8 (a ThinkPad
|
||||
// reporting "LNV-5B11K63024@\xd0" in model_name is a real example).
|
||||
bats := normalizeBatteries([]Battery{{Name: "LNV-5B11K63024@\xd0"}, {Name: "\xff\xfe"}})
|
||||
assert.Equal(t, "LNV-5B11K63024@", bats[0].Name)
|
||||
// A name made up entirely of invalid bytes falls back to the generic name.
|
||||
assert.Equal(t, "Battery 2", bats[1].Name)
|
||||
for _, b := range bats {
|
||||
assert.True(t, utf8.ValidString(b.Name))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,14 +72,30 @@ func discoverHwmonFans(root string) ([]fanSensor, error) {
|
||||
var sensors []fanSensor
|
||||
for _, entry := range entries {
|
||||
chipDir := filepath.Join(root, entry.Name())
|
||||
chipName := utils.ReadStringFile(filepath.Join(chipDir, "name"))
|
||||
sensorDir := chipDir
|
||||
inputs, _ := filepath.Glob(filepath.Join(sensorDir, "fan*_input"))
|
||||
|
||||
// Some legacy hwmon drivers (notably applesmc) register a hwmon class
|
||||
// device but create fan attributes on the parent platform device. In
|
||||
// sysfs that parent is exposed through hwmonN/device.
|
||||
if len(inputs) == 0 {
|
||||
deviceDir := filepath.Join(chipDir, "device")
|
||||
if deviceInputs, _ := filepath.Glob(filepath.Join(deviceDir, "fan*_input")); len(deviceInputs) > 0 {
|
||||
sensorDir = deviceDir
|
||||
inputs = deviceInputs
|
||||
}
|
||||
}
|
||||
|
||||
chipName := utils.ReadStringFile(filepath.Join(sensorDir, "name"))
|
||||
if chipName == "" {
|
||||
chipName = utils.ReadStringFile(filepath.Join(chipDir, "name"))
|
||||
}
|
||||
if chipName == "" {
|
||||
chipName = entry.Name()
|
||||
}
|
||||
inputs, _ := filepath.Glob(filepath.Join(chipDir, "fan*_input"))
|
||||
for _, inputPath := range inputs {
|
||||
base := strings.TrimSuffix(filepath.Base(inputPath), "_input")
|
||||
label := utils.ReadStringFile(filepath.Join(chipDir, base+"_label"))
|
||||
label := utils.ReadStringFile(filepath.Join(sensorDir, base+"_label"))
|
||||
key := chipName + "_" + base
|
||||
if label != "" {
|
||||
key = chipName + "_" + label
|
||||
|
||||
@@ -50,6 +50,24 @@ func TestReadHwmonFans(t *testing.T) {
|
||||
}, fans)
|
||||
}
|
||||
|
||||
// TestReadHwmonFansLegacyParent verifies legacy hwmon layouts such as applesmc,
|
||||
// where the hwmon class node exists but fan attributes live on hwmonN/device.
|
||||
func TestReadHwmonFansLegacyParent(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
deviceDir := filepath.Join(root, "devices", "applesmc.768")
|
||||
writeFile(t, filepath.Join(deviceDir, "name"), "applesmc\n")
|
||||
writeFile(t, filepath.Join(deviceDir, "fan1_input"), "1202\n")
|
||||
writeFile(t, filepath.Join(deviceDir, "fan1_label"), "Exhaust\n")
|
||||
|
||||
chipDir := filepath.Join(root, "hwmon1")
|
||||
require.NoError(t, os.MkdirAll(chipDir, 0o755))
|
||||
require.NoError(t, os.Symlink(deviceDir, filepath.Join(chipDir, "device")))
|
||||
|
||||
fans, err := readHwmonFans(root)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, map[string]uint16{"applesmc_Exhaust": 1202}, fans)
|
||||
}
|
||||
|
||||
// TestReadHwmonFansMissingRoot returns an error rather than panicking when the
|
||||
// hwmon root doesn't exist (e.g. running on a kernel without hwmon support).
|
||||
func TestReadHwmonFansMissingRoot(t *testing.T) {
|
||||
|
||||
@@ -50,6 +50,9 @@ func generateFingerprint(hostname, cpuModel string) string {
|
||||
if info, err := cpu.Info(); err == nil && len(info) > 0 {
|
||||
cpuModel = info[0].ModelName
|
||||
}
|
||||
if cpuModel == "" {
|
||||
cpuModel = getCpuModelFromCpuinfo()
|
||||
}
|
||||
}
|
||||
fingerprint = hostname + cpuModel
|
||||
}
|
||||
|
||||
@@ -17,15 +17,17 @@ import (
|
||||
var mdraidSysfsRoot = "/sys"
|
||||
|
||||
type mdraidHealth struct {
|
||||
level string
|
||||
arrayState string
|
||||
degraded uint64
|
||||
raidDisks uint64
|
||||
syncAction string
|
||||
syncCompleted string
|
||||
syncSpeed string
|
||||
mismatchCnt uint64
|
||||
capacity uint64
|
||||
level string
|
||||
arrayState string
|
||||
degraded uint64
|
||||
faultyDisks uint64
|
||||
populatedDisks uint64
|
||||
raidDisks uint64
|
||||
syncAction string
|
||||
syncCompleted string
|
||||
syncSpeed string
|
||||
mismatchCnt uint64
|
||||
capacity uint64
|
||||
}
|
||||
|
||||
// scanMdraidDevices discovers Linux md arrays exposed in sysfs.
|
||||
@@ -92,6 +94,9 @@ func (sm *SmartManager) collectMdraidHealth(deviceInfo *DeviceInfo) (bool, error
|
||||
if health.degraded > 0 {
|
||||
attrs = append(attrs, &smart.SmartAttribute{Name: "Degraded", RawValue: health.degraded})
|
||||
}
|
||||
if health.faultyDisks > 0 {
|
||||
attrs = append(attrs, &smart.SmartAttribute{Name: "FaultyDisks", RawValue: health.faultyDisks})
|
||||
}
|
||||
if health.syncAction != "" {
|
||||
attrs = append(attrs, &smart.SmartAttribute{Name: "SyncAction", RawString: health.syncAction})
|
||||
}
|
||||
@@ -152,6 +157,7 @@ func readMdraidHealth(blockName string) (mdraidHealth, bool) {
|
||||
if val, ok := utils.ReadUintFile(filepath.Join(mdDir, "degraded")); ok {
|
||||
out.degraded = val
|
||||
}
|
||||
out.faultyDisks, out.populatedDisks = countMdraidMemberStates(blockName, mdraidSysfsRoot)
|
||||
if val, ok := utils.ReadUintFile(filepath.Join(mdDir, "mismatch_cnt")); ok {
|
||||
out.mismatchCnt = val
|
||||
}
|
||||
@@ -177,7 +183,19 @@ func mdraidSmartStatus(health mdraidHealth) string {
|
||||
case "resync", "recover", "reshape":
|
||||
return "WARNING"
|
||||
}
|
||||
// Use actual faulty member count rather than the degraded counter, which
|
||||
// equals raid_disks minus active_disks. On QNAP systems raid_disks may be
|
||||
// set to a large value (e.g. 32) while only a few slots are ever used,
|
||||
// making degraded misleadingly large despite zero failed disks.
|
||||
if health.faultyDisks > 0 {
|
||||
return "FAILED"
|
||||
}
|
||||
if health.degraded > 0 {
|
||||
if isSparseSlotDegraded(health) {
|
||||
// A sysfs snapshot cannot distinguish reserved slots from a removed
|
||||
// member on sparse arrays, so report the ambiguity as a warning.
|
||||
return "WARNING"
|
||||
}
|
||||
return "FAILED"
|
||||
}
|
||||
if health.mismatchCnt > 0 {
|
||||
@@ -196,6 +214,43 @@ func mdraidSmartStatus(health mdraidHealth) string {
|
||||
return "UNKNOWN"
|
||||
}
|
||||
|
||||
// countMdraidMemberStates reads member device directories under
|
||||
// block/<name>/md and returns how many are explicitly marked "faulty", plus
|
||||
// how many are populated at all (regardless of state). populatedDisks lets
|
||||
// callers distinguish RAID slots that were never used (QNAP reserves far
|
||||
// more raid_disks than it ever populates) from members that went missing.
|
||||
func countMdraidMemberStates(blockName, root string) (faultyDisks, populatedDisks uint64) {
|
||||
devDir := filepath.Join(root, "block", blockName, "md")
|
||||
entries, err := os.ReadDir(devDir)
|
||||
if err != nil {
|
||||
return 0, 0
|
||||
}
|
||||
for _, ent := range entries {
|
||||
if !strings.HasPrefix(ent.Name(), "dev-") {
|
||||
continue
|
||||
}
|
||||
populatedDisks++
|
||||
statePath := filepath.Join(devDir, ent.Name(), "state")
|
||||
state := utils.ReadStringFile(statePath)
|
||||
if strings.Contains(state, "faulty") {
|
||||
faultyDisks++
|
||||
}
|
||||
}
|
||||
return faultyDisks, populatedDisks
|
||||
}
|
||||
|
||||
// isSparseSlotDegraded reports whether a non-zero "degraded" count may be
|
||||
// explained by RAID slots that were never populated. QNAP configures system
|
||||
// arrays with raid_disks set to a large fixed maximum (e.g. 32) far beyond the
|
||||
// handful of slots it ever populates, so sparse slots outnumber populated ones.
|
||||
func isSparseSlotDegraded(health mdraidHealth) bool {
|
||||
if health.populatedDisks == 0 || health.raidDisks <= health.populatedDisks {
|
||||
return false
|
||||
}
|
||||
sparseSlots := health.raidDisks - health.populatedDisks
|
||||
return sparseSlots > health.populatedDisks
|
||||
}
|
||||
|
||||
// isMdraidBlockName matches /dev/mdN-style block device names.
|
||||
func isMdraidBlockName(name string) bool {
|
||||
if !strings.HasPrefix(name, "md") {
|
||||
|
||||
@@ -40,6 +40,15 @@ func TestMdraidMockSysfsScanAndCollect(t *testing.T) {
|
||||
write(filepath.Join(mdDir, "sync_completed"), "10%\n")
|
||||
write(filepath.Join(mdDir, "sync_speed"), "100M\n")
|
||||
write(filepath.Join(mdDir, "mismatch_cnt"), "0\n")
|
||||
|
||||
// Simulate two healthy member devices (no faulty state).
|
||||
for _, dev := range []string{"dev-sda", "dev-sdb"} {
|
||||
devPath := filepath.Join(mdDir, dev)
|
||||
if err := os.MkdirAll(devPath, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
write(filepath.Join(devPath, "state"), "in_sync\n")
|
||||
}
|
||||
write(filepath.Join(queueDir, "logical_block_size"), "512\n")
|
||||
write(filepath.Join(tmp, "block", "md0", "size"), "2048\n")
|
||||
|
||||
@@ -81,15 +90,77 @@ func TestMdraidMockSysfsScanAndCollect(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCountMdraidMemberStates(t *testing.T) {
|
||||
tmp := t.TempDir()
|
||||
|
||||
write := func(path, content string) {
|
||||
t.Helper()
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
mdDir := filepath.Join(tmp, "block", "md0", "md")
|
||||
|
||||
// No dev-* entries: zero faulty, zero populated.
|
||||
if faulty, populated := countMdraidMemberStates("md0", tmp); faulty != 0 || populated != 0 {
|
||||
t.Fatalf("no members: got (faulty=%d populated=%d), want (0,0)", faulty, populated)
|
||||
}
|
||||
|
||||
// Two healthy members.
|
||||
write(filepath.Join(mdDir, "dev-sda", "state"), "in_sync\n")
|
||||
write(filepath.Join(mdDir, "dev-sdb", "state"), "in_sync\n")
|
||||
if faulty, populated := countMdraidMemberStates("md0", tmp); faulty != 0 || populated != 2 {
|
||||
t.Fatalf("all in_sync: got (faulty=%d populated=%d), want (0,2)", faulty, populated)
|
||||
}
|
||||
|
||||
// One faulty member.
|
||||
write(filepath.Join(mdDir, "dev-sdb", "state"), "faulty\n")
|
||||
if faulty, populated := countMdraidMemberStates("md0", tmp); faulty != 1 || populated != 2 {
|
||||
t.Fatalf("one faulty: got (faulty=%d populated=%d), want (1,2)", faulty, populated)
|
||||
}
|
||||
|
||||
// QNAP-style: 28 degraded slots but no dev-* entries for them, 4 in_sync.
|
||||
write(filepath.Join(mdDir, "dev-sdb", "state"), "in_sync\n")
|
||||
write(filepath.Join(mdDir, "dev-sdc", "state"), "in_sync\n")
|
||||
write(filepath.Join(mdDir, "dev-sdd", "state"), "in_sync\n")
|
||||
if faulty, populated := countMdraidMemberStates("md0", tmp); faulty != 0 || populated != 4 {
|
||||
t.Fatalf("qnap sparse: got (faulty=%d populated=%d), want (0,4)", faulty, populated)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMdraidSmartStatus(t *testing.T) {
|
||||
if got := mdraidSmartStatus(mdraidHealth{arrayState: "inactive"}); got != "FAILED" {
|
||||
t.Fatalf("mdraidSmartStatus(inactive) = %q, want FAILED", got)
|
||||
}
|
||||
if got := mdraidSmartStatus(mdraidHealth{arrayState: "active", degraded: 1, syncAction: "recover"}); got != "WARNING" {
|
||||
if got := mdraidSmartStatus(mdraidHealth{arrayState: "active", degraded: 1, faultyDisks: 1, syncAction: "recover"}); got != "WARNING" {
|
||||
t.Fatalf("mdraidSmartStatus(degraded+recover) = %q, want WARNING", got)
|
||||
}
|
||||
if got := mdraidSmartStatus(mdraidHealth{arrayState: "active", degraded: 1}); got != "FAILED" {
|
||||
t.Fatalf("mdraidSmartStatus(degraded) = %q, want FAILED", got)
|
||||
if got := mdraidSmartStatus(mdraidHealth{arrayState: "active", degraded: 1, faultyDisks: 1}); got != "FAILED" {
|
||||
t.Fatalf("mdraidSmartStatus(degraded+faulty) = %q, want FAILED", got)
|
||||
}
|
||||
// QNAP-style: raid_disks=32 but only 4 populated; degraded=28 but no faulty devices.
|
||||
if got := mdraidSmartStatus(mdraidHealth{arrayState: "clean", degraded: 28, faultyDisks: 0, raidDisks: 32, populatedDisks: 4}); got != "WARNING" {
|
||||
t.Fatalf("mdraidSmartStatus(qnap sparse) = %q, want WARNING", got)
|
||||
}
|
||||
// A member disappearing from the same sparse array is indistinguishable
|
||||
// from another reserved slot, so it must not be reported as healthy.
|
||||
if got := mdraidSmartStatus(mdraidHealth{arrayState: "clean", degraded: 29, faultyDisks: 0, raidDisks: 32, populatedDisks: 3}); got != "WARNING" {
|
||||
t.Fatalf("mdraidSmartStatus(qnap sparse missing member) = %q, want WARNING", got)
|
||||
}
|
||||
// A genuinely missing member (removed dev-* entry, not just an unpopulated
|
||||
// QNAP reserve slot) must still fail: raid_disks=4, only 3 populated, all
|
||||
// of them in_sync, so faultyDisks==0 but degraded==1.
|
||||
if got := mdraidSmartStatus(mdraidHealth{arrayState: "clean", degraded: 1, faultyDisks: 0, raidDisks: 4, populatedDisks: 3}); got != "FAILED" {
|
||||
t.Fatalf("mdraidSmartStatus(missing member) = %q, want FAILED", got)
|
||||
}
|
||||
// Degraded with no member-state info at all (e.g. sysfs read failed) must
|
||||
// still fail rather than being silently treated as a sparse QNAP array.
|
||||
if got := mdraidSmartStatus(mdraidHealth{arrayState: "clean", degraded: 1, faultyDisks: 0, raidDisks: 4, populatedDisks: 0}); got != "FAILED" {
|
||||
t.Fatalf("mdraidSmartStatus(degraded, no member info) = %q, want FAILED", got)
|
||||
}
|
||||
if got := mdraidSmartStatus(mdraidHealth{arrayState: "active", syncAction: "recover"}); got != "WARNING" {
|
||||
t.Fatalf("mdraidSmartStatus(recover) = %q, want WARNING", got)
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"os"
|
||||
"runtime"
|
||||
@@ -78,6 +79,12 @@ func (a *Agent) refreshSystemDetails() {
|
||||
if info, err := cpu.Info(); err == nil && len(info) > 0 {
|
||||
a.systemDetails.CpuModel = info[0].ModelName
|
||||
}
|
||||
// gopsutil doesn't parse the "cpu model" field from /proc/cpuinfo, which
|
||||
// is the only source of the CPU model name on MIPS. Fall back to reading
|
||||
// it directly when ModelName is empty.
|
||||
if a.systemDetails.CpuModel == "" {
|
||||
a.systemDetails.CpuModel = getCpuModelFromCpuinfo()
|
||||
}
|
||||
// cores / threads
|
||||
cores, _ := cpu.Counts(false)
|
||||
threads := hostInfo.NCPU
|
||||
@@ -258,13 +265,66 @@ func (a *Agent) getSystemStats(cacheTimeMs uint16) system.Stats {
|
||||
a.systemInfo.MemPct = systemStats.MemPct
|
||||
a.systemInfo.DiskPct = systemStats.DiskPct
|
||||
a.systemInfo.Battery = systemStats.Battery
|
||||
a.systemInfo.Uptime, _ = host.Uptime()
|
||||
a.systemInfo.Uptime, _ = getUptime()
|
||||
a.systemInfo.BandwidthBytes = systemStats.Bandwidth[0] + systemStats.Bandwidth[1]
|
||||
a.systemInfo.Threads = a.systemDetails.Threads
|
||||
|
||||
return systemStats
|
||||
}
|
||||
|
||||
// cpuModelFallbackKeys are the field names to look for in /proc/cpuinfo when
|
||||
// gopsutil fails to return a ModelName. The "cpu model" key is used on MIPS
|
||||
// (e.g. "MIPS 1004Kc V2.15"), while "system type" provides SoC information
|
||||
// on various embedded architectures.
|
||||
var cpuModelFallbackKeys = []string{"cpu model", "system type"}
|
||||
|
||||
// getCpuModelFromCpuinfo reads /proc/cpuinfo and returns a CPU model string.
|
||||
// This is a fallback for architectures where gopsutil's cpu.Info() does not
|
||||
// populate ModelName, most notably MIPS.
|
||||
func getCpuModelFromCpuinfo() string {
|
||||
file, err := os.Open("/proc/cpuinfo")
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
defer file.Close()
|
||||
return parseCpuModel(file)
|
||||
}
|
||||
|
||||
// parseCpuModel scans r (expected to be /proc/cpuinfo content) and returns
|
||||
// a combined CPU model string. It collects values from all matching keys
|
||||
// and joins them with " / " when multiple are found.
|
||||
func parseCpuModel(r io.Reader) string {
|
||||
lines := readLines(r)
|
||||
var parts []string
|
||||
for _, key := range cpuModelFallbackKeys {
|
||||
for _, line := range lines {
|
||||
after, found := strings.CutPrefix(line, key)
|
||||
if !found {
|
||||
continue
|
||||
}
|
||||
after = strings.TrimSpace(after)
|
||||
if len(after) < 2 || after[0] != ':' {
|
||||
continue
|
||||
}
|
||||
if value := strings.TrimSpace(after[1:]); value != "" {
|
||||
parts = append(parts, value)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return strings.Join(parts, " / ")
|
||||
}
|
||||
|
||||
// readLines reads all lines from r into a slice.
|
||||
func readLines(r io.Reader) []string {
|
||||
scanner := bufio.NewScanner(r)
|
||||
var lines []string
|
||||
for scanner.Scan() {
|
||||
lines = append(lines, scanner.Text())
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
||||
// calculateHostMemoryUsage derives counters defensively because /proc/meminfo may
|
||||
// change while gopsutil reads it. Invalid unsigned subtractions saturate at zero.
|
||||
func calculateHostMemoryUsage(v *mem.VirtualMemoryStat, htop bool) (used, cacheBuff, swapUsed uint64) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/henrygd/beszel/internal/common"
|
||||
@@ -113,3 +114,81 @@ func TestUpdateSystemDetailsMarksDetailsDirty(t *testing.T) {
|
||||
assert.False(t, agent.detailsDirty)
|
||||
assert.Nil(t, original.Details)
|
||||
}
|
||||
|
||||
func TestParseCpuModel(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "MIPS with both cpu model and system type",
|
||||
input: `system type : MediaTek MT7621 ver:1 eco:3
|
||||
machine : ASUS RT-AX53U
|
||||
processor : 0
|
||||
cpu model : MIPS 1004Kc V2.15
|
||||
BogoMIPS : 586.13
|
||||
wait instruction : yes`,
|
||||
expected: "MIPS 1004Kc V2.15 / MediaTek MT7621 ver:1 eco:3",
|
||||
},
|
||||
{
|
||||
name: "MIPS with different SoC",
|
||||
input: `system type : Atheros AR7161 rev 2
|
||||
machine : NETGEAR WNDR3700
|
||||
processor : 0
|
||||
cpu model : MIPS 24Kc V7.4
|
||||
BogoMIPS : 452.19`,
|
||||
expected: "MIPS 24Kc V7.4 / Atheros AR7161 rev 2",
|
||||
},
|
||||
{
|
||||
name: "only system type when cpu model missing",
|
||||
input: `system type : Broadcom BCM47xx
|
||||
processor : 0
|
||||
BogoMIPS : 296.11`,
|
||||
expected: "Broadcom BCM47xx",
|
||||
},
|
||||
{
|
||||
name: "only cpu model when system type missing",
|
||||
input: `processor : 0
|
||||
cpu model : MIPS 34Kc V2.15
|
||||
BogoMIPS : 300.00`,
|
||||
expected: "MIPS 34Kc V2.15",
|
||||
},
|
||||
{
|
||||
name: "x86 cpuinfo returns empty",
|
||||
input: `processor : 0
|
||||
vendor_id : GenuineIntel
|
||||
cpu family : 6
|
||||
model : 142
|
||||
model name : Intel(R) Core(TM) i5-8250U CPU @ 1.60GHz
|
||||
stepping : 10`,
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "empty input",
|
||||
input: "",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "cpu model with extra whitespace",
|
||||
input: `processor : 0
|
||||
cpu model : MIPS 34Kc V2.15
|
||||
BogoMIPS : 300.00`,
|
||||
expected: "MIPS 34Kc V2.15",
|
||||
},
|
||||
{
|
||||
name: "cpu model without value",
|
||||
input: `processor : 0
|
||||
cpu model :
|
||||
BogoMIPS : 300.00`,
|
||||
expected: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := parseCpuModel(strings.NewReader(tt.input))
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
44
agent/uptime_linux.go
Normal file
44
agent/uptime_linux.go
Normal file
@@ -0,0 +1,44 @@
|
||||
//go:build linux
|
||||
|
||||
package agent
|
||||
|
||||
import (
|
||||
"math"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/shirou/gopsutil/v4/host"
|
||||
)
|
||||
|
||||
// uptimeFilePath is a variable so tests can point it at a fixture.
|
||||
var uptimeFilePath = "/proc/uptime"
|
||||
|
||||
// getUptime returns the system uptime in seconds.
|
||||
//
|
||||
// This reads /proc/uptime instead of using host.Uptime(), which calls the
|
||||
// sysinfo(2) syscall. Inside an LXC container lxcfs virtualizes /proc/uptime
|
||||
// but cannot intercept a syscall, so sysinfo(2) reports the host's uptime
|
||||
// rather than the container's.
|
||||
//
|
||||
// Falls back to host.Uptime() if /proc/uptime is missing or unparseable, so
|
||||
// behavior is unchanged anywhere the file isn't available.
|
||||
func getUptime() (uint64, error) {
|
||||
data, err := os.ReadFile(uptimeFilePath)
|
||||
if err != nil {
|
||||
return host.Uptime()
|
||||
}
|
||||
fields := strings.Fields(string(data))
|
||||
if len(fields) == 0 {
|
||||
return host.Uptime()
|
||||
}
|
||||
seconds, err := strconv.ParseFloat(fields[0], 64)
|
||||
if err != nil ||
|
||||
math.IsNaN(seconds) ||
|
||||
math.IsInf(seconds, 0) ||
|
||||
seconds < 0 ||
|
||||
seconds >= 1<<64 {
|
||||
return host.Uptime()
|
||||
}
|
||||
return uint64(seconds), nil
|
||||
}
|
||||
101
agent/uptime_linux_test.go
Normal file
101
agent/uptime_linux_test.go
Normal file
@@ -0,0 +1,101 @@
|
||||
//go:build linux
|
||||
|
||||
package agent
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetUptimeFromProc(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
contents string
|
||||
want uint64
|
||||
}{
|
||||
{"typical", "12345.67 98765.43\n", 12345},
|
||||
{"zero", "0.00 0.00\n", 0},
|
||||
{"no trailing newline", "42.99 7.00", 42},
|
||||
{"single field", "600.5", 600},
|
||||
{"large value", "266030.12 1000000.00\n", 266030},
|
||||
}
|
||||
|
||||
prev := uptimeFilePath
|
||||
t.Cleanup(func() { uptimeFilePath = prev })
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "uptime")
|
||||
if err := os.WriteFile(path, []byte(tt.contents), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
uptimeFilePath = path
|
||||
|
||||
got, err := getUptime()
|
||||
if err != nil {
|
||||
t.Fatalf("getUptime() returned error: %v", err)
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("getUptime() = %d, want %d", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func writeUptime(contents string) func(t *testing.T) string {
|
||||
return func(t *testing.T) string {
|
||||
path := filepath.Join(t.TempDir(), "uptime")
|
||||
if err := os.WriteFile(path, []byte(contents), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return path
|
||||
}
|
||||
}
|
||||
|
||||
// Malformed, missing, or out-of-range input must fall back to host.Uptime()
|
||||
// rather than returning a bogus value, so the agent still reports something sane.
|
||||
func TestGetUptimeFallsBack(t *testing.T) {
|
||||
prev := uptimeFilePath
|
||||
t.Cleanup(func() { uptimeFilePath = prev })
|
||||
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
prepare func(t *testing.T) string
|
||||
}{
|
||||
{"missing file", func(t *testing.T) string {
|
||||
return filepath.Join(t.TempDir(), "does-not-exist")
|
||||
}},
|
||||
{"empty file", func(t *testing.T) string {
|
||||
path := filepath.Join(t.TempDir(), "uptime")
|
||||
if err := os.WriteFile(path, nil, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return path
|
||||
}},
|
||||
{"unparseable", func(t *testing.T) string {
|
||||
path := filepath.Join(t.TempDir(), "uptime")
|
||||
if err := os.WriteFile(path, []byte("not-a-number 1.0\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return path
|
||||
}},
|
||||
{"NaN", writeUptime("NaN 1.0\n")},
|
||||
{"positive infinity", writeUptime("+Inf 1.0\n")},
|
||||
{"negative infinity", writeUptime("-Inf 1.0\n")},
|
||||
{"negative", writeUptime("-42.5 1.0\n")},
|
||||
{"exceeds uint64 range", writeUptime("1e20 1.0\n")},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
uptimeFilePath = tt.prepare(t)
|
||||
|
||||
got, err := getUptime()
|
||||
if err != nil {
|
||||
t.Fatalf("getUptime() returned error: %v", err)
|
||||
}
|
||||
if got == 0 {
|
||||
t.Error("getUptime() = 0, expected fallback to host.Uptime()")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
10
agent/uptime_stub.go
Normal file
10
agent/uptime_stub.go
Normal file
@@ -0,0 +1,10 @@
|
||||
//go:build !linux
|
||||
|
||||
package agent
|
||||
|
||||
import "github.com/shirou/gopsutil/v4/host"
|
||||
|
||||
// getUptime returns the system uptime in seconds.
|
||||
func getUptime() (uint64, error) {
|
||||
return host.Uptime()
|
||||
}
|
||||
@@ -2,9 +2,9 @@ apiVersion: v1
|
||||
description: Installs beszel-agent in kubernetes
|
||||
home: https://github.com/henrygd/beszel/tree/main/supplemental/helm/beszel-agent
|
||||
name: beszel-agent
|
||||
appVersion: "0.18.7"
|
||||
appVersion: "0.18.8"
|
||||
# Bump this version when publishing chart changes.
|
||||
version: 0.1.4
|
||||
version: 0.1.5
|
||||
sources:
|
||||
- https://github.com/henrygd/beszel/tree/main/supplemental/helm/beszel-agent
|
||||
- https://www.beszel.dev/
|
||||
|
||||
@@ -80,7 +80,7 @@ Essential parameters to configure:
|
||||
| `secret.sshKey` | `ssh-key` | Key name in the secret for the SSH public key |
|
||||
| `secret.tokenKey` | `token` | Key name in the secret for the authentication token |
|
||||
| `image.repository` | `henrygd/beszel-agent` | Container image |
|
||||
| `image.tag` | Chart AppVersion (0.18.7) | Image version |
|
||||
| `image.tag` | Chart AppVersion (0.18.8) | Image version |
|
||||
| `hostNetwork` | `false` | Use host network for network monitoring |
|
||||
| `tolerations` | Allows all taints | Tolerations for running on tainted nodes |
|
||||
|
||||
@@ -385,7 +385,7 @@ helm upgrade beszel-agent ./beszel-agent \
|
||||
|
||||
# Change image version
|
||||
helm upgrade beszel-agent ./beszel-agent \
|
||||
--set image.tag="0.18.7"
|
||||
--set image.tag="0.18.8"
|
||||
```
|
||||
|
||||
### Restart All Agents
|
||||
@@ -522,7 +522,7 @@ kubectl get secret beszel-agent -o jsonpath='{.data.ssh-key}' | base64 -d
|
||||
## Chart Information
|
||||
|
||||
- **Chart Version**: 0.1.0
|
||||
- **App Version**: 0.18.7
|
||||
- **App Version**: 0.18.8
|
||||
- **Kubernetes Version**: 1.19+
|
||||
- **Maintainer**: cloudwithdan (nikoloskid@pm.me)
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@ apiVersion: v1
|
||||
description: Installs beszel-hub in kubernetes
|
||||
home: https://github.com/henrygd/beszel/tree/main/supplemental/helm/beszel-hub
|
||||
name: beszel-hub
|
||||
appVersion: "0.18.7"
|
||||
appVersion: "0.18.8"
|
||||
# Bump this version when publishing chart changes.
|
||||
version: 0.1.4
|
||||
version: 0.1.5
|
||||
sources:
|
||||
- https://github.com/henrygd/beszel/tree/main/supplemental/helm/beszel-hub
|
||||
- https://www.beszel.dev/
|
||||
|
||||
@@ -47,7 +47,7 @@ Key configuration options in `values.yaml`:
|
||||
|-----------|---------|-------------|
|
||||
| `replicaCount` | `1` | Number of Beszel Hub replicas |
|
||||
| `image.repository` | `henrygd/beszel` | Container image repository |
|
||||
| `image.tag` | Chart AppVersion (0.18.7) | Container image tag |
|
||||
| `image.tag` | Chart AppVersion (0.18.8) | Container image tag |
|
||||
| `image.pullPolicy` | `IfNotPresent` | Image pull policy |
|
||||
| `service.port` | `8090` | Service port |
|
||||
| `persistentVolumeClaim.enabled` | `true` | Enable persistent volume |
|
||||
@@ -169,7 +169,7 @@ tolerations:
|
||||
```yaml
|
||||
replicaCount: 3
|
||||
image:
|
||||
tag: "0.18.7"
|
||||
tag: "0.18.8"
|
||||
service:
|
||||
type: LoadBalancer
|
||||
ingress:
|
||||
@@ -330,7 +330,7 @@ By default, Beszel Hub uses a PersistentVolumeClaim for data storage. Ensure you
|
||||
## Chart Information
|
||||
|
||||
- **Chart Version**: 0.1.0
|
||||
- **App Version**: 0.18.7
|
||||
- **App Version**: 0.18.8
|
||||
- **Kubernetes Version**: 1.19+
|
||||
- **Maintainer**: cloudwithdan (nikoloskid@pm.me)
|
||||
|
||||
|
||||
@@ -9,9 +9,21 @@ param (
|
||||
[string]$NSSMPath = "",
|
||||
[switch]$ConfigureFirewall,
|
||||
[ValidateSet("Auto", "Scoop", "WinGet")]
|
||||
[string]$InstallMethod = "Auto"
|
||||
[string]$InstallMethod = "Auto",
|
||||
# Set automatically from $PSBoundParameters below, or forwarded through an elevated relaunch.
|
||||
# Used so a reinstall only overwrites Token/Url/Port on an existing service if the caller
|
||||
# actually asked to change them, instead of wiping them with their unset defaults.
|
||||
[switch]$TokenProvided,
|
||||
[switch]$UrlProvided,
|
||||
[switch]$PortProvided
|
||||
)
|
||||
|
||||
if (-not $Elevated) {
|
||||
$TokenProvided = $PSBoundParameters.ContainsKey('Token')
|
||||
$UrlProvided = $PSBoundParameters.ContainsKey('Url')
|
||||
$PortProvided = $PSBoundParameters.ContainsKey('Port')
|
||||
}
|
||||
|
||||
# Check if required parameters are provided
|
||||
if ([string]::IsNullOrWhiteSpace($Key)) {
|
||||
Write-Host "ERROR: SSH Key is required." -ForegroundColor Red
|
||||
@@ -312,7 +324,10 @@ function Install-NSSMService {
|
||||
[string]$HubUrl = "",
|
||||
[Parameter(Mandatory=$true)]
|
||||
[int]$Port,
|
||||
[string]$NSSMPath = ""
|
||||
[string]$NSSMPath = "",
|
||||
[switch]$TokenProvided,
|
||||
[switch]$UrlProvided,
|
||||
[switch]$PortProvided
|
||||
)
|
||||
|
||||
Write-Host "Installing beszel-agent service..."
|
||||
@@ -330,15 +345,26 @@ function Install-NSSMService {
|
||||
$existingService = Get-Service -Name "beszel-agent" -ErrorAction SilentlyContinue
|
||||
if ($existingService) {
|
||||
Write-Host "Service already exists. Checking if path update is needed..."
|
||||
|
||||
# Get current service path
|
||||
|
||||
# Get current service path
|
||||
$pathNeedsUpdate = $true
|
||||
try {
|
||||
$currentPath = & $nssmCommand get beszel-agent Application
|
||||
if ($LASTEXITCODE -eq 0 -and $currentPath.Trim() -eq $AgentPath) {
|
||||
Write-Host "Service already configured with correct path. Skipping service recreation." -ForegroundColor Green
|
||||
Write-Host "Service path is already correct. Updating environment variables..."
|
||||
& $nssmCommand set beszel-agent AppEnvironmentExtra "+KEY=$Key"
|
||||
if ($TokenProvided) { & $nssmCommand set beszel-agent AppEnvironmentExtra "+TOKEN=$Token" }
|
||||
if ($UrlProvided) { & $nssmCommand set beszel-agent AppEnvironmentExtra "+HUB_URL=$HubUrl" }
|
||||
if ($PortProvided) { & $nssmCommand set beszel-agent AppEnvironmentExtra "+PORT=$Port" }
|
||||
|
||||
# Restart the service so the running process picks up the new environment variables
|
||||
if ($existingService.Status -eq "Running") {
|
||||
Write-Host "Restarting service to apply updated environment variables..."
|
||||
& $nssmCommand restart beszel-agent
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
Write-Host "Service path needs updating. Stopping and removing existing service..."
|
||||
Write-Host " Current path: $($currentPath.Trim())"
|
||||
Write-Host " New path: $AgentPath"
|
||||
@@ -346,7 +372,7 @@ function Install-NSSMService {
|
||||
Write-Host "Could not retrieve current service path, will recreate service: $($_.Exception.Message)" -ForegroundColor Yellow
|
||||
Write-Host "Service path needs updating. Stopping and removing existing service..."
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
& $nssmCommand stop beszel-agent
|
||||
& $nssmCommand remove beszel-agent confirm
|
||||
@@ -589,6 +615,12 @@ try {
|
||||
$argumentList += "`"$NSSMPath`""
|
||||
}
|
||||
|
||||
# Forward which optional values were explicitly provided, so the elevated
|
||||
# instance knows whether to overwrite them on an existing service
|
||||
if ($TokenProvided) { $argumentList += "-TokenProvided" }
|
||||
if ($UrlProvided) { $argumentList += "-UrlProvided" }
|
||||
if ($PortProvided) { $argumentList += "-PortProvided" }
|
||||
|
||||
if ($ConfigureFirewall) {
|
||||
$argumentList += "-ConfigureFirewall"
|
||||
}
|
||||
@@ -601,7 +633,7 @@ try {
|
||||
# Third: If we have admin rights, install service and configure firewall
|
||||
if ($isAdmin -or $Elevated) {
|
||||
# Install the service
|
||||
Install-NSSMService -AgentPath $AgentPath -Key $Key -Token $Token -HubUrl $Url -Port $Port -NSSMPath $NSSMPath
|
||||
Install-NSSMService -AgentPath $AgentPath -Key $Key -Token $Token -HubUrl $Url -Port $Port -NSSMPath $NSSMPath -TokenProvided:$TokenProvided -UrlProvided:$UrlProvided -PortProvided:$PortProvided
|
||||
|
||||
if ($ConfigureFirewall) {
|
||||
Configure-Firewall -Port $Port
|
||||
|
||||
@@ -97,6 +97,37 @@ ensure_trailing_slash() {
|
||||
fi
|
||||
}
|
||||
|
||||
# Read the listen address from the active service configuration. Existing
|
||||
# service files are kept as they are, so the configured address can differ from
|
||||
# $PORT, which falls back to the default when -p is not passed. LISTEN is
|
||||
# checked before PORT to match the agent's own precedence, and the value is read
|
||||
# as text so host:port and unix socket paths survive.
|
||||
configured_address() {
|
||||
if is_alpine || is_openwrt; then
|
||||
address_file=/etc/init.d/beszel-agent
|
||||
elif is_freebsd; then
|
||||
address_file="$AGENT_DIR/env"
|
||||
else
|
||||
address_file=/etc/systemd/system/beszel-agent.service
|
||||
fi
|
||||
|
||||
[ -f "$address_file" ] || return 0
|
||||
|
||||
address_value=$(sed -n 's/.*LISTEN="\{0,1\}\([^"]*\)"\{0,1\}.*/\1/p' "$address_file" | head -n 1)
|
||||
if [ -z "$address_value" ]; then
|
||||
address_value=$(sed -n 's/.*PORT="\{0,1\}\([^"]*\)"\{0,1\}.*/\1/p' "$address_file" | head -n 1)
|
||||
fi
|
||||
|
||||
printf '%s\n' "$address_value"
|
||||
}
|
||||
|
||||
# Escape text for use in the replacement portion of a sed s command whose
|
||||
# delimiter is |. This only escapes sed replacement metacharacters; quoting
|
||||
# for the destination configuration syntax is handled separately.
|
||||
escape_sed_replacement() {
|
||||
printf '%s' "$1" | sed 's/[\\&|]/\\&/g'
|
||||
}
|
||||
|
||||
# Generate FreeBSD rc service content
|
||||
generate_freebsd_rc_service() {
|
||||
cat <<'EOF'
|
||||
@@ -264,6 +295,12 @@ KEY=""
|
||||
TOKEN=""
|
||||
HUB_URL=""
|
||||
AUTO_UPDATE_FLAG="" # empty string means prompt, "true" means auto-enable, "false" means skip
|
||||
# Track which of the reconfigurable values were explicitly passed as arguments,
|
||||
# so a reinstall only overwrites the fields the caller actually asked to change.
|
||||
KEY_PROVIDED=false
|
||||
PORT_PROVIDED=false
|
||||
TOKEN_PROVIDED=false
|
||||
HUB_URL_PROVIDED=false
|
||||
VERSION="latest"
|
||||
|
||||
# Check for help flag
|
||||
@@ -294,10 +331,10 @@ build_sudo_args() {
|
||||
if [ -n "$QUOTED_ARGS" ]; then
|
||||
QUOTED_ARGS="$QUOTED_ARGS "
|
||||
fi
|
||||
QUOTED_ARGS="$QUOTED_ARGS'$(echo "$1" | sed "s/'/'\\\\''/g")'"
|
||||
QUOTED_ARGS="$QUOTED_ARGS'$(printf '%s' "$1" | sed "s/'/'\\\\''/g")'"
|
||||
shift
|
||||
done
|
||||
echo "$QUOTED_ARGS"
|
||||
printf '%s\n' "$QUOTED_ARGS"
|
||||
}
|
||||
|
||||
# Check if running as root and re-execute with sudo if needed
|
||||
@@ -319,18 +356,22 @@ while [ $# -gt 0 ]; do
|
||||
-k)
|
||||
shift
|
||||
KEY="$1"
|
||||
KEY_PROVIDED=true
|
||||
;;
|
||||
-p)
|
||||
shift
|
||||
PORT="$1"
|
||||
PORT_PROVIDED=true
|
||||
;;
|
||||
-t)
|
||||
shift
|
||||
TOKEN="$1"
|
||||
TOKEN_PROVIDED=true
|
||||
;;
|
||||
-url)
|
||||
shift
|
||||
HUB_URL="$1"
|
||||
HUB_URL_PROVIDED=true
|
||||
;;
|
||||
-v | --version)
|
||||
shift
|
||||
@@ -566,7 +607,7 @@ if [ -z "$KEY" ]; then
|
||||
fi
|
||||
|
||||
# Remove newlines from KEY
|
||||
KEY=$(echo "$KEY" | tr -d '\n')
|
||||
KEY=$(printf '%s' "$KEY" | tr -d '\n')
|
||||
|
||||
# TOKEN and HUB_URL are optional for backwards compatibility - no interactive prompts
|
||||
# They will be set as empty environment variables if not provided
|
||||
@@ -804,7 +845,15 @@ EOF
|
||||
chmod +x /etc/init.d/beszel-agent
|
||||
rc-update add beszel-agent default
|
||||
else
|
||||
echo "Alpine OpenRC service file already exists. Skipping creation."
|
||||
echo "Alpine OpenRC service file already exists. Updating environment variables..."
|
||||
SED_PORT=$(escape_sed_replacement "$PORT")
|
||||
SED_KEY=$(escape_sed_replacement "$KEY")
|
||||
SED_TOKEN=$(escape_sed_replacement "$TOKEN")
|
||||
SED_HUB_URL=$(escape_sed_replacement "$HUB_URL")
|
||||
[ "$PORT_PROVIDED" = "true" ] && sed -i "s|^export PORT=.*|export PORT=\"$SED_PORT\"|" /etc/init.d/beszel-agent
|
||||
[ "$KEY_PROVIDED" = "true" ] && sed -i "s|^export KEY=.*|export KEY=\"$SED_KEY\"|" /etc/init.d/beszel-agent
|
||||
[ "$TOKEN_PROVIDED" = "true" ] && sed -i "s|^export TOKEN=.*|export TOKEN=\"$SED_TOKEN\"|" /etc/init.d/beszel-agent
|
||||
[ "$HUB_URL_PROVIDED" = "true" ] && sed -i "s|^export HUB_URL=.*|export HUB_URL=\"$SED_HUB_URL\"|" /etc/init.d/beszel-agent
|
||||
fi
|
||||
|
||||
# Create log files with proper permissions
|
||||
@@ -886,7 +935,24 @@ EOF
|
||||
chmod +x /etc/init.d/beszel-agent
|
||||
/etc/init.d/beszel-agent enable
|
||||
else
|
||||
echo "OpenWRT init script already exists. Skipping creation."
|
||||
echo "OpenWRT init script already exists. Updating environment variables..."
|
||||
# The env vars live on a single procd_set_param line, so merge any values
|
||||
# that weren't explicitly provided in from the existing line before rewriting it.
|
||||
CUR_ENV_LINE=$(sed -n '/^[[:space:]]*procd_set_param env PORT=/{p;q;}' /etc/init.d/beszel-agent)
|
||||
if [ -z "$CUR_ENV_LINE" ] || ! printf '%s\n' "$CUR_ENV_LINE" | grep -q 'PORT="[^"]*" KEY="[^"]*" TOKEN="[^"]*" HUB_URL="[^"]*"'; then
|
||||
echo "Error: Could not parse the existing environment configuration in /etc/init.d/beszel-agent."
|
||||
echo "Expected a procd_set_param env line containing PORT, KEY, TOKEN, and HUB_URL."
|
||||
exit 1
|
||||
fi
|
||||
[ "$PORT_PROVIDED" = "true" ] || PORT=$(printf '%s\n' "$CUR_ENV_LINE" | sed -n 's/.*PORT="\([^"]*\)".*/\1/p')
|
||||
[ "$KEY_PROVIDED" = "true" ] || KEY=$(printf '%s\n' "$CUR_ENV_LINE" | sed -n 's/.*KEY="\([^"]*\)".*/\1/p')
|
||||
[ "$TOKEN_PROVIDED" = "true" ] || TOKEN=$(printf '%s\n' "$CUR_ENV_LINE" | sed -n 's/.*TOKEN="\([^"]*\)".*/\1/p')
|
||||
[ "$HUB_URL_PROVIDED" = "true" ] || HUB_URL=$(printf '%s\n' "$CUR_ENV_LINE" | sed -n 's/.*HUB_URL="\([^"]*\)".*/\1/p')
|
||||
SED_PORT=$(escape_sed_replacement "$PORT")
|
||||
SED_KEY=$(escape_sed_replacement "$KEY")
|
||||
SED_TOKEN=$(escape_sed_replacement "$TOKEN")
|
||||
SED_HUB_URL=$(escape_sed_replacement "$HUB_URL")
|
||||
sed -i "s|procd_set_param env PORT=.*|procd_set_param env PORT=\"$SED_PORT\" KEY=\"$SED_KEY\" TOKEN=\"$SED_TOKEN\" HUB_URL=\"$SED_HUB_URL\"|" /etc/init.d/beszel-agent
|
||||
fi
|
||||
|
||||
# Start the service
|
||||
@@ -929,17 +995,25 @@ elif is_freebsd; then
|
||||
# Ensure rc.d directory exists on minimal FreeBSD installs
|
||||
mkdir -p /usr/local/etc/rc.d
|
||||
|
||||
# Create environment configuration file with proper permissions if it doesn't exist
|
||||
if [ ! -f "$AGENT_DIR/env" ]; then
|
||||
echo "Creating environment configuration file..."
|
||||
# Create or update environment configuration file
|
||||
if [ -f "$AGENT_DIR/env" ]; then
|
||||
echo "Environment configuration file already exists. Updating environment variables..."
|
||||
SED_PORT=$(escape_sed_replacement "$PORT")
|
||||
SED_KEY=$(escape_sed_replacement "$KEY")
|
||||
SED_TOKEN=$(escape_sed_replacement "$TOKEN")
|
||||
SED_HUB_URL=$(escape_sed_replacement "$HUB_URL")
|
||||
[ "$PORT_PROVIDED" = "true" ] && sed -i '' -e "s|^LISTEN=.*|LISTEN=$SED_PORT|" "$AGENT_DIR/env"
|
||||
[ "$KEY_PROVIDED" = "true" ] && sed -i '' -e "s|^KEY=.*|KEY=\"$SED_KEY\"|" "$AGENT_DIR/env"
|
||||
[ "$TOKEN_PROVIDED" = "true" ] && sed -i '' -e "s|^TOKEN=.*|TOKEN=$SED_TOKEN|" "$AGENT_DIR/env"
|
||||
[ "$HUB_URL_PROVIDED" = "true" ] && sed -i '' -e "s|^HUB_URL=.*|HUB_URL=$SED_HUB_URL|" "$AGENT_DIR/env"
|
||||
else
|
||||
echo "Writing environment configuration file..."
|
||||
cat >"$AGENT_DIR/env" <<EOF
|
||||
LISTEN=$PORT
|
||||
KEY="$KEY"
|
||||
TOKEN=$TOKEN
|
||||
HUB_URL=$HUB_URL
|
||||
EOF
|
||||
else
|
||||
echo "FreeBSD environment file already exists. Skipping creation."
|
||||
fi
|
||||
chmod 640 "$AGENT_DIR/env"
|
||||
chown "root:${AGENT_USER}" "$AGENT_DIR/env"
|
||||
@@ -1074,7 +1148,15 @@ $(if [ -n "$NVIDIA_DEVICES" ]; then printf "%b" "# NVIDIA device permissions\n${
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
else
|
||||
echo "Systemd service file already exists. Skipping creation."
|
||||
echo "Systemd service file already exists. Updating environment variables..."
|
||||
SED_PORT=$(escape_sed_replacement "$PORT")
|
||||
SED_KEY=$(escape_sed_replacement "$KEY")
|
||||
SED_TOKEN=$(escape_sed_replacement "$TOKEN")
|
||||
SED_HUB_URL=$(escape_sed_replacement "$HUB_URL")
|
||||
[ "$PORT_PROVIDED" = "true" ] && sed -i "s|^Environment=\"PORT=.*\"|Environment=\"PORT=$SED_PORT\"|" /etc/systemd/system/beszel-agent.service
|
||||
[ "$KEY_PROVIDED" = "true" ] && sed -i "s|^Environment=\"KEY=.*\"|Environment=\"KEY=$SED_KEY\"|" /etc/systemd/system/beszel-agent.service
|
||||
[ "$TOKEN_PROVIDED" = "true" ] && sed -i "s|^Environment=\"TOKEN=.*\"|Environment=\"TOKEN=$SED_TOKEN\"|" /etc/systemd/system/beszel-agent.service
|
||||
[ "$HUB_URL_PROVIDED" = "true" ] && sed -i "s|^Environment=\"HUB_URL=.*\"|Environment=\"HUB_URL=$SED_HUB_URL\"|" /etc/systemd/system/beszel-agent.service
|
||||
fi
|
||||
|
||||
# Load and start the service
|
||||
@@ -1140,4 +1222,7 @@ EOF
|
||||
fi
|
||||
fi
|
||||
|
||||
printf "\n\033[32mBeszel Agent has been installed successfully! It is now running on $PORT.\033[0m\n"
|
||||
RUNNING_ADDRESS=$(configured_address)
|
||||
[ -n "$RUNNING_ADDRESS" ] || RUNNING_ADDRESS=$PORT
|
||||
|
||||
printf "\n\033[32mBeszel Agent has been installed successfully! It is now running on $RUNNING_ADDRESS.\033[0m\n"
|
||||
|
||||
Reference in New Issue
Block a user