329 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			329 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Go
		
	
	
	
| package main
 | |
| 
 | |
| import (
 | |
| 	"bytes"
 | |
| 	"crypto/tls"
 | |
| 	"encoding/json"
 | |
| 	"errors"
 | |
| 	"io"
 | |
| 	"io/ioutil"
 | |
| 	"log"
 | |
| 	"net/http"
 | |
| 	"net/http/cookiejar"
 | |
| 	"os"
 | |
| 	"strconv"
 | |
| 	"time"
 | |
| 
 | |
| 	influx "github.com/influxdata/influxdb/client/v2"
 | |
| )
 | |
| 
 | |
| const (
 | |
| 	// LoginPath is Unifi Controller Login API Path
 | |
| 	LoginPath = "/api/login"
 | |
| 	// ClientPath is Unifi Clients API Path
 | |
| 	ClientPath = "/api/s/default/stat/sta"
 | |
| )
 | |
| 
 | |
| // ClientResponse marshalls the payload from the controller.
 | |
| type ClientResponse struct {
 | |
| 	Clients []Client `json:"data"`
 | |
| 	Meta    struct {
 | |
| 		Rc string `json:"rc"`
 | |
| 	} `json:"meta"`
 | |
| }
 | |
| 
 | |
| // Config represents the data needed to poll a controller and report to influxdb.
 | |
| type Config struct {
 | |
| 	Interval   time.Duration `json:"interval",toml:"interval",yaml:"interval"`
 | |
| 	InfluxAddr string        `json:"influx_addr",toml:"influx_addr",yaml:"influx_addr"`
 | |
| 	InfluxUser string        `json:"influx_user",toml:"influx_user",yaml:"influx_user"`
 | |
| 	InfluxPass string        `json:"influx_pass",toml:"influx_pass",yaml:"influx_pass"`
 | |
| 	InfluxDB   string        `json:"influx_db",toml:"influx_db",yaml:"influx_db"`
 | |
| 	UnifiUser  string        `json:"unifi_user",toml:"unifi_user",yaml:"unifi_user"`
 | |
| 	UnifiPass  string        `json:"unifi_pass"toml:"unifi_pass",yaml:"unifi_pass"`
 | |
| 	UnifiBase  string        `json:"unifi_url",toml:"unifi_url",yaml:"unifi_url"`
 | |
| 	uniClient  *http.Client
 | |
| }
 | |
| 
 | |
| // DpiStat is for deep packet inspection stats.
 | |
| // Does not seem to exist in Unifi 5.7.20.
 | |
| type DpiStat struct {
 | |
| 	App       int64
 | |
| 	Cat       int64
 | |
| 	RxBytes   int64
 | |
| 	RxPackets int64
 | |
| 	TxBytes   int64
 | |
| 	TxPackets int64
 | |
| }
 | |
| 
 | |
| // Client defines all the data a connected-network client contains.
 | |
