diff --git a/core/unifi/devices.go b/core/unifi/devices.go index 13a5bac0..35003694 100644 --- a/core/unifi/devices.go +++ b/core/unifi/devices.go @@ -20,18 +20,89 @@ func (u *Unifi) GetDevices(sites []*Site) (*Devices, error) { return nil, err } - loopDevices := u.parseDevices(response.Data, site.SiteName) + loopDevices := u.parseDevices(response.Data, site) devices.UAPs = append(devices.UAPs, loopDevices.UAPs...) devices.USGs = append(devices.USGs, loopDevices.USGs...) devices.USWs = append(devices.USWs, loopDevices.USWs...) devices.UDMs = append(devices.UDMs, loopDevices.UDMs...) + devices.UXGs = append(devices.UXGs, loopDevices.UXGs...) } return devices, nil } +// GetUSWs returns all switches, an error, or nil if there are no switches. +func (u *Unifi) GetUSWs(site *Site) ([]*USW, error) { + var response struct { + Data []json.RawMessage `json:"data"` + } + + err := u.GetData(fmt.Sprintf(APIDevicePath, site.Name), &response) + if err != nil { + return nil, err + } + + return u.parseDevices(response.Data, site).USWs, nil +} + +// GetUSWs returns all access points, an error, or nil if there are no APs. +func (u *Unifi) GetUAPs(site *Site) ([]*UAP, error) { + var response struct { + Data []json.RawMessage `json:"data"` + } + + err := u.GetData(fmt.Sprintf(APIDevicePath, site.Name), &response) + if err != nil { + return nil, err + } + + return u.parseDevices(response.Data, site).UAPs, nil +} + +// GetUSWs returns all dream machines, an error, or nil if there are no UDMs. +func (u *Unifi) GetUDMs(site *Site) ([]*UDM, error) { + var response struct { + Data []json.RawMessage `json:"data"` + } + + err := u.GetData(fmt.Sprintf(APIDevicePath, site.Name), &response) + if err != nil { + return nil, err + } + + return u.parseDevices(response.Data, site).UDMs, nil +} + +// GetUSWs returns all 10Gb gateways, an error, or nil if there are no UXGs. +func (u *Unifi) GetUXGs(site *Site) ([]*UXG, error) { + var response struct { + Data []json.RawMessage `json:"data"` + } + + err := u.GetData(fmt.Sprintf(APIDevicePath, site.Name), &response) + if err != nil { + return nil, err + } + + return u.parseDevices(response.Data, site).UXGs, nil +} + +// GetUSWs returns all 1Gb gateways, an error, or nil if there are no USGs. +func (u *Unifi) GetUSGs(site *Site) ([]*USG, error) { + var response struct { + Data []json.RawMessage `json:"data"` + } + + err := u.GetData(fmt.Sprintf(APIDevicePath, site.Name), &response) + if err != nil { + return nil, err + } + + return u.parseDevices(response.Data, site).USGs, nil +} + // parseDevices parses the raw JSON from the Unifi Controller into device structures. -func (u *Unifi) parseDevices(data []json.RawMessage, siteName string) *Devices { +func (u *Unifi) parseDevices(data []json.RawMessage, site *Site) *Devices { devices := new(Devices) for _, r := range data { @@ -43,20 +114,20 @@ func (u *Unifi) parseDevices(data []json.RawMessage, siteName string) *Devices { } assetType, _ := o["type"].(string) - u.DebugLog("Unmarshalling Device Type: %v, site %s ", assetType, siteName) + u.DebugLog("Unmarshalling Device Type: %v, site %s ", assetType, site.Name) // Choose which type to unmarshal into based on the "type" json key. switch assetType { // Unmarshal again into the correct type.. case "uap": - u.unmarshallUAP(siteName, r, devices) + u.unmarshallUAP(site, r, devices) case "ugw", "usg": // in case they ever fix the name in the api. - u.unmarshallUSG(siteName, r, devices) + u.unmarshallUSG(site, r, devices) case "usw": - u.unmarshallUSW(siteName, r, devices) + u.unmarshallUSW(site, r, devices) case "udm": - u.unmarshallUDM(siteName, r, devices) + u.unmarshallUDM(site, r, devices) case "uxg": - u.unmarshallUXG(siteName, r, devices) + u.unmarshallUXG(site, r, devices) default: u.ErrorLog("unknown asset type - %v - skipping", assetType) } @@ -65,42 +136,47 @@ func (u *Unifi) parseDevices(data []json.RawMessage, siteName string) *Devices { return devices } -func (u *Unifi) unmarshallUAP(siteName string, payload json.RawMessage, devices *Devices) { - dev := &UAP{SiteName: siteName, SourceName: u.URL} +func (u *Unifi) unmarshallUAP(site *Site, payload json.RawMessage, devices *Devices) { + dev := &UAP{SiteName: site.Name, SourceName: u.URL} if u.unmarshalDevice("uap", payload, dev) == nil { dev.Name = strings.TrimSpace(pick(dev.Name, dev.Mac)) + dev.site = site devices.UAPs = append(devices.UAPs, dev) } } -func (u *Unifi) unmarshallUSG(siteName string, payload json.RawMessage, devices *Devices) { - dev := &USG{SiteName: siteName, SourceName: u.URL} +func (u *Unifi) unmarshallUSG(site *Site, payload json.RawMessage, devices *Devices) { + dev := &USG{SiteName: site.Name, SourceName: u.URL} if u.unmarshalDevice("ugw", payload, dev) == nil { dev.Name = strings.TrimSpace(pick(dev.Name, dev.Mac)) + dev.site = site devices.USGs = append(devices.USGs, dev) } } -func (u *Unifi) unmarshallUSW(siteName string, payload json.RawMessage, devices *Devices) { - dev := &USW{SiteName: siteName, SourceName: u.URL} +func (u *Unifi) unmarshallUSW(site *Site, payload json.RawMessage, devices *Devices) { + dev := &USW{SiteName: site.Name, SourceName: u.URL} if u.unmarshalDevice("usw", payload, dev) == nil { dev.Name = strings.TrimSpace(pick(dev.Name, dev.Mac)) + dev.site = site devices.USWs = append(devices.USWs, dev) } } -func (u *Unifi) unmarshallUXG(siteName string, payload json.RawMessage, devices *Devices) { - dev := &UXG{SiteName: siteName, SourceName: u.URL} +func (u *Unifi) unmarshallUXG(site *Site, payload json.RawMessage, devices *Devices) { + dev := &UXG{SiteName: site.Name, SourceName: u.URL} if u.unmarshalDevice("uxg", payload, dev) == nil { dev.Name = strings.TrimSpace(pick(dev.Name, dev.Mac)) + dev.site = site devices.UXGs = append(devices.UXGs, dev) } } -func (u *Unifi) unmarshallUDM(siteName string, payload json.RawMessage, devices *Devices) { - dev := &UDM{SiteName: siteName, SourceName: u.URL} +func (u *Unifi) unmarshallUDM(site *Site, payload json.RawMessage, devices *Devices) { + dev := &UDM{SiteName: site.Name, SourceName: u.URL} if u.unmarshalDevice("udm", payload, dev) == nil { dev.Name = strings.TrimSpace(pick(dev.Name, dev.Mac)) + dev.site = site devices.UDMs = append(devices.UDMs, dev) } } diff --git a/core/unifi/devmgr.go b/core/unifi/devmgr.go new file mode 100644 index 00000000..679f6648 --- /dev/null +++ b/core/unifi/devmgr.go @@ -0,0 +1,299 @@ +package unifi + +import ( + "encoding/json" + "fmt" +) + +// Known commands that can be sent to device manager. All of these are implemented. +//nolint:lll // https://ubntwiki.com/products/software/unifi-controller/api#callable +const ( + DevMgrPowerCycle = "power-cycle" // mac = switch mac (required), port_idx = PoE port to cycle (required) + DevMgrAdopt = "adopt" // mac = device mac (required) + DevMgrRestart = "restart" // mac = device mac (required) + DevMgrForceProvision = "force-provision" // mac = device mac (required) + DevMgrSpeedTest = "speedtest" // Start a speed test + DevMgrSpeedTestStatus = "speedtest-status" // Get current state of the speed test + DevMgrSetLocate = "set-locate" // mac = device mac (required): blink unit to locate + DevMgrUnsetLocate = "unset-locate" // mac = device mac (required): led to normal state + DevMgrUpgrade = "upgrade" // mac = device mac (required): upgrade firmware + DevMgrUpgradeExternal = "upgrade-external" // mac = device mac (required), url = firmware URL (required) + DevMgrMigrate = "migrate" // mac = device mac (required), inform_url = New Inform URL for device (required) + DevMgrCancelMigrate = "cancel-migrate" // mac = device mac (required) + DevMgrSpectrumScan = "spectrum-scan" // mac = AP mac (required): trigger RF scan +) + +// devMgrCmd is the type marshalled and sent to APIDevMgrPath. +type devMgrCmd struct { + Cmd string `json:"cmd"` // Required. + Mac string `json:"mac"` // Device MAC (required for most, but not all). + URL string `json:"url,omitempty"` // External Upgrade only. + Inform string `json:"inform_url,omitempty"` // Migration only. + Port int `json:"port_idx,omitempty"` // Power Cycle only. +} + +// devMgrCommandReply is for commands with a return value. +func (s *Site) devMgrCommandReply(cmd *devMgrCmd) ([]byte, error) { + data, err := json.Marshal(cmd) + if err != nil { + return nil, fmt.Errorf("json marshal: %w", err) + } + + b, err := s.controller.GetJSON(fmt.Sprintf(APIDevMgrPath, s.Name), string(data)) + if err != nil { + return nil, fmt.Errorf("controller: %w", err) + } + + return b, nil +} + +// devMgrCommandSimple is for commands with no return value. +func (s *Site) devMgrCommandSimple(cmd *devMgrCmd) error { + _, err := s.devMgrCommandReply(cmd) + return err +} + +// PowerCycle shuts off the PoE and turns it back on for a specific port. +// Get a USW from the device list to call this. +func (u *USW) PowerCycle(portIndex int) error { + return u.site.devMgrCommandSimple(&devMgrCmd{ + Cmd: DevMgrPowerCycle, + Mac: u.Mac, + Port: portIndex, + }) +} + +// ScanRF begins a spectrum scan on an access point. +func (u *UAP) ScanRF() error { + return u.site.devMgrCommandSimple(&devMgrCmd{Cmd: DevMgrSpectrumScan, Mac: u.Mac}) +} + +// Restart a device by MAC address on your site. +func (s *Site) Restart(mac string) error { + return s.devMgrCommandSimple(&devMgrCmd{Cmd: DevMgrRestart, Mac: mac}) +} + +// Restart an access point. +func (u *UAP) Restart() error { + return u.site.Restart(u.Mac) +} + +// Restart a switch. +func (u *USW) Restart() error { + return u.site.Restart(u.Mac) +} + +// Restart a security gateway. +func (u *USG) Restart() error { + return u.site.Restart(u.Mac) +} + +// Restart a dream machine. +func (u *UDM) Restart() error { + return u.site.Restart(u.Mac) +} + +// Restart a 10Gb security gateway. +func (u *UXG) Restart() error { + return u.site.Restart(u.Mac) +} + +// Locate a device by MAC address on your site. This makes it blink. +func (s *Site) Locate(mac string) error { + return s.devMgrCommandSimple(&devMgrCmd{Cmd: DevMgrSetLocate, Mac: mac}) +} + +// Locate an access point. +func (u *UAP) Locate() error { + return u.site.Locate(u.Mac) +} + +// Locate a switch. +func (u *USW) Locate() error { + return u.site.Locate(u.Mac) +} + +// Locate a security gateway. +func (u *USG) Locate() error { + return u.site.Locate(u.Mac) +} + +// Locate a dream machine. +func (u *UDM) Locate() error { + return u.site.Locate(u.Mac) +} + +// Locate a 10Gb security gateway. +func (u *UXG) Locate() error { + return u.site.Locate(u.Mac) +} + +// Unlocate a device by MAC address on your site. This makes it stop blinking. +func (s *Site) Unlocate(mac string) error { + return s.devMgrCommandSimple(&devMgrCmd{Cmd: DevMgrUnsetLocate, Mac: mac}) +} + +// Unlocate an access point (stop blinking). +func (u *UAP) Unlocate() error { + return u.site.Unlocate(u.Mac) +} + +// Unlocate a switch (stop blinking). +func (u *USW) Unlocate() error { + return u.site.Unlocate(u.Mac) +} + +// Unlocate a security gateway (stop blinking). +func (u *USG) Unlocate() error { + return u.site.Unlocate(u.Mac) +} + +// Unlocate a dream machine (stop blinking). +func (u *UDM) Unlocate() error { + return u.site.Unlocate(u.Mac) +} + +// Unlocate a 10Gb security gateway (stop blinking). +func (u *UXG) Unlocate() error { + return u.site.Unlocate(u.Mac) +} + +// Provision force provisions a device by MAC address on your site. +func (s *Site) Provision(mac string) error { + return s.devMgrCommandSimple(&devMgrCmd{Cmd: DevMgrForceProvision, Mac: mac}) +} + +// Provision an access point forcefully. +func (u *UAP) Provision() error { + return u.site.Provision(u.Mac) +} + +// Provision a switch forcefully. +func (u *USW) Provision() error { + return u.site.Provision(u.Mac) +} + +// Provision a security gateway forcefully. +func (u *USG) Provision() error { + return u.site.Provision(u.Mac) +} + +// Provision a dream machine forcefully. +func (u *UDM) Provision() error { + return u.site.Provision(u.Mac) +} + +// Provision a 10Gb security gateway forcefully. +func (u *UXG) Provision() error { + return u.site.Provision(u.Mac) +} + +// Upgrade starts a firmware upgrade on a device by MAC address on your site. +// URL is optional. If URL is not "" an external upgrade is performed. +func (s *Site) Upgrade(mac string, url string) error { + if url == "" { + return s.devMgrCommandSimple(&devMgrCmd{Cmd: DevMgrUpgrade, Mac: mac}) + } + + return s.devMgrCommandSimple(&devMgrCmd{Cmd: DevMgrUpgradeExternal, Mac: mac, URL: url}) +} + +// Upgrade firmware on an access point. +// URL is optional. If URL is not "" an external upgrade is performed. +func (u *UAP) Upgrade(url string) error { + return u.site.Upgrade(u.Mac, url) +} + +// Upgrade firmware on a switch. +// URL is optional. If URL is not "" an external upgrade is performed. +func (u *USW) Upgrade(url string) error { + return u.site.Upgrade(u.Mac, url) +} + +// Upgrade firmware on a security gateway. +// URL is optional. If URL is not "" an external upgrade is performed. +func (u *USG) Upgrade(url string) error { + return u.site.Upgrade(u.Mac, url) +} + +// Upgrade firmware on a dream machine. +// URL is optional. If URL is not "" an external upgrade is performed. +func (u *UDM) Upgrade(url string) error { + return u.site.Upgrade(u.Mac, url) +} + +// Upgrade formware on a 10Gb security gateway. +// URL is optional. If URL is not "" an external upgrade is performed. +func (u *UXG) Upgrade(url string) error { + return u.site.Upgrade(u.Mac, url) +} + +// Migrate sends a device to another controller's URL. +// Probably does not work on devices with built-in controllers like UDM & UXG. +func (s *Site) Migrate(mac string, url string) error { + return s.devMgrCommandSimple(&devMgrCmd{Cmd: DevMgrMigrate, Mac: mac, Inform: url}) +} + +// Migrate sends an access point to another controller's URL. +func (u *UAP) Migrate(url string) error { + return u.site.Migrate(u.Mac, url) +} + +// Migrate sends a switch to another controller's URL. +func (u *USW) Migrate(url string) error { + return u.site.Migrate(u.Mac, url) +} + +// Migrate sends a security gateway to another controller's URL. +func (u *USG) Migrate(url string) error { + return u.site.Migrate(u.Mac, url) +} + +// Migrate sends a 10Gb gateway to another controller's URL. +func (u *UXG) Migrate(url string) error { + return u.site.Migrate(u.Mac, url) +} + +// CancelMigrate stops a migration in progress. +// Probably does not work on devices with built-in controllers like UDM & UXG. +func (s *Site) CancelMigrate(mac string) error { + return s.devMgrCommandSimple(&devMgrCmd{Cmd: DevMgrCancelMigrate, Mac: mac}) +} + +// CancelMigrate stops an access point migration in progress. +func (u *UAP) CancelMigrate() error { + return u.site.CancelMigrate(u.Mac) +} + +// CancelMigrate stops a switch migration in progress. +func (u *USW) CancelMigrate() error { + return u.site.CancelMigrate(u.Mac) +} + +// CancelMigrate stops a security gateway migration in progress. +func (u *USG) CancelMigrate() error { + return u.site.CancelMigrate(u.Mac) +} + +// CancelMigrate stops 10Gb gateway a migration in progress. +func (u *UXG) CancelMigrate() error { + return u.site.CancelMigrate(u.Mac) +} + +// Adopt a device by MAC address to your site. +func (s *Site) Adopt(mac string) error { + return s.devMgrCommandSimple(&devMgrCmd{Cmd: DevMgrAdopt, Mac: mac}) +} + +// SpeedTest begins a speed test on a site. +func (s *Site) SpeedTest() error { + return s.devMgrCommandSimple(&devMgrCmd{Cmd: DevMgrSpeedTest}) +} + +// SpeedTestStatus returns the raw response for the status of a speed test. +// XXX: marshal the response into a data structure. This method will change! +func (s *Site) SpeedTestStatus() ([]byte, error) { + body, err := s.devMgrCommandReply(&devMgrCmd{Cmd: DevMgrSpeedTestStatus}) + // marshal into struct here. + return body, err +} diff --git a/core/unifi/site.go b/core/unifi/site.go index 0d3826a1..25030bfa 100644 --- a/core/unifi/site.go +++ b/core/unifi/site.go @@ -20,6 +20,8 @@ func (u *Unifi) GetSites() ([]*Site, error) { sites := []string{} // used for debug log only for i, d := range response.Data { + // Add the unifi struct to the site. + response.Data[i].controller = u // Add special SourceName value. response.Data[i].SourceName = u.URL // If the human name is missing (description), set it to the cryptic name. @@ -67,6 +69,7 @@ func (u *Unifi) GetSiteDPI(sites []*Site) ([]*DPITable, error) { // Site represents a site's data. type Site struct { + controller *Unifi SourceName string `json:"-"` ID string `json:"_id"` Name string `json:"name"` diff --git a/core/unifi/types.go b/core/unifi/types.go index 787a3c04..d31aa239 100644 --- a/core/unifi/types.go +++ b/core/unifi/types.go @@ -44,6 +44,8 @@ const ( APIPrefixNew string = "/proxy/network" // APIAnomaliesPath returns site anomalies. APIAnomaliesPath string = "/api/s/%s/stat/anomalies" + APICommandPath string = "/api/s/%s/cmd" + APIDevMgrPath string = APICommandPath + "/devmgr" ) // path returns the correct api path based on the new variable. diff --git a/core/unifi/uap.go b/core/unifi/uap.go index c11b38eb..dca0dfbb 100644 --- a/core/unifi/uap.go +++ b/core/unifi/uap.go @@ -9,6 +9,7 @@ import ( // UAP represents all the data from the Ubiquiti Controller for a Unifi Access Point. // This was auto generated then edited by hand to get all the data types right. type UAP struct { + site *Site SourceName string `json:"-"` ID string `json:"_id"` Adopted FlexBool `json:"adopted"` diff --git a/core/unifi/udm.go b/core/unifi/udm.go index 49053539..547c5f0e 100644 --- a/core/unifi/udm.go +++ b/core/unifi/udm.go @@ -3,6 +3,7 @@ package unifi // UDM represents all the data from the Ubiquiti Controller for a Unifi Dream Machine. // The UDM shares several structs/type-data with USW and USG. type UDM struct { + site *Site SourceName string `json:"-"` SiteID string `json:"site_id"` SiteName string `json:"-"` diff --git a/core/unifi/usg.go b/core/unifi/usg.go index ca126634..9cdc653e 100644 --- a/core/unifi/usg.go +++ b/core/unifi/usg.go @@ -7,6 +7,7 @@ import ( // USG represents all the data from the Ubiquiti Controller for a Unifi Security Gateway. type USG struct { + site *Site SourceName string `json:"-"` ID string `json:"_id"` Adopted FlexBool `json:"adopted"` diff --git a/core/unifi/usw.go b/core/unifi/usw.go index d95f2569..b1533467 100644 --- a/core/unifi/usw.go +++ b/core/unifi/usw.go @@ -7,6 +7,7 @@ import ( // USW represents all the data from the Ubiquiti Controller for a Unifi Switch. type USW struct { + site *Site SourceName string `json:"-"` SiteName string `json:"-"` ID string `json:"_id"` diff --git a/core/unifi/uxg.go b/core/unifi/uxg.go index ae39e069..ed688718 100644 --- a/core/unifi/uxg.go +++ b/core/unifi/uxg.go @@ -3,6 +3,7 @@ package unifi // UXG represents all the data from the Ubiquiti Controller for a UniFi 10Gb Gateway. // The UDM shares several structs/type-data with USW and USG. type UXG struct { + site *Site SourceName string `json:"-"` SiteName string `json:"-"` ID string `json:"_id"`