mirror of
https://github.com/henrygd/beszel.git
synced 2026-09-21 08:57:48 +02:00
Compare commits
21 Commits
bc69c16331
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c69197d2d | ||
|
|
97e6f64bdc | ||
|
|
4a5915b141 | ||
|
|
e68372dce4 | ||
|
|
c52f3acb94 | ||
|
|
c09eb8c6df | ||
|
|
a0dc19eacf | ||
|
|
912bc50874 | ||
|
|
c54dbfba7c | ||
|
|
dd3f7d58b5 | ||
|
|
0509053a69 | ||
|
|
187dc886a9 | ||
|
|
2784460621 | ||
|
|
b0bc727411 | ||
|
|
6937453282 | ||
|
|
97de1471d7 | ||
|
|
bd7e359dcd | ||
|
|
d2352e8882 | ||
|
|
f7fd3ef403 | ||
|
|
db3afeabd9 | ||
|
|
b347599928 |
8
.github/workflows/docker-images.yml
vendored
8
.github/workflows/docker-images.yml
vendored
@@ -29,6 +29,7 @@ jobs:
|
||||
# henrygd/beszel-agent:alpine
|
||||
- image: henrygd/beszel-agent
|
||||
dockerfile: ./internal/dockerfile_agent_alpine
|
||||
flavor: latest=false
|
||||
registry: docker.io
|
||||
username_secret: DOCKERHUB_USERNAME
|
||||
password_secret: DOCKERHUB_TOKEN
|
||||
@@ -55,6 +56,7 @@ jobs:
|
||||
# henrygd/beszel-agent-nvidia:slim
|
||||
- image: henrygd/beszel-agent-nvidia
|
||||
dockerfile: ./internal/dockerfile_agent_nvidia_slim
|
||||
flavor: latest=false
|
||||
platforms: linux/amd64,linux/arm64
|
||||
registry: docker.io
|
||||
username_secret: DOCKERHUB_USERNAME
|
||||
@@ -123,6 +125,7 @@ jobs:
|
||||
# ghcr.io/henrygd/beszel-agent-nvidia:slim
|
||||
- image: ghcr.io/${{ github.repository }}/beszel-agent-nvidia
|
||||
dockerfile: ./internal/dockerfile_agent_nvidia_slim
|
||||
flavor: latest=false
|
||||
platforms: linux/amd64,linux/arm64
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
@@ -150,6 +153,7 @@ jobs:
|
||||
# ghcr.io/henrygd/beszel-agent:alpine
|
||||
- image: ghcr.io/${{ github.repository }}/beszel-agent
|
||||
dockerfile: ./internal/dockerfile_agent_alpine
|
||||
flavor: latest=false
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password_secret: GITHUB_TOKEN
|
||||
@@ -159,7 +163,7 @@ jobs:
|
||||
type=semver,pattern={{major}}.{{minor}}-alpine
|
||||
type=semver,pattern={{major}}-alpine
|
||||
|
||||
# henrygd/beszel-agent (keep at bottom so it gets built after :alpine and gets the latest tag)
|
||||
# henrygd/beszel-agent
|
||||
- image: henrygd/beszel-agent
|
||||
dockerfile: ./internal/dockerfile_agent
|
||||
registry: docker.io
|
||||
@@ -200,6 +204,8 @@ jobs:
|
||||
uses: docker/metadata-action@v6
|
||||
with:
|
||||
images: ${{ matrix.image }}
|
||||
# Variant images must not overwrite the standard image's latest tag.
|
||||
flavor: ${{ matrix.flavor || 'latest=auto' }}
|
||||
tags: ${{ matrix.tags }}
|
||||
|
||||
# https://github.com/docker/login-action
|
||||
|
||||
@@ -201,12 +201,9 @@ func mdraidSmartStatus(health mdraidHealth) string {
|
||||
if health.mismatchCnt > 0 {
|
||||
return "WARNING"
|
||||
}
|
||||
// "check" scans for consistency problems without repairing mismatches.
|
||||
// With no mismatches, keep it green while reporting progress attributes.
|
||||
switch syncAction {
|
||||
case "repair":
|
||||
return "WARNING"
|
||||
}
|
||||
// "check" and "repair" are requested consistency scans, not evidence of
|
||||
// array failure. With no health issues above, keep scrubbing green while
|
||||
// reporting the sync action and progress attributes.
|
||||
switch state {
|
||||
case "clean", "active", "active-idle", "write-pending", "read-auto", "readonly":
|
||||
return "PASSED"
|
||||
|
||||
@@ -174,8 +174,25 @@ func TestMdraidSmartStatus(t *testing.T) {
|
||||
if got := mdraidSmartStatus(mdraidHealth{arrayState: "clean", mismatchCnt: 1}); got != "WARNING" {
|
||||
t.Fatalf("mdraidSmartStatus(clean+mismatch) = %q, want WARNING", got)
|
||||
}
|
||||
if got := mdraidSmartStatus(mdraidHealth{arrayState: "clean", syncAction: "repair"}); got != "WARNING" {
|
||||
t.Fatalf("mdraidSmartStatus(repair) = %q, want WARNING", got)
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
health mdraidHealth
|
||||
want string
|
||||
}{
|
||||
{"clean", mdraidHealth{arrayState: "clean"}, "PASSED"},
|
||||
{"active", mdraidHealth{arrayState: "active"}, "PASSED"},
|
||||
{"mismatch", mdraidHealth{arrayState: "active", mismatchCnt: 1}, "WARNING"},
|
||||
{"degraded", mdraidHealth{arrayState: "active", degraded: 1}, "FAILED"},
|
||||
{"faulty member", mdraidHealth{arrayState: "active", faultyDisks: 1}, "FAILED"},
|
||||
{"inactive", mdraidHealth{arrayState: "inactive"}, "FAILED"},
|
||||
{"unknown", mdraidHealth{arrayState: "unknown"}, "UNKNOWN"},
|
||||
} {
|
||||
t.Run("repair/"+tc.name, func(t *testing.T) {
|
||||
tc.health.syncAction = "repair"
|
||||
if got := mdraidSmartStatus(tc.health); got != tc.want {
|
||||
t.Fatalf("mdraidSmartStatus(%+v) = %q, want %s", tc.health, got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
if got := mdraidSmartStatus(mdraidHealth{arrayState: "clean"}); got != "PASSED" {
|
||||
t.Fatalf("mdraidSmartStatus(clean) = %q, want PASSED", got)
|
||||
|
||||
36
go.mod
36
go.mod
@@ -7,22 +7,23 @@ require (
|
||||
github.com/coreos/go-systemd/v22 v22.7.0
|
||||
github.com/distribution/reference v0.6.0
|
||||
github.com/ebitengine/purego v0.11.0
|
||||
github.com/fxamacker/cbor/v2 v2.9.3
|
||||
github.com/fxamacker/cbor/v2 v2.9.4
|
||||
github.com/gliderlabs/ssh v0.3.8
|
||||
github.com/lxzan/gws v1.10.1
|
||||
github.com/nicholas-fedor/shoutrrr v0.20.0
|
||||
github.com/lxzan/gws v1.10.2
|
||||
github.com/nicholas-fedor/shoutrrr v0.21.0
|
||||
github.com/opencontainers/go-digest v1.0.0
|
||||
github.com/pocketbase/dbx v1.12.0
|
||||
github.com/pocketbase/pocketbase v0.40.2
|
||||
github.com/pocketbase/pocketbase v0.40.4
|
||||
github.com/shirou/gopsutil/v4 v4.26.8
|
||||
github.com/spf13/cast v1.10.0
|
||||
github.com/spf13/cobra v1.10.2
|
||||
github.com/spf13/pflag v1.0.10
|
||||
github.com/stretchr/testify v1.12.1
|
||||
golang.org/x/crypto v0.56.0
|
||||
golang.org/x/exp v0.0.0-20260824195058-e88cd73687aa
|
||||
golang.org/x/net v0.58.0
|
||||
golang.org/x/sys v0.47.0
|
||||
golang.org/x/crypto v0.57.0
|
||||
golang.org/x/exp v0.0.0-20260908205506-85c1c2202aba
|
||||
golang.org/x/net v0.59.0
|
||||
golang.org/x/oauth2 v0.37.0
|
||||
golang.org/x/sys v0.48.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
howett.net/plist v1.0.1
|
||||
)
|
||||
@@ -32,7 +33,7 @@ require (
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
|
||||
github.com/disintegration/imaging v1.6.2 // indirect
|
||||
github.com/domodwyer/mailyak/v3 v3.6.2 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/dustin/go-humanize v1.1.0 // indirect
|
||||
github.com/eclipse/paho.golang v0.23.0 // indirect
|
||||
github.com/fatih/color v1.19.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.10.1 // indirect
|
||||
@@ -51,18 +52,23 @@ require (
|
||||
github.com/mattn/go-isatty v0.0.24 // indirect
|
||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||
github.com/pocketbase/ozzo-validation/v4 v4.3.0 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20260805114148-88456608a4f6 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20260916203055-22a1a467d9f0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/tklauser/go-sysconf v0.4.0 // indirect
|
||||
github.com/tklauser/numcpus v0.12.0 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.5 // indirect
|
||||
golang.org/x/image v0.45.0 // indirect
|
||||
golang.org/x/oauth2 v0.36.0 // indirect
|
||||
golang.org/x/sync v0.22.0 // indirect
|
||||
golang.org/x/term v0.45.0 // indirect
|
||||
golang.org/x/text v0.41.0 // indirect
|
||||
golang.org/x/image v0.46.0 // indirect
|
||||
golang.org/x/mod v0.41.0 // indirect
|
||||
golang.org/x/sync v0.23.0 // indirect
|
||||
golang.org/x/term v0.46.0 // indirect
|
||||
golang.org/x/text v0.42.0 // indirect
|
||||
golang.org/x/tools v0.50.0 // indirect
|
||||
mellium.im/reader v0.1.0 // indirect
|
||||
mellium.im/sasl v0.3.2 // indirect
|
||||
mellium.im/xmlstream v0.15.4 // indirect
|
||||
mellium.im/xmpp v0.23.0 // indirect
|
||||
modernc.org/libc v1.74.4 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.12.1 // indirect
|
||||
|
||||
80
go.sum
80
go.sum
@@ -19,8 +19,8 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/domodwyer/mailyak/v3 v3.6.2 h1:x3tGMsyFhTCaxp6ycgR0FE/bu5QiNp+hetUuCOBXMn8=
|
||||
github.com/domodwyer/mailyak/v3 v3.6.2/go.mod h1:lOm/u9CyCVWHeaAmHIdF4RiKVxKUT/H5XX10lIKAL6c=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/dustin/go-humanize v1.1.0 h1:dbKTrvD0klcbBV/h4AWJdMuZogJACoMlvWIWZ5b2xWg=
|
||||
github.com/dustin/go-humanize v1.1.0/go.mod h1:hc1CvRkJMsgxqjmjMQF3QNRAZBwY8AXBAzKYoSX9sFI=
|
||||
github.com/ebitengine/purego v0.11.0 h1:jhp/D+Nyv7UUW8HAcmcjt2N2rYrYi9m3SL21k0Ua/NI=
|
||||
github.com/ebitengine/purego v0.11.0/go.mod h1:DCHPP08djqhNSoTfImcnHYQRZmd0qhakvrozqaEYhGQ=
|
||||
github.com/eclipse/paho.golang v0.23.0 h1:KHgl2wz6EJo7cMBmkuhpt7C576vP+kpPv7jjvSyR6Mk=
|
||||
@@ -31,8 +31,8 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho=
|
||||
github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo=
|
||||
github.com/fxamacker/cbor/v2 v2.9.3 h1:oQBnFATpNdY8gJHTndDDv5Xl4QqNaz51G5LLEPhng3Q=
|
||||
github.com/fxamacker/cbor/v2 v2.9.3/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/fxamacker/cbor/v2 v2.9.4 h1:xwjVlxEMR3S605oUlgBjKLTTeGFciYPGYCtF/35LKGo=
|
||||
github.com/fxamacker/cbor/v2 v2.9.4/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/gabriel-vasile/mimetype v1.4.15 h1:05iP/CYtZ/w455R/KZM6rZ5ieAdh99UPtd+d3YzLmaI=
|
||||
github.com/gabriel-vasile/mimetype v1.4.15/go.mod h1:azpTcoLcDZRNgFou5j+APrqQx9HqVPWa6ijYQIIVswQ=
|
||||
github.com/ganigeorgiev/fexpr v0.6.0 h1:Fza3O/QMBKEudUvxV862qe6GjxM60GJjjKytdp+VQus=
|
||||
@@ -77,18 +77,18 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/lufia/plan9stats v0.0.0-20260802145828-341c2f0c90b5 h1:eveIIGn4BGM3qknO74omf6HYr30/exH+eVUTuAgwjZ0=
|
||||
github.com/lufia/plan9stats v0.0.0-20260802145828-341c2f0c90b5/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
||||
github.com/lxzan/gws v1.10.1 h1:1xG+tDOV0lgDeVPf0wNT74u3cn0K3LpcavRrTPTrMwQ=
|
||||
github.com/lxzan/gws v1.10.1/go.mod h1:gXHSCPmTGryWJ4icuqy8Yho32E4YIMHH0fkDRYJRbdc=
|
||||
github.com/lxzan/gws v1.10.2 h1:htReTvcY89iMk1ScVtUbk6J96kIZWaafj6r/lasK/NA=
|
||||
github.com/lxzan/gws v1.10.2/go.mod h1:gXHSCPmTGryWJ4icuqy8Yho32E4YIMHH0fkDRYJRbdc=
|
||||
github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY=
|
||||
github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI=
|
||||
github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A=
|
||||
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/nicholas-fedor/shoutrrr v0.20.0 h1:hMAxIYlfAeZ1FcTDgU0kUOvVXUsOirWo8IWlnzGLkac=
|
||||
github.com/nicholas-fedor/shoutrrr v0.20.0/go.mod h1:hgde37yNWCXh8+N6WemyDRMNYLOFTf326GsBx8Z7CFA=
|
||||
github.com/onsi/ginkgo/v2 v2.32.1 h1:6tlvcDm/3sE8lGJbZ4+d4mO3RLy24/tQWOFzVSQNIfw=
|
||||
github.com/onsi/ginkgo/v2 v2.32.1/go.mod h1:+aXOY+vzZ5mu2iI2HpTZUPmM//oQfsNFX6gU9kNcA44=
|
||||
github.com/nicholas-fedor/shoutrrr v0.21.0 h1:as/mEwdaZMijCVu0FkTUEXashhvC3Y7C5g9dsXMcmQc=
|
||||
github.com/nicholas-fedor/shoutrrr v0.21.0/go.mod h1:dgg4kJv9K0tLXBH/1TXiSibNbM2hcd4SK6xb0sglyU4=
|
||||
github.com/onsi/ginkgo/v2 v2.32.2 h1:2o6vyFvR6snrJWgRVztC+OwuqqPEMI1UzYl2s2iU7Cg=
|
||||
github.com/onsi/ginkgo/v2 v2.32.2/go.mod h1:+aXOY+vzZ5mu2iI2HpTZUPmM//oQfsNFX6gU9kNcA44=
|
||||
github.com/onsi/gomega v1.43.0 h1:VlG/1FxqNxhSO+lq/OHBNaaqwiBK/mO8JbVkX9Y+FeU=
|
||||
github.com/onsi/gomega v1.43.0/go.mod h1:REff/hsDsodHoKlWsP2mAPhu1+5/6hVYNf9rIEBpeSg=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
@@ -98,10 +98,10 @@ github.com/pocketbase/dbx v1.12.0 h1:/oLErM+A0b4xI0PWTGPqSDVjzix48PqI/bng2l0PzoA
|
||||
github.com/pocketbase/dbx v1.12.0/go.mod h1:xXRCIAKTHMgUCyCKZm55pUOdvFziJjQfXaWKhu2vhMs=
|
||||
github.com/pocketbase/ozzo-validation/v4 v4.3.0 h1:uKBDVma7bZqgR2a6AwE+k9hkuDFfiZMpBHQdZ1z3iQs=
|
||||
github.com/pocketbase/ozzo-validation/v4 v4.3.0/go.mod h1:6XNjSTw/Jb2F8LOkKO3oyzIWExbrGiYoS4uVxVwz90g=
|
||||
github.com/pocketbase/pocketbase v0.40.2 h1:7gTqvt3bmilkphyZZ1QNhX19g3BXHqT7ynDyU81RVT4=
|
||||
github.com/pocketbase/pocketbase v0.40.2/go.mod h1:jc3YuyToy+ZXM4CeO7uSCN/htgR8yv+tjSE3eJZ8eh8=
|
||||
github.com/power-devops/perfstat v0.0.0-20260805114148-88456608a4f6 h1:jL3a8soXdzuTCcRnKhOmtcsVOObdDTFf4O2B403HPRU=
|
||||
github.com/power-devops/perfstat v0.0.0-20260805114148-88456608a4f6/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/pocketbase/pocketbase v0.40.4 h1:0SvSUreR3NhUMCs9LchE59oEG53efZ3cKiMGyAGBN9U=
|
||||
github.com/pocketbase/pocketbase v0.40.4/go.mod h1:2mU+80FLiY1fb13WZRg8Xx/lKg4nTjgKVJMigyRH6k0=
|
||||
github.com/power-devops/perfstat v0.0.0-20260916203055-22a1a467d9f0 h1:XA01Vk/wv9YikCi1V51yRzIHPMT5of9+cMpZoZvDn/M=
|
||||
github.com/power-devops/perfstat v0.0.0-20260916203055-22a1a467d9f0/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
@@ -136,37 +136,37 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
|
||||
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.56.0 h1:GUh5Ii4J5jtcseSMiRqr1jXCNHoxjeV9Fmekc2oLy6Y=
|
||||
golang.org/x/crypto v0.56.0/go.mod h1:OMW5y6CY9l38uPLmxU6l6pwcXp1obtLo3e6gT7gQR2I=
|
||||
golang.org/x/exp v0.0.0-20260824195058-e88cd73687aa h1:QSyA8ishJCyT21kER9KwNt0b7BM3iRK4x9QXhjN5Fdk=
|
||||
golang.org/x/exp v0.0.0-20260824195058-e88cd73687aa/go.mod h1:zeBbvyFKDaLwa7CH/zI8KXt7gTl14SF7sO08Pl5jBCM=
|
||||
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
|
||||
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
|
||||
golang.org/x/exp v0.0.0-20260908205506-85c1c2202aba h1:Ck8QetSgk912qxWLMCKxd0in+aiyBQyDSMae6e/xmpU=
|
||||
golang.org/x/exp v0.0.0-20260908205506-85c1c2202aba/go.mod h1:50RgIsmK7OwqzTTeqcSXQW8SswW0o8fRcDxmqGluJ8E=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.45.0 h1:FMb1nTbH5H9vF55SriQHgFw5GnNL9Jg6L25BwXKzhB0=
|
||||
golang.org/x/image v0.45.0/go.mod h1:n62x/7RqlwXDvGsSU4u6IUTUf6KghUZ9Bt7cG/T9Fx4=
|
||||
golang.org/x/mod v0.40.0 h1:hUv+3cXcdRHz08UmSiOob7sadHig73uo5bkXxQ/tvUs=
|
||||
golang.org/x/mod v0.40.0/go.mod h1:0/weTWkPWGBikyTWAX3dkjVztMmBA5hM0DH6BElSupE=
|
||||
golang.org/x/image v0.46.0 h1:b1+oYj0Jbp6K5MDT4i4/eZpYlk3V8SJhhDKh6LBHAyQ=
|
||||
golang.org/x/image v0.46.0/go.mod h1:3B3W05VGVQyuXucLINLjXKrqISASfi4Xj+iCVkLMwew=
|
||||
golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c=
|
||||
golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
|
||||
golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
|
||||
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
||||
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
||||
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
|
||||
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/net v0.59.0 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues=
|
||||
golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
|
||||
golang.org/x/oauth2 v0.37.0 h1:JUlcxA8oAtauLfiH8FX2/FkAWHAdi0QtGCGc+hofE98=
|
||||
golang.org/x/oauth2 v0.37.0/go.mod h1:IxwZNxUULJmpBFf9K/9NTMSIfZZuvuTy1gGxhigP/58=
|
||||
golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk=
|
||||
golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
|
||||
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
|
||||
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
|
||||
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
|
||||
golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE=
|
||||
golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
|
||||
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
|
||||
golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
|
||||
golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI=
|
||||
golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo=
|
||||
golang.org/x/tools v0.50.0 h1:c2ifzfcuY7L90lZ2aKd8S4K2NpASF08SZx9ZuJkHmSU=
|
||||
golang.org/x/tools v0.50.0/go.mod h1:7ulVMw3831Mwi5EZD6RomGyffr4VFjuNYXf2BbCEAV0=
|
||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
@@ -176,6 +176,14 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
howett.net/plist v1.0.1 h1:37GdZ8tP09Q35o9ych3ehygcsL+HqKSwzctveSlarvM=
|
||||
howett.net/plist v1.0.1/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
|
||||
mellium.im/reader v0.1.0 h1:UUEMev16gdvaxxZC7fC08j7IzuDKh310nB6BlwnxTww=
|
||||
mellium.im/reader v0.1.0/go.mod h1:F+X5HXpkIfJ9EE1zHQG9lM/hO946iYAmU7xjg5dsQHI=
|
||||
mellium.im/sasl v0.3.2 h1:PT6Xp7ccn9XaXAnJ03FcEjmAn7kK1x7aoXV6F+Vmrl0=
|
||||
mellium.im/sasl v0.3.2/go.mod h1:NKXDi1zkr+BlMHLQjY3ofYuU4KSPFxknb8mfEu6SveY=
|
||||
mellium.im/xmlstream v0.15.4 h1:gLKxcWl4rLMUpKgtzrTBvr4OexPeO/edYus+uK3F6ZI=
|
||||
mellium.im/xmlstream v0.15.4/go.mod h1:yXaCW2++fmVO4L9piKVkyLDqnCmictVYF7FDQW8prb4=
|
||||
mellium.im/xmpp v0.23.0 h1:rvKvOvMdIURCLaAWEJN8J0QpO3AJYCDKjxLsqtTPSjY=
|
||||
mellium.im/xmpp v0.23.0/go.mod h1:GHDKlKKQe0LNmD9YqExyxnFEEBiz84KGqnfiA2VNzb8=
|
||||
modernc.org/cc/v4 v4.29.1 h1:MKgdCV3WykTSPqpVrnxdEDS0HEd2FHpKZDzxzU5LyeI=
|
||||
modernc.org/cc/v4 v4.29.1/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
|
||||
modernc.org/ccgo/v4 v4.34.6 h1:sBgfIwyN0TQ9C5hwIeuqyeAKyMWnbvj2fvpF4L11uzU=
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"testing/synctest"
|
||||
|
||||
beszelTests "github.com/henrygd/beszel/internal/tests"
|
||||
pbTests "github.com/pocketbase/pocketbase/tests"
|
||||
@@ -533,6 +534,20 @@ func TestSendTestNotification(t *testing.T) {
|
||||
|
||||
for _, url := range []string{localURL, "smtp://user:pass@127.0.0.1/?fromAddress=sender@example.com&toAddresses=recipient@example.com", "mqtt://127.0.0.1/topic"} {
|
||||
scenarios = append(scenarios, beszelTests.ApiScenario{
|
||||
BeforeTestFunc: func(tb testing.TB, _ *pbTests.TestApp, e *core.ServeEvent) {
|
||||
if !strings.HasPrefix(url, "mqtt://") {
|
||||
return
|
||||
}
|
||||
// Keep the real MQTT rejection path, but advance its library's
|
||||
// fixed timeout using virtual time instead of waiting 10 seconds.
|
||||
e.Router.BindFunc(func(re *core.RequestEvent) error {
|
||||
var err error
|
||||
synctest.Test(tb.(*testing.T), func(t *testing.T) {
|
||||
err = re.Next()
|
||||
})
|
||||
return err
|
||||
})
|
||||
},
|
||||
Name: "readonly cannot send to " + url,
|
||||
Method: http.MethodPost,
|
||||
URL: "/api/beszel/test-notification",
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"testing/synctest"
|
||||
|
||||
"github.com/nicholas-fedor/shoutrrr/pkg/types"
|
||||
"golang.org/x/net/dns/dnsmessage"
|
||||
@@ -178,39 +179,45 @@ func TestPublicNotificationTCP(t *testing.T) {
|
||||
} {
|
||||
t.Run(rawURL, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
// MQTT waits for a fixed library timeout even after a dial failure.
|
||||
// Virtual time preserves the full send/cleanup path without that delay.
|
||||
t.Run("internal destination", func(t *testing.T) {
|
||||
err := sendPublicNotification(strings.ReplaceAll(rawURL, "HOST", "127.0.0.1"), "test")
|
||||
if !errors.Is(err, errInternalDestination) {
|
||||
t.Fatalf("expected blocked destination, got %v", err)
|
||||
}
|
||||
synctest.Test(t, func(t *testing.T) {
|
||||
err := sendPublicNotification(strings.ReplaceAll(rawURL, "HOST", "127.0.0.1"), "test")
|
||||
if !errors.Is(err, errInternalDestination) {
|
||||
t.Fatalf("expected blocked destination, got %v", err)
|
||||
}
|
||||
})
|
||||
})
|
||||
t.Run("public destination uses injected dialer", func(t *testing.T) {
|
||||
var calls atomic.Int32
|
||||
stopped := errors.New("test dial stopped")
|
||||
service, err := newPublicNotificationService(strings.ReplaceAll(rawURL, "HOST", "8.8.8.8"), types.SenderOptions{
|
||||
DialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
calls.Add(1)
|
||||
if network != "tcp" || !strings.HasPrefix(address, "8.8.8.8:") {
|
||||
t.Errorf("unexpected dial: %s %s", network, address)
|
||||
}
|
||||
if err := checkNotificationAddress(address); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
return nil, stopped
|
||||
},
|
||||
synctest.Test(t, func(t *testing.T) {
|
||||
var calls atomic.Int32
|
||||
stopped := errors.New("test dial stopped")
|
||||
service, err := newPublicNotificationService(strings.ReplaceAll(rawURL, "HOST", "8.8.8.8"), types.SenderOptions{
|
||||
DialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
calls.Add(1)
|
||||
if network != "tcp" || !strings.HasPrefix(address, "8.8.8.8:") {
|
||||
t.Errorf("unexpected dial: %s %s", network, address)
|
||||
}
|
||||
if err := checkNotificationAddress(address); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
return nil, stopped
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if closer, ok := service.(io.Closer); ok {
|
||||
defer closer.Close()
|
||||
}
|
||||
if err := service.Send("test", &types.Params{}); err == nil {
|
||||
t.Fatal("expected dial failure")
|
||||
}
|
||||
if calls.Load() == 0 {
|
||||
t.Fatal("custom dialer was not used")
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if closer, ok := service.(io.Closer); ok {
|
||||
defer closer.Close()
|
||||
}
|
||||
if err := service.Send("test", &types.Params{}); err == nil {
|
||||
t.Fatal("expected dial failure")
|
||||
}
|
||||
if calls.Load() == 0 {
|
||||
t.Fatal("custom dialer was not used")
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ RUN apk add --no-cache ca-certificates && update-ca-certificates
|
||||
|
||||
# Build
|
||||
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 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags "-w -s" -o /agent ./internal/cmd/agent
|
||||
|
||||
RUN rm -rf /tmp/*
|
||||
|
||||
|
||||
@@ -10,14 +10,14 @@ COPY . ./
|
||||
|
||||
# Build
|
||||
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 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags "-w -s" -o /agent ./internal/cmd/agent
|
||||
|
||||
RUN rm -rf /tmp/*
|
||||
|
||||
# --------------------------
|
||||
# Final image: default scratch-based agent
|
||||
# --------------------------
|
||||
FROM alpine:3.23
|
||||
FROM alpine:3.24
|
||||
COPY --from=builder /agent /agent
|
||||
|
||||
# AMD GPU name lookup (used by agent on Linux when /usr/share/libdrm/amdgpu.ids is read)
|
||||
@@ -28,4 +28,4 @@ RUN apk add --no-cache smartmontools zfs
|
||||
# Ensure data persistence across container recreations
|
||||
VOLUME ["/var/lib/beszel-agent"]
|
||||
|
||||
ENTRYPOINT ["/agent"]
|
||||
ENTRYPOINT ["/agent"]
|
||||
|
||||
@@ -10,13 +10,13 @@ COPY . ./
|
||||
|
||||
# Build
|
||||
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 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags "-w -s" -o /agent ./internal/cmd/agent
|
||||
|
||||
# --------------------------
|
||||
# Final image
|
||||
# Note: must cap_add: [CAP_PERFMON] and mount /dev/dri/ as volume
|
||||
# --------------------------
|
||||
FROM alpine:3.23
|
||||
FROM alpine:3.24
|
||||
|
||||
COPY --from=builder /agent /agent
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ COPY . ./
|
||||
|
||||
# Build
|
||||
ARG TARGETOS TARGETARCH
|
||||
RUN CGO_ENABLED=0 GOGC=75 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -tags glibc -ldflags "-w -s" -o /agent ./internal/cmd/agent
|
||||
RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -tags glibc -ldflags "-w -s" -o /agent ./internal/cmd/agent
|
||||
|
||||
# --------------------------
|
||||
# Smartmontools builder stage
|
||||
|
||||
@@ -17,7 +17,7 @@ RUN set -eux; \
|
||||
if [ "$TARGETARCH" = "arm" ] && [ -n "$TARGETVARIANT" ]; then \
|
||||
export GOARM="${TARGETVARIANT#v}"; \
|
||||
fi; \
|
||||
CGO_ENABLED=0 GOGC=75 GOOS=$TARGETOS GOARCH=$TARGETARCH \
|
||||
CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH \
|
||||
go build -tags glibc -ldflags "-w -s" -o /agent ./internal/cmd/agent
|
||||
|
||||
# --------------------------
|
||||
@@ -70,7 +70,9 @@ RUN set -eux; \
|
||||
# --------------------------
|
||||
FROM --platform=$TARGETPLATFORM debian:bookworm-slim AS zfsutils-builder
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
# zfsutils-linux is distributed in Debian's contrib component.
|
||||
RUN sed -i 's/Components: main/Components: main contrib/' /etc/apt/sources.list.d/debian.sources \
|
||||
&& apt-get update && apt-get install -y --no-install-recommends \
|
||||
zfsutils-linux \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ RUN update-ca-certificates
|
||||
|
||||
# Build
|
||||
ARG TARGETOS TARGETARCH
|
||||
RUN CGO_ENABLED=0 GOGC=75 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags "-w -s" -o /beszel ./internal/cmd/hub
|
||||
RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags "-w -s" -o /beszel ./internal/cmd/hub
|
||||
|
||||
# ? -------------------------
|
||||
FROM scratch
|
||||
@@ -31,4 +31,4 @@ VOLUME ["/beszel_data"]
|
||||
EXPOSE 8090
|
||||
|
||||
ENTRYPOINT [ "/beszel" ]
|
||||
CMD ["serve", "--http=0.0.0.0:8090"]
|
||||
CMD ["serve", "--http=0.0.0.0:8090"]
|
||||
|
||||
@@ -978,11 +978,18 @@ func TestAgentWebSocketIntegration(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Verify system status
|
||||
updatedSystemRecord, err := testApp.FindRecordById("systems", systemRecord.Id)
|
||||
require.NoError(t, err)
|
||||
status := updatedSystemRecord.GetString("status")
|
||||
assert.Equal(t, tc.expectSystemStatus, status, "System status should match expected value")
|
||||
// A connected WebSocket does not mean the hub has finished verifying
|
||||
// the agent and updating the system. Wait for the database state rather
|
||||
// than assuming that work completes within a fixed sleep under load.
|
||||
var status string
|
||||
require.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
updatedSystemRecord, err := testApp.FindRecordById("systems", systemRecord.Id)
|
||||
if !assert.NoError(c, err) {
|
||||
return
|
||||
}
|
||||
status = updatedSystemRecord.GetString("status")
|
||||
assert.Equal(c, tc.expectSystemStatus, status, "System status should match expected value")
|
||||
}, 5*time.Second, 20*time.Millisecond)
|
||||
|
||||
t.Logf("%s - System status: %s, Fingerprint: %s", tc.description, status, finalFingerprint)
|
||||
})
|
||||
@@ -1142,42 +1149,43 @@ func TestMultipleSystemsWithSameUniversalToken(t *testing.T) {
|
||||
|
||||
// Verify system creation/reuse behavior
|
||||
if tc.expectConnection {
|
||||
// Count systems after connection
|
||||
systemsAfter, err := testApp.FindRecordsByFilter("systems", "users ~ {:userId}", "", -1, 0, map[string]any{"userId": userRecord.Id})
|
||||
require.NoError(t, err)
|
||||
systemsAfterCount := len(systemsAfter)
|
||||
|
||||
expectedSystemsAfter := systemsBeforeCount
|
||||
if tc.expectNewSystem {
|
||||
// Should have created a new system
|
||||
expectedSystemsAfter++
|
||||
systemCount++
|
||||
assert.Equal(t, systemsBeforeCount+1, systemsAfterCount, "Should have created a new system")
|
||||
assert.Equal(t, systemCount, systemsAfterCount, "Total system count should match expected")
|
||||
} else {
|
||||
// Should have reused existing system
|
||||
assert.Equal(t, systemsBeforeCount, systemsAfterCount, "Should not have created a new system")
|
||||
assert.Equal(t, systemCount, systemsAfterCount, "Total system count should remain the same")
|
||||
}
|
||||
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
// WebSocket connection precedes the hub's asynchronous system
|
||||
// setup. Re-read all database state until setup is complete.
|
||||
var systemId, status string
|
||||
require.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
systemsAfter, err := testApp.FindRecordsByFilter("systems", "users ~ {:userId}", "", -1, 0, map[string]any{"userId": userRecord.Id})
|
||||
if !assert.NoError(c, err) {
|
||||
return
|
||||
}
|
||||
assert.Len(c, systemsAfter, expectedSystemsAfter, "System creation/reuse should match expected behavior")
|
||||
assert.Len(c, systemsAfter, systemCount, "Total system count should match expected")
|
||||
|
||||
// Verify that a fingerprint record exists for this fingerprint
|
||||
fingerprints, err := testApp.FindRecordsByFilter("fingerprints", "token = {:token} && fingerprint = {:fingerprint}", "", -1, 0, map[string]any{
|
||||
"token": universalToken,
|
||||
"fingerprint": tc.agentFingerprint,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, fingerprints, 1, "Should have exactly one fingerprint record for this token+fingerprint combination")
|
||||
fingerprints, err := testApp.FindRecordsByFilter("fingerprints", "token = {:token} && fingerprint = {:fingerprint}", "", -1, 0, map[string]any{
|
||||
"token": universalToken,
|
||||
"fingerprint": tc.agentFingerprint,
|
||||
})
|
||||
if !assert.NoError(c, err) || !assert.Len(c, fingerprints, 1, "Should have exactly one fingerprint record for this token+fingerprint combination") {
|
||||
return
|
||||
}
|
||||
|
||||
fingerprint := fingerprints[0]
|
||||
assert.Equal(t, universalToken, fingerprint.GetString("token"), "Fingerprint should have the universal token")
|
||||
assert.Equal(t, tc.agentFingerprint, fingerprint.GetString("fingerprint"), "Fingerprint should match agent's fingerprint")
|
||||
fingerprint := fingerprints[0]
|
||||
assert.Equal(c, universalToken, fingerprint.GetString("token"), "Fingerprint should have the universal token")
|
||||
assert.Equal(c, tc.agentFingerprint, fingerprint.GetString("fingerprint"), "Fingerprint should match agent's fingerprint")
|
||||
|
||||
// Verify system status
|
||||
systemId := fingerprint.GetString("system")
|
||||
system, err := testApp.FindRecordById("systems", systemId)
|
||||
require.NoError(t, err)
|
||||
status := system.GetString("status")
|
||||
assert.Equal(t, tc.expectSystemStatus, status, "System status should match expected value")
|
||||
systemId = fingerprint.GetString("system")
|
||||
system, err := testApp.FindRecordById("systems", systemId)
|
||||
if !assert.NoError(c, err) {
|
||||
return
|
||||
}
|
||||
status = system.GetString("status")
|
||||
assert.Equal(c, tc.expectSystemStatus, status, "System status should match expected value")
|
||||
}, 5*time.Second, 20*time.Millisecond)
|
||||
|
||||
t.Logf("%s - System ID: %s, Status: %s, New System: %v", tc.description, systemId, status, tc.expectNewSystem)
|
||||
}
|
||||
|
||||
@@ -106,6 +106,7 @@ func (h *Hub) StartHub() error {
|
||||
|
||||
// TODO: move to users package
|
||||
// handle default values for user / user_settings creation
|
||||
h.App.OnRecordAuthWithOAuth2Request("users").BindFunc(h.um.InitializeOAuthUserRole)
|
||||
h.App.OnRecordCreate("users").BindFunc(h.um.InitializeUserRole)
|
||||
h.App.OnRecordCreate("user_settings").BindFunc(h.um.InitializeUserSettings)
|
||||
|
||||
|
||||
159
internal/hub/systems/network_monitor_ssh_test.go
Normal file
159
internal/hub/systems/network_monitor_ssh_test.go
Normal file
@@ -0,0 +1,159 @@
|
||||
//go:build testing
|
||||
|
||||
package systems
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"net"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/fxamacker/cbor/v2"
|
||||
"github.com/henrygd/beszel/internal/common"
|
||||
"github.com/henrygd/beszel/internal/entities/monitor"
|
||||
esystem "github.com/henrygd/beszel/internal/entities/system"
|
||||
"github.com/henrygd/beszel/internal/hub/expirymap"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
func TestSSHNetworkMonitorReconnectSync(t *testing.T) {
|
||||
sys, app := newTestSystemWithHub(t)
|
||||
sys.manager.zfsFetchMap = expirymap.New[zfsFetchState](time.Hour)
|
||||
t.Cleanup(sys.manager.zfsFetchMap.StopCleaner)
|
||||
sys.ctx = context.Background()
|
||||
sys.Status = up
|
||||
_, key, err := ed25519.GenerateKey(rand.Reader)
|
||||
require.NoError(t, err)
|
||||
signer, err := ssh.NewSignerFromKey(key)
|
||||
require.NoError(t, err)
|
||||
config := &ssh.ServerConfig{NoClientAuth: true, ServerVersion: "SSH-2.0-beszel_0.20.0"}
|
||||
config.AddHostKey(signer)
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = listener.Close() })
|
||||
sys.Host, sys.Port, err = net.SplitHostPort(listener.Addr().String())
|
||||
require.NoError(t, err)
|
||||
sys.manager.sshConfig = &ssh.ClientConfig{User: "test", HostKeyCallback: ssh.InsecureIgnoreHostKey(), Timeout: time.Second}
|
||||
t.Cleanup(sys.closeSSHConnection)
|
||||
requests := make(chan monitor.SyncRequest, 10)
|
||||
var failSync atomic.Bool
|
||||
go func() {
|
||||
for {
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
server, channels, reqs, err := ssh.NewServerConn(conn, config)
|
||||
if err != nil {
|
||||
_ = conn.Close()
|
||||
return
|
||||
}
|
||||
defer server.Close()
|
||||
go ssh.DiscardRequests(reqs)
|
||||
for channel := range channels {
|
||||
ch, reqs, err := channel.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
defer ch.Close()
|
||||
for req := range reqs {
|
||||
if req.Type != "shell" {
|
||||
_ = req.Reply(false, nil)
|
||||
continue
|
||||
}
|
||||
_ = req.Reply(true, nil)
|
||||
var request common.HubRequest[cbor.RawMessage]
|
||||
if cbor.NewDecoder(ch).Decode(&request) != nil {
|
||||
return
|
||||
}
|
||||
response := common.AgentResponse{}
|
||||
switch request.Action {
|
||||
case common.GetData:
|
||||
response.SystemData = &esystem.CombinedData{}
|
||||
case common.SyncNetworkMonitors:
|
||||
var syncReq monitor.SyncRequest
|
||||
if cbor.Unmarshal(request.Data, &syncReq) != nil {
|
||||
return
|
||||
}
|
||||
requests <- syncReq
|
||||
if failSync.Load() {
|
||||
response.Error = "test sync failure"
|
||||
} else {
|
||||
response.Data, _ = cbor.Marshal(monitor.SyncResponse{})
|
||||
}
|
||||
}
|
||||
_ = cbor.NewEncoder(ch).Encode(response)
|
||||
_, _ = ch.SendRequest("exit-status", false, ssh.Marshal(struct{ Status uint32 }{0}))
|
||||
return
|
||||
}
|
||||
}()
|
||||
}
|
||||
}()
|
||||
}
|
||||
}()
|
||||
collection, err := app.FindCachedCollectionByNameOrId("network_monitors")
|
||||
require.NoError(t, err)
|
||||
probe := core.NewRecord(collection)
|
||||
probe.Load(map[string]any{"system": sys.Id, "target": "localhost", "protocol": "tcp", "port": 80, "interval": 60, "enabled": true})
|
||||
require.NoError(t, app.SaveNoValidate(probe))
|
||||
fetch := func() {
|
||||
t.Helper()
|
||||
_, err := sys.fetchDataFromAgent(common.DataRequestOptions{})
|
||||
require.NoError(t, err, "monitor sync failure must not fail stats fetching")
|
||||
}
|
||||
receive := func() monitor.SyncRequest {
|
||||
t.Helper()
|
||||
select {
|
||||
case req := <-requests:
|
||||
require.Equal(t, monitor.SyncActionReplace, req.Action)
|
||||
return req
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("missing full monitor sync")
|
||||
return monitor.SyncRequest{}
|
||||
}
|
||||
}
|
||||
fetch()
|
||||
require.Equal(t, probe.Id, receive().Configs[0].ID)
|
||||
require.False(t, sys.monitorsNeedSync.Load())
|
||||
fetch()
|
||||
require.Empty(t, requests, "steady-state fetch must not resync")
|
||||
|
||||
// Simulate loss of the agent process/connection and its in-memory monitors.
|
||||
require.NoError(t, sys.client.Load().Close())
|
||||
fetch()
|
||||
require.Equal(t, probe.Id, receive().Configs[0].ID)
|
||||
require.False(t, sys.monitorsNeedSync.Load())
|
||||
|
||||
// Failed replacements are retried on the next successful stats fetch.
|
||||
require.NoError(t, sys.client.Load().Close())
|
||||
failSync.Store(true)
|
||||
fetch()
|
||||
receive()
|
||||
require.True(t, sys.monitorsNeedSync.Load())
|
||||
failSync.Store(false)
|
||||
fetch()
|
||||
receive()
|
||||
require.False(t, sys.monitorsNeedSync.Load())
|
||||
|
||||
probe.Set("enabled", false)
|
||||
require.NoError(t, app.SaveNoValidate(probe))
|
||||
require.NoError(t, sys.client.Load().Close())
|
||||
fetch()
|
||||
require.Empty(t, receive().Configs, "empty replacement must clear stale monitors")
|
||||
}
|
||||
|
||||
func TestPendingNetworkMonitorSyncQueryFailure(t *testing.T) {
|
||||
sys, app := newTestSystemWithHub(t)
|
||||
_, err := app.DB().NewQuery("DROP TABLE network_monitors").Execute()
|
||||
require.NoError(t, err)
|
||||
sys.monitorsNeedSync.Store(true)
|
||||
sys.syncPendingNetworkMonitors()
|
||||
require.True(t, sys.monitorsNeedSync.Load())
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -13,6 +14,7 @@ import (
|
||||
"github.com/fxamacker/cbor/v2"
|
||||
"github.com/henrygd/beszel/internal/common"
|
||||
"github.com/henrygd/beszel/internal/entities/monitor"
|
||||
esystem "github.com/henrygd/beszel/internal/entities/system"
|
||||
"github.com/henrygd/beszel/internal/hub/ws"
|
||||
"github.com/lxzan/gws"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
@@ -22,17 +24,31 @@ import (
|
||||
type monitorSyncClient struct {
|
||||
gws.BuiltinEventHandler
|
||||
requests chan common.HubRequest[monitor.SyncRequest]
|
||||
failSync atomic.Bool
|
||||
}
|
||||
|
||||
func (c *monitorSyncClient) OnMessage(conn *gws.Conn, message *gws.Message) {
|
||||
defer message.Close()
|
||||
var req common.HubRequest[monitor.SyncRequest]
|
||||
var req common.HubRequest[cbor.RawMessage]
|
||||
if err := cbor.Unmarshal(message.Bytes(), &req); err != nil {
|
||||
return
|
||||
}
|
||||
c.requests <- req
|
||||
data, _ := cbor.Marshal(monitor.SyncResponse{})
|
||||
response, _ := cbor.Marshal(common.AgentResponse{Id: req.Id, Data: data})
|
||||
resp := common.AgentResponse{Id: req.Id}
|
||||
if req.Action == common.GetData {
|
||||
resp.SystemData = &esystem.CombinedData{}
|
||||
} else {
|
||||
var data monitor.SyncRequest
|
||||
if err := cbor.Unmarshal(req.Data, &data); err != nil {
|
||||
return
|
||||
}
|
||||
c.requests <- common.HubRequest[monitor.SyncRequest]{Id: req.Id, Action: req.Action, Data: data}
|
||||
if c.failSync.Load() {
|
||||
resp.Error = "test sync failure"
|
||||
} else {
|
||||
resp.Data, _ = cbor.Marshal(monitor.SyncResponse{})
|
||||
}
|
||||
}
|
||||
response, _ := cbor.Marshal(resp)
|
||||
_ = conn.WriteMessage(gws.OpcodeBinary, response)
|
||||
}
|
||||
|
||||
@@ -57,7 +73,7 @@ func TestNetworkMonitorSyncSkipsOlderAgents(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNetworkMonitorReconnectSync(t *testing.T) {
|
||||
for _, change := range []string{"delete", "disable"} {
|
||||
for _, change := range []string{"delete", "disable", "retry"} {
|
||||
t.Run(change, func(t *testing.T) {
|
||||
sys, app := newTestSystemWithHub(t)
|
||||
record, err := app.FindRecordById("systems", sys.Id)
|
||||
@@ -120,9 +136,33 @@ func TestNetworkMonitorReconnectSync(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
client.failSync.Store(change == "retry")
|
||||
initial := connect()
|
||||
require.Len(t, initial.Configs, 1)
|
||||
require.Equal(t, probe.Id, initial.Configs[0].ID)
|
||||
if change == "retry" {
|
||||
system, err := sm.GetSystem(sys.Id)
|
||||
require.NoError(t, err)
|
||||
require.Eventually(t, system.monitorsNeedSync.Load, time.Second, time.Millisecond)
|
||||
// A second failed sync must not fail the stats fetch or clear pending state.
|
||||
_, err = system.fetchDataFromAgent(common.DataRequestOptions{})
|
||||
require.NoError(t, err)
|
||||
require.True(t, system.monitorsNeedSync.Load())
|
||||
require.Len(t, client.requests, 1)
|
||||
<-client.requests
|
||||
client.failSync.Store(false)
|
||||
_, err = system.fetchDataFromAgent(common.DataRequestOptions{})
|
||||
require.NoError(t, err)
|
||||
require.False(t, system.monitorsNeedSync.Load())
|
||||
require.Len(t, client.requests, 1)
|
||||
retry := <-client.requests
|
||||
require.Equal(t, monitor.SyncActionReplace, retry.Data.Action)
|
||||
require.Equal(t, initial.Configs, retry.Data.Configs)
|
||||
_, err = system.fetchDataFromAgent(common.DataRequestOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, client.requests, "successful sync must not repeat on every fetch")
|
||||
return
|
||||
}
|
||||
require.NoError(t, sm.RemoveSystem(sys.Id))
|
||||
if change == "delete" {
|
||||
require.NoError(t, app.Delete(probe))
|
||||
|
||||
@@ -2,6 +2,7 @@ package systems
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/henrygd/beszel"
|
||||
@@ -9,6 +10,27 @@ import (
|
||||
"github.com/henrygd/beszel/internal/entities/monitor"
|
||||
)
|
||||
|
||||
// syncPendingNetworkMonitors runs on WebSocket connect and after successful stats
|
||||
// fetches. Failed syncs retry on the next update without taking the system down.
|
||||
func (sys *System) syncPendingNetworkMonitors() {
|
||||
if !sys.monitorsNeedSync.Swap(false) {
|
||||
return
|
||||
}
|
||||
if err := sys.syncAllNetworkMonitors(); err != nil {
|
||||
sys.monitorsNeedSync.Store(true)
|
||||
sys.manager.hub.Logger().Warn("failed to sync monitors to agent", "system", sys.Id, "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (sys *System) syncAllNetworkMonitors() error {
|
||||
configs, err := sys.manager.GetMonitorConfigsForSystem(sys.Id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load monitors: %w", err)
|
||||
}
|
||||
// An empty set must also replace probes retained across a disconnect.
|
||||
return sys.SyncNetworkMonitors(configs)
|
||||
}
|
||||
|
||||
// SyncNetworkMonitors sends monitor configurations to the agent.
|
||||
func (sys *System) SyncNetworkMonitors(configs []monitor.Config) error {
|
||||
_, err := sys.syncNetworkMonitors(monitor.SyncRequest{Action: monitor.SyncActionReplace, Configs: configs})
|
||||
|
||||
@@ -56,6 +56,9 @@ type System struct {
|
||||
smartInterval time.Duration // Interval for periodic SMART data updates
|
||||
zfsFetching atomic.Bool // True if ZFS pools are currently being fetched
|
||||
zfsInterval time.Duration // Interval for periodic ZFS detail data updates
|
||||
|
||||
// A fresh connection needs a full monitor configuration sync.
|
||||
monitorsNeedSync atomic.Bool
|
||||
// Serialize persistence from scheduled updates and resumes through commit.
|
||||
recordsMu sync.Mutex
|
||||
// Protected by recordsMu; realtime reads don't consume probes.
|
||||
@@ -630,7 +633,10 @@ func (sys *System) request(ctx context.Context, action common.WebSocketAction, r
|
||||
err := sys.sshTransport.RequestWithRetry(ctx, action, req, dest, 1)
|
||||
// Keep legacy SSH client/version fields in sync for other code paths.
|
||||
if sys.sshTransport != nil {
|
||||
sys.client.Store(sys.sshTransport.GetClient())
|
||||
client := sys.sshTransport.GetClient()
|
||||
if previous := sys.client.Swap(client); client != nil && client != previous {
|
||||
sys.monitorsNeedSync.Store(true)
|
||||
}
|
||||
sys.agentVersion = sys.sshTransport.GetAgentVersion()
|
||||
}
|
||||
return err
|
||||
@@ -688,6 +694,7 @@ func (sys *System) fetchDataFromAgent(options common.DataRequestOptions) (*syste
|
||||
if sys.WsConn != nil && sys.WsConn.IsConnected() {
|
||||
wsData, err := sys.fetchDataViaWebSocket(options)
|
||||
if err == nil {
|
||||
sys.syncPendingNetworkMonitors()
|
||||
return wsData, nil
|
||||
}
|
||||
// close the WebSocket connection if error and try SSH
|
||||
@@ -698,6 +705,7 @@ func (sys *System) fetchDataFromAgent(options common.DataRequestOptions) (*syste
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sys.syncPendingNetworkMonitors()
|
||||
return sshData, nil
|
||||
}
|
||||
|
||||
@@ -932,6 +940,7 @@ func (s *System) createSSHClient() error {
|
||||
return err
|
||||
}
|
||||
s.agentVersion, _ = extractAgentVersion(string(client.Conn.ServerVersion()))
|
||||
s.monitorsNeedSync.Store(true)
|
||||
s.manager.resetFailedSmartFetchState(s.Id)
|
||||
s.manager.resetFailedZfsFetchState(s.Id)
|
||||
return nil
|
||||
|
||||
@@ -349,23 +349,14 @@ func (sm *SystemManager) AddWebSocketSystem(systemId string, agentVersion semver
|
||||
system := sm.NewSystem(systemId)
|
||||
system.WsConn = wsConn
|
||||
system.agentVersion = agentVersion
|
||||
system.monitorsNeedSync.Store(true)
|
||||
|
||||
if err := sm.AddRecord(systemRecord, system); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Sync network monitors to the newly connected agent
|
||||
go func() {
|
||||
configs, err := sm.GetMonitorConfigsForSystem(systemId)
|
||||
if err != nil {
|
||||
sm.hub.Logger().Warn("failed to load monitors for agent", "system", systemId, "err", err)
|
||||
return
|
||||
}
|
||||
// An empty set must also replace any probes retained across a disconnect.
|
||||
if err := system.SyncNetworkMonitors(configs); err != nil {
|
||||
sm.hub.Logger().Warn("failed to sync monitors to agent", "system", systemId, "err", err)
|
||||
}
|
||||
}()
|
||||
go system.syncPendingNetworkMonitors()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1057,6 +1057,15 @@ func init() {
|
||||
"required": true,
|
||||
"system": false,
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "bool2084032502",
|
||||
"name": "updatable",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
@@ -1,27 +0,0 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(app core.App) error {
|
||||
c, err := app.FindCollectionByNameOrId("zfs_pools")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.Fields.Add(&core.TextField{Name: "display_name"})
|
||||
c.Fields.Add(&core.BoolField{Name: "raw"})
|
||||
return app.Save(c)
|
||||
}, func(app core.App) error {
|
||||
c, err := app.FindCollectionByNameOrId("zfs_pools")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.Fields.RemoveByName("display_name")
|
||||
c.Fields.RemoveByName("raw")
|
||||
|
||||
return app.Save(c)
|
||||
})
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("containers")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
collection.Fields.Add(&core.BoolField{Name: "updatable"})
|
||||
return app.Save(collection)
|
||||
}, func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("containers")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
collection.Fields.RemoveByName("updatable")
|
||||
return app.Save(collection)
|
||||
})
|
||||
}
|
||||
@@ -59,7 +59,11 @@ export const containerChartCols: ColumnDef<ContainerRecord>[] = [
|
||||
const allSystems = $allSystemsById.get()
|
||||
const systemNameA = allSystems[a.original.system]?.name ?? ""
|
||||
const systemNameB = allSystems[b.original.system]?.name ?? ""
|
||||
return systemNameA.localeCompare(systemNameB)
|
||||
const primary = systemNameA.localeCompare(systemNameB)
|
||||
if (primary !== 0) {
|
||||
return primary
|
||||
}
|
||||
return a.original.name.localeCompare(b.original.name)
|
||||
},
|
||||
header: ({ column }) => <HeaderButton column={column} name={t`System`} Icon={ServerIcon} />,
|
||||
cell: ({ getValue }) => {
|
||||
@@ -192,12 +196,12 @@ export const containerChartCols: ColumnDef<ContainerRecord>[] = [
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
className="shrink-0 rounded-sm text-emerald-600 dark:text-emerald-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
aria-label={t`Image update available`}
|
||||
aria-label={t({ message: "Image update available", context: "Docker image" })}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
<CircleArrowUpIcon className="size-4" aria-hidden="true" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{t`Image update available`}</TooltipContent>
|
||||
<TooltipContent>{t({ message: "Image update available", context: "Docker image" })}</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -242,7 +242,7 @@ function SystemMultiSelect({
|
||||
? t`Select systems`
|
||||
: selectedSystemIds.size === 1
|
||||
? systems.find((s) => selectedSystemIds.has(s.id))?.name
|
||||
: t`${selectedSystemIds.size} systems selected`}
|
||||
: t`${selectedSystemIds.size} selected`}
|
||||
</span>
|
||||
<ChevronDownIcon className="size-4 absolute end-4 top-1/2 -translate-y-1/2 opacity-50" />
|
||||
</Button>
|
||||
@@ -450,7 +450,7 @@ export function AddMonitorDialog({ systemId, monitors }: { systemId?: string; mo
|
||||
<div className="w-px h-full bg-muted"></div>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" className="px-2 rounded-s-none border-s-0" aria-label={t`More monitor actions`}>
|
||||
<Button variant="outline" className="px-2 rounded-s-none border-s-0" aria-label={`More actions`}>
|
||||
<ChevronDownIcon className="size-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
@@ -759,13 +759,7 @@ function MonitorDialogContent({
|
||||
type="submit"
|
||||
disabled={loading || (!systemId && (isEditing ? !selectedSystemId : !selectedSystemIds.size))}
|
||||
>
|
||||
{loading ? (
|
||||
isEditing ? (
|
||||
<Trans>Saving...</Trans>
|
||||
) : (
|
||||
<Trans>Creating...</Trans>
|
||||
)
|
||||
) : isEditing ? (
|
||||
{isEditing ? (
|
||||
<Trans>Save {{ foo: t`Monitor` }}</Trans>
|
||||
) : (
|
||||
<Trans>Add {{ foo: t`Monitor` }}</Trans>
|
||||
|
||||
@@ -254,7 +254,6 @@ export default function NetworkMonitorsTableNew({
|
||||
<div className="relative">
|
||||
<Input
|
||||
placeholder={t`Filter...`}
|
||||
title={t`Use commas to match any of multiple terms, e.g. "system1, system2"`}
|
||||
value={globalFilter}
|
||||
onChange={(e) => setGlobalFilter(e.target.value)}
|
||||
className="ms-auto px-4 w-full max-w-full md:w-50"
|
||||
|
||||
@@ -211,7 +211,6 @@ export default memo(function SystemDetail({ id }: { id: string }) {
|
||||
<TemperatureChart {...coreProps} setPageBottomExtraMargin={setPageBottomExtraMargin} />
|
||||
<FanChart {...coreProps} />
|
||||
<BatteryChart system={system} {...coreProps} />
|
||||
<SwapChart chartData={chartData} grid={grid} dataEmpty={dataEmpty} systemStats={systemStats} />
|
||||
{pageBottomExtraMargin > 0 && <div style={{ marginBottom: pageBottomExtraMargin }}></div>}
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
@@ -44,7 +44,6 @@ export function FilterBar({ store = $containerFilter }: { store?: typeof $contai
|
||||
<>
|
||||
<Input
|
||||
placeholder={t`Filter...`}
|
||||
title={t`Use commas to match any of multiple terms, e.g. "system1, system2"`}
|
||||
className="ps-4 pe-8 w-full sm:w-44"
|
||||
onChange={handleChange}
|
||||
value={inputValue}
|
||||
|
||||
@@ -186,7 +186,7 @@ export function AvgMinMaxResponseChart({ monitorStats, monitor, chartData, empty
|
||||
|
||||
export function LossChart({ monitorStats, grid, monitors, chartData, empty, titlePrefix }: MonitorChartProps) {
|
||||
const { t } = useLingui()
|
||||
const lossTitle = t`Loss`
|
||||
const lossTitle = t({ message: "Loss", context: "Packet loss" })
|
||||
const title = titlePrefix ? `${titlePrefix} — ${lossTitle}` : lossTitle
|
||||
|
||||
return (
|
||||
|
||||
@@ -2305,4 +2305,3 @@ msgstr "نعم"
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Your user settings have been updated."
|
||||
msgstr "تم تحديث إعدادات المستخدم الخاصة بك."
|
||||
|
||||
|
||||
@@ -2305,4 +2305,3 @@ msgstr "Да"
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Your user settings have been updated."
|
||||
msgstr "Настройките за потребителя ти са обновени."
|
||||
|
||||
|
||||
@@ -2305,4 +2305,3 @@ msgstr "Ano"
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Your user settings have been updated."
|
||||
msgstr "Vaše uživatelská nastavení byla aktualizována."
|
||||
|
||||
|
||||
@@ -2305,4 +2305,3 @@ msgstr "Ja"
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Your user settings have been updated."
|
||||
msgstr "Dine brugerindstillinger er opdateret."
|
||||
|
||||
|
||||
@@ -2305,4 +2305,3 @@ msgstr "Ja"
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Your user settings have been updated."
|
||||
msgstr "Deine Benutzereinstellungen wurden aktualisiert."
|
||||
|
||||
|
||||
@@ -2305,4 +2305,3 @@ msgstr "Ναι"
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Your user settings have been updated."
|
||||
msgstr "Οι ρυθμίσεις χρήστη σας ενημερώθηκαν."
|
||||
|
||||
|
||||
@@ -25,6 +25,12 @@ msgstr "{0} available"
|
||||
msgid "{0} of {1} row(s) selected."
|
||||
msgstr "{0} of {1} row(s) selected."
|
||||
|
||||
#. placeholder {0}: selectedSystemIds.size
|
||||
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||
msgid "{0} selected"
|
||||
msgstr "{0} selected"
|
||||
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
msgid "{cores, plural, one {# core} other {# cores}}"
|
||||
msgstr "{cores, plural, one {# core} other {# cores}}"
|
||||
@@ -116,6 +122,10 @@ msgstr "Active state"
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/navbar.tsx
|
||||
#: src/components/navbar.tsx
|
||||
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||
#: src/components/routes/settings/quiet-hours.tsx
|
||||
#: src/components/routes/settings/quiet-hours.tsx
|
||||
msgid "Add {foo}"
|
||||
@@ -183,6 +193,7 @@ msgstr "All Systems"
|
||||
msgid "Are you sure you want to delete {name}?"
|
||||
msgstr "Are you sure you want to delete {name}?"
|
||||
|
||||
#: src/components/network-monitors-table/network-monitors-table.tsx
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
msgid "Are you sure?"
|
||||
msgstr "Are you sure?"
|
||||
@@ -240,6 +251,14 @@ msgstr "Average utilization of {0}"
|
||||
msgid "Average utilization of GPU engines"
|
||||
msgstr "Average utilization of GPU engines"
|
||||
|
||||
#: src/components/routes/system/charts/monitors-charts.tsx
|
||||
msgid "Average, minimum, and maximum response time"
|
||||
msgstr "Average, minimum, and maximum response time"
|
||||
|
||||
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||
msgid "Avg 1h"
|
||||
msgstr "Avg 1h"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/navbar.tsx
|
||||
msgid "Backups"
|
||||
@@ -301,6 +320,19 @@ msgstr "Bits (Kbps, Mbps, Gbps)"
|
||||
msgid "Boot state"
|
||||
msgstr "Boot state"
|
||||
|
||||
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||
msgid "Bulk Add"
|
||||
msgstr "Bulk Add"
|
||||
|
||||
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||
msgid "Bulk Add {foo}"
|
||||
msgstr "Bulk Add {foo}"
|
||||
|
||||
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||
msgid "Bulk copy"
|
||||
msgstr "Bulk copy"
|
||||
|
||||
#: src/components/routes/settings/general.tsx
|
||||
#: src/components/routes/settings/general.tsx
|
||||
msgid "Bytes (KB/s, MB/s, GB/s)"
|
||||
@@ -322,6 +354,7 @@ msgstr "Can start"
|
||||
msgid "Can stop"
|
||||
msgstr "Can stop"
|
||||
|
||||
#: src/components/network-monitors-table/network-monitors-table.tsx
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
#: src/components/routes/settings/quiet-hours.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
@@ -391,6 +424,7 @@ msgid "Checksum errors"
|
||||
msgstr "Checksum errors"
|
||||
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/network-monitors-table/network-monitors-table.tsx
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
#: src/components/routes/system/storage-pools-table.tsx
|
||||
#: src/components/routes/system/storage-pools-table.tsx
|
||||
@@ -398,6 +432,14 @@ msgstr "Checksum errors"
|
||||
msgid "Clear"
|
||||
msgstr "Clear"
|
||||
|
||||
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||
msgid "Clear all"
|
||||
msgstr "Clear all"
|
||||
|
||||
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||
msgid "Clear matches"
|
||||
msgstr "Clear matches"
|
||||
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
msgid "Click on a container to view more information."
|
||||
msgstr "Click on a container to view more information."
|
||||
@@ -427,6 +469,10 @@ msgstr "Command line instructions"
|
||||
msgid "Configure how you receive alert notifications."
|
||||
msgstr "Configure how you receive alert notifications."
|
||||
|
||||
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||
msgid "Configure response monitoring from this agent."
|
||||
msgstr "Configure response monitoring from this agent."
|
||||
|
||||
#: src/components/login/auth-form.tsx
|
||||
#: src/components/login/auth-form.tsx
|
||||
msgid "Confirm password"
|
||||
@@ -452,6 +498,7 @@ msgstr "Container Health"
|
||||
msgid "Containers"
|
||||
msgstr "Containers"
|
||||
|
||||
#: src/components/network-monitors-table/network-monitors-table.tsx
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Continue"
|
||||
@@ -512,6 +559,10 @@ msgstr "Copy the installation command for the agent below, or register agents au
|
||||
msgid "Copy the<0>docker-compose.yml</0> content for the agent below, or register agents automatically with a <1>universal token</1>."
|
||||
msgstr "Copy the<0>docker-compose.yml</0> content for the agent below, or register agents automatically with a <1>universal token</1>."
|
||||
|
||||
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||
msgid "Copy to system"
|
||||
msgstr "Copy to system"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Copy YAML"
|
||||
msgstr "Copy YAML"
|
||||
@@ -625,6 +676,7 @@ msgstr "Default"
|
||||
msgid "Default time period"
|
||||
msgstr "Default time period"
|
||||
|
||||
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
#: src/components/routes/settings/quiet-hours.tsx
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
@@ -713,12 +765,14 @@ msgstr "Download"
|
||||
msgid "Duration"
|
||||
msgstr "Duration"
|
||||
|
||||
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||
#: src/components/routes/settings/quiet-hours.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Edit"
|
||||
msgstr "Edit"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||
#: src/components/routes/settings/quiet-hours.tsx
|
||||
msgid "Edit {foo}"
|
||||
msgstr "Edit {foo}"
|
||||
@@ -767,6 +821,11 @@ msgstr "Enter your one-time password."
|
||||
msgid "Ephemeral"
|
||||
msgstr "Ephemeral"
|
||||
|
||||
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||
#: src/components/network-monitors-table/network-monitors-table.tsx
|
||||
#: src/components/network-monitors-table/network-monitors-table.tsx
|
||||
#: src/components/network-monitors-table/network-monitors-table.tsx
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
#: src/components/routes/settings/config-yaml.tsx
|
||||
#: src/components/routes/settings/heartbeat.tsx
|
||||
@@ -839,6 +898,11 @@ msgstr "Failed Services"
|
||||
msgid "Failed to authenticate"
|
||||
msgstr "Failed to authenticate"
|
||||
|
||||
#: src/components/network-monitors-table/network-monitors-table.tsx
|
||||
#: src/components/network-monitors-table/network-monitors-table.tsx
|
||||
msgid "Failed to delete monitors."
|
||||
msgstr "Failed to delete monitors."
|
||||
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
#: src/components/routes/settings/notifications.tsx
|
||||
#: src/components/routes/settings/quiet-hours.tsx
|
||||
@@ -857,6 +921,10 @@ msgstr "Failed to send test notification"
|
||||
msgid "Failed to update alert"
|
||||
msgstr "Failed to update alert"
|
||||
|
||||
#: src/components/network-monitors-table/network-monitors-table.tsx
|
||||
msgid "Failed to update monitors."
|
||||
msgstr "Failed to update monitors."
|
||||
|
||||
#. placeholder {0}: statusTotals[ServiceStatus.Failed]
|
||||
#: src/components/systemd-table/systemd-table.tsx
|
||||
msgid "Failed: {0}"
|
||||
@@ -867,6 +935,7 @@ msgid "Fans"
|
||||
msgstr "Fans"
|
||||
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/network-monitors-table/network-monitors-table.tsx
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
#: src/components/routes/system/chart-card.tsx
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
@@ -1006,14 +1075,25 @@ msgctxt "Docker image"
|
||||
msgid "Image"
|
||||
msgstr "Image"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
msgctxt "Docker image"
|
||||
msgid "Image update available"
|
||||
msgstr "Image update available"
|
||||
|
||||
#: src/components/routes/settings/quiet-hours.tsx
|
||||
msgid "Inactive"
|
||||
msgstr "Inactive"
|
||||
|
||||
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||
#: src/components/routes/settings/heartbeat.tsx
|
||||
msgid "Interval"
|
||||
msgstr "Interval"
|
||||
|
||||
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||
msgid "Interval (seconds)"
|
||||
msgstr "Interval (seconds)"
|
||||
|
||||
#: src/components/login/auth-form.tsx
|
||||
msgid "Invalid email address."
|
||||
msgstr "Invalid email address."
|
||||
@@ -1092,6 +1172,15 @@ msgstr "Logs"
|
||||
msgid "Looking instead for where to create alerts? Click the bell <0/> icons in the systems table."
|
||||
msgstr "Looking instead for where to create alerts? Click the bell <0/> icons in the systems table."
|
||||
|
||||
#: src/components/routes/system/charts/monitors-charts.tsx
|
||||
msgctxt "Packet loss"
|
||||
msgid "Loss"
|
||||
msgstr "Loss"
|
||||
|
||||
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||
msgid "Loss 1h"
|
||||
msgstr "Loss 1h"
|
||||
|
||||
#: src/components/systemd-table/systemd-table.tsx
|
||||
msgid "Main PID"
|
||||
msgstr "Main PID"
|
||||
@@ -1110,6 +1199,10 @@ msgstr "Manual setup instructions"
|
||||
msgid "Max 1 min"
|
||||
msgstr "Max 1 min"
|
||||
|
||||
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||
msgid "Max 1h"
|
||||
msgstr "Max 1h"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/routes/system/info-bar.tsx
|
||||
#: src/components/systemd-table/systemd-table-columns.tsx
|
||||
@@ -1136,11 +1229,25 @@ msgstr "Memory Usage"
|
||||
msgid "Memory usage of containers"
|
||||
msgstr "Memory usage of containers"
|
||||
|
||||
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||
msgid "Min 1h"
|
||||
msgstr "Min 1h"
|
||||
|
||||
#. Device model
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
msgid "Model"
|
||||
msgstr "Model"
|
||||
|
||||
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||
msgid "Monitor"
|
||||
msgstr "Monitor"
|
||||
|
||||
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||
msgid "Monitors created"
|
||||
msgstr "Monitors created"
|
||||
|
||||
#: src/components/routes/system/storage-pools-table.tsx
|
||||
msgid "Mountpoint"
|
||||
msgstr "Mountpoint"
|
||||
@@ -1158,6 +1265,29 @@ msgstr "Name"
|
||||
msgid "Net"
|
||||
msgstr "Net"
|
||||
|
||||
#: src/components/routes/system.tsx
|
||||
msgid "Network"
|
||||
msgstr "Network"
|
||||
|
||||
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||
msgid "Network Monitor"
|
||||
msgstr "Network Monitor"
|
||||
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Network Monitor Loss"
|
||||
msgstr "Network Monitor Loss"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/navbar.tsx
|
||||
#: src/components/navbar.tsx
|
||||
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||
#: src/components/network-monitors-table/network-monitors-table.tsx
|
||||
#: src/components/routes/monitors.tsx
|
||||
msgid "Network Monitors"
|
||||
msgstr "Network Monitors"
|
||||
|
||||
#: src/components/routes/system/charts/network-charts.tsx
|
||||
msgid "Network traffic of containers"
|
||||
msgstr "Network traffic of containers"
|
||||
@@ -1192,6 +1322,7 @@ msgstr "No results found."
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/containers-table/containers-table.tsx
|
||||
#: src/components/network-monitors-table/network-monitors-table.tsx
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
#: src/components/systemd-table/systemd-table.tsx
|
||||
@@ -1202,6 +1333,7 @@ msgstr "No results."
|
||||
msgid "No S.M.A.R.T. attributes available for this device."
|
||||
msgstr "No S.M.A.R.T. attributes available for this device."
|
||||
|
||||
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
#: src/components/systems-table/systems-table.tsx
|
||||
msgid "No systems found."
|
||||
@@ -1233,6 +1365,11 @@ msgstr "On each restart, systems in the database will be updated to match the sy
|
||||
msgid "One or more containers are unhealthy"
|
||||
msgstr "One or more containers are unhealthy"
|
||||
|
||||
#. placeholder {0}: alert.value
|
||||
#: src/components/active-alerts.tsx
|
||||
msgid "One or more monitors exceed {0}% loss"
|
||||
msgstr "One or more monitors exceed {0}% loss"
|
||||
|
||||
#: src/lib/alerts.ts
|
||||
msgid "One or more services are in a failed state"
|
||||
msgstr "One or more services are in a failed state"
|
||||
@@ -1246,6 +1383,7 @@ msgstr "One-time"
|
||||
msgid "One-time password"
|
||||
msgstr "One-time password"
|
||||
|
||||
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||
#: src/components/routes/settings/quiet-hours.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
@@ -1267,6 +1405,11 @@ msgstr "Other"
|
||||
msgid "Overwrite existing alerts"
|
||||
msgstr "Overwrite existing alerts"
|
||||
|
||||
#: src/components/routes/system/charts/monitors-charts.tsx
|
||||
msgid "Packet loss (%)"
|
||||
msgstr "Packet loss (%)"
|
||||
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
#: src/components/command-palette.tsx
|
||||
@@ -1304,6 +1447,7 @@ msgstr "Password reset request received"
|
||||
msgid "Past"
|
||||
msgstr "Past"
|
||||
|
||||
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Pause"
|
||||
msgstr "Pause"
|
||||
@@ -1387,6 +1531,7 @@ msgid "Pool Usage"
|
||||
msgstr "Pool Usage"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||
msgid "Port"
|
||||
msgstr "Port"
|
||||
|
||||
@@ -1413,6 +1558,11 @@ msgstr "Preferred Language"
|
||||
msgid "Process started"
|
||||
msgstr "Process started"
|
||||
|
||||
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||
msgid "Protocol"
|
||||
msgstr "Protocol"
|
||||
|
||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||
msgid "Public key"
|
||||
msgstr "Public key"
|
||||
@@ -1498,10 +1648,20 @@ msgstr "Reset Password"
|
||||
msgid "Resolved"
|
||||
msgstr "Resolved"
|
||||
|
||||
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||
#: src/components/routes/system/charts/monitors-charts.tsx
|
||||
msgid "Response"
|
||||
msgstr "Response"
|
||||
|
||||
#: src/components/network-monitors-table/network-monitors-table.tsx
|
||||
msgid "Response time monitoring from agents."
|
||||
msgstr "Response time monitoring from agents."
|
||||
|
||||
#: src/components/systemd-table/systemd-table.tsx
|
||||
msgid "Restarts"
|
||||
msgstr "Restarts"
|
||||
|
||||
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||
#: src/components/systems-table/systems-table-columns.tsx
|
||||
msgid "Resume"
|
||||
msgstr "Resume"
|
||||
@@ -1534,6 +1694,7 @@ msgid "S.M.A.R.T. Self-Test"
|
||||
msgstr "S.M.A.R.T. Self-Test"
|
||||
|
||||
#: src/components/add-system.tsx
|
||||
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||
msgid "Save {foo}"
|
||||
msgstr "Save {foo}"
|
||||
|
||||
@@ -1570,6 +1731,11 @@ msgstr "Search"
|
||||
msgid "Search for systems or settings..."
|
||||
msgstr "Search for systems or settings..."
|
||||
|
||||
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||
msgid "Search systems"
|
||||
msgstr "Search systems"
|
||||
|
||||
#: src/components/routes/settings/heartbeat.tsx
|
||||
msgid "Seconds between pings (default: 60)"
|
||||
msgstr "Seconds between pings (default: 60)"
|
||||
@@ -1582,6 +1748,27 @@ msgstr "See <0>notification settings</0> to configure how you receive alerts."
|
||||
msgid "Select {foo}"
|
||||
msgstr "Select {foo}"
|
||||
|
||||
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||
msgid "Select a system"
|
||||
msgstr "Select a system"
|
||||
|
||||
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||
msgid "Select all"
|
||||
msgstr "Select all"
|
||||
|
||||
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||
msgid "Select matches"
|
||||
msgstr "Select matches"
|
||||
|
||||
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||
msgid "Select row"
|
||||
msgstr "Select row"
|
||||
|
||||
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||
msgid "Select systems"
|
||||
msgstr "Select systems"
|
||||
|
||||
#: src/components/routes/settings/heartbeat.tsx
|
||||
msgid "Send a single heartbeat ping to verify your endpoint is working."
|
||||
msgstr "Send a single heartbeat ping to verify your endpoint is working."
|
||||
@@ -1687,6 +1874,8 @@ msgstr "Switch theme"
|
||||
#: src/components/alerts-history-columns.tsx
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/navbar.tsx
|
||||
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||
#: src/components/routes/settings/quiet-hours.tsx
|
||||
#: src/components/routes/settings/quiet-hours.tsx
|
||||
#: src/components/routes/settings/quiet-hours.tsx
|
||||
@@ -1711,6 +1900,8 @@ msgid "Systemd Services"
|
||||
msgstr "Systemd Services"
|
||||
|
||||
#: src/components/navbar.tsx
|
||||
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||
msgid "Systems"
|
||||
msgstr "Systems"
|
||||
|
||||
@@ -1727,6 +1918,11 @@ msgctxt "Tabs system layout option"
|
||||
msgid "Tabs"
|
||||
msgstr "Tabs"
|
||||
|
||||
#: src/components/network-monitors-table/monitor-dialog.tsx
|
||||
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||
msgid "Target"
|
||||
msgstr "Target"
|
||||
|
||||
#: src/components/systemd-table/systemd-table.tsx
|
||||
msgid "Tasks"
|
||||
msgstr "Tasks"
|
||||
@@ -1774,6 +1970,7 @@ msgstr "Then log into the backend and reset your user account password in the us
|
||||
msgid "This action cannot be undone. This will permanently delete all current records for {name} from the database."
|
||||
msgstr "This action cannot be undone. This will permanently delete all current records for {name} from the database."
|
||||
|
||||
#: src/components/network-monitors-table/network-monitors-table.tsx
|
||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||
msgid "This will permanently delete all selected records from the database."
|
||||
msgstr "This will permanently delete all selected records from the database."
|
||||
@@ -1896,6 +2093,10 @@ msgstr "Triggers when GPU usage exceeds a threshold"
|
||||
msgid "Triggers when memory usage exceeds a threshold"
|
||||
msgstr "Triggers when memory usage exceeds a threshold"
|
||||
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Triggers when one hour loss exceeds a threshold"
|
||||
msgstr "Triggers when one hour loss exceeds a threshold"
|
||||
|
||||
#: src/lib/alerts.ts
|
||||
msgid "Triggers when status switches between up and down"
|
||||
msgstr "Triggers when status switches between up and down"
|
||||
@@ -1955,6 +2156,7 @@ msgid "Update"
|
||||
msgstr "Update"
|
||||
|
||||
#: src/components/containers-table/containers-table-columns.tsx
|
||||
#: src/components/network-monitors-table/network-monitors-columns.tsx
|
||||
#: src/components/routes/system/smart-table.tsx
|
||||
#: src/components/routes/system/storage-pools-table.tsx
|
||||
#: src/components/systemd-table/systemd-table-columns.tsx
|
||||
|
||||
@@ -2305,4 +2305,3 @@ msgstr "Sí"
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Your user settings have been updated."
|
||||
msgstr "Tu configuración de usuario ha sido actualizada."
|
||||
|
||||
|
||||
@@ -2305,4 +2305,3 @@ msgstr "بله"
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Your user settings have been updated."
|
||||
msgstr "تنظیمات کاربری شما بهروزرسانی شد."
|
||||
|
||||
|
||||
@@ -2305,4 +2305,3 @@ msgstr "Oui"
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Your user settings have been updated."
|
||||
msgstr "Vos paramètres utilisateur ont été mis à jour."
|
||||
|
||||
|
||||
@@ -2305,4 +2305,3 @@ msgstr "כן"
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Your user settings have been updated."
|
||||
msgstr "הגדרות המשתמש שלך עודכנו."
|
||||
|
||||
|
||||
@@ -2305,4 +2305,3 @@ msgstr "Da"
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Your user settings have been updated."
|
||||
msgstr "Vaše korisničke postavke su ažurirane."
|
||||
|
||||
|
||||
@@ -2305,4 +2305,3 @@ msgstr "Igen"
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Your user settings have been updated."
|
||||
msgstr "A felhasználói beállítások frissítésre kerültek."
|
||||
|
||||
|
||||
@@ -2305,4 +2305,3 @@ msgstr "Ya"
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Your user settings have been updated."
|
||||
msgstr "Pengaturan pengguna anda telah diperbarui."
|
||||
|
||||
|
||||
@@ -2305,4 +2305,3 @@ msgstr "Sì"
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Your user settings have been updated."
|
||||
msgstr "Le impostazioni utente sono state aggiornate."
|
||||
|
||||
|
||||
@@ -2305,4 +2305,3 @@ msgstr "はい"
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Your user settings have been updated."
|
||||
msgstr "ユーザー設定が更新されました。"
|
||||
|
||||
|
||||
@@ -2305,4 +2305,3 @@ msgstr "예"
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Your user settings have been updated."
|
||||
msgstr "사용자 설정이 업데이트되었습니다."
|
||||
|
||||
|
||||
@@ -2305,4 +2305,3 @@ msgstr "Ja"
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Your user settings have been updated."
|
||||
msgstr "Je gebruikersinstellingen zijn bijgewerkt."
|
||||
|
||||
|
||||
@@ -2305,4 +2305,3 @@ msgstr "Ja"
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Your user settings have been updated."
|
||||
msgstr "Dine brukerinnstillinger har blitt oppdatert."
|
||||
|
||||
|
||||
@@ -2305,4 +2305,3 @@ msgstr "Tak"
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Your user settings have been updated."
|
||||
msgstr "Twoje ustawienia użytkownika zostały zaktualizowane."
|
||||
|
||||
|
||||
@@ -2305,4 +2305,3 @@ msgstr "Sim"
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Your user settings have been updated."
|
||||
msgstr "As configurações do seu usuário foram atualizadas."
|
||||
|
||||
|
||||
@@ -2305,4 +2305,3 @@ msgstr "Да"
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Your user settings have been updated."
|
||||
msgstr "Ваши настройки пользователя были обновлены."
|
||||
|
||||
|
||||
@@ -2305,4 +2305,3 @@ msgstr "Da"
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Your user settings have been updated."
|
||||
msgstr "Vaše uporabniške nastavitve so posodobljene."
|
||||
|
||||
|
||||
@@ -2305,4 +2305,3 @@ msgstr "Да"
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Your user settings have been updated."
|
||||
msgstr "Ваша корисничка подешавања су ажурирана."
|
||||
|
||||
|
||||
@@ -2305,4 +2305,3 @@ msgstr "Ja"
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Your user settings have been updated."
|
||||
msgstr "Dina användarinställningar har uppdaterats."
|
||||
|
||||
|
||||
@@ -2305,4 +2305,3 @@ msgstr "Evet"
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Your user settings have been updated."
|
||||
msgstr "Kullanıcı ayarlarınız güncellendi."
|
||||
|
||||
|
||||
@@ -2305,4 +2305,3 @@ msgstr "Так"
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Your user settings have been updated."
|
||||
msgstr "Ваші налаштування користувача були оновлені."
|
||||
|
||||
|
||||
@@ -2305,4 +2305,3 @@ msgstr "Ha"
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Your user settings have been updated."
|
||||
msgstr "Foydalanuvchi sozlamalaringiz yangilandi."
|
||||
|
||||
|
||||
@@ -2305,4 +2305,3 @@ msgstr "Có"
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Your user settings have been updated."
|
||||
msgstr "Cài đặt người dùng của bạn đã được cập nhật."
|
||||
|
||||
|
||||
@@ -2305,4 +2305,3 @@ msgstr "是"
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Your user settings have been updated."
|
||||
msgstr "您的用户设置已更新。"
|
||||
|
||||
|
||||
@@ -2305,4 +2305,3 @@ msgstr "是"
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Your user settings have been updated."
|
||||
msgstr "您的用戶設置已更新。"
|
||||
|
||||
|
||||
@@ -2305,4 +2305,3 @@ msgstr "是"
|
||||
#: src/components/routes/settings/layout.tsx
|
||||
msgid "Your user settings have been updated."
|
||||
msgstr "已更新您的使用者設定"
|
||||
|
||||
|
||||
100
internal/users/oauth_test.go
Normal file
100
internal/users/oauth_test.go
Normal file
@@ -0,0 +1,100 @@
|
||||
//go:build testing
|
||||
|
||||
package users_test
|
||||
|
||||
import (
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
beszelTests "github.com/henrygd/beszel/internal/tests"
|
||||
"github.com/pocketbase/pocketbase/apis"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/tools/auth"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
type roleTestProvider struct {
|
||||
auth.BaseProvider
|
||||
}
|
||||
|
||||
func (p *roleTestProvider) FetchToken(string, ...oauth2.AuthCodeOption) (*oauth2.Token, error) {
|
||||
return &oauth2.Token{AccessToken: "test-token"}, nil
|
||||
}
|
||||
|
||||
func (p *roleTestProvider) FetchAuthUser(*oauth2.Token) (*auth.AuthUser, error) {
|
||||
return &auth.AuthUser{Id: "role-test-user", Email: "oauth@example.com"}, nil
|
||||
}
|
||||
|
||||
func TestOAuthUserRole(t *testing.T) {
|
||||
t.Setenv("USER_CREATION", "true")
|
||||
const provider = "beszel-role-test"
|
||||
auth.Providers[provider] = func() auth.Provider { return &roleTestProvider{} }
|
||||
t.Cleanup(func() { delete(auth.Providers, provider) })
|
||||
|
||||
for _, createData := range []string{`{}`, `{"role":"admin"}`, `{"role":"readonly"}`} {
|
||||
t.Run(createData, func(t *testing.T) {
|
||||
h, err := beszelTests.NewTestHub(t.TempDir())
|
||||
require.NoError(t, err)
|
||||
defer h.Cleanup()
|
||||
h.StartHub()
|
||||
|
||||
collection, err := h.FindCollectionByNameOrId("users")
|
||||
require.NoError(t, err)
|
||||
collection.OAuth2.Enabled = true
|
||||
collection.OAuth2.Providers = []core.OAuth2ProviderConfig{{
|
||||
Name: provider, ClientId: "test-client", ClientSecret: "test-secret",
|
||||
}}
|
||||
require.NoError(t, h.Save(collection))
|
||||
r, err := apis.NewRouter(h.App)
|
||||
require.NoError(t, err)
|
||||
mux, err := r.BuildMux()
|
||||
require.NoError(t, err)
|
||||
login := func() {
|
||||
body := `{"provider":"` + provider + `","code":"test-code","codeVerifier":"test-verifier","redirectUrl":"http://localhost/callback","createData":` + createData + `}`
|
||||
req := httptest.NewRequest("POST", "/api/collections/users/auth-with-oauth2", strings.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
res := httptest.NewRecorder()
|
||||
mux.ServeHTTP(res, req)
|
||||
require.Equal(t, 200, res.Code, res.Body.String())
|
||||
}
|
||||
login()
|
||||
user, err := h.FindAuthRecordByEmail("users", "oauth@example.com")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "user", user.GetString("role"))
|
||||
|
||||
// A later OAuth login must preserve a role assigned by an administrator.
|
||||
user.Set("role", "admin")
|
||||
require.NoError(t, h.Save(user))
|
||||
login()
|
||||
user, err = h.FindRecordById("users", user.Id)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "admin", user.GetString("role"))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInternalUserRole(t *testing.T) {
|
||||
for _, role := range []string{"", "user", "admin", "readonly"} {
|
||||
t.Run("role="+role, func(t *testing.T) {
|
||||
h, err := beszelTests.NewTestHub(t.TempDir())
|
||||
require.NoError(t, err)
|
||||
defer h.Cleanup()
|
||||
h.StartHub()
|
||||
collection, err := h.FindCollectionByNameOrId("users")
|
||||
require.NoError(t, err)
|
||||
user := core.NewRecord(collection)
|
||||
user.SetEmail("internal@example.com")
|
||||
user.SetPassword("password12345")
|
||||
user.Set("role", role)
|
||||
require.NoError(t, h.Save(user))
|
||||
user, err = h.FindRecordById("users", user.Id)
|
||||
require.NoError(t, err)
|
||||
if role == "" {
|
||||
role = "user"
|
||||
}
|
||||
require.Equal(t, role, user.GetString("role"))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,17 @@ func NewUserManager(app core.App) *UserManager {
|
||||
}
|
||||
}
|
||||
|
||||
// InitializeOAuthUserRole prevents self-registration from assigning a privileged role.
|
||||
func (um *UserManager) InitializeOAuthUserRole(e *core.RecordAuthWithOAuth2RequestEvent) error {
|
||||
if e.IsNewRecord {
|
||||
if e.CreateData == nil {
|
||||
e.CreateData = make(map[string]any)
|
||||
}
|
||||
e.CreateData["role"] = "user"
|
||||
}
|
||||
return e.Next()
|
||||
}
|
||||
|
||||
// Initialize user role if not set
|
||||
func (um *UserManager) InitializeUserRole(e *core.RecordEvent) error {
|
||||
if e.Record.GetString("role") == "" {
|
||||
|
||||
@@ -108,6 +108,9 @@ func TestCreateFirstUserAtomic(t *testing.T) {
|
||||
count, err := h.CountRecords("users")
|
||||
require.NoError(t, err)
|
||||
require.EqualValues(t, 1, count)
|
||||
bootstrapUsers, err := h.FindAllRecords("users")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "admin", bootstrapUsers[0].GetString("role"))
|
||||
admins, err := h.FindAllRecords(core.CollectionNameSuperusers)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, admins, 1)
|
||||
|
||||
@@ -15,9 +15,10 @@ It has a friendly web interface, simple configuration, and is ready to use out o
|
||||
|
||||
- **Lightweight**: Smaller and less resource-intensive than leading solutions.
|
||||
- **Simple**: Easy setup with little manual configuration required.
|
||||
- **Alerts**: Configurable alerts for most metrics. Supports many notification services.
|
||||
- **Docker stats**: Tracks CPU, memory, and network usage history for each container.
|
||||
- **ZFS**: Tracks pool capacity, health, and I/O, plus per-dataset usage.
|
||||
- **Alerts**: Configurable alerts for CPU, memory, disk, bandwidth, temperature, fan speed, load average, and status.
|
||||
- **Network monitoring**: Monitor response time and interruptions directly from agents.
|
||||
- **S.M.A.R.T.**: Disk health data and notifications on drive failure.
|
||||
- **Multi-user**: Users manage their own systems. Admins can share systems across users.
|
||||
- **OAuth / OIDC**: Supports many OAuth2 providers. Password auth can be disabled.
|
||||
- **Automatic backups**: Save to and restore from disk or S3-compatible storage.
|
||||
@@ -51,7 +52,7 @@ The [quick start guide](https://beszel.dev/guide/getting-started) and other docu
|
||||
- **Temperature** - Host system sensors.
|
||||
- **Fan speed** - Host system sensors (Linux, via `/sys/class/hwmon`).
|
||||
- **GPU usage / power draw** - Nvidia, AMD, and Intel.
|
||||
- **Battery** - Host system battery charge.
|
||||
- **Battery charge** - Host system and some peripherals.
|
||||
- **Containers** - Status and metrics of all running Docker / Podman containers.
|
||||
- **S.M.A.R.T.** - Host system disk health (includes eMMC wear/EOL and Linux mdraid array health via sysfs when available).
|
||||
- **ZFS** - Pool capacity, usage, health, I/O throughput, scrub status, and per-dataset usage.
|
||||
|
||||
@@ -1,3 +1,39 @@
|
||||
## 0.20.0
|
||||
|
||||
- Add network monitoring from agents (#2266, #1911)
|
||||
|
||||
- Add Docker image update available flag (#2211)
|
||||
|
||||
- Add btrfs filesystem reporting as storage pools (#2315)
|
||||
|
||||
- Add persistence of view preferences and language to user settings (#1831)
|
||||
|
||||
- Add `TRUSTED_PROXY_IPS` allowlist for `TRUSTED_AUTH_HEADER` (#2327)
|
||||
|
||||
- Add ZFS utilities to Intel and NVIDIA agent images (#2288, #2311)
|
||||
|
||||
- Revert SMART warnings for certain attributes (#2296, #2308, #2347)
|
||||
|
||||
- Improve NVMe data units display as human-readable GB/TB (#2303)
|
||||
|
||||
- Fix agent disconnects during slow collections by extending WebSocket deadline (#2294)
|
||||
|
||||
- Fix missing root CA certificates in base agent image (#2291)
|
||||
|
||||
- Fix false RAID health warnings during healthy data scrubbing (#2109)
|
||||
|
||||
- Fix ZFS monitoring when /dev/zfs is unavailable (#2325)
|
||||
|
||||
- Fix spurious `HUB_URL` warning in SSH-only mode (#2316)
|
||||
|
||||
- Fix idle GPU utilization display in systems table (#2312)
|
||||
|
||||
- Fix session handling to clear auth store after token expiry (#2310)
|
||||
|
||||
- Fix chart history handling when switching to live charts (#2333)
|
||||
|
||||
- Update Go dependencies
|
||||
|
||||
## 0.19.0
|
||||
|
||||
- **Potential breaking change:** Agents now verify HTTPS certificates. If an agent connects to a hub using a self-signed or otherwise untrusted certificate, configure `CA_CERT_FILE` with the appropriate CA certificate or the connection will be rejected.
|
||||
|
||||
@@ -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.19.0"
|
||||
appVersion: "0.20.0"
|
||||
# Bump this version when publishing chart changes.
|
||||
version: 0.1.6
|
||||
version: 0.1.7
|
||||
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.19.0) | Image version |
|
||||
| `image.tag` | Chart AppVersion (0.20.0) | 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.19.0"
|
||||
--set image.tag="0.20.0"
|
||||
```
|
||||
|
||||
### 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.19.0
|
||||
- **App Version**: 0.20.0
|
||||
- **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.19.0"
|
||||
appVersion: "0.20.0"
|
||||
# Bump this version when publishing chart changes.
|
||||
version: 0.1.6
|
||||
version: 0.1.7
|
||||
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.19.0) | Container image tag |
|
||||
| `image.tag` | Chart AppVersion (0.20.0) | 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.19.0"
|
||||
tag: "0.20.0"
|
||||
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.19.0
|
||||
- **App Version**: 0.20.0
|
||||
- **Kubernetes Version**: 1.19+
|
||||
- **Maintainer**: cloudwithdan (nikoloskid@pm.me)
|
||||
|
||||
|
||||
@@ -298,6 +298,94 @@ warn() {
|
||||
echo "Warning: $*" >&2
|
||||
}
|
||||
|
||||
# Keep IDs within 16 bits, including on older OpenWrt versions whose account
|
||||
# helpers start automatic allocation at 65536.
|
||||
openwrt_unused_id() {
|
||||
awk -F: '
|
||||
{ used[$3] = 1 }
|
||||
END {
|
||||
for (id = 32768; id < 65534; id++) {
|
||||
if (!(id in used)) { print id; exit }
|
||||
}
|
||||
exit 1
|
||||
}
|
||||
' "$1"
|
||||
}
|
||||
|
||||
validate_openwrt_account() {
|
||||
# Duplicate numeric IDs share permissions even when the names differ.
|
||||
for account_file in /etc/passwd /etc/group; do
|
||||
if ! awk -F: '
|
||||
$1 == "beszel" { id = $3; count++ }
|
||||
{ ids[$3]++ }
|
||||
END { if (count && (count != 1 || id == 0 || ids[id] != 1)) exit 1 }
|
||||
' "$account_file"; then
|
||||
fail "The beszel account has a duplicate or root ID in $account_file. Stop beszel-agent and assign beszel an unused UID/GID before reinstalling. Update ownership only in $AGENT_DIR; do not change all files owned by the shared ID."
|
||||
fi
|
||||
done
|
||||
if grep -q '^beszel:' /etc/passwd; then
|
||||
account_gid=$(awk -F: '$1 == "beszel" { print $4 }' /etc/passwd)
|
||||
group_gid=$(awk -F: '$1 == "beszel" { print $3 }' /etc/group)
|
||||
[ "$account_gid" = "$group_gid" ] || fail "The beszel user's primary group is not the dedicated beszel group. Repair the account before reinstalling."
|
||||
fi
|
||||
}
|
||||
|
||||
configure_openwrt_account() (
|
||||
validate_openwrt_account
|
||||
[ -r /lib/functions.sh ] || fail "OpenWrt account helpers (/lib/functions.sh) are required."
|
||||
# OpenWrt's library expects unset variables and defines generic functions.
|
||||
# Source it in a subshell so neither affects the rest of the installer.
|
||||
set +u
|
||||
. /lib/functions.sh
|
||||
IPKG_INSTROOT=""
|
||||
|
||||
if ! grep -q '^beszel:' /etc/group; then
|
||||
account_gid=$(openwrt_unused_id /etc/group) || fail "No unused service GID available."
|
||||
group_add beszel "$account_gid" || fail "Could not create the beszel group."
|
||||
fi
|
||||
account_gid=$(awk -F: '$1 == "beszel" { print $3 }' /etc/group)
|
||||
[ -n "$account_gid" ] || fail "The beszel group was not created."
|
||||
if ! grep -q '^beszel:' /etc/passwd; then
|
||||
account_uid=$(openwrt_unused_id /etc/passwd) || fail "No unused service UID available."
|
||||
user_add beszel "$account_uid" "$account_gid" "Beszel agent" /nonexistent /bin/false || fail "Could not create the beszel user."
|
||||
fi
|
||||
grep -q '^beszel:' /etc/passwd || fail "The beszel account is incomplete."
|
||||
validate_openwrt_account
|
||||
# Previous installers omitted the shadow entry. Repair it with a locked
|
||||
# password, without rewriting an existing account or changing its IDs.
|
||||
if ! grep -q '^beszel:' /etc/shadow; then
|
||||
lock /var/lock/passwd || fail "Could not lock the account database."
|
||||
shadow_status=0
|
||||
if ! grep -q '^beszel:' /etc/shadow; then
|
||||
printf 'beszel:!:0:0:99999:7:::\n' >> /etc/shadow || shadow_status=$?
|
||||
fi
|
||||
lock -u /var/lock/passwd || fail "Could not unlock the account database."
|
||||
[ "$shadow_status" -eq 0 ] || fail "Could not create the beszel shadow entry."
|
||||
fi
|
||||
|
||||
if grep -q '^docker:' /etc/group; then
|
||||
# Match complete member names and avoid a leading comma for empty groups.
|
||||
if ! awk -F: '$1 == "docker" { n = split($4, members, ","); for (i = 1; i <= n; i++) if (members[i] == "beszel") found = 1 } END { exit !found }' /etc/group; then
|
||||
echo "Adding beszel to docker group"
|
||||
if grep -q '^docker:[^:]*:[^:]*:$' /etc/group; then
|
||||
sed -i '/^docker:/s/$/beszel/' /etc/group
|
||||
else
|
||||
sed -i '/^docker:/s/$/,beszel/' /etc/group
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
)
|
||||
|
||||
remove_openwrt_account() {
|
||||
if command -v userdel >/dev/null 2>&1; then
|
||||
userdel beszel || fail "Could not remove the beszel user."
|
||||
elif command -v deluser >/dev/null 2>&1; then
|
||||
deluser beszel || fail "Could not remove the beszel user."
|
||||
else
|
||||
warn "Neither userdel nor deluser is available; the beszel account has been retained."
|
||||
fi
|
||||
}
|
||||
|
||||
require_value() {
|
||||
[ "$#" -ge 2 ] && [ -n "$2" ] || fail "Option $1 requires a value."
|
||||
}
|
||||
@@ -672,7 +760,9 @@ if [ "$UNINSTALL" = true ]; then
|
||||
echo "Removing the dedicated user for the agent service..."
|
||||
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_openwrt; then
|
||||
remove_openwrt_account
|
||||
elif is_alpine; then
|
||||
deluser beszel || fail "Could not remove the beszel user."
|
||||
elif is_freebsd; then
|
||||
pw user del beszel || fail "Could not remove the beszel user."
|
||||
@@ -772,32 +862,7 @@ if is_alpine; then
|
||||
fi
|
||||
|
||||
elif is_openwrt; then
|
||||
# Create beszel group first if it doesn't exist (check /etc/group directly)
|
||||
if ! grep -q "^beszel:" /etc/group >/dev/null 2>&1; then
|
||||
echo "beszel:x:999:" >> /etc/group
|
||||
fi
|
||||
|
||||
# Create beszel user if it doesn't exist (double-check to prevent duplicates)
|
||||
if ! id -u beszel >/dev/null 2>&1 && ! grep -q "^beszel:" /etc/passwd >/dev/null 2>&1; then
|
||||
echo "beszel:x:999:999::/nonexistent:/bin/false" >> /etc/passwd
|
||||
fi
|
||||
|
||||
# Add the user to the docker group if docker group exists and user is not already in it
|
||||
if grep -q "^docker:" /etc/group >/dev/null 2>&1; then
|
||||
echo "Adding beszel to docker group"
|
||||
# Check if beszel is already in docker group
|
||||
if ! grep "^docker:" /etc/group | grep -q "beszel"; then
|
||||
# Add beszel to docker group by modifying /etc/group
|
||||
# Handle both cases: group with existing members and group without members
|
||||
if grep "^docker:" /etc/group | grep -q ":.*:.*$"; then
|
||||
# Group has existing members, append with comma
|
||||
sed -i 's/^docker:\([^:]*:[^:]*:\)\(.*\)$/docker:\1\2,beszel/' /etc/group
|
||||
else
|
||||
# Group has no members, just append
|
||||
sed -i 's/^docker:\([^:]*:[^:]*:\)$/docker:\1beszel/' /etc/group
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
configure_openwrt_account
|
||||
|
||||
elif is_freebsd; then
|
||||
if is_opnsense || is_pfsense; then
|
||||
|
||||
Reference in New Issue
Block a user