From e109c9be3353d16624f64c6ff3371019bdafffff Mon Sep 17 00:00:00 2001 From: Dev Scribe Date: Fri, 4 Sep 2026 13:14:57 -0400 Subject: [PATCH] clientinfo: skip unavailable Ubios Mongo discovery --- internal/clientinfo/ubios.go | 28 ++++++++++++++++++++++++-- internal/clientinfo/ubios_test.go | 22 ++++++++++++++++++++ internal/clientinfo/ubios_unix_test.go | 27 +++++++++++++++++++++++++ 3 files changed, 75 insertions(+), 2 deletions(-) create mode 100644 internal/clientinfo/ubios_unix_test.go diff --git a/internal/clientinfo/ubios.go b/internal/clientinfo/ubios.go index 0ffd6e5..cffc689 100644 --- a/internal/clientinfo/ubios.go +++ b/internal/clientinfo/ubios.go @@ -3,8 +3,10 @@ package clientinfo import ( "bytes" "encoding/json" + "errors" "fmt" "io" + "os" "os/exec" "strings" "sync" @@ -13,9 +15,12 @@ import ( "github.com/Control-D-Inc/ctrld/internal/router/ubios" ) +const defaultUbiosMongoPath = "/usr/bin/mongo" + // ubiosDiscover provides client discovery functionality on Ubios routers. type ubiosDiscover struct { - hostname sync.Map // mac => hostname + hostname sync.Map // mac => hostname + mongoPath string } // refresh reloads unifi devices from database. @@ -42,7 +47,19 @@ func (u *ubiosDiscover) LookupHostnameByMac(mac string) string { // refreshDevices updates unifi devices name from local mongodb. func (u *ubiosDiscover) refreshDevices() error { - cmd := exec.Command("/usr/bin/mongo", "localhost:27117/ace", "--quiet", "--eval", ` + mongoPath := u.mongoExecutable() + fi, err := os.Stat(mongoPath) + if errors.Is(err, os.ErrNotExist) { + return nil + } + if err != nil { + return fmt.Errorf("stat mongo executable: %w", err) + } + if !fi.Mode().IsRegular() || fi.Mode().Perm()&0111 == 0 { + return nil + } + + cmd := exec.Command(mongoPath, "localhost:27117/ace", "--quiet", "--eval", ` DBQuery.shellBatchSize = 256; db.user.find({name: {$exists: true, $ne: ""}}, {_id:0, mac:1, name:1});`) b, err := cmd.CombinedOutput() @@ -52,6 +69,13 @@ func (u *ubiosDiscover) refreshDevices() error { return u.storeDevices(bytes.NewReader(b)) } +func (u *ubiosDiscover) mongoExecutable() string { + if u.mongoPath != "" { + return u.mongoPath + } + return defaultUbiosMongoPath +} + // storeDevices saves unifi devices name for caching. func (u *ubiosDiscover) storeDevices(r io.Reader) error { decoder := json.NewDecoder(r) diff --git a/internal/clientinfo/ubios_test.go b/internal/clientinfo/ubios_test.go index 657cf18..4b788ba 100644 --- a/internal/clientinfo/ubios_test.go +++ b/internal/clientinfo/ubios_test.go @@ -1,10 +1,32 @@ package clientinfo import ( + "os" + "path/filepath" "strings" "testing" ) +func Test_ubiosDiscover_refreshDevices_skipsUnavailableMongo(t *testing.T) { + t.Run("missing", func(t *testing.T) { + ud := &ubiosDiscover{mongoPath: filepath.Join(t.TempDir(), "mongo")} + if err := ud.refreshDevices(); err != nil { + t.Fatalf("refreshDevices() error = %v", err) + } + }) + + t.Run("not executable", func(t *testing.T) { + path := filepath.Join(t.TempDir(), "mongo") + if err := os.WriteFile(path, nil, 0600); err != nil { + t.Fatal(err) + } + ud := &ubiosDiscover{mongoPath: path} + if err := ud.refreshDevices(); err != nil { + t.Fatalf("refreshDevices() error = %v", err) + } + }) +} + func Test_ubiosDiscover_storeDevices(t *testing.T) { ud := &ubiosDiscover{} r := strings.NewReader(`{ "mac": "00:00:00:00:00:01", "name": "device 1" } diff --git a/internal/clientinfo/ubios_unix_test.go b/internal/clientinfo/ubios_unix_test.go new file mode 100644 index 0000000..edb89ec --- /dev/null +++ b/internal/clientinfo/ubios_unix_test.go @@ -0,0 +1,27 @@ +//go:build unix + +package clientinfo + +import ( + "os" + "path/filepath" + "testing" +) + +func Test_ubiosDiscover_refreshDevices_usesExecutableMongo(t *testing.T) { + path := filepath.Join(t.TempDir(), "mongo") + const script = `#!/bin/sh +printf '%s\n' '{"mac":"00:00:00:00:00:01","name":"device 1"}' +` + if err := os.WriteFile(path, []byte(script), 0700); err != nil { + t.Fatal(err) + } + + ud := &ubiosDiscover{mongoPath: path} + if err := ud.refreshDevices(); err != nil { + t.Fatalf("refreshDevices() error = %v", err) + } + if got := ud.LookupHostnameByMac("00:00:00:00:00:01"); got != "device 1" { + t.Fatalf("LookupHostnameByMac() = %q, want %q", got, "device 1") + } +}