From 0a6d9d44548683f4c7cc844959b9ed43a8b77002 Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Wed, 3 Jan 2024 17:56:04 +0700 Subject: [PATCH] internal/clientinfo: add Ubios custom device name --- internal/clientinfo/client_info.go | 25 +++++++--- internal/clientinfo/ubios.go | 78 ++++++++++++++++++++++++++++++ internal/clientinfo/ubios_test.go | 43 ++++++++++++++++ 3 files changed, 140 insertions(+), 6 deletions(-) create mode 100644 internal/clientinfo/ubios.go create mode 100644 internal/clientinfo/ubios_test.go diff --git a/internal/clientinfo/client_info.go b/internal/clientinfo/client_info.go index f51cf88..72ef971 100644 --- a/internal/clientinfo/client_info.go +++ b/internal/clientinfo/client_info.go @@ -73,6 +73,7 @@ type Table struct { dhcp *dhcp merlin *merlinDiscover + ubios *ubiosDiscover arp *arpDiscover ndp *ndpDiscover ptr *ptrDiscover @@ -138,14 +139,26 @@ func (t *Table) init() { // Otherwise, process all possible sources in order, that means // the first result of IP/MAC/Hostname lookup will be used. // - // Merlin custom clients. + // Routers custom clients: + // - Merlin + // - Ubios if t.discoverDHCP() || t.discoverARP() { t.merlin = &merlinDiscover{} - if err := t.merlin.refresh(); err != nil { - ctrld.ProxyLogger.Load().Error().Err(err).Msg("could not init Merlin discover") - } else { - t.hostnameResolvers = append(t.hostnameResolvers, t.merlin) - t.refreshers = append(t.refreshers, t.merlin) + t.ubios = &ubiosDiscover{} + discovers := map[string]interface { + refresher + HostnameResolver + }{ + "Merlin": t.merlin, + "Ubios": t.ubios, + } + for platform, discover := range discovers { + if err := discover.refresh(); err != nil { + ctrld.ProxyLogger.Load().Error().Err(err).Msgf("could not init %s discover", platform) + } else { + t.hostnameResolvers = append(t.hostnameResolvers, discover) + t.refreshers = append(t.refreshers, discover) + } } } // Hosts file mapping. diff --git a/internal/clientinfo/ubios.go b/internal/clientinfo/ubios.go new file mode 100644 index 0000000..1a60de0 --- /dev/null +++ b/internal/clientinfo/ubios.go @@ -0,0 +1,78 @@ +package clientinfo + +import ( + "bytes" + "encoding/json" + "io" + "os/exec" + "strings" + "sync" + + "github.com/Control-D-Inc/ctrld/internal/router" + "github.com/Control-D-Inc/ctrld/internal/router/ubios" +) + +// ubiosDiscover provides client discovery functionality on Ubios routers. +type ubiosDiscover struct { + hostname sync.Map // mac => hostname +} + +// refresh reloads unifi devices from database. +func (u *ubiosDiscover) refresh() error { + if router.Name() != ubios.Name { + return nil + } + return u.refreshDevices() +} + +// LookupHostnameByIP returns hostname for given IP. +func (u *ubiosDiscover) LookupHostnameByIP(ip string) string { + return "" +} + +// LookupHostnameByMac returns unifi device custom hostname for the given MAC address. +func (u *ubiosDiscover) LookupHostnameByMac(mac string) string { + val, ok := u.hostname.Load(mac) + if !ok { + return "" + } + return val.(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", ` + DBQuery.shellBatchSize = 256; + db.user.find({name: {$exists: true, $ne: ""}}, {_id:0, mac:1, name:1});`) + b, err := cmd.Output() + if err != nil { + return err + } + return u.storeDevices(bytes.NewReader(b)) +} + +// storeDevices saves unifi devices name for caching. +func (u *ubiosDiscover) storeDevices(r io.Reader) error { + decoder := json.NewDecoder(r) + device := struct { + MAC string + Name string + }{} + for { + err := decoder.Decode(&device) + if err == io.EOF { + break + } + if err != nil { + return err + } + mac := strings.ToLower(device.MAC) + u.hostname.Store(mac, normalizeHostname(device.Name)) + } + return nil +} + +// String returns human-readable format of ubiosDiscover. +func (u *ubiosDiscover) String() string { + return "ubios" +} diff --git a/internal/clientinfo/ubios_test.go b/internal/clientinfo/ubios_test.go new file mode 100644 index 0000000..657cf18 --- /dev/null +++ b/internal/clientinfo/ubios_test.go @@ -0,0 +1,43 @@ +package clientinfo + +import ( + "strings" + "testing" +) + +func Test_ubiosDiscover_storeDevices(t *testing.T) { + ud := &ubiosDiscover{} + r := strings.NewReader(`{ "mac": "00:00:00:00:00:01", "name": "device 1" } +{ "mac": "00:00:00:00:00:02", "name": "device 2" } +`) + if err := ud.storeDevices(r); err != nil { + t.Fatal(err) + } + + tests := []struct { + name string + mac string + hostname string + }{ + {"device 1", "00:00:00:00:00:01", "device 1"}, + {"device 2", "00:00:00:00:00:02", "device 2"}, + {"non-existed", "00:00:00:00:00:03", ""}, + } + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + if got := ud.LookupHostnameByMac(tc.mac); got != tc.hostname { + t.Errorf("hostname mismatched, want: %q, got: %q", tc.hostname, got) + } + }) + } + + // Test for invalid input. + r = strings.NewReader(`{ "mac": "00:00:00:00:00:01", "name": "device 1"`) + if err := ud.storeDevices(r); err == nil { + t.Fatal("expected error, got nil") + } else { + t.Log(err) + } +}