| type Client struct {
 | |
| 	ID                  string    `json:"_id"`
 | |
| 	IsGuestByUAP        bool      `json:"_is_guest_by_uap"`
 | |
| 	IsGuestByUGW        bool      `json:"_is_guest_by_ugw"`
 | |
| 	IsGuestByUSW        bool      `json:"_is_guest_by_usw"`
 | |
| 	LastSeenByUAP       int64     `json:"_last_seen_by_uap"`
 | |
| 	LastSeenByUGW       int64     `json:"_last_seen_by_ugw"`
 | |
| 	LastSeenByUSW       int64     `json:"_last_seen_by_usw"`
 | |
| 	UptimeByUAP         int64     `json:"_uptime_by_uap"`
 | |
| 	UptimeByUGW         int64     `json:"_uptime_by_ugw"`
 | |
| 	UptimeByUSW         int64     `json:"_uptime_by_usw"`
 | |
| 	ApMac               string    `json:"ap_mac"`
 | |
| 	AssocTime           int64     `json:"assoc_time"`
 | |
| 	Authorized          bool      `json:"authorized"`
 | |
| 	Bssid               string    `json:"bssid"`
 | |
| 	BytesR              int64     `json:"bytes-r"`
 | |
| 	Ccq                 int64     `json:"ccq"`
 | |
| 	Channel             int       `json:"channel"`
 | |
| 	DpiStats            []DpiStat `json:"dpi_stats"`
 | |
| 	DpiStatsLastUpdated int64     `json:"dpi_stats_last_updated"`
 | |
| 	Essid               string    `json:"essid"`
 | |
| 	FirstSeen           int64     `json:"first_seen"`
 | |
| 	FixedIP             string    `json:"fixed_ip"`
 | |
| 	Hostname            string    `json:"hostname"`
 | |
| 	GwMac               string    `json:"gw_mac"`
 | |
| 	IdleTime            int64     `json:"idle_time"`
 | |
| 	IP                  string    `json:"ip"`
 | |
| 	Is11R               bool      `json:"is_11r"`
 | |
| 	IsGuest             bool      `json:"is_guest"`
 | |
| 	IsWired             bool      `json:"is_wired"`
 | |
| 	LastSeen            int64     `json:"last_seen"`
 | |
| 	LatestAssocTime     int64     `json:"latest_assoc_time"`
 | |
| 	Mac                 string    `json:"mac"`
 | |
| 	Name                string    `json:"name"`
 | |
| 	Network             string    `json:"network"`
 | |
| 	NetworkID           string    `json:"network_id"`
 | |
| 	Noise               int64     `json:"noise"`
 | |
| 	Note                string    `json:"note"`
 | |
| 	Noted               bool      `json:"noted"`
 | |
| 	Oui                 string    `json:"oui"`
 | |
| 	PowersaveEnabled    bool      `json:"powersave_enabled"`
 | |
| 	QosPolicyApplied    bool      `json:"qos_policy_applied"`
 | |
| 	Radio               string    `json:"radio"`
 | |
| 	RadioName           string    `json:"radio_name"`
 | |
| 	RadioProto          string    `json:"radio_proto"`
 | |
| 	RoamCount           int64     `json:"roam_count"`
 | |
| 	Rssi                int64     `json:"rssi"`
 | |
| 	RxBytes             int64     `json:"rx_bytes"`
 | |
| 	RxBytesR            int64     `json:"rx_bytes-r"`
 | |
| 	RxPackets           int64     `json:"rx_packets"`
 | |
| 	RxRate              int64     `json:"rx_rate"`
 | |
| 	Signal              int64     `json:"signal"`
 | |
| 	SiteID              string    `json:"site_id"`
 | |
| 	SwDepth             int       `json:"sw_depth"`
 | |
| 	SwMac               string    `json:"sw_mac"`
 | |
| 	SwPort              int       `json:"sw_port"`
 | |
| 	TxBytes             int64     `json:"tx_bytes"`
 | |
| 	TxBytesR            int64     `json:"tx_bytes-r"`
 | |
| 	TxPackets           int64     `json:"tx_packets"`
 | |
| 	TxPower             int64     `json:"tx_power"`
 | |
| 	TxRate              int64     `json:"tx_rate"`
 | |
| 	Uptime              int64     `json:"uptime"`
 | |
| 	UserID              string    `json:"user_id"`
 | |
| 	UserGroupID         string    `json:"usergroup_id"`
 | |
| 	UseFixedIP          bool      `json:"use_fixedip"`
 | |
| 	Vlan                int       `json:"vlan"`
 | |
| }
 | |
| 
 | |
| // Point generates a client's datapoint for InfluxDB.
 | |
