diff --git a/.env.example b/.env.example deleted file mode 100644 index e5fd1f32..00000000 --- a/.env.example +++ /dev/null @@ -1,17 +0,0 @@ -UNIFI_ADDR="107.170.232.179" -UNIFI_PORT="8443" -# Go to Settings -> Admins and add (or use) a read-only user for this. -UNIFI_USERNAME="username" -UNIFI_PASSWORD="password" - -# Can be 1m15s for 1 minute 0 seconds, 15s for 15 seconds, etc. -INTERVAL="15s" - -INFLUXDB_ADDR="http://hostname:8086" -INFLUXDB_DATABASE="unifi" -INFLUXDB_USERNAME="unifi" -INFLUXDB_PASSWORD="password" - -export UNIFI_ADDR UNIFI_PORT UNIFI_USERNAME UNIFI_PASSWORD -export INTERVAL -export INFLUXDB_ADDR INFLUXDB_DATABASE INFLUXDB_USERNAME INFLUXDB_PASSWORD diff --git a/.gitignore b/.gitignore index 3d8b3da5..913e8eb4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -.env +/up.conf /unifi-poller /*.1.gz /*.1 diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index 2e53a112..d5c6895b 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -21,6 +21,26 @@ "Comment": "v1.5.0-149-g14dcc5d", "Rev": "14dcc5d6e7a6b15e17aba7b104b8ad0ca6c91ad2" }, + { + "ImportPath": "github.com/naoina/go-stringutil", + "Comment": "v0.1.0", + "Rev": "6b638e95a32d0c1131db0e7fe83775cbea4a0d0b" + }, + { + "ImportPath": "github.com/naoina/toml", + "Comment": "v0.1.0", + "Rev": "751171607256bb66e64c9f0220c00662420c38e9" + }, + { + "ImportPath": "github.com/naoina/toml/ast", + "Comment": "v0.1.0", + "Rev": "751171607256bb66e64c9f0220c00662420c38e9" + }, + { + "ImportPath": "github.com/ogier/pflag", + "Comment": "v0.0.1-7-g45c278a", + "Rev": "45c278ab3607870051a2ea9040bb85fcb8557481" + }, { "ImportPath": "github.com/pkg/errors", "Comment": "v0.8.0-6-g2b3a18b", diff --git a/README.md b/README.md index 13c95a62..8e850026 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,12 @@ # Unifi -Collect your Unifi Controller Client data and send it to an InfluxDB instance. +Collect your Unifi Controller Client data and send it to an InfluxDB instance. Grafana dashboard included. ![image](https://raw.githubusercontent.com/davidnewhall/unifi/master/grafana-unifi-dashboard.png) -## Deploying +## Installation - -Clone the repository and using `.env.example` create your own `.env` file with your Unifi GUI and InfluxDB credentials. - - -Set your environment variables before running: - -``` -source .env ; ./unifi-poller -``` +[See the Wiki!](https://github.com/davidnewhall/unifi-poller/wiki/Installation) ## Copyright & License -Copyright © 2016 Garrett Bjerkhoel. See [MIT-LICENSE](http://github.com/dewski/unifi/blob/master/MIT-LICENSE) for details. +Copyright © 2016 Garrett Bjerkhoel. See [MIT-LICENSE](MIT-LICENSE) for details. diff --git a/cmd/unifi-poller/config.go b/cmd/unifi-poller/config.go index 590b8202..b09fffac 100644 --- a/cmd/unifi-poller/config.go +++ b/cmd/unifi-poller/config.go @@ -5,6 +5,9 @@ import ( "time" ) +// Version will be injected at build time. +var Version = "v0.1" + const ( // LoginPath is Unifi Controller Login API Path LoginPath = "/api/login" @@ -17,6 +20,7 @@ const ( // UserGroupPath contains usergroup configurations. UserGroupPath = "/api/s/default/rest/usergroup" // App defaults in case they're missing from the config. + defaultConfFile = "/usr/local/etc/unifi-poller/up.conf" defaultInterval = 30 * time.Second defaultInfxDb = "unifi" defaultInfxUser = "unifi" @@ -28,13 +32,28 @@ const ( // Config represents the data needed to poll a controller and report to influxdb. type Config struct { - Interval time.Duration `json:"interval" toml:"interval" xml:"interval" yaml:"interval"` - InfluxURL string `json:"influx_url" toml:"influx_addr" xml:"influx_addr" yaml:"influx_addr"` - InfluxUser string `json:"influx_user" toml:"influx_user" xml:"influx_user" yaml:"influx_user"` - InfluxPass string `json:"influx_pass" toml:"influx_pass" xml:"influx_pass" yaml:"influx_pass"` - InfluxDB string `json:"influx_db" toml:"influx_db" xml:"influx_db" yaml:"influx_db"` - UnifiUser string `json:"unifi_user" toml:"unifi_user" xml:"unifi_user" yaml:"unifi_user"` - UnifiPass string `json:"unifi_pass" toml:"unifi_pass" xml:"unifi_pass" yaml:"unifi_pass"` - UnifiBase string `json:"unifi_url" toml:"unifi_url" xml:"unifi_url" yaml:"unifi_url"` + Interval Dur `json:"interval" toml:"interval" xml:"interval" yaml:"interval"` + InfluxURL string `json:"influx_url" toml:"influx_url" xml:"influx_url" yaml:"influx_url"` + InfluxUser string `json:"influx_user" toml:"influx_user" xml:"influx_user" yaml:"influx_user"` + InfluxPass string `json:"influx_pass" toml:"influx_pass" xml:"influx_pass" yaml:"influx_pass"` + InfluxDB string `json:"influx_db" toml:"influx_db" xml:"influx_db" yaml:"influx_db"` + UnifiUser string `json:"unifi_user" toml:"unifi_user" xml:"unifi_user" yaml:"unifi_user"` + UnifiPass string `json:"unifi_pass" toml:"unifi_pass" xml:"unifi_pass" yaml:"unifi_pass"` + UnifiBase string `json:"unifi_url" toml:"unifi_url" xml:"unifi_url" yaml:"unifi_url"` uniClient *http.Client } + +// Dur is used to UnmarshalTOML into a time.Duration value. +type Dur struct { + value time.Duration +} + +// UnmarshalTOML parses a duration type from a config file. +func (v *Dur) UnmarshalTOML(data []byte) error { + unquoted := string(data[1 : len(data)-1]) + dur, err := time.ParseDuration(unquoted) + if err == nil { + v.value = dur + } + return err +} diff --git a/cmd/unifi-poller/main.go b/cmd/unifi-poller/main.go index 505873ca..66f4d705 100644 --- a/cmd/unifi-poller/main.go +++ b/cmd/unifi-poller/main.go @@ -4,6 +4,7 @@ import ( "bytes" "crypto/tls" "encoding/json" + "fmt" "io/ioutil" "log" "net/http" @@ -12,15 +13,36 @@ import ( "time" influx "github.com/influxdata/influxdb/client/v2" + "github.com/naoina/toml" + flg "github.com/ogier/pflag" "github.com/pkg/errors" ) func main() { - config := GetConfig() + flg.Usage = func() { + fmt.Println("Usage: unifi-poller [--config=filepath] [--debug] [--version]") + flg.PrintDefaults() + } + configFile := flg.StringP("config", "c", defaultConfFile, "Poller Config File (TOML Format)") + debug := flg.BoolP("debug", "D", false, "Turn on the Spam (default false)") + version := flg.BoolP("version", "v", false, "Print the version and exit.") + flg.Parse() + if *version { + fmt.Println("unifi-poller version:", Version) + os.Exit(0) // don't run anything else. + } + if log.SetFlags(0); *debug { + log.SetFlags(log.Lshortfile | log.Lmicroseconds | log.Ldate) + } + config, errc := GetConfig(*configFile) + if errc != nil { + flg.Usage() + log.Fatalln("Config Error:", errc) + } if err := config.AuthController(); err != nil { log.Fatal(err) } - log.Println("Authenticated to Unifi Controller", config.UnifiBase, "as user", config.UnifiUser) + log.Println("Authenticated to Unifi Controller @", config.UnifiBase, "as user", config.UnifiUser) infdb, err := influx.NewHTTPClient(influx.HTTPConfig{ Addr: config.InfluxURL, @@ -31,28 +53,30 @@ func main() { log.Fatal(err) } log.Println("Logging Unifi Metrics to InfluXDB @", config.InfluxURL, "as user", config.InfluxUser) - log.Println("Polling Unifi Controller, interval:", config.Interval) + log.Println("Polling Unifi Controller, interval:", config.Interval.value) config.PollUnifiController(infdb) } // GetConfig parses and returns our configuration data. -func GetConfig() Config { - // TODO: A real config file. - interval, err := time.ParseDuration(os.Getenv("INTERVAL")) - if err != nil { - log.Println("Invalid Interval, defaulting to", defaultInterval) - interval = time.Duration(defaultInterval) - } - return Config{ - InfluxURL: os.Getenv("INFLUXDB_URL"), - InfluxUser: os.Getenv("INFLUXDB_USERNAME"), - InfluxPass: os.Getenv("INFLUXDB_PASSWORD"), - InfluxDB: os.Getenv("INFLUXDB_DATABASE"), - UnifiUser: os.Getenv("UNIFI_USERNAME"), +func GetConfig(configFile string) (Config, error) { + // Preload our defaults. + config := Config{ + InfluxURL: defaultInfxURL, + InfluxUser: defaultInfxUser, + InfluxPass: defaultInfxPass, + InfluxDB: defaultInfxDb, + UnifiUser: defaultUnifUser, UnifiPass: os.Getenv("UNIFI_PASSWORD"), - UnifiBase: "https://" + os.Getenv("UNIFI_ADDR") + ":" + os.Getenv("UNIFI_PORT"), - Interval: interval, + UnifiBase: defaultUnifURL, + Interval: Dur{value: defaultInterval}, } + if buf, err := ioutil.ReadFile(configFile); err != nil { + return config, err + // This is where the defaults in the config variable are overwritten. + } else if err := toml.Unmarshal(buf, &config); err != nil { + return config, errors.Wrap(err, "invalid config") + } + return config, nil } // AuthController creates a http.Client with authenticated cookies. @@ -80,7 +104,7 @@ func (c *Config) AuthController() error { // PollUnifiController runs forever, polling and pushing. func (c *Config) PollUnifiController(infdb influx.Client) { - ticker := time.NewTicker(c.Interval) + ticker := time.NewTicker(c.Interval.value) for range ticker.C { clients, err := c.GetUnifiClients() if err != nil { diff --git a/grafana-unifi-dashboard.png b/grafana-unifi-dashboard.png index ad2071fd..472c8ea3 100644 Binary files a/grafana-unifi-dashboard.png and b/grafana-unifi-dashboard.png differ diff --git a/up.conf.example b/up.conf.example new file mode 100644 index 00000000..eddce77f --- /dev/null +++ b/up.conf.example @@ -0,0 +1,15 @@ +# The Unifi Controller only updates traffic stats about every 30 seconds. +# Setting this to something lower may lead to "zeros" in your data. You've been warned. +interval = "30s" + +# InfluxDB does not require auth by default, so the user/password are probably unimportant. +influx_url = "http://127.0.0.1:8086" +influx_user = "unifi" +influx_pass = "unifi" +# Be sure to create this database. +influx_db = "unifi" + +# Make a read-only user in the Unifi Admin Settings. +unifi_user = "influxdb" +unifi_pass = "4BB9345C-2341-48D7-99F5-E01B583FF77F" +unifi_url = "https://127.0.0.1:8443"