mirror of
https://github.com/henrygd/beszel.git
synced 2026-09-21 17:07:47 +02:00
Compare commits
12 Commits
v0.19.0
...
997adc19bb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
997adc19bb | ||
|
|
08d813620c | ||
|
|
6cb302fcf6 | ||
|
|
59eed073c3 | ||
|
|
266a74bab8 | ||
|
|
ad24484caa | ||
|
|
027d0c204d | ||
|
|
46d94a9804 | ||
|
|
82fc772882 | ||
|
|
c157c2026d | ||
|
|
e1d9ebc61d | ||
|
|
5af6b6b184 |
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
## Reporting a Vulnerability
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
If you find a vulnerability in the latest version, please [submit a private advisory](https://github.com/henrygd/beszel/security/advisories/new).
|
**PLEASE ONLY USE SECURITY ADVISORIES FOR REAL HIGH SEVERITY VULNERABILITIES.**
|
||||||
|
|
||||||
If it's low severity (use best judgement) you may open an issue instead of an advisory.
|
If you find a vulnerability in the latest version, and it is not high severity, open an issue instead of an advisory.
|
||||||
|
|
||||||
|
I am overwhelmed with advisories, often erroneous, which are clearly found and written by AI. I don't have the capacity to review all of them.
|
||||||
|
|||||||
@@ -25,7 +25,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
wsDeadline = 70 * time.Second
|
// Keep the connection alive long enough for a slow collection cycle to
|
||||||
|
// finish before the hub considers the agent disconnected.
|
||||||
|
wsDeadline = 120 * time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
type caCertFileError struct {
|
type caCertFileError struct {
|
||||||
|
|||||||
@@ -700,3 +700,11 @@ func TestGetToken(t *testing.T) {
|
|||||||
assert.Equal(t, expectedToken, token, "Whitespace should be stripped from token file content")
|
assert.Equal(t, expectedToken, token, "Whitespace should be stripped from token file content")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestWebSocketDeadlineCoversSlowCollection(t *testing.T) {
|
||||||
|
const minimumDeadline = 120 * time.Second
|
||||||
|
|
||||||
|
if wsDeadline < minimumDeadline {
|
||||||
|
t.Fatalf("WebSocket deadline %s is shorter than the slow-collection window of %s", wsDeadline, minimumDeadline)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ RUN go mod download
|
|||||||
# Copy source files
|
# Copy source files
|
||||||
COPY . ./
|
COPY . ./
|
||||||
|
|
||||||
|
RUN apk add --no-cache ca-certificates && update-ca-certificates
|
||||||
|
|
||||||
# Build
|
# Build
|
||||||
ARG TARGETOS TARGETARCH
|
ARG TARGETOS TARGETARCH
|
||||||
RUN CGO_ENABLED=0 GOGC=75 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags "-w -s" -o /agent ./internal/cmd/agent
|
RUN CGO_ENABLED=0 GOGC=75 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags "-w -s" -o /agent ./internal/cmd/agent
|
||||||
@@ -19,6 +21,7 @@ RUN rm -rf /tmp/*
|
|||||||
# --------------------------
|
# --------------------------
|
||||||
FROM scratch
|
FROM scratch
|
||||||
COPY --from=builder /agent /agent
|
COPY --from=builder /agent /agent
|
||||||
|
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||||
|
|
||||||
# this is so we don't need to create the /tmp directory in the scratch container
|
# this is so we don't need to create the /tmp directory in the scratch container
|
||||||
COPY --from=builder /tmp /tmp
|
COPY --from=builder /tmp /tmp
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ FROM alpine:3.23
|
|||||||
|
|
||||||
COPY --from=builder /agent /agent
|
COPY --from=builder /agent /agent
|
||||||
|
|
||||||
RUN apk add --no-cache -X https://dl-cdn.alpinelinux.org/alpine/edge/testing igt-gpu-tools nvtop smartmontools
|
RUN apk add --no-cache -X https://dl-cdn.alpinelinux.org/alpine/edge/testing igt-gpu-tools nvtop smartmontools zfs
|
||||||
|
|
||||||
# Ensure data persistence across container recreations
|
# Ensure data persistence across container recreations
|
||||||
VOLUME ["/var/lib/beszel-agent"]
|
VOLUME ["/var/lib/beszel-agent"]
|
||||||
|
|||||||
@@ -43,6 +43,11 @@ COPY --from=builder /app/agent/test-data/amdgpu.ids /usr/share/libdrm/amdgpu.ids
|
|||||||
# Copy smartmontools binaries and config files
|
# Copy smartmontools binaries and config files
|
||||||
COPY --from=smartmontools-builder /usr/sbin/smartctl /usr/sbin/smartctl
|
COPY --from=smartmontools-builder /usr/sbin/smartctl /usr/sbin/smartctl
|
||||||
|
|
||||||
|
# Install ZFS userspace utilities (zpool, zfs) for pool/dataset monitoring
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
zfsutils-linux \
|
||||||
|
&& apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Ensure data persistence across container recreations
|
# Ensure data persistence across container recreations
|
||||||
VOLUME ["/var/lib/beszel-agent"]
|
VOLUME ["/var/lib/beszel-agent"]
|
||||||
|
|
||||||
|
|||||||
@@ -65,6 +65,32 @@ RUN set -eux; \
|
|||||||
cp -v "$interp" "/out/rootfs$interp"; \
|
cp -v "$interp" "/out/rootfs$interp"; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# --------------------------
|
||||||
|
# ZFS utilities builder stage
|
||||||
|
# --------------------------
|
||||||
|
FROM --platform=$TARGETPLATFORM debian:bookworm-slim AS zfsutils-builder
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
zfsutils-linux \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Copy the zpool/zfs binaries and their required runtime libraries
|
||||||
|
RUN set -eux; \
|
||||||
|
mkdir -p /out/rootfs/lib /out/rootfs/lib64 /out/rootfs/usr/lib; \
|
||||||
|
for bin in /usr/sbin/zpool /usr/sbin/zfs; do \
|
||||||
|
mkdir -p "/out/rootfs$(dirname "$bin")"; \
|
||||||
|
cp -v "$bin" "/out/rootfs$bin"; \
|
||||||
|
ldd "$bin" \
|
||||||
|
| awk '{print $3}' \
|
||||||
|
| grep '^/' \
|
||||||
|
| xargs -r -I '{}' sh -c 'mkdir -p "/out/rootfs$(dirname "{}")"; cp -v "{}" "/out/rootfs{}"'; \
|
||||||
|
interp="$(ldd "$bin" | awk "/ld-linux/ {print \$1}")"; \
|
||||||
|
if [ -n "$interp" ] && [ -e "$interp" ]; then \
|
||||||
|
mkdir -p "/out/rootfs$(dirname "$interp")"; \
|
||||||
|
cp -v "$interp" "/out/rootfs$interp"; \
|
||||||
|
fi; \
|
||||||
|
done
|
||||||
|
|
||||||
# --------------------------
|
# --------------------------
|
||||||
# Final image: lightweight multi-arch NVIDIA agent (slim)
|
# Final image: lightweight multi-arch NVIDIA agent (slim)
|
||||||
# --------------------------
|
# --------------------------
|
||||||
@@ -78,6 +104,9 @@ COPY --from=builder /app/agent/test-data/amdgpu.ids /usr/share/libdrm/amdgpu.ids
|
|||||||
COPY --from=smartmontools-builder /usr/sbin/smartctl /usr/sbin/smartctl
|
COPY --from=smartmontools-builder /usr/sbin/smartctl /usr/sbin/smartctl
|
||||||
COPY --from=smartmontools-builder /out/rootfs/ /
|
COPY --from=smartmontools-builder /out/rootfs/ /
|
||||||
|
|
||||||
|
# Copy ZFS utilities (zpool, zfs) binaries and required runtime libraries
|
||||||
|
COPY --from=zfsutils-builder /out/rootfs/ /
|
||||||
|
|
||||||
# nvidia-smi is intentionally not bundled.
|
# nvidia-smi is intentionally not bundled.
|
||||||
# Mount the host binary instead, for example:
|
# Mount the host binary instead, for example:
|
||||||
# - /usr/bin/nvidia-smi:/usr/bin/nvidia-smi:ro
|
# - /usr/bin/nvidia-smi:/usr/bin/nvidia-smi:ro
|
||||||
|
|||||||
@@ -272,7 +272,15 @@ 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)
|
// Distinguish an idle GPU from a system without GPU data (#2312)
|
||||||
|
info := struct {
|
||||||
|
system.Info
|
||||||
|
GpuPct *float64 `json:"g,omitempty"`
|
||||||
|
}{Info: data.Info}
|
||||||
|
if len(data.Stats.GPUData) > 0 {
|
||||||
|
info.GpuPct = &data.Info.GpuPct
|
||||||
|
}
|
||||||
|
systemRecord.Set("info", info)
|
||||||
if err := txApp.SaveNoValidate(systemRecord); err != nil {
|
if err := txApp.SaveNoValidate(systemRecord); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
44
internal/hub/systems/system_gpu_test.go
Normal file
44
internal/hub/systems/system_gpu_test.go
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
//go:build testing
|
||||||
|
|
||||||
|
package systems
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/henrygd/beszel/internal/entities/system"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCreateRecordsGPUUtilization(t *testing.T) {
|
||||||
|
sys, app := newTestSystemWithHub(t)
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
gpu bool
|
||||||
|
usage float64
|
||||||
|
}{
|
||||||
|
{"no GPU", false, 0},
|
||||||
|
{"active GPU", true, 42.5},
|
||||||
|
{"idle GPU", true, 0},
|
||||||
|
{"GPU removed", false, 0},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
data := &system.CombinedData{Info: system.Info{GpuPct: tc.usage, Cpu: 12.5}}
|
||||||
|
if tc.gpu {
|
||||||
|
data.Stats.GPUData = map[string]system.GPUData{"0": {Name: "GPU", Usage: tc.usage}}
|
||||||
|
}
|
||||||
|
_, err := sys.createRecords(data)
|
||||||
|
require.NoError(t, err)
|
||||||
|
record, err := app.FindRecordById("systems", sys.Id)
|
||||||
|
require.NoError(t, err)
|
||||||
|
var info map[string]any
|
||||||
|
require.NoError(t, record.UnmarshalJSONField("info", &info))
|
||||||
|
assert.Equal(t, 12.5, info["cpu"])
|
||||||
|
if tc.gpu {
|
||||||
|
assert.Equal(t, tc.usage, info["g"])
|
||||||
|
} else {
|
||||||
|
assert.NotContains(t, info, "g")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/henrygd/beszel/internal/hub/ws"
|
"github.com/henrygd/beszel/internal/hub/ws"
|
||||||
@@ -47,6 +48,10 @@ type SystemManager struct {
|
|||||||
sshConfig *ssh.ClientConfig // SSH client configuration for system connections
|
sshConfig *ssh.ClientConfig // SSH client configuration for system connections
|
||||||
smartFetchMap *expirymap.ExpiryMap[smartFetchState] // Stores last SMART fetch time/result; TTL is only for cleanup
|
smartFetchMap *expirymap.ExpiryMap[smartFetchState] // Stores last SMART fetch time/result; TTL is only for cleanup
|
||||||
zfsFetchMap *expirymap.ExpiryMap[zfsFetchState] // Stores last ZFS fetch time/result; TTL is only for cleanup
|
zfsFetchMap *expirymap.ExpiryMap[zfsFetchState] // Stores last ZFS fetch time/result; TTL is only for cleanup
|
||||||
|
realtimeMutex sync.Mutex // Protects all realtime worker and subscription state
|
||||||
|
activeSubscriptions map[string]*subscriptionInfo // Realtime subscriptions keyed by system ID
|
||||||
|
realtimeWorkerStop chan struct{} // Stops the current realtime worker generation
|
||||||
|
realtimeWorkerRun bool // Whether a realtime worker has been started
|
||||||
ctx context.Context // Cancelled when the app terminates
|
ctx context.Context // Cancelled when the app terminates
|
||||||
cancel context.CancelFunc // Cancels ctx and all child system contexts
|
cancel context.CancelFunc // Cancels ctx and all child system contexts
|
||||||
}
|
}
|
||||||
@@ -71,6 +76,7 @@ func NewSystemManager(hub hubLike) *SystemManager {
|
|||||||
hub: hub,
|
hub: hub,
|
||||||
smartFetchMap: expirymap.New[smartFetchState](time.Hour),
|
smartFetchMap: expirymap.New[smartFetchState](time.Hour),
|
||||||
zfsFetchMap: expirymap.New[zfsFetchState](time.Hour),
|
zfsFetchMap: expirymap.New[zfsFetchState](time.Hour),
|
||||||
|
activeSubscriptions: make(map[string]*subscriptionInfo),
|
||||||
}
|
}
|
||||||
sm.ctx, sm.cancel = context.WithCancel(context.Background())
|
sm.ctx, sm.cancel = context.WithCancel(context.Background())
|
||||||
return sm
|
return sm
|
||||||
@@ -138,6 +144,7 @@ func (sm *SystemManager) bindEventHooks() {
|
|||||||
// onTerminate cancels SystemManager context on app shutdown
|
// onTerminate cancels SystemManager context on app shutdown
|
||||||
func (sm *SystemManager) onTerminate(e *core.TerminateEvent) error {
|
func (sm *SystemManager) onTerminate(e *core.TerminateEvent) error {
|
||||||
sm.cancel()
|
sm.cancel()
|
||||||
|
sm.stopRealtimeWorker()
|
||||||
return e.Next()
|
return e.Next()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,25 +3,27 @@ package systems
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/henrygd/beszel/internal/common"
|
"github.com/henrygd/beszel/internal/common"
|
||||||
|
"github.com/henrygd/beszel/internal/hub/utils"
|
||||||
|
"github.com/pocketbase/dbx"
|
||||||
|
"github.com/pocketbase/pocketbase/apis"
|
||||||
"github.com/pocketbase/pocketbase/core"
|
"github.com/pocketbase/pocketbase/core"
|
||||||
"github.com/pocketbase/pocketbase/tools/subscriptions"
|
"github.com/pocketbase/pocketbase/tools/subscriptions"
|
||||||
)
|
)
|
||||||
|
|
||||||
type subscriptionInfo struct {
|
type subscriptionInfo struct {
|
||||||
subscription string
|
subscription string
|
||||||
connectedClients uint8
|
connectedClients int
|
||||||
|
fetching bool
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
type realtimeFetch struct {
|
||||||
activeSubscriptions = make(map[string]*subscriptionInfo)
|
systemID string
|
||||||
workerRunning bool
|
subscription string
|
||||||
tickerStopChan chan struct{}
|
info *subscriptionInfo
|
||||||
realtimeMutex sync.Mutex
|
}
|
||||||
)
|
|
||||||
|
|
||||||
// onRealtimeConnectRequest handles client connection events for realtime subscriptions.
|
// onRealtimeConnectRequest handles client connection events for realtime subscriptions.
|
||||||
// It cleans up existing subscriptions when a client connects.
|
// It cleans up existing subscriptions when a client connects.
|
||||||
@@ -38,6 +40,19 @@ func (sm *SystemManager) onRealtimeConnectRequest(e *core.RealtimeConnectRequest
|
|||||||
// onRealtimeSubscribeRequest handles client subscription events for realtime metrics.
|
// onRealtimeSubscribeRequest handles client subscription events for realtime metrics.
|
||||||
// It tracks new subscriptions and unsubscriptions to manage the realtime worker lifecycle.
|
// It tracks new subscriptions and unsubscriptions to manage the realtime worker lifecycle.
|
||||||
func (sm *SystemManager) onRealtimeSubscribeRequest(e *core.RealtimeSubscribeRequestEvent) error {
|
func (sm *SystemManager) onRealtimeSubscribeRequest(e *core.RealtimeSubscribeRequestEvent) error {
|
||||||
|
// Parse with PocketBase's own subscription parser before changing the real
|
||||||
|
// client. Reject the entire request if any metrics target is inaccessible.
|
||||||
|
requested := subscriptions.NewDefaultClient()
|
||||||
|
requested.Subscribe(e.Subscriptions...)
|
||||||
|
for topic, options := range requested.Subscriptions() {
|
||||||
|
if !strings.HasPrefix(topic, "rt_metrics") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
system, err := sm.GetSystem(options.Query["system"])
|
||||||
|
if err != nil || !system.HasUser(e.App, e.Auth) {
|
||||||
|
return e.NotFoundError("", nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
oldSubs := e.Client.Subscriptions()
|
oldSubs := e.Client.Subscriptions()
|
||||||
// after e.Next() is the result of the subscribe request
|
// after e.Next() is the result of the subscribe request
|
||||||
err := e.Next()
|
err := e.Next()
|
||||||
@@ -47,14 +62,7 @@ func (sm *SystemManager) onRealtimeSubscribeRequest(e *core.RealtimeSubscribeReq
|
|||||||
for k, options := range newSubs {
|
for k, options := range newSubs {
|
||||||
if _, ok := oldSubs[k]; !ok {
|
if _, ok := oldSubs[k]; !ok {
|
||||||
if strings.HasPrefix(k, "rt_metrics") {
|
if strings.HasPrefix(k, "rt_metrics") {
|
||||||
systemId := options.Query["system"]
|
sm.addRealtimeSubscription(options.Query["system"], k)
|
||||||
if _, ok := activeSubscriptions[systemId]; !ok {
|
|
||||||
activeSubscriptions[systemId] = &subscriptionInfo{
|
|
||||||
subscription: k,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
activeSubscriptions[systemId].connectedClients += 1
|
|
||||||
sm.onRealtimeSubscriptionAdded()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -68,72 +76,76 @@ func (sm *SystemManager) onRealtimeSubscribeRequest(e *core.RealtimeSubscribeReq
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// onRealtimeSubscriptionAdded initializes or starts the realtime worker when the first subscription is added.
|
// addRealtimeSubscription tracks a subscriber and starts a worker if necessary.
|
||||||
// It ensures only one worker runs at a time.
|
func (sm *SystemManager) addRealtimeSubscription(systemID, subscription string) {
|
||||||
func (sm *SystemManager) onRealtimeSubscriptionAdded() {
|
sm.realtimeMutex.Lock()
|
||||||
realtimeMutex.Lock()
|
defer sm.realtimeMutex.Unlock()
|
||||||
defer realtimeMutex.Unlock()
|
|
||||||
|
|
||||||
// Start the worker if it's not already running
|
if sm.activeSubscriptions == nil {
|
||||||
if !workerRunning {
|
sm.activeSubscriptions = make(map[string]*subscriptionInfo)
|
||||||
workerRunning = true
|
}
|
||||||
// Create a new stop channel for this worker instance
|
info, ok := sm.activeSubscriptions[systemID]
|
||||||
tickerStopChan = make(chan struct{})
|
if !ok {
|
||||||
go sm.startRealtimeWorker()
|
info = &subscriptionInfo{subscription: subscription}
|
||||||
|
sm.activeSubscriptions[systemID] = info
|
||||||
|
}
|
||||||
|
info.connectedClients++
|
||||||
|
|
||||||
|
if !sm.realtimeWorkerRun {
|
||||||
|
sm.realtimeWorkerRun = true
|
||||||
|
stop := make(chan struct{})
|
||||||
|
sm.realtimeWorkerStop = stop
|
||||||
|
go sm.startRealtimeWorker(stop)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkSubscriptions stops the realtime worker when there are no active subscriptions.
|
// stopRealtimeWorker stops the current worker generation, if any.
|
||||||
// This prevents unnecessary resource usage when no clients are listening for realtime data.
|
func (sm *SystemManager) stopRealtimeWorker() {
|
||||||
func (sm *SystemManager) checkSubscriptions() {
|
sm.realtimeMutex.Lock()
|
||||||
if !workerRunning || len(activeSubscriptions) > 0 {
|
defer sm.realtimeMutex.Unlock()
|
||||||
|
sm.stopRealtimeWorkerLocked()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sm *SystemManager) stopRealtimeWorkerLocked() {
|
||||||
|
if !sm.realtimeWorkerRun {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
close(sm.realtimeWorkerStop)
|
||||||
realtimeMutex.Lock()
|
sm.realtimeWorkerStop = nil
|
||||||
defer realtimeMutex.Unlock()
|
sm.realtimeWorkerRun = false
|
||||||
|
|
||||||
// Signal the worker to stop
|
|
||||||
if tickerStopChan != nil {
|
|
||||||
select {
|
|
||||||
case tickerStopChan <- struct{}{}:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mark worker as stopped (will be reset when next subscription comes in)
|
|
||||||
workerRunning = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// removeRealtimeSubscription removes a realtime subscription and checks if the worker should be stopped.
|
// removeRealtimeSubscription removes a realtime subscription and checks if the worker should be stopped.
|
||||||
// It only processes subscriptions with the "rt_metrics" prefix and triggers cleanup when subscriptions are removed.
|
// It only processes subscriptions with the "rt_metrics" prefix and triggers cleanup when subscriptions are removed.
|
||||||
func (sm *SystemManager) removeRealtimeSubscription(subscription string, options subscriptions.SubscriptionOptions) {
|
func (sm *SystemManager) removeRealtimeSubscription(subscription string, options subscriptions.SubscriptionOptions) {
|
||||||
if strings.HasPrefix(subscription, "rt_metrics") {
|
if strings.HasPrefix(subscription, "rt_metrics") {
|
||||||
systemId := options.Query["system"]
|
systemID := options.Query["system"]
|
||||||
if info, ok := activeSubscriptions[systemId]; ok {
|
sm.realtimeMutex.Lock()
|
||||||
info.connectedClients -= 1
|
if info, ok := sm.activeSubscriptions[systemID]; ok {
|
||||||
|
info.connectedClients--
|
||||||
if info.connectedClients <= 0 {
|
if info.connectedClients <= 0 {
|
||||||
delete(activeSubscriptions, systemId)
|
delete(sm.activeSubscriptions, systemID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sm.checkSubscriptions()
|
if len(sm.activeSubscriptions) == 0 {
|
||||||
|
sm.stopRealtimeWorkerLocked()
|
||||||
|
}
|
||||||
|
sm.realtimeMutex.Unlock()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// startRealtimeWorker runs the main loop for fetching realtime data from agents.
|
// startRealtimeWorker runs the main loop for fetching realtime data from agents.
|
||||||
// It continuously fetches system data and broadcasts it to subscribed clients via WebSocket.
|
// It continuously fetches system data and broadcasts it to subscribed clients via WebSocket.
|
||||||
func (sm *SystemManager) startRealtimeWorker() {
|
func (sm *SystemManager) startRealtimeWorker(stop <-chan struct{}) {
|
||||||
sm.fetchRealtimeDataAndNotify()
|
sm.fetchRealtimeDataAndNotify()
|
||||||
tick := time.Tick(1 * time.Second)
|
ticker := time.NewTicker(time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-tickerStopChan:
|
case <-stop:
|
||||||
return
|
return
|
||||||
case <-tick:
|
case <-ticker.C:
|
||||||
if len(activeSubscriptions) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
sm.fetchRealtimeDataAndNotify()
|
sm.fetchRealtimeDataAndNotify()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -141,27 +153,79 @@ func (sm *SystemManager) startRealtimeWorker() {
|
|||||||
|
|
||||||
// fetchRealtimeDataAndNotify fetches realtime data for all active subscriptions and notifies the clients.
|
// fetchRealtimeDataAndNotify fetches realtime data for all active subscriptions and notifies the clients.
|
||||||
func (sm *SystemManager) fetchRealtimeDataAndNotify() {
|
func (sm *SystemManager) fetchRealtimeDataAndNotify() {
|
||||||
for systemId, info := range activeSubscriptions {
|
for _, fetch := range sm.claimRealtimeFetches() {
|
||||||
system, err := sm.GetSystem(systemId)
|
system, err := sm.GetSystem(fetch.systemID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
sm.finishRealtimeFetch(fetch)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
go func() {
|
go func(fetch realtimeFetch) {
|
||||||
|
defer sm.finishRealtimeFetch(fetch)
|
||||||
data, err := system.fetchDataFromAgent(common.DataRequestOptions{CacheTimeMs: 1000})
|
data, err := system.fetchDataFromAgent(common.DataRequestOptions{CacheTimeMs: 1000})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
bytes, err := json.Marshal(data)
|
bytes, err := json.Marshal(data)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
notify(sm.hub, info.subscription, bytes)
|
notify(sm.hub, system, fetch.subscription, bytes)
|
||||||
}
|
}
|
||||||
}()
|
}(fetch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// claimRealtimeFetches takes a stable snapshot and marks each selected system as
|
||||||
|
// in flight. Slow agents are skipped on later ticks until their fetch completes.
|
||||||
|
func (sm *SystemManager) claimRealtimeFetches() []realtimeFetch {
|
||||||
|
sm.realtimeMutex.Lock()
|
||||||
|
defer sm.realtimeMutex.Unlock()
|
||||||
|
|
||||||
|
fetches := make([]realtimeFetch, 0, len(sm.activeSubscriptions))
|
||||||
|
for systemID, info := range sm.activeSubscriptions {
|
||||||
|
if info.fetching {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
info.fetching = true
|
||||||
|
fetches = append(fetches, realtimeFetch{
|
||||||
|
systemID: systemID,
|
||||||
|
subscription: info.subscription,
|
||||||
|
info: info,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return fetches
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sm *SystemManager) finishRealtimeFetch(fetch realtimeFetch) {
|
||||||
|
sm.realtimeMutex.Lock()
|
||||||
|
defer sm.realtimeMutex.Unlock()
|
||||||
|
// A subscription may have been removed and recreated while the old request
|
||||||
|
// was running. Only release the exact entry claimed by this request.
|
||||||
|
if info := sm.activeSubscriptions[fetch.systemID]; info == fetch.info {
|
||||||
|
info.fetching = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// notify broadcasts realtime data to all clients subscribed to a specific subscription.
|
// notify broadcasts realtime data to all clients subscribed to a specific subscription.
|
||||||
// It iterates through all connected clients and sends the data only to those with matching subscriptions.
|
// Custom topics bypass collection rules, so check current access for every
|
||||||
func notify(app core.App, subscription string, data []byte) error {
|
// recipient, including clients whose authentication or membership was revoked.
|
||||||
|
func notify(app core.App, system *System, subscription string, data []byte) error {
|
||||||
|
shareAll, _ := utils.GetEnv("SHARE_ALL_SYSTEMS")
|
||||||
|
members := make(map[string]struct{})
|
||||||
|
if shareAll != "true" {
|
||||||
|
// Refresh once per broadcast so membership changes take effect on the
|
||||||
|
// next update without querying the database for every recipient.
|
||||||
|
var recordData struct{ Users string }
|
||||||
|
if err := app.DB().NewQuery("SELECT users FROM systems WHERE id={:id}").
|
||||||
|
Bind(dbx.Params{"id": system.Id}).One(&recordData); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var userIDs []string
|
||||||
|
if err := json.Unmarshal([]byte(recordData.Users), &userIDs); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, id := range userIDs {
|
||||||
|
members[id] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
message := subscriptions.Message{
|
message := subscriptions.Message{
|
||||||
Name: subscription,
|
Name: subscription,
|
||||||
Data: data,
|
Data: data,
|
||||||
@@ -170,6 +234,13 @@ func notify(app core.App, subscription string, data []byte) error {
|
|||||||
if !client.HasSubscription(subscription) {
|
if !client.HasSubscription(subscription) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
auth, _ := client.Get(apis.RealtimeClientAuthKey).(*core.Record)
|
||||||
|
if auth == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, member := members[auth.Id]; shareAll != "true" && !member {
|
||||||
|
continue
|
||||||
|
}
|
||||||
client.Send(message)
|
client.Send(message)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
229
internal/hub/systems/system_realtime_test.go
Normal file
229
internal/hub/systems/system_realtime_test.go
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
package systems
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/pocketbase/pocketbase/apis"
|
||||||
|
"github.com/pocketbase/pocketbase/core"
|
||||||
|
pbtests "github.com/pocketbase/pocketbase/tests"
|
||||||
|
"github.com/pocketbase/pocketbase/tools/hook"
|
||||||
|
"github.com/pocketbase/pocketbase/tools/store"
|
||||||
|
"github.com/pocketbase/pocketbase/tools/subscriptions"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRealtimeAuthorization(t *testing.T) {
|
||||||
|
t.Setenv("SHARE_ALL_SYSTEMS", "false")
|
||||||
|
t.Setenv("BESZEL_HUB_SHARE_ALL_SYSTEMS", "")
|
||||||
|
app, err := pbtests.NewTestApp(t.TempDir())
|
||||||
|
require.NoError(t, err)
|
||||||
|
t.Cleanup(app.Cleanup)
|
||||||
|
_, err = app.DB().NewQuery(`CREATE TABLE IF NOT EXISTS systems (id TEXT PRIMARY KEY, users TEXT)`).Execute()
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, err = app.DB().NewQuery(`INSERT INTO systems (id, users) VALUES ('target', '["member"]')`).Execute()
|
||||||
|
require.NoError(t, err)
|
||||||
|
member := core.NewRecord(core.NewAuthCollection("users"))
|
||||||
|
member.Id = "member"
|
||||||
|
outsider := core.NewRecord(member.Collection())
|
||||||
|
outsider.Id = "outsider"
|
||||||
|
system := &System{Id: "target"}
|
||||||
|
sm := newRealtimeTestManager()
|
||||||
|
sm.systems.Set(system.Id, system)
|
||||||
|
// Keep the lifecycle bookkeeping active without starting an agent worker.
|
||||||
|
sm.realtimeWorkerRun = true
|
||||||
|
sm.realtimeWorkerStop = make(chan struct{})
|
||||||
|
t.Cleanup(sm.stopRealtimeWorker)
|
||||||
|
topic := `rt_metrics?options={"query":{"system":"target"}}`
|
||||||
|
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
auth *core.Record
|
||||||
|
topic string
|
||||||
|
share bool
|
||||||
|
allowed bool
|
||||||
|
}{
|
||||||
|
{"guest", nil, topic, false, false},
|
||||||
|
{"outsider", outsider, topic, false, false},
|
||||||
|
{"member", member, topic, false, true},
|
||||||
|
{"missing system", member, `rt_metrics`, false, false},
|
||||||
|
{"unknown system", member, `rt_metrics?options={"query":{"system":"missing"}}`, false, false},
|
||||||
|
{"malformed options", member, `rt_metrics?options=invalid`, false, false},
|
||||||
|
{"prefix variant", outsider, `rt_metrics_extra?options={"query":{"system":"target"}}`, false, false},
|
||||||
|
{"shared outsider", outsider, topic, true, true},
|
||||||
|
{"shared guest", nil, topic, true, false},
|
||||||
|
{"other topic", nil, "systems/*", false, true},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
if tc.share {
|
||||||
|
t.Setenv("BESZEL_HUB_SHARE_ALL_SYSTEMS", "true")
|
||||||
|
}
|
||||||
|
client := subscriptions.NewDefaultClient()
|
||||||
|
client.Subscribe("existing")
|
||||||
|
e := &core.RealtimeSubscribeRequestEvent{
|
||||||
|
RequestEvent: &core.RequestEvent{App: app, Auth: tc.auth},
|
||||||
|
Client: client, Subscriptions: []string{tc.topic},
|
||||||
|
}
|
||||||
|
called := false
|
||||||
|
h := &hook.Hook[*core.RealtimeSubscribeRequestEvent]{}
|
||||||
|
h.BindFunc(sm.onRealtimeSubscribeRequest)
|
||||||
|
err := h.Trigger(e, func(e *core.RealtimeSubscribeRequestEvent) error {
|
||||||
|
called = true
|
||||||
|
client.Unsubscribe()
|
||||||
|
client.Subscribe(e.Subscriptions...)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if tc.allowed {
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.True(t, called)
|
||||||
|
} else {
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.False(t, called)
|
||||||
|
assert.True(t, client.HasSubscription("existing"))
|
||||||
|
assert.False(t, client.HasSubscription(tc.topic))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("broadcast checks current access", func(t *testing.T) {
|
||||||
|
client := subscriptions.NewDefaultClient()
|
||||||
|
client.Subscribe(topic)
|
||||||
|
app.SubscriptionsBroker().Register(client)
|
||||||
|
defer app.SubscriptionsBroker().Unregister(client.Id())
|
||||||
|
secondClient := subscriptions.NewDefaultClient()
|
||||||
|
secondClient.Subscribe(topic)
|
||||||
|
app.SubscriptionsBroker().Register(secondClient)
|
||||||
|
defer app.SubscriptionsBroker().Unregister(secondClient.Id())
|
||||||
|
check := func(auth *core.Record, allowed bool) {
|
||||||
|
t.Helper()
|
||||||
|
client.Set(apis.RealtimeClientAuthKey, auth)
|
||||||
|
secondClient.Set(apis.RealtimeClientAuthKey, auth)
|
||||||
|
done := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
notify(app, system, topic, []byte(`{"cpu":42}`))
|
||||||
|
close(done)
|
||||||
|
}()
|
||||||
|
// Even on failure, drain pending sends and join the broadcaster before
|
||||||
|
// unregistering clients, which closes their channels.
|
||||||
|
defer func() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-client.Channel():
|
||||||
|
case <-secondClient.Channel():
|
||||||
|
case <-done:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
var received [2]int
|
||||||
|
timer := time.NewTimer(time.Second)
|
||||||
|
defer timer.Stop()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case msg := <-client.Channel():
|
||||||
|
received[0]++
|
||||||
|
assert.Equal(t, topic, msg.Name)
|
||||||
|
case msg := <-secondClient.Channel():
|
||||||
|
received[1]++
|
||||||
|
assert.Equal(t, topic, msg.Name)
|
||||||
|
case <-done:
|
||||||
|
want := [2]int{}
|
||||||
|
if allowed {
|
||||||
|
want = [2]int{1, 1}
|
||||||
|
}
|
||||||
|
assert.Equal(t, want, received)
|
||||||
|
return
|
||||||
|
case <-timer.C:
|
||||||
|
t.Fatal("broadcast did not finish")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
check(nil, false)
|
||||||
|
check(outsider, false)
|
||||||
|
check(member, true)
|
||||||
|
_, err := app.DB().NewQuery(`UPDATE systems SET users = '[]'`).Execute()
|
||||||
|
require.NoError(t, err)
|
||||||
|
check(member, false)
|
||||||
|
t.Setenv("BESZEL_HUB_SHARE_ALL_SYSTEMS", "true")
|
||||||
|
check(outsider, true)
|
||||||
|
check(nil, false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func newRealtimeTestManager() *SystemManager {
|
||||||
|
return &SystemManager{
|
||||||
|
systems: store.New(map[string]*System{}),
|
||||||
|
activeSubscriptions: make(map[string]*subscriptionInfo),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRealtimeFetchesDoNotOverlapPerSystem(t *testing.T) {
|
||||||
|
sm := newRealtimeTestManager()
|
||||||
|
sm.activeSubscriptions["one"] = &subscriptionInfo{subscription: "rt_metrics_one"}
|
||||||
|
sm.activeSubscriptions["two"] = &subscriptionInfo{subscription: "rt_metrics_two"}
|
||||||
|
|
||||||
|
first := sm.claimRealtimeFetches()
|
||||||
|
require.Len(t, first, 2)
|
||||||
|
assert.Empty(t, sm.claimRealtimeFetches())
|
||||||
|
|
||||||
|
sm.finishRealtimeFetch(first[0])
|
||||||
|
next := sm.claimRealtimeFetches()
|
||||||
|
require.Len(t, next, 1)
|
||||||
|
assert.Equal(t, first[0].systemID, next[0].systemID)
|
||||||
|
|
||||||
|
sm.finishRealtimeFetch(first[1])
|
||||||
|
sm.finishRealtimeFetch(next[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFinishingOldRealtimeFetchDoesNotReleaseReplacement(t *testing.T) {
|
||||||
|
sm := newRealtimeTestManager()
|
||||||
|
oldInfo := &subscriptionInfo{subscription: "old"}
|
||||||
|
sm.activeSubscriptions["system"] = oldInfo
|
||||||
|
|
||||||
|
fetch := sm.claimRealtimeFetches()[0]
|
||||||
|
newInfo := &subscriptionInfo{subscription: "new", fetching: true}
|
||||||
|
sm.activeSubscriptions["system"] = newInfo
|
||||||
|
|
||||||
|
sm.finishRealtimeFetch(fetch)
|
||||||
|
assert.True(t, newInfo.fetching)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRealtimeSubscriptionLifecycle(t *testing.T) {
|
||||||
|
sm := newRealtimeTestManager()
|
||||||
|
options := subscriptions.SubscriptionOptions{Query: map[string]string{"system": "system"}}
|
||||||
|
|
||||||
|
sm.addRealtimeSubscription("system", "rt_metrics")
|
||||||
|
sm.addRealtimeSubscription("system", "rt_metrics")
|
||||||
|
|
||||||
|
sm.realtimeMutex.Lock()
|
||||||
|
firstStop := sm.realtimeWorkerStop
|
||||||
|
assert.True(t, sm.realtimeWorkerRun)
|
||||||
|
assert.Equal(t, 2, sm.activeSubscriptions["system"].connectedClients)
|
||||||
|
sm.realtimeMutex.Unlock()
|
||||||
|
|
||||||
|
sm.removeRealtimeSubscription("rt_metrics", options)
|
||||||
|
sm.realtimeMutex.Lock()
|
||||||
|
assert.True(t, sm.realtimeWorkerRun)
|
||||||
|
assert.Equal(t, 1, sm.activeSubscriptions["system"].connectedClients)
|
||||||
|
sm.realtimeMutex.Unlock()
|
||||||
|
|
||||||
|
sm.removeRealtimeSubscription("rt_metrics", options)
|
||||||
|
sm.realtimeMutex.Lock()
|
||||||
|
assert.False(t, sm.realtimeWorkerRun)
|
||||||
|
assert.Empty(t, sm.activeSubscriptions)
|
||||||
|
sm.realtimeMutex.Unlock()
|
||||||
|
select {
|
||||||
|
case <-firstStop:
|
||||||
|
default:
|
||||||
|
t.Fatal("worker stop channel was not closed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// A later subscription must get a new stop channel owned by its worker.
|
||||||
|
sm.addRealtimeSubscription("system", "rt_metrics")
|
||||||
|
sm.realtimeMutex.Lock()
|
||||||
|
secondStop := sm.realtimeWorkerStop
|
||||||
|
assert.NotEqual(t, firstStop, secondStop)
|
||||||
|
sm.realtimeMutex.Unlock()
|
||||||
|
sm.stopRealtimeWorker()
|
||||||
|
}
|
||||||
@@ -75,7 +75,12 @@ export const smartColumns: ColumnDef<SmartAttribute>[] = [
|
|||||||
header: "Name",
|
header: "Name",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorFn: (row) => row.rs || row.rv?.toString(),
|
accessorFn: (row) => {
|
||||||
|
if (row.n === "DataUnitsWritten" || row.n === "DataUnitsRead") {
|
||||||
|
return formatDataUnits(Number(row.rv ?? 0))
|
||||||
|
}
|
||||||
|
return row.rs || row.rv?.toString()
|
||||||
|
},
|
||||||
header: "Value",
|
header: "Value",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -103,6 +108,12 @@ function formatCapacity(bytes: number): string {
|
|||||||
return `${toFixedFloat(value, value >= 10 ? 1 : 2)} ${unit}`
|
return `${toFixedFloat(value, value >= 10 ? 1 : 2)} ${unit}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Function to format NVMe data units
|
||||||
|
// (1 unit = 1000 * 512 bytes) as a human-readable size
|
||||||
|
function formatDataUnits(units: number): string {
|
||||||
|
return formatCapacity(units * 1000 * 512)
|
||||||
|
}
|
||||||
|
|
||||||
const SMART_DEVICE_FIELDS = "id,system,name,model,state,capacity,temp,type,hours,cycles,updated"
|
const SMART_DEVICE_FIELDS = "id,system,name,model,state,capacity,temp,type,hours,cycles,updated"
|
||||||
|
|
||||||
export const createColumns = (
|
export const createColumns = (
|
||||||
|
|||||||
@@ -193,7 +193,7 @@ export function SystemsTableColumns(viewMode: "table" | "grid"): ColumnDef<Syste
|
|||||||
header: sortableHeader,
|
header: sortableHeader,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorFn: ({ info }) => info.g || undefined,
|
accessorFn: ({ info }) => info.g,
|
||||||
id: "gpu",
|
id: "gpu",
|
||||||
name: () => "GPU",
|
name: () => "GPU",
|
||||||
cell: (info) => {
|
cell: (info) => {
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ apiVersion: v1
|
|||||||
description: Installs beszel-agent in kubernetes
|
description: Installs beszel-agent in kubernetes
|
||||||
home: https://github.com/henrygd/beszel/tree/main/supplemental/helm/beszel-agent
|
home: https://github.com/henrygd/beszel/tree/main/supplemental/helm/beszel-agent
|
||||||
name: beszel-agent
|
name: beszel-agent
|
||||||
appVersion: "0.18.8"
|
appVersion: "0.19.0"
|
||||||
# Bump this version when publishing chart changes.
|
# Bump this version when publishing chart changes.
|
||||||
version: 0.1.5
|
version: 0.1.6
|
||||||
sources:
|
sources:
|
||||||
- https://github.com/henrygd/beszel/tree/main/supplemental/helm/beszel-agent
|
- https://github.com/henrygd/beszel/tree/main/supplemental/helm/beszel-agent
|
||||||
- https://www.beszel.dev/
|
- 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.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 |
|
| `secret.tokenKey` | `token` | Key name in the secret for the authentication token |
|
||||||
| `image.repository` | `henrygd/beszel-agent` | Container image |
|
| `image.repository` | `henrygd/beszel-agent` | Container image |
|
||||||
| `image.tag` | Chart AppVersion (0.18.8) | Image version |
|
| `image.tag` | Chart AppVersion (0.19.0) | Image version |
|
||||||
| `hostNetwork` | `false` | Use host network for network monitoring |
|
| `hostNetwork` | `false` | Use host network for network monitoring |
|
||||||
| `tolerations` | Allows all taints | Tolerations for running on tainted nodes |
|
| `tolerations` | Allows all taints | Tolerations for running on tainted nodes |
|
||||||
|
|
||||||
@@ -385,7 +385,7 @@ helm upgrade beszel-agent ./beszel-agent \
|
|||||||
|
|
||||||
# Change image version
|
# Change image version
|
||||||
helm upgrade beszel-agent ./beszel-agent \
|
helm upgrade beszel-agent ./beszel-agent \
|
||||||
--set image.tag="0.18.8"
|
--set image.tag="0.19.0"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Restart All Agents
|
### Restart All Agents
|
||||||
@@ -522,7 +522,7 @@ kubectl get secret beszel-agent -o jsonpath='{.data.ssh-key}' | base64 -d
|
|||||||
## Chart Information
|
## Chart Information
|
||||||
|
|
||||||
- **Chart Version**: 0.1.0
|
- **Chart Version**: 0.1.0
|
||||||
- **App Version**: 0.18.8
|
- **App Version**: 0.19.0
|
||||||
- **Kubernetes Version**: 1.19+
|
- **Kubernetes Version**: 1.19+
|
||||||
- **Maintainer**: cloudwithdan (nikoloskid@pm.me)
|
- **Maintainer**: cloudwithdan (nikoloskid@pm.me)
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ apiVersion: v1
|
|||||||
description: Installs beszel-hub in kubernetes
|
description: Installs beszel-hub in kubernetes
|
||||||
home: https://github.com/henrygd/beszel/tree/main/supplemental/helm/beszel-hub
|
home: https://github.com/henrygd/beszel/tree/main/supplemental/helm/beszel-hub
|
||||||
name: beszel-hub
|
name: beszel-hub
|
||||||
appVersion: "0.18.8"
|
appVersion: "0.19.0"
|
||||||
# Bump this version when publishing chart changes.
|
# Bump this version when publishing chart changes.
|
||||||
version: 0.1.5
|
version: 0.1.6
|
||||||
sources:
|
sources:
|
||||||
- https://github.com/henrygd/beszel/tree/main/supplemental/helm/beszel-hub
|
- https://github.com/henrygd/beszel/tree/main/supplemental/helm/beszel-hub
|
||||||
- https://www.beszel.dev/
|
- https://www.beszel.dev/
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ Key configuration options in `values.yaml`:
|
|||||||
|-----------|---------|-------------|
|
|-----------|---------|-------------|
|
||||||
| `replicaCount` | `1` | Number of Beszel Hub replicas |
|
| `replicaCount` | `1` | Number of Beszel Hub replicas |
|
||||||
| `image.repository` | `henrygd/beszel` | Container image repository |
|
| `image.repository` | `henrygd/beszel` | Container image repository |
|
||||||
| `image.tag` | Chart AppVersion (0.18.8) | Container image tag |
|
| `image.tag` | Chart AppVersion (0.19.0) | Container image tag |
|
||||||
| `image.pullPolicy` | `IfNotPresent` | Image pull policy |
|
| `image.pullPolicy` | `IfNotPresent` | Image pull policy |
|
||||||
| `service.port` | `8090` | Service port |
|
| `service.port` | `8090` | Service port |
|
||||||
| `persistentVolumeClaim.enabled` | `true` | Enable persistent volume |
|
| `persistentVolumeClaim.enabled` | `true` | Enable persistent volume |
|
||||||
@@ -169,7 +169,7 @@ tolerations:
|
|||||||
```yaml
|
```yaml
|
||||||
replicaCount: 3
|
replicaCount: 3
|
||||||
image:
|
image:
|
||||||
tag: "0.18.8"
|
tag: "0.19.0"
|
||||||
service:
|
service:
|
||||||
type: LoadBalancer
|
type: LoadBalancer
|
||||||
ingress:
|
ingress:
|
||||||
@@ -330,7 +330,7 @@ By default, Beszel Hub uses a PersistentVolumeClaim for data storage. Ensure you
|
|||||||
## Chart Information
|
## Chart Information
|
||||||
|
|
||||||
- **Chart Version**: 0.1.0
|
- **Chart Version**: 0.1.0
|
||||||
- **App Version**: 0.18.8
|
- **App Version**: 0.19.0
|
||||||
- **Kubernetes Version**: 1.19+
|
- **Kubernetes Version**: 1.19+
|
||||||
- **Maintainer**: cloudwithdan (nikoloskid@pm.me)
|
- **Maintainer**: cloudwithdan (nikoloskid@pm.me)
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ is_glibc() {
|
|||||||
set_selinux_context() {
|
set_selinux_context() {
|
||||||
# Check if SELinux is enabled and in enforcing or permissive mode
|
# Check if SELinux is enabled and in enforcing or permissive mode
|
||||||
if command -v getenforce >/dev/null 2>&1; then
|
if command -v getenforce >/dev/null 2>&1; then
|
||||||
SELINUX_MODE=$(getenforce)
|
SELINUX_MODE=$(getenforce) || { warn "Could not query SELinux mode."; return 0; }
|
||||||
if [ "$SELINUX_MODE" != "Disabled" ]; then
|
if [ "$SELINUX_MODE" != "Disabled" ]; then
|
||||||
echo "SELinux is enabled (${SELINUX_MODE} mode). Setting appropriate context..."
|
echo "SELinux is enabled (${SELINUX_MODE} mode). Setting appropriate context..."
|
||||||
|
|
||||||
@@ -51,7 +51,7 @@ set_selinux_context() {
|
|||||||
if command -v semanage >/dev/null 2>&1; then
|
if command -v semanage >/dev/null 2>&1; then
|
||||||
echo "Attempting to set persistent SELinux context..."
|
echo "Attempting to set persistent SELinux context..."
|
||||||
if semanage fcontext -a -t bin_t "$BIN_PATH" >/dev/null 2>&1; then
|
if semanage fcontext -a -t bin_t "$BIN_PATH" >/dev/null 2>&1; then
|
||||||
restorecon -v "$BIN_PATH" >/dev/null 2>&1
|
restorecon -v "$BIN_PATH" >/dev/null 2>&1 || warn "Failed to restore persistent SELinux context; trying chcon."
|
||||||
else
|
else
|
||||||
echo "Warning: Failed to set persistent context, falling back to temporary context."
|
echo "Warning: Failed to set persistent context, falling back to temporary context."
|
||||||
fi
|
fi
|
||||||
@@ -271,7 +271,7 @@ detect_mips_endianness() {
|
|||||||
for bin_to_check in $bins; do
|
for bin_to_check in $bins; do
|
||||||
if [ -f "$bin_to_check" ]; then
|
if [ -f "$bin_to_check" ]; then
|
||||||
# The 6th byte in ELF header: 01 = little, 02 = big
|
# The 6th byte in ELF header: 01 = little, 02 = big
|
||||||
endian=$(hexdump -n 1 -s 5 -e '1/1 "%02x"' "$bin_to_check" 2>/dev/null)
|
endian=$(hexdump -n 1 -s 5 -e '1/1 "%02x"' "$bin_to_check" 2>/dev/null) || continue
|
||||||
if [ "$endian" = "01" ]; then
|
if [ "$endian" = "01" ]; then
|
||||||
echo "mipsle"
|
echo "mipsle"
|
||||||
return
|
return
|
||||||
@@ -286,6 +286,136 @@ detect_mips_endianness() {
|
|||||||
echo "mips"
|
echo "mips"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Expected failures must be handled explicitly; unexpected failures abort installation.
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
fail() {
|
||||||
|
echo "Error: $*" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
warn() {
|
||||||
|
echo "Warning: $*" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
require_value() {
|
||||||
|
[ "$#" -ge 2 ] && [ -n "$2" ] || fail "Option $1 requires a value."
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_platform() {
|
||||||
|
case "$(uname -s)" in
|
||||||
|
Linux)
|
||||||
|
if is_alpine; then
|
||||||
|
command -v rc-service >/dev/null && command -v rc-update >/dev/null || fail "OpenRC is required."
|
||||||
|
elif is_openwrt; then
|
||||||
|
[ -f /etc/rc.common ] || fail "OpenWrt procd is required."
|
||||||
|
else
|
||||||
|
command -v systemctl >/dev/null && [ -d /run/systemd/system ] || fail "This Linux installer requires a running systemd, OpenRC (Alpine), or procd (OpenWrt)."
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
FreeBSD)
|
||||||
|
command -v service >/dev/null && command -v sysrc >/dev/null || fail "FreeBSD service and sysrc commands are required."
|
||||||
|
;;
|
||||||
|
Darwin) fail "For macOS, use the Homebrew installer: https://github.com/henrygd/beszel/blob/main/supplemental/scripts/install-agent-brew.sh" ;;
|
||||||
|
*) fail "Unsupported operating system: $(uname -s)" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
agent_service() {
|
||||||
|
if is_alpine; then
|
||||||
|
rc-service beszel-agent "$1"
|
||||||
|
elif is_openwrt; then
|
||||||
|
/etc/init.d/beszel-agent "$1"
|
||||||
|
elif is_freebsd; then
|
||||||
|
service beszel-agent "$1"
|
||||||
|
else
|
||||||
|
systemctl "$1" beszel-agent.service
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Match the files preserved by the service setup below. A binary or rc script
|
||||||
|
# alone is not reusable configuration (FreeBSD stores its environment separately).
|
||||||
|
agent_configuration_exists() {
|
||||||
|
if is_alpine || is_openwrt; then
|
||||||
|
[ -f /etc/init.d/beszel-agent ]
|
||||||
|
elif is_freebsd; then
|
||||||
|
[ -f "$AGENT_DIR/env" ]
|
||||||
|
else
|
||||||
|
[ -f /etc/systemd/system/beszel-agent.service ]
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# An orphaned binary can remain after a failed install. It does not imply
|
||||||
|
# that the service manager knows about the agent yet.
|
||||||
|
agent_service_registered() {
|
||||||
|
if is_alpine || is_openwrt; then
|
||||||
|
[ -f /etc/init.d/beszel-agent ]
|
||||||
|
elif is_freebsd; then
|
||||||
|
[ -f /usr/local/etc/rc.d/beszel-agent ]
|
||||||
|
else
|
||||||
|
service_load_state=$(systemctl show --property=LoadState --value beszel-agent.service) || return 2
|
||||||
|
case "$service_load_state" in
|
||||||
|
not-found) return 1 ;;
|
||||||
|
"") return 2 ;;
|
||||||
|
*) return 0 ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
TEMP_DIR=""
|
||||||
|
STAGED_BINARY=""
|
||||||
|
INSTALL_STEP="validating installation options"
|
||||||
|
UPGRADE_PENDING=false
|
||||||
|
cleanup() {
|
||||||
|
cleanup_status=$?
|
||||||
|
trap - 0 HUP INT TERM
|
||||||
|
if [ "$cleanup_status" -ne 0 ]; then
|
||||||
|
warn "Installer failed while $INSTALL_STEP (exit $cleanup_status)."
|
||||||
|
if [ "$UPGRADE_PENDING" = true ]; then
|
||||||
|
warn "Restoring the previous binary and restarting its service if registered."
|
||||||
|
if [ -n "$STAGED_BINARY" ]; then
|
||||||
|
rm -f "$STAGED_BINARY" || warn "Could not remove staged binary."
|
||||||
|
fi
|
||||||
|
if STAGED_BINARY=$(mktemp "$BIN_PATH.XXXXXX") && cp -p "$BIN_PATH.bak" "$STAGED_BINARY" && mv -f "$STAGED_BINARY" "$BIN_PATH"; then
|
||||||
|
# The temporary inode does not inherit the installed binary's SELinux label.
|
||||||
|
set_selinux_context || warn "Could not restore SELinux context on the previous agent."
|
||||||
|
if agent_service_registered; then
|
||||||
|
agent_service restart || warn "Could not restart the previous agent; check the service configuration and logs."
|
||||||
|
else
|
||||||
|
service_check_status=$?
|
||||||
|
[ "$service_check_status" -eq 1 ] || warn "Could not determine whether the previous agent service is registered; check it manually."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
warn "Could not restore $BIN_PATH.bak. Restore it manually before restarting the service."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if [ -n "$STAGED_BINARY" ]; then
|
||||||
|
rm -f "$STAGED_BINARY" || warn "Could not remove staged binary."
|
||||||
|
fi
|
||||||
|
if [ -n "$TEMP_DIR" ]; then
|
||||||
|
rm -rf "$TEMP_DIR" || warn "Could not remove temporary directory $TEMP_DIR."
|
||||||
|
fi
|
||||||
|
exit "$cleanup_status"
|
||||||
|
}
|
||||||
|
trap cleanup 0
|
||||||
|
trap 'exit 129' HUP
|
||||||
|
trap 'exit 130' INT
|
||||||
|
trap 'exit 143' TERM
|
||||||
|
|
||||||
|
# A missing crontab is normal. Keep the producer alive so the new job is written.
|
||||||
|
read_root_crontab() {
|
||||||
|
crontab -u root -l 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
prompt_auto_update() {
|
||||||
|
printf "\nEnable automatic daily updates for beszel-agent? (y/n): "
|
||||||
|
if ! read -r AUTO_UPDATE; then
|
||||||
|
AUTO_UPDATE=n
|
||||||
|
echo "Skipping automatic updates (no input)."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
# Default values
|
# Default values
|
||||||
PORT=45876
|
PORT=45876
|
||||||
UNINSTALL=false
|
UNINSTALL=false
|
||||||
@@ -304,7 +434,7 @@ HUB_URL_PROVIDED=false
|
|||||||
VERSION="latest"
|
VERSION="latest"
|
||||||
|
|
||||||
# Check for help flag
|
# Check for help flag
|
||||||
case "$1" in
|
case "${1-}" in
|
||||||
-h | --help)
|
-h | --help)
|
||||||
printf "Beszel Agent installation script\n\n"
|
printf "Beszel Agent installation script\n\n"
|
||||||
printf "Usage: ./install-agent.sh [options]\n\n"
|
printf "Usage: ./install-agent.sh [options]\n\n"
|
||||||
@@ -324,6 +454,9 @@ case "$1" in
|
|||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
|
# Reject unsupported hosts before sudo or any system changes.
|
||||||
|
validate_platform
|
||||||
|
|
||||||
# Build sudo args by properly quoting everything
|
# Build sudo args by properly quoting everything
|
||||||
build_sudo_args() {
|
build_sudo_args() {
|
||||||
QUOTED_ARGS=""
|
QUOTED_ARGS=""
|
||||||
@@ -354,26 +487,31 @@ fi
|
|||||||
while [ $# -gt 0 ]; do
|
while [ $# -gt 0 ]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
-k)
|
-k)
|
||||||
|
require_value "$@"
|
||||||
shift
|
shift
|
||||||
KEY="$1"
|
KEY="$1"
|
||||||
KEY_PROVIDED=true
|
KEY_PROVIDED=true
|
||||||
;;
|
;;
|
||||||
-p)
|
-p)
|
||||||
|
require_value "$@"
|
||||||
shift
|
shift
|
||||||
PORT="$1"
|
PORT="$1"
|
||||||
PORT_PROVIDED=true
|
PORT_PROVIDED=true
|
||||||
;;
|
;;
|
||||||
-t)
|
-t)
|
||||||
|
require_value "$@"
|
||||||
shift
|
shift
|
||||||
TOKEN="$1"
|
TOKEN="$1"
|
||||||
TOKEN_PROVIDED=true
|
TOKEN_PROVIDED=true
|
||||||
;;
|
;;
|
||||||
-url)
|
-url)
|
||||||
|
require_value "$@"
|
||||||
shift
|
shift
|
||||||
HUB_URL="$1"
|
HUB_URL="$1"
|
||||||
HUB_URL_PROVIDED=true
|
HUB_URL_PROVIDED=true
|
||||||
;;
|
;;
|
||||||
-v | --version)
|
-v | --version)
|
||||||
|
require_value "$@"
|
||||||
shift
|
shift
|
||||||
VERSION="$1"
|
VERSION="$1"
|
||||||
;;
|
;;
|
||||||
@@ -392,7 +530,7 @@ while [ $# -gt 0 ]; do
|
|||||||
GITHUB_PROXY_URL="https://gh.beszel.dev"
|
GITHUB_PROXY_URL="https://gh.beszel.dev"
|
||||||
GITHUB_URL="$GITHUB_PROXY_URL"
|
GITHUB_URL="$GITHUB_PROXY_URL"
|
||||||
fi
|
fi
|
||||||
elif [ "$2" != "" ] && ! echo "$2" | grep -q '^-'; then
|
elif [ "${2-}" != "" ] && ! echo "$2" | grep -q '^-'; then
|
||||||
# use custom proxy URL provided as next argument
|
# use custom proxy URL provided as next argument
|
||||||
GITHUB_PROXY_URL="$2"
|
GITHUB_PROXY_URL="$2"
|
||||||
GITHUB_URL="$(ensure_trailing_slash "$2")https://github.com"
|
GITHUB_URL="$(ensure_trailing_slash "$2")https://github.com"
|
||||||
@@ -415,7 +553,7 @@ while [ $# -gt 0 ]; do
|
|||||||
else
|
else
|
||||||
echo "Invalid value for --auto-update flag: $AUTO_UPDATE_VALUE. Using default (prompt)."
|
echo "Invalid value for --auto-update flag: $AUTO_UPDATE_VALUE. Using default (prompt)."
|
||||||
fi
|
fi
|
||||||
elif [ "$2" = "true" ] || [ "$2" = "false" ]; then
|
elif [ "${2-}" = "true" ] || [ "${2-}" = "false" ]; then
|
||||||
# Value provided as next argument
|
# Value provided as next argument
|
||||||
AUTO_UPDATE_FLAG="$2"
|
AUTO_UPDATE_FLAG="$2"
|
||||||
shift
|
shift
|
||||||
@@ -443,20 +581,7 @@ else
|
|||||||
BIN_PATH="/opt/beszel-agent/beszel-agent"
|
BIN_PATH="/opt/beszel-agent/beszel-agent"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Stop existing service if it exists (for upgrades)
|
INSTALL_STEP="uninstalling the agent"
|
||||||
if [ "$UNINSTALL" != true ] && [ -f "$BIN_PATH" ]; then
|
|
||||||
echo "Existing installation detected. Stopping service for upgrade..."
|
|
||||||
if is_alpine; then
|
|
||||||
rc-service beszel-agent stop 2>/dev/null || true
|
|
||||||
elif is_openwrt; then
|
|
||||||
/etc/init.d/beszel-agent stop 2>/dev/null || true
|
|
||||||
elif is_freebsd; then
|
|
||||||
service beszel-agent stop 2>/dev/null || true
|
|
||||||
else
|
|
||||||
systemctl stop beszel-agent.service 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Uninstall process
|
# Uninstall process
|
||||||
if [ "$UNINSTALL" = true ]; then
|
if [ "$UNINSTALL" = true ]; then
|
||||||
# Clean up SELinux contexts before removing files
|
# Clean up SELinux contexts before removing files
|
||||||
@@ -464,8 +589,8 @@ if [ "$UNINSTALL" = true ]; then
|
|||||||
|
|
||||||
if is_alpine; then
|
if is_alpine; then
|
||||||
echo "Stopping and disabling the agent service..."
|
echo "Stopping and disabling the agent service..."
|
||||||
rc-service beszel-agent stop
|
rc-service beszel-agent stop || warn "Cleanup command failed: rc-service beszel-agent stop"
|
||||||
rc-update del beszel-agent default
|
rc-update del beszel-agent default || warn "Cleanup command failed: rc-update del beszel-agent default"
|
||||||
|
|
||||||
echo "Removing the OpenRC service files..."
|
echo "Removing the OpenRC service files..."
|
||||||
rm -f /etc/init.d/beszel-agent
|
rm -f /etc/init.d/beszel-agent
|
||||||
@@ -481,8 +606,8 @@ if [ "$UNINSTALL" = true ]; then
|
|||||||
rm -f /var/log/beszel-agent.log /var/log/beszel-agent.err
|
rm -f /var/log/beszel-agent.log /var/log/beszel-agent.err
|
||||||
elif is_openwrt; then
|
elif is_openwrt; then
|
||||||
echo "Stopping and disabling the agent service..."
|
echo "Stopping and disabling the agent service..."
|
||||||
/etc/init.d/beszel-agent stop
|
/etc/init.d/beszel-agent stop || warn "Cleanup command failed: /etc/init.d/beszel-agent stop"
|
||||||
/etc/init.d/beszel-agent disable
|
/etc/init.d/beszel-agent disable || warn "Cleanup command failed: /etc/init.d/beszel-agent disable"
|
||||||
|
|
||||||
echo "Removing the OpenWRT service files..."
|
echo "Removing the OpenWRT service files..."
|
||||||
rm -f /etc/init.d/beszel-agent
|
rm -f /etc/init.d/beszel-agent
|
||||||
@@ -498,7 +623,7 @@ if [ "$UNINSTALL" = true ]; then
|
|||||||
|
|
||||||
elif is_freebsd; then
|
elif is_freebsd; then
|
||||||
echo "Stopping and disabling the agent service..."
|
echo "Stopping and disabling the agent service..."
|
||||||
service beszel-agent stop
|
service beszel-agent stop || warn "Cleanup command failed: service beszel-agent stop"
|
||||||
sysrc beszel_agent_enable="NO"
|
sysrc beszel_agent_enable="NO"
|
||||||
|
|
||||||
echo "Removing the FreeBSD service files..."
|
echo "Removing the FreeBSD service files..."
|
||||||
@@ -525,16 +650,16 @@ if [ "$UNINSTALL" = true ]; then
|
|||||||
|
|
||||||
else
|
else
|
||||||
echo "Stopping and disabling the agent service..."
|
echo "Stopping and disabling the agent service..."
|
||||||
systemctl stop beszel-agent.service
|
systemctl stop beszel-agent.service || warn "Cleanup command failed: systemctl stop beszel-agent.service"
|
||||||
systemctl disable beszel-agent.service >/dev/null 2>&1
|
systemctl disable beszel-agent.service >/dev/null 2>&1 || warn "Cleanup command failed: systemctl disable beszel-agent.service"
|
||||||
|
|
||||||
echo "Removing the systemd service file..."
|
echo "Removing the systemd service file..."
|
||||||
rm /etc/systemd/system/beszel-agent.service
|
rm -f /etc/systemd/system/beszel-agent.service
|
||||||
|
|
||||||
# Remove the update timer and service if they exist
|
# Remove the update timer and service if they exist
|
||||||
echo "Removing the daily update service and timer..."
|
echo "Removing the daily update service and timer..."
|
||||||
systemctl stop beszel-agent-update.timer 2>/dev/null
|
systemctl stop beszel-agent-update.timer 2>/dev/null || warn "Cleanup command failed: systemctl stop beszel-agent-update.timer"
|
||||||
systemctl disable beszel-agent-update.timer >/dev/null 2>&1
|
systemctl disable beszel-agent-update.timer >/dev/null 2>&1 || warn "Cleanup command failed: systemctl disable beszel-agent-update.timer"
|
||||||
rm -f /etc/systemd/system/beszel-agent-update.service
|
rm -f /etc/systemd/system/beszel-agent-update.service
|
||||||
rm -f /etc/systemd/system/beszel-agent-update.timer
|
rm -f /etc/systemd/system/beszel-agent-update.timer
|
||||||
|
|
||||||
@@ -545,13 +670,15 @@ if [ "$UNINSTALL" = true ]; then
|
|||||||
rm -rf "$AGENT_DIR"
|
rm -rf "$AGENT_DIR"
|
||||||
|
|
||||||
echo "Removing the dedicated user for the agent service..."
|
echo "Removing the dedicated user for the agent service..."
|
||||||
killall beszel-agent 2>/dev/null
|
killall beszel-agent 2>/dev/null || true # Usually already stopped by the service manager.
|
||||||
|
if id -u beszel >/dev/null 2>&1; then
|
||||||
if is_alpine || is_openwrt; then
|
if is_alpine || is_openwrt; then
|
||||||
deluser beszel 2>/dev/null
|
deluser beszel || fail "Could not remove the beszel user."
|
||||||
elif is_freebsd; then
|
elif is_freebsd; then
|
||||||
pw user del beszel 2>/dev/null
|
pw user del beszel || fail "Could not remove the beszel user."
|
||||||
else
|
else
|
||||||
userdel beszel 2>/dev/null
|
userdel beszel || fail "Could not remove the beszel user."
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Beszel Agent has been uninstalled successfully!"
|
echo "Beszel Agent has been uninstalled successfully!"
|
||||||
@@ -563,6 +690,7 @@ package_installed() {
|
|||||||
command -v "$1" >/dev/null 2>&1
|
command -v "$1" >/dev/null 2>&1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
INSTALL_STEP="installing required packages"
|
||||||
# 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 apk; then
|
if package_installed apk; then
|
||||||
if ! package_installed tar || ! package_installed curl || ! package_installed sha256sum; then
|
if ! package_installed tar || ! package_installed curl || ! package_installed sha256sum; then
|
||||||
@@ -596,13 +724,18 @@ else
|
|||||||
echo "Warning: Please ensure 'tar' and 'curl' and 'sha256sum (coreutils)' are installed."
|
echo "Warning: Please ensure 'tar' and 'curl' and 'sha256sum (coreutils)' are installed."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# If no SSH key is provided, ask for the SSH key interactively (skip if upgrading)
|
for required_command in tar curl; do
|
||||||
|
command -v "$required_command" >/dev/null || fail "Required command is missing: $required_command"
|
||||||
|
done
|
||||||
|
|
||||||
|
# If no SSH key is provided, prompt unless service setup will reuse configuration.
|
||||||
if [ -z "$KEY" ]; then
|
if [ -z "$KEY" ]; then
|
||||||
if [ -f "$BIN_PATH" ]; then
|
if agent_configuration_exists; then
|
||||||
echo "Upgrading existing installation. Using existing service configuration."
|
echo "Using existing service configuration."
|
||||||
else
|
else
|
||||||
printf "Enter your SSH key: "
|
printf "Enter your SSH key: "
|
||||||
read KEY
|
read -r KEY || fail "No SSH key received. Supply -k for noninteractive installation."
|
||||||
|
[ -n "$KEY" ] || fail "SSH key must not be empty."
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -623,6 +756,7 @@ else
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
INSTALL_STEP="configuring the service user"
|
||||||
# Create a dedicated user for the service if it doesn't exist
|
# Create a dedicated user for the service if it doesn't exist
|
||||||
AGENT_USER="beszel"
|
AGENT_USER="beszel"
|
||||||
echo "Configuring the dedicated user for the Beszel Agent service..."
|
echo "Configuring the dedicated user for the Beszel Agent service..."
|
||||||
@@ -678,6 +812,11 @@ elif is_freebsd; then
|
|||||||
echo "Adding beszel to wheel group for self-updates"
|
echo "Adding beszel to wheel group for self-updates"
|
||||||
pw group mod wheel -m beszel
|
pw group mod wheel -m beszel
|
||||||
fi
|
fi
|
||||||
|
# Add the user to the operator group for device access (SMART, /dev/xpt0, /dev/nvme*)
|
||||||
|
if pw group show operator >/dev/null 2>&1; then
|
||||||
|
echo "Adding beszel to operator group for device access"
|
||||||
|
pw group mod operator -m beszel
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
else
|
else
|
||||||
@@ -696,6 +835,7 @@ else
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
INSTALL_STEP="creating installation directories"
|
||||||
# Create the directory for the Beszel Agent
|
# Create the directory for the Beszel Agent
|
||||||
|
|
||||||
if [ ! -d "$AGENT_DIR" ]; then
|
if [ ! -d "$AGENT_DIR" ]; then
|
||||||
@@ -709,6 +849,7 @@ if [ ! -d "$BIN_DIR" ]; then
|
|||||||
mkdir -p "$BIN_DIR"
|
mkdir -p "$BIN_DIR"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
INSTALL_STEP="downloading and verifying the agent"
|
||||||
# Download and install the Beszel Agent
|
# Download and install the Beszel Agent
|
||||||
|
|
||||||
OS=$(uname -s | sed -e 'y/ABCDEFGHIJKLMNOPQRSTUVWXYZ/abcdefghijklmnopqrstuvwxyz/')
|
OS=$(uname -s | sed -e 'y/ABCDEFGHIJKLMNOPQRSTUVWXYZ/abcdefghijklmnopqrstuvwxyz/')
|
||||||
@@ -720,11 +861,12 @@ fi
|
|||||||
|
|
||||||
# Determine version to install
|
# Determine version to install
|
||||||
if [ "$VERSION" = "latest" ]; then
|
if [ "$VERSION" = "latest" ]; then
|
||||||
INSTALL_VERSION=$(curl -s "https://get.beszel.dev/latest-version")
|
INSTALL_VERSION=$(curl -fsS --connect-timeout 10 --max-time 30 "https://get.beszel.dev/latest-version") || INSTALL_VERSION=""
|
||||||
if [ -z "$INSTALL_VERSION" ]; then
|
if [ -z "$INSTALL_VERSION" ]; then
|
||||||
# Fallback to GitHub API
|
# Fallback to GitHub API
|
||||||
API_RELEASE_URL="https://api.github.com/repos/henrygd/beszel/releases/latest"
|
API_RELEASE_URL="https://api.github.com/repos/henrygd/beszel/releases/latest"
|
||||||
INSTALL_VERSION=$(curl -s "$API_RELEASE_URL" | grep -o '"tag_name": "v[^"]*"' | cut -d'"' -f4 | tr -d 'v')
|
RELEASE_JSON=$(curl -fsS --connect-timeout 10 --max-time 30 "$API_RELEASE_URL") || fail "Could not fetch the latest release from GitHub."
|
||||||
|
INSTALL_VERSION=$(printf '%s\n' "$RELEASE_JSON" | grep -o '"tag_name": "v[^"]*"' | cut -d'"' -f4 | tr -d 'v')
|
||||||
fi
|
fi
|
||||||
if [ -z "$INSTALL_VERSION" ]; then
|
if [ -z "$INSTALL_VERSION" ]; then
|
||||||
echo "Failed to get latest version"
|
echo "Failed to get latest version"
|
||||||
@@ -741,7 +883,8 @@ echo "Downloading beszel-agent v${INSTALL_VERSION}..."
|
|||||||
# Download checksums file
|
# Download checksums file
|
||||||
TEMP_DIR=$(mktemp -d)
|
TEMP_DIR=$(mktemp -d)
|
||||||
cd "$TEMP_DIR" || exit 1
|
cd "$TEMP_DIR" || exit 1
|
||||||
CHECKSUM=$(curl -fsSL "$GITHUB_URL/henrygd/beszel/releases/download/v${INSTALL_VERSION}/beszel_${INSTALL_VERSION}_checksums.txt" | grep "$FILE_NAME" | cut -d' ' -f1)
|
curl -fsSL --connect-timeout 10 --max-time 60 "$GITHUB_URL/henrygd/beszel/releases/download/v${INSTALL_VERSION}/beszel_${INSTALL_VERSION}_checksums.txt" -o checksums.txt || fail "Could not download checksums. Try --mirror if GitHub is unreachable."
|
||||||
|
CHECKSUM=$(awk -v name="$FILE_NAME" '$2 == name { print $1 }' checksums.txt)
|
||||||
if [ -z "$CHECKSUM" ] || ! echo "$CHECKSUM" | grep -qE "^[a-fA-F0-9]{64}$"; then
|
if [ -z "$CHECKSUM" ] || ! echo "$CHECKSUM" | grep -qE "^[a-fA-F0-9]{64}$"; then
|
||||||
echo "Failed to get checksum or invalid checksum format"
|
echo "Failed to get checksum or invalid checksum format"
|
||||||
echo "Try again with --mirror (or --mirror <url>) if GitHub is not reachable."
|
echo "Try again with --mirror (or --mirror <url>) if GitHub is not reachable."
|
||||||
@@ -763,10 +906,10 @@ if ! tar -tzf "$FILE_NAME" >/dev/null 2>&1; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$($CHECK_CMD "$FILE_NAME" | cut -d' ' -f1)" != "$CHECKSUM" ]; then
|
ACTUAL_CHECKSUM=$($CHECK_CMD "$FILE_NAME") || fail "Could not calculate archive checksum."
|
||||||
echo "Checksum verification failed: $($CHECK_CMD "$FILE_NAME" | cut -d' ' -f1) & $CHECKSUM"
|
ACTUAL_CHECKSUM=${ACTUAL_CHECKSUM%% *}
|
||||||
rm -rf "$TEMP_DIR"
|
if [ "$ACTUAL_CHECKSUM" != "$CHECKSUM" ]; then
|
||||||
exit 1
|
fail "Checksum verification failed: $ACTUAL_CHECKSUM != $CHECKSUM"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if ! tar -xzf "$FILE_NAME" beszel-agent; then
|
if ! tar -xzf "$FILE_NAME" beszel-agent; then
|
||||||
@@ -781,20 +924,34 @@ if [ ! -s "$TEMP_DIR/beszel-agent" ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
INSTALL_STEP="replacing the agent binary"
|
||||||
|
# Stage on the destination filesystem so replacement and rollback use atomic renames.
|
||||||
|
STAGED_BINARY=$(mktemp "$BIN_PATH.XXXXXX") || fail "Could not create a staged binary."
|
||||||
|
cp beszel-agent "$STAGED_BINARY" || fail "Could not stage the agent binary."
|
||||||
|
chown "${AGENT_USER}:${AGENT_USER}" "$STAGED_BINARY" || fail "Could not set binary ownership."
|
||||||
|
chmod 755 "$STAGED_BINARY" || fail "Could not set binary permissions."
|
||||||
|
|
||||||
if [ -f "$BIN_PATH" ]; then
|
if [ -f "$BIN_PATH" ]; then
|
||||||
echo "Backing up existing binary..."
|
echo "Backing up existing binary..."
|
||||||
cp "$BIN_PATH" "$BIN_PATH.bak"
|
cp -p "$BIN_PATH" "$BIN_PATH.bak" || fail "Could not back up the existing binary."
|
||||||
|
UPGRADE_PENDING=true
|
||||||
|
if agent_service_registered; then
|
||||||
|
agent_service stop || fail "Could not stop the existing agent."
|
||||||
|
else
|
||||||
|
service_check_status=$?
|
||||||
|
[ "$service_check_status" -eq 1 ] || fail "Could not determine whether the existing agent service is registered."
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
mv beszel-agent "$BIN_PATH"
|
mv -f "$STAGED_BINARY" "$BIN_PATH" || fail "Could not install the agent binary."
|
||||||
chown "${AGENT_USER}:${AGENT_USER}" "$BIN_PATH"
|
STAGED_BINARY=""
|
||||||
chmod 755 "$BIN_PATH"
|
|
||||||
|
|
||||||
# Set SELinux context if needed
|
# Set SELinux context if needed
|
||||||
set_selinux_context
|
set_selinux_context
|
||||||
|
|
||||||
# Cleanup
|
# Cleanup
|
||||||
rm -rf "$TEMP_DIR"
|
rm -rf "$TEMP_DIR"
|
||||||
|
TEMP_DIR=""
|
||||||
|
|
||||||
# Make sure /etc/machine-id exists and is non-empty for persistent fingerprint
|
# Make sure /etc/machine-id exists and is non-empty for persistent fingerprint
|
||||||
if [ ! -s /etc/machine-id ]; then
|
if [ ! -s /etc/machine-id ]; then
|
||||||
@@ -819,6 +976,7 @@ detect_nvidia_devices() {
|
|||||||
echo "$devices"
|
echo "$devices"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
INSTALL_STEP="configuring and starting the service"
|
||||||
# Modify service installation part, add Alpine check before systemd service creation
|
# Modify service installation part, add Alpine check before systemd service creation
|
||||||
if is_alpine; then
|
if is_alpine; then
|
||||||
if [ ! -f /etc/init.d/beszel-agent ]; then
|
if [ ! -f /etc/init.d/beszel-agent ]; then
|
||||||
@@ -868,7 +1026,7 @@ EOF
|
|||||||
chown "${AGENT_USER}:${AGENT_USER}" /var/log/beszel-agent.log /var/log/beszel-agent.err
|
chown "${AGENT_USER}:${AGENT_USER}" /var/log/beszel-agent.log /var/log/beszel-agent.err
|
||||||
|
|
||||||
# Start the service
|
# Start the service
|
||||||
rc-service beszel-agent restart
|
rc-service beszel-agent restart || fail "Could not start the agent; check service logs."
|
||||||
|
|
||||||
# Check if service started successfully
|
# Check if service started successfully
|
||||||
sleep 2
|
sleep 2
|
||||||
@@ -884,8 +1042,7 @@ EOF
|
|||||||
elif [ "$AUTO_UPDATE_FLAG" = "false" ]; then
|
elif [ "$AUTO_UPDATE_FLAG" = "false" ]; then
|
||||||
AUTO_UPDATE="n"
|
AUTO_UPDATE="n"
|
||||||
else
|
else
|
||||||
printf "\nEnable automatic daily updates for beszel-agent? (y/n): "
|
prompt_auto_update
|
||||||
read AUTO_UPDATE
|
|
||||||
fi
|
fi
|
||||||
case "$AUTO_UPDATE" in
|
case "$AUTO_UPDATE" in
|
||||||
[Yy]*)
|
[Yy]*)
|
||||||
@@ -893,7 +1050,7 @@ EOF
|
|||||||
|
|
||||||
# Create cron job to run beszel-agent update command daily at midnight
|
# Create cron job to run beszel-agent update command daily at midnight
|
||||||
if ! crontab -u root -l 2>/dev/null | grep -q "beszel-agent.*update"; then
|
if ! crontab -u root -l 2>/dev/null | grep -q "beszel-agent.*update"; then
|
||||||
(crontab -u root -l 2>/dev/null; echo "12 0 * * * $BIN_PATH update >/dev/null 2>&1") | crontab -u root -
|
(read_root_crontab; echo "12 0 * * * $BIN_PATH update >/dev/null 2>&1") | crontab -u root -
|
||||||
fi
|
fi
|
||||||
|
|
||||||
printf "\nDaily updates have been enabled via cron job.\n"
|
printf "\nDaily updates have been enabled via cron job.\n"
|
||||||
@@ -963,7 +1120,7 @@ EOF
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Start the service
|
# Start the service
|
||||||
/etc/init.d/beszel-agent restart
|
/etc/init.d/beszel-agent restart || fail "Could not start the agent; check service logs."
|
||||||
|
|
||||||
# Auto-update service for OpenWRT using a crontab job
|
# Auto-update service for OpenWRT using a crontab job
|
||||||
if [ "$AUTO_UPDATE_FLAG" = "true" ]; then
|
if [ "$AUTO_UPDATE_FLAG" = "true" ]; then
|
||||||
@@ -973,15 +1130,14 @@ EOF
|
|||||||
AUTO_UPDATE="n"
|
AUTO_UPDATE="n"
|
||||||
sleep 1 # give time for the service to start
|
sleep 1 # give time for the service to start
|
||||||
else
|
else
|
||||||
printf "\nEnable automatic daily updates for beszel-agent? (y/n): "
|
prompt_auto_update
|
||||||
read AUTO_UPDATE
|
|
||||||
fi
|
fi
|
||||||
case "$AUTO_UPDATE" in
|
case "$AUTO_UPDATE" in
|
||||||
[Yy]*)
|
[Yy]*)
|
||||||
echo "Setting up daily automatic updates for beszel-agent..."
|
echo "Setting up daily automatic updates for beszel-agent..."
|
||||||
|
|
||||||
if ! crontab -u root -l 2>/dev/null | grep -q "beszel-agent.*update"; then
|
if ! crontab -u root -l 2>/dev/null | grep -q "beszel-agent.*update"; then
|
||||||
(crontab -u root -l 2>/dev/null; echo "12 0 * * * /etc/init.d/beszel-agent update") | crontab -u root -
|
(read_root_crontab; echo "12 0 * * * /etc/init.d/beszel-agent update") | crontab -u root -
|
||||||
fi
|
fi
|
||||||
|
|
||||||
/etc/init.d/cron restart
|
/etc/init.d/cron restart
|
||||||
@@ -1072,7 +1228,7 @@ EOF
|
|||||||
esac
|
esac
|
||||||
fi
|
fi
|
||||||
|
|
||||||
service beszel-agent restart
|
service beszel-agent restart || fail "Could not start the agent; check service logs."
|
||||||
|
|
||||||
# Check if service started successfully
|
# Check if service started successfully
|
||||||
sleep 2
|
sleep 2
|
||||||
@@ -1088,8 +1244,7 @@ EOF
|
|||||||
elif [ "$AUTO_UPDATE_FLAG" = "false" ]; then
|
elif [ "$AUTO_UPDATE_FLAG" = "false" ]; then
|
||||||
AUTO_UPDATE="n"
|
AUTO_UPDATE="n"
|
||||||
else
|
else
|
||||||
printf "\nEnable automatic daily updates for beszel-agent? (y/n): "
|
prompt_auto_update
|
||||||
read AUTO_UPDATE
|
|
||||||
fi
|
fi
|
||||||
case "$AUTO_UPDATE" in
|
case "$AUTO_UPDATE" in
|
||||||
[Yy]*)
|
[Yy]*)
|
||||||
@@ -1170,7 +1325,7 @@ EOF
|
|||||||
printf "\nLoading and starting the agent service...\n"
|
printf "\nLoading and starting the agent service...\n"
|
||||||
systemctl daemon-reload
|
systemctl daemon-reload
|
||||||
systemctl enable beszel-agent.service >/dev/null 2>&1
|
systemctl enable beszel-agent.service >/dev/null 2>&1
|
||||||
systemctl restart beszel-agent.service
|
systemctl restart beszel-agent.service || fail "Could not start the agent; check service logs."
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1182,8 +1337,7 @@ EOF
|
|||||||
AUTO_UPDATE="n"
|
AUTO_UPDATE="n"
|
||||||
sleep 1 # give time for the service to start
|
sleep 1 # give time for the service to start
|
||||||
else
|
else
|
||||||
printf "\nEnable automatic daily updates for beszel-agent? (y/n): "
|
prompt_auto_update
|
||||||
read AUTO_UPDATE
|
|
||||||
fi
|
fi
|
||||||
case "$AUTO_UPDATE" in
|
case "$AUTO_UPDATE" in
|
||||||
[Yy]*)
|
[Yy]*)
|
||||||
@@ -1229,6 +1383,7 @@ EOF
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
UPGRADE_PENDING=false
|
||||||
RUNNING_ADDRESS=$(configured_address)
|
RUNNING_ADDRESS=$(configured_address)
|
||||||
[ -n "$RUNNING_ADDRESS" ] || RUNNING_ADDRESS=$PORT
|
[ -n "$RUNNING_ADDRESS" ] || RUNNING_ADDRESS=$PORT
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user