| func (c Client) Point() *influx.Point {
 | |
| 	if c.Name == "" && c.Hostname != "" {
 | |
| 		c.Name = c.Hostname
 | |
| 	} else if c.Hostname == "" && c.Name != "" {
 | |
| 		c.Hostname = c.Name
 | |
| 	} else if c.Hostname == "" && c.Name == "" {
 | |
| 		c.Hostname = "-no-name-"
 | |
| 		c.Name = "-no-name-"
 | |
| 	}
 | |
| 	tags := map[string]string{
 | |
| 		"id":                 c.ID,
 | |
| 		"mac":                c.Mac,
 | |
| 		"user_id":            c.UserID,
 | |
| 		"site_id":            c.SiteID,
 | |
| 		"ip":                 c.IP,
 | |
| 		"fixed_ip":           c.FixedIP,
 | |
| 		"essid":              c.Essid,
 | |
| 		"bssid":              c.Bssid,
 | |
| 		"network":            c.Network,
 | |
| 		"network_id":         c.NetworkID,
 | |
| 		"usergroup_id":       c.UserGroupID,
 | |
| 		"ap_mac":             c.ApMac,
 | |
| 		"gw_mac":             c.GwMac,
 | |
| 		"sw_mac":             c.SwMac,
 | |
| 		"sw_port":            strconv.Itoa(c.SwPort),
 | |
| 		"oui":                c.Oui,
 | |
| 		"name":               c.Name,
 | |
| 		"hostname":           c.Hostname,
 | |
| 		"radio_name":         c.RadioName,
 | |
| 		"radio":              c.Radio,
 | |
| 		"radio_proto":        c.RadioProto,
 | |
| 		"authorized":         strconv.FormatBool(c.Authorized),
 | |
| 		"is_11r":             strconv.FormatBool(c.Is11R),
 | |
| 		"is_wired":           strconv.FormatBool(c.IsWired),
 | |
| 		"is_guest":           strconv.FormatBool(c.IsGuest),
 | |
| 		"is_guest_by_uap":    strconv.FormatBool(c.IsGuestByUAP),
 | |
| 		"is_guest_by_ugw":    strconv.FormatBool(c.IsGuestByUGW),
 | |
| 		"is_guest_by_usw":    strconv.FormatBool(c.IsGuestByUSW),
 | |
| 		"noted":              strconv.FormatBool(c.Noted),
 | |
| 		"powersave_enabled":  strconv.FormatBool(c.PowersaveEnabled),
 | |
| 		"qos_policy_applied": strconv.FormatBool(c.QosPolicyApplied),
 | |
| 		"use_fixedip":        strconv.FormatBool(c.UseFixedIP),
 | |
| 		"channel":            strconv.Itoa(c.Channel),
 | |
| 		"vlan":               strconv.Itoa(c.Vlan),
 | |
| 	}
 | |
| 	fields := map[string]interface{}{
 | |
| 		"dpi_stats_last_updated": c.DpiStatsLastUpdated,
 | |
| 		"last_seen_by_uap":       c.LastSeenByUAP,
 | |
| 		"last_seen_by_ugw":       c.LastSeenByUGW,
 | |
| 		"last_seen_by_usw":       c.LastSeenByUSW,
 | |
| 		"uptime_by_uap":          c.UptimeByUAP,
 | |
| 		"uptime_by_ugw":          c.UptimeByUGW,
 | |
| 		"uptime_by_usw":          c.UptimeByUSW,
 | |
| 		"assoc_time":             c.AssocTime,
 | |
| 		"bytes_r":                c.BytesR,
 | |
| 		"ccq":                    c.Ccq,
 | |
| 		"first_seen":             c.FirstSeen,
 | |
| 		"idle_time":              c.IdleTime,
 | |
| 		"last_seen":              c.LastSeen,
 | |
| 		"latest_assoc_time":      c.LatestAssocTime,
 | |
| 		"noise":                  c.Noise,
 | |
| 		"note":                   c.Note,
 | |
| 		"roam_count":             c.RoamCount,
 | |
| 		"rssi":                   c.Rssi,
 | |
| 		"rx_bytes":               c.RxBytes,
 | |
| 		"rx_bytes_r":             c.RxBytesR,
 | |
| 		"rx_packets":             c.RxPackets,
 | |
| 		"rx_rate":                c.RxRate,
 | |
| 		"signal":                 c.Signal,
 | |
| 		"tx_bytes":               c.TxBytes,
 | |
| 		"tx_bytes_r":             c.TxBytesR,
 | |
| 		"tx_packets":             c.TxPackets,
 | |
| 		"tx_power":               c.TxPower,
 | |
| 		"tx_rate":                c.TxRate,
 | |
| 		"uptime":                 c.Uptime,
 | |
| 	}
 | |
| 
 | |
| 	pt, err := influx.NewPoint("client_state", tags, fields, time.Now())
 | |
| 	if err != nil {
 | |
| 		log.Println("Error creating point:", err)
 | |
| 		return nil
 | |
| 	}
 | |
| 
 | |
| 	return pt
 | |
| }
 | |
| 
 | |
| func main() {
 | |
| 	config := GetConfig()
 | |
| 	if err := config.AuthController(); err != nil {
 | |
| 		log.Fatal(err)
 | |
| 	}
 | |
| 	log.Println("Successfully authenticated to Unifi Controller!")
 | |
| 
 | |
| 	infdb, err := influx.NewHTTPClient(influx.HTTPConfig{
 | |
| 		Addr:     config.InfluxAddr,
 | |
| 		Username: config.InfluxUser,
 | |
| 		Password: config.InfluxPass,
 | |
| 	})
 | |
| 	if err != nil {
 | |
| 		log.Fatal(err)
 | |
| 	}
 | |
| 	log.Println("Polling Unifi Controller, interval:", config.Interval)
 | |
| 	config.PollUnifiController(infdb)
 | |
| }
 | |
| 
 | |
| // PollUnifiController runs forever, polling and pushing.
 | |
| func (c *Config) PollUnifiController(infdb influx.Client) {
 | |
| 	ticker := time.NewTicker(c.Interval)
 | |
| 	for range ticker.C {
 | |
| 		uniResponse, err := c.GetUnifiClients()
 | |
| 		if err != nil {
 | |
| 			log.Println("GetUnifiClients(unifi):", err)
 | |
| 			continue
 | |
| 		}
 | |
| 		bp, err := influx.NewBatchPoints(influx.BatchPointsConfig{
 | |
| 			Database: c.InfluxDB,
 | |
| 		})
 | |
| 		if err != nil {
 | |
| 			log.Println("influx.NewBatchPoints:", err)
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		for _, client := range uniResponse.Clients {
 | |
| 			bp.AddPoint(client.Point())
 | |
| 		}
 | |
| 		if err = infdb.Write(bp); err != nil {
 | |
| 			log.Println("infdb.Write(bp):", err)
 | |
| 			continue
 | |
| 		}
 | |
| 		log.Println("Logged client state. Clients:", len(uniResponse.Clients))
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // GetConfig parses and returns our configuration data.
 | |
| func GetConfig() Config {
 | |
| 	// TODO: A real config file.
 | |
| 	var err error
 | |
| 	config := Config{
 | |
| 		InfluxAddr: os.Getenv("INFLUXDB_ADDR"),
 | |
| 		InfluxUser: os.Getenv("INFLUXDB_USERNAME"),
 | |
| 		InfluxPass: os.Getenv("INFLUXDB_PASSWORD"),
 | |
| 		InfluxDB:   os.Getenv("INFLUXDB_DATABASE"),
 | |
| 		UnifiUser:  os.Getenv("UNIFI_USERNAME"),
 | |
| 		UnifiPass:  os.Getenv("UNIFI_PASSWORD"),
 | |
| 		UnifiBase:  "https://" + os.Getenv("UNIFI_ADDR") + ":" + os.Getenv("UNIFI_PORT"),
 | |
| 	}
 | |
| 	if config.Interval, err = time.ParseDuration(os.Getenv("INTERVAL")); err != nil {
 | |
| 		log.Println("Invalid Interval, defaulting to 15 seconds.")
 | |
| 		config.Interval = time.Duration(time.Second * 15)
 | |
| 	}
 | |
| 	return config
 | |
| }
 | |
| 
 | |
| // GetUnifiClients returns a response full of clients' data from the Unifi Controller.
 | |
| func (c *Config) GetUnifiClients() (*ClientResponse, error) {
 | |
| 	response := &ClientResponse{}
 | |
| 	if req, err := uniRequest("GET", c.UnifiBase+ClientPath, nil); err != nil {
 | |
| 		return nil, err
 | |
| 	} else if resp, err := c.uniClient.Do(req); err != nil {
 | |
| 		return nil, err
 | |
| 	} else if body, err := ioutil.ReadAll(resp.Body); err != nil {
 | |
| 		return nil, err
 | |
| 	} else if err = json.Unmarshal(body, response); err != nil {
 | |
| 		return nil, err
 | |
| 	} else if err = resp.Body.Close(); err != nil {
 | |
| 		log.Println("resp.Body.Close():", err) // Not fatal? Just log it.
 | |
| 	}
 | |
| 	return response, nil
 | |
| }
 | |
| 
 | |
| // AuthController creates a http.Client with authenticated cookies.
 | |
| // Used to make additional, authenticated requests to the APIs.
 | |
| func (c *Config) AuthController() error {
 | |
| 	json := bytes.NewBufferString(`{"username": "` + c.UnifiUser + `","password": "` + c.UnifiPass + `"}`)
 | |
| 	jar, err := cookiejar.New(nil)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	c.uniClient = &http.Client{
 | |
| 		Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}},
 | |
| 		Jar:       jar,
 | |
| 	}
 | |
| 	if req, err := uniRequest("POST", c.UnifiBase+LoginPath, json); err != nil {
 | |
| 		return err
 | |
| 	} else if resp, err := c.uniClient.Do(req); err != nil {
 | |
| 		return err
 | |
| 	} else if resp.StatusCode != http.StatusOK {
 | |
| 		return errors.New("Error Authenticating with Unifi Controller")
 | |
| 	}
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func uniRequest(method string, url string, data io.Reader) (*http.Request, error) {
 | |
| 	req, err := http.NewRequest(method, url, data)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	req.Header.Add("Accept", "application/json")
 | |
| 	return req, nil
 | |
| }
 |