Projectize the project.

This commit is contained in:
DN2 2018-04-22 14:10:55 -07:00
parent e45eaafd65
commit c5a4a33791
25 changed files with 2402 additions and 2493 deletions

View File

@ -1 +1,5 @@
.env .env
/unifi-poller
/*.1.gz
/*.1
/vendor

View File

@ -1,24 +1,30 @@
{ {
"ImportPath": "github.com/dewski/unifi", "ImportPath": "github.com/davidnewhall/unifi",
"GoVersion": "go1.6", "GoVersion": "go1.10",
"GodepVersion": "v79",
"Packages": [ "Packages": [
"./..." "./..."
], ],
"Deps": [ "Deps": [
{ {
"ImportPath": "github.com/influxdata/influxdb/client/v2", "ImportPath": "github.com/influxdata/influxdb/client/v2",
"Comment": "v0.10.0-565-ga4b00ae", "Comment": "v1.5.0-149-g14dcc5d",
"Rev": "a4b00aeeba630bda96db70bd9fc5eb2d4c0b7580" "Rev": "14dcc5d6e7a6b15e17aba7b104b8ad0ca6c91ad2"
}, },
{ {
"ImportPath": "github.com/influxdata/influxdb/models", "ImportPath": "github.com/influxdata/influxdb/models",
"Comment": "v0.10.0-565-ga4b00ae", "Comment": "v1.5.0-149-g14dcc5d",
"Rev": "a4b00aeeba630bda96db70bd9fc5eb2d4c0b7580" "Rev": "14dcc5d6e7a6b15e17aba7b104b8ad0ca6c91ad2"
}, },
{ {
"ImportPath": "github.com/influxdata/influxdb/pkg/escape", "ImportPath": "github.com/influxdata/influxdb/pkg/escape",
"Comment": "v0.10.0-565-ga4b00ae", "Comment": "v1.5.0-149-g14dcc5d",
"Rev": "a4b00aeeba630bda96db70bd9fc5eb2d4c0b7580" "Rev": "14dcc5d6e7a6b15e17aba7b104b8ad0ca6c91ad2"
},
{
"ImportPath": "github.com/pkg/errors",
"Comment": "v0.8.0-6-g2b3a18b",
"Rev": "2b3a18b5f0fb6b4f9190549597d3f962c02bc5eb"
} }
] ]
} }

View File

@ -1,2 +0,0 @@
/pkg
/bin

View File

@ -1,20 +0,0 @@
The MIT License (MIT)
Copyright (c) 2013-2016 Errplane Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -1,27 +0,0 @@
# List
- bootstrap 3.3.5 [MIT LICENSE](https://github.com/twbs/bootstrap/blob/master/LICENSE)
- collectd.org [ISC LICENSE](https://github.com/collectd/go-collectd/blob/master/LICENSE)
- github.com/armon/go-metrics [MIT LICENSE](https://github.com/armon/go-metrics/blob/master/LICENSE)
- github.com/BurntSushi/toml [WTFPL LICENSE](https://github.com/BurntSushi/toml/blob/master/COPYING)
- github.com/bmizerany/pat [MIT LICENSE](https://github.com/bmizerany/pat#license)
- github.com/boltdb/bolt [MIT LICENSE](https://github.com/boltdb/bolt/blob/master/LICENSE)
- github.com/dgryski/go-bits [MIT LICENSE](https://github.com/dgryski/go-bits/blob/master/LICENSE)
- github.com/dgryski/go-bitstream [MIT LICENSE](https://github.com/dgryski/go-bitstream/blob/master/LICENSE)
- github.com/gogo/protobuf/proto [BSD LICENSE](https://github.com/gogo/protobuf/blob/master/LICENSE)
- github.com/davecgh/go-spew/spew [ISC LICENSE](https://github.com/davecgh/go-spew/blob/master/LICENSE)
- github.com/golang/snappy [BSD LICENSE](https://github.com/golang/snappy/blob/master/LICENSE)
- github.com/hashicorp/go-msgpack [BSD LICENSE](https://github.com/hashicorp/go-msgpack/blob/master/LICENSE)
- github.com/hashicorp/raft [MPL LICENSE](https://github.com/hashicorp/raft/blob/master/LICENSE)
- github.com/hashicorp/raft-boltdb [MOZILLA PUBLIC LICENSE](https://github.com/hashicorp/raft-boltdb/blob/master/LICENSE)
- github.com/influxdata/usage-client [MIT LICENSE](https://github.com/influxdata/usage-client/blob/master/LICENSE.txt)
- github.com/jwilder/encoding [MIT LICENSE](https://github.com/jwilder/encoding/blob/master/LICENSE)
- github.com/kimor79/gollectd [BSD LICENSE](https://github.com/kimor79/gollectd/blob/master/LICENSE)
- github.com/paulbellamy/ratecounter [MIT LICENSE](https://github.com/paulbellamy/ratecounter/blob/master/LICENSE)
- github.com/peterh/liner [MIT LICENSE](https://github.com/peterh/liner/blob/master/COPYING)
- github.com/rakyll/statik [APACHE LICENSE](https://github.com/rakyll/statik/blob/master/LICENSE)
- glyphicons [LICENSE](http://glyphicons.com/license/)
- golang.org/x/crypto [BSD LICENSE](https://github.com/golang/crypto/blob/master/LICENSE)
- golang.org/x/tools [BSD LICENSE](https://github.com/golang/tools/blob/master/LICENSE)
- gopkg.in/fatih/pool.v2 [MIT LICENSE](https://github.com/fatih/pool/blob/v2.0.0/LICENSE)
- jquery 2.1.4 [MIT LICENSE](https://github.com/jquery/jquery/blob/master/LICENSE.txt)
- react 0.13.3 [BSD LICENSE](https://github.com/facebook/react/blob/master/LICENSE)

View File

@ -1,573 +0,0 @@
package client
import (
"bytes"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net"
"net/http"
"net/url"
"time"
"github.com/influxdata/influxdb/models"
)
// UDPPayloadSize is a reasonable default payload size for UDP packets that
// could be travelling over the internet.
const (
UDPPayloadSize = 512
)
// HTTPConfig is the config data needed to create an HTTP Client
type HTTPConfig struct {
// Addr should be of the form "http://host:port"
// or "http://[ipv6-host%zone]:port".
Addr string
// Username is the influxdb username, optional
Username string
// Password is the influxdb password, optional
Password string
// UserAgent is the http User Agent, defaults to "InfluxDBClient"
UserAgent string
// Timeout for influxdb writes, defaults to no timeout
Timeout time.Duration
// InsecureSkipVerify gets passed to the http client, if true, it will
// skip https certificate verification. Defaults to false
InsecureSkipVerify bool
// TLSConfig allows the user to set their own TLS config for the HTTP
// Client. If set, this option overrides InsecureSkipVerify.
TLSConfig *tls.Config
}
// UDPConfig is the config data needed to create a UDP Client
type UDPConfig struct {
// Addr should be of the form "host:port"
// or "[ipv6-host%zone]:port".
Addr string
// PayloadSize is the maximum size of a UDP client message, optional
// Tune this based on your network. Defaults to UDPBufferSize.
PayloadSize int
}
// BatchPointsConfig is the config data needed to create an instance of the BatchPoints struct
type BatchPointsConfig struct {
// Precision is the write precision of the points, defaults to "ns"
Precision string
// Database is the database to write points to
Database string
// RetentionPolicy is the retention policy of the points
RetentionPolicy string
// Write consistency is the number of servers required to confirm write
WriteConsistency string
}
// Client is a client interface for writing & querying the database
type Client interface {
// Ping checks that status of cluster
Ping(timeout time.Duration) (time.Duration, string, error)
// Write takes a BatchPoints object and writes all Points to InfluxDB.
Write(bp BatchPoints) error
// Query makes an InfluxDB Query on the database. This will fail if using
// the UDP client.
Query(q Query) (*Response, error)
// Close releases any resources a Client may be using.
Close() error
}
// NewHTTPClient creates a client interface from the given config.
func NewHTTPClient(conf HTTPConfig) (Client, error) {
if conf.UserAgent == "" {
conf.UserAgent = "InfluxDBClient"
}
u, err := url.Parse(conf.Addr)
if err != nil {
return nil, err
} else if u.Scheme != "http" && u.Scheme != "https" {
m := fmt.Sprintf("Unsupported protocol scheme: %s, your address"+
" must start with http:// or https://", u.Scheme)
return nil, errors.New(m)
}
tr := &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: conf.InsecureSkipVerify,
},
}
if conf.TLSConfig != nil {
tr.TLSClientConfig = conf.TLSConfig
}
return &client{
url: u,
username: conf.Username,
password: conf.Password,
useragent: conf.UserAgent,
httpClient: &http.Client{
Timeout: conf.Timeout,
Transport: tr,
},
}, nil
}
// Ping will check to see if the server is up with an optional timeout on waiting for leader.
// Ping returns how long the request took, the version of the server it connected to, and an error if one occurred.
func (c *client) Ping(timeout time.Duration) (time.Duration, string, error) {
now := time.Now()
u := c.url
u.Path = "ping"
req, err := http.NewRequest("GET", u.String(), nil)
if err != nil {
return 0, "", err
}
req.Header.Set("User-Agent", c.useragent)
if c.username != "" {
req.SetBasicAuth(c.username, c.password)
}
if timeout > 0 {
params := req.URL.Query()
params.Set("wait_for_leader", fmt.Sprintf("%.0fs", timeout.Seconds()))
req.URL.RawQuery = params.Encode()
}
resp, err := c.httpClient.Do(req)
if err != nil {
return 0, "", err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return 0, "", err
}
if resp.StatusCode != http.StatusNoContent {
var err = fmt.Errorf(string(body))
return 0, "", err
}
version := resp.Header.Get("X-Influxdb-Version")
return time.Since(now), version, nil
}
// Close releases the client's resources.
func (c *client) Close() error {
return nil
}
// NewUDPClient returns a client interface for writing to an InfluxDB UDP
// service from the given config.
func NewUDPClient(conf UDPConfig) (Client, error) {
var udpAddr *net.UDPAddr
udpAddr, err := net.ResolveUDPAddr("udp", conf.Addr)
if err != nil {
return nil, err
}
conn, err := net.DialUDP("udp", nil, udpAddr)
if err != nil {
return nil, err
}
payloadSize := conf.PayloadSize
if payloadSize == 0 {
payloadSize = UDPPayloadSize
}
return &udpclient{
conn: conn,
payloadSize: payloadSize,
}, nil
}
// Ping will check to see if the server is up with an optional timeout on waiting for leader.
// Ping returns how long the request took, the version of the server it connected to, and an error if one occurred.
func (uc *udpclient) Ping(timeout time.Duration) (time.Duration, string, error) {
return 0, "", nil
}
// Close releases the udpclient's resources.
func (uc *udpclient) Close() error {
return uc.conn.Close()
}
type client struct {
url *url.URL
username string
password string
useragent string
httpClient *http.Client
}
type udpclient struct {
conn *net.UDPConn
payloadSize int
}
// BatchPoints is an interface into a batched grouping of points to write into
// InfluxDB together. BatchPoints is NOT thread-safe, you must create a separate
// batch for each goroutine.
type BatchPoints interface {
// AddPoint adds the given point to the Batch of points
AddPoint(p *Point)
// AddPoints adds the given points to the Batch of points
AddPoints(ps []*Point)
// Points lists the points in the Batch
Points() []*Point
// Precision returns the currently set precision of this Batch
Precision() string
// SetPrecision sets the precision of this batch.
SetPrecision(s string) error
// Database returns the currently set database of this Batch
Database() string
// SetDatabase sets the database of this Batch
SetDatabase(s string)
// WriteConsistency returns the currently set write consistency of this Batch
WriteConsistency() string
// SetWriteConsistency sets the write consistency of this Batch
SetWriteConsistency(s string)
// RetentionPolicy returns the currently set retention policy of this Batch
RetentionPolicy() string
// SetRetentionPolicy sets the retention policy of this Batch
SetRetentionPolicy(s string)
}
// NewBatchPoints returns a BatchPoints interface based on the given config.
func NewBatchPoints(conf BatchPointsConfig) (BatchPoints, error) {
if conf.Precision == "" {
conf.Precision = "ns"
}
if _, err := time.ParseDuration("1" + conf.Precision); err != nil {
return nil, err
}
bp := &batchpoints{
database: conf.Database,
precision: conf.Precision,
retentionPolicy: conf.RetentionPolicy,
writeConsistency: conf.WriteConsistency,
}
return bp, nil
}
type batchpoints struct {
points []*Point
database string
precision string
retentionPolicy string
writeConsistency string
}
func (bp *batchpoints) AddPoint(p *Point) {
bp.points = append(bp.points, p)
}
func (bp *batchpoints) AddPoints(ps []*Point) {
bp.points = append(bp.points, ps...)
}
func (bp *batchpoints) Points() []*Point {
return bp.points
}
func (bp *batchpoints) Precision() string {
return bp.precision
}
func (bp *batchpoints) Database() string {
return bp.database
}
func (bp *batchpoints) WriteConsistency() string {
return bp.writeConsistency
}
func (bp *batchpoints) RetentionPolicy() string {
return bp.retentionPolicy
}
func (bp *batchpoints) SetPrecision(p string) error {
if _, err := time.ParseDuration("1" + p); err != nil {
return err
}
bp.precision = p
return nil
}
func (bp *batchpoints) SetDatabase(db string) {
bp.database = db
}
func (bp *batchpoints) SetWriteConsistency(wc string) {
bp.writeConsistency = wc
}
func (bp *batchpoints) SetRetentionPolicy(rp string) {
bp.retentionPolicy = rp
}
// Point represents a single data point
type Point struct {
pt models.Point
}
// NewPoint returns a point with the given timestamp. If a timestamp is not
// given, then data is sent to the database without a timestamp, in which case
// the server will assign local time upon reception. NOTE: it is recommended to
// send data with a timestamp.
func NewPoint(
name string,
tags map[string]string,
fields map[string]interface{},
t ...time.Time,
) (*Point, error) {
var T time.Time
if len(t) > 0 {
T = t[0]
}
pt, err := models.NewPoint(name, tags, fields, T)
if err != nil {
return nil, err
}
return &Point{
pt: pt,
}, nil
}
// String returns a line-protocol string of the Point
func (p *Point) String() string {
return p.pt.String()
}
// PrecisionString returns a line-protocol string of the Point, at precision
func (p *Point) PrecisionString(precison string) string {
return p.pt.PrecisionString(precison)
}
// Name returns the measurement name of the point
func (p *Point) Name() string {
return p.pt.Name()
}
// Tags returns the tags associated with the point
func (p *Point) Tags() map[string]string {
return p.pt.Tags()
}
// Time return the timestamp for the point
func (p *Point) Time() time.Time {
return p.pt.Time()
}
// UnixNano returns the unix nano time of the point
func (p *Point) UnixNano() int64 {
return p.pt.UnixNano()
}
// Fields returns the fields for the point
func (p *Point) Fields() map[string]interface{} {
return p.pt.Fields()
}
// NewPointFrom returns a point from the provided models.Point.
func NewPointFrom(pt models.Point) *Point {
return &Point{pt: pt}
}
func (uc *udpclient) Write(bp BatchPoints) error {
var b bytes.Buffer
var d time.Duration
d, _ = time.ParseDuration("1" + bp.Precision())
for _, p := range bp.Points() {
pointstring := p.pt.RoundedString(d) + "\n"
// Write and reset the buffer if we reach the max size
if b.Len()+len(pointstring) >= uc.payloadSize {
if _, err := uc.conn.Write(b.Bytes()); err != nil {
return err
}
b.Reset()
}
if _, err := b.WriteString(pointstring); err != nil {
return err
}
}
_, err := uc.conn.Write(b.Bytes())
return err
}
func (c *client) Write(bp BatchPoints) error {
var b bytes.Buffer
for _, p := range bp.Points() {
if _, err := b.WriteString(p.pt.PrecisionString(bp.Precision())); err != nil {
return err
}
if err := b.WriteByte('\n'); err != nil {
return err
}
}
u := c.url
u.Path = "write"
req, err := http.NewRequest("POST", u.String(), &b)
if err != nil {
return err
}
req.Header.Set("Content-Type", "")
req.Header.Set("User-Agent", c.useragent)
if c.username != "" {
req.SetBasicAuth(c.username, c.password)
}
params := req.URL.Query()
params.Set("db", bp.Database())
params.Set("rp", bp.RetentionPolicy())
params.Set("precision", bp.Precision())
params.Set("consistency", bp.WriteConsistency())
req.URL.RawQuery = params.Encode()
resp, err := c.httpClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
if resp.StatusCode != http.StatusNoContent && resp.StatusCode != http.StatusOK {
var err = fmt.Errorf(string(body))
return err
}
return nil
}
// Query defines a query to send to the server
type Query struct {
Command string
Database string
Precision string
}
// NewQuery returns a query object
// database and precision strings can be empty strings if they are not needed
// for the query.
func NewQuery(command, database, precision string) Query {
return Query{
Command: command,
Database: database,
Precision: precision,
}
}
// Response represents a list of statement results.
type Response struct {
Results []Result
Err string `json:"error,omitempty"`
}
// Error returns the first error from any statement.
// Returns nil if no errors occurred on any statements.
func (r *Response) Error() error {
if r.Err != "" {
return fmt.Errorf(r.Err)
}
for _, result := range r.Results {
if result.Err != "" {
return fmt.Errorf(result.Err)
}
}
return nil
}
// Result represents a resultset returned from a single statement.
type Result struct {
Series []models.Row
Err string `json:"error,omitempty"`
}
func (uc *udpclient) Query(q Query) (*Response, error) {
return nil, fmt.Errorf("Querying via UDP is not supported")
}
// Query sends a command to the server and returns the Response
func (c *client) Query(q Query) (*Response, error) {
u := c.url
u.Path = "query"
req, err := http.NewRequest("GET", u.String(), nil)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "")
req.Header.Set("User-Agent", c.useragent)
if c.username != "" {
req.SetBasicAuth(c.username, c.password)
}
params := req.URL.Query()
params.Set("q", q.Command)
params.Set("db", q.Database)
if q.Precision != "" {
params.Set("epoch", q.Precision)
}
req.URL.RawQuery = params.Encode()
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var response Response
dec := json.NewDecoder(resp.Body)
dec.UseNumber()
decErr := dec.Decode(&response)
// ignore this error if we got an invalid status code
if decErr != nil && decErr.Error() == "EOF" && resp.StatusCode != http.StatusOK {
decErr = nil
}
// If we got a valid decode error, send that back
if decErr != nil {
return nil, decErr
}
// If we don't have an error in our json response, and didn't get statusOK
// then send back an error
if resp.StatusCode != http.StatusOK && response.Error() == nil {
return &response, fmt.Errorf("received status code %d from server",
resp.StatusCode)
}
return &response, nil
}

View File

@ -1,46 +0,0 @@
package models
import (
"errors"
"strings"
)
// ConsistencyLevel represent a required replication criteria before a write can
// be returned as successful
type ConsistencyLevel int
const (
// ConsistencyLevelAny allows for hinted hand off, potentially no write happened yet
ConsistencyLevelAny ConsistencyLevel = iota
// ConsistencyLevelOne requires at least one data node acknowledged a write
ConsistencyLevelOne
// ConsistencyLevelQuorum requires a quorum of data nodes to acknowledge a write
ConsistencyLevelQuorum
// ConsistencyLevelAll requires all data nodes to acknowledge a write
ConsistencyLevelAll
)
var (
// ErrInvalidConsistencyLevel is returned when parsing the string version
// of a consistency level.
ErrInvalidConsistencyLevel = errors.New("invalid consistency level")
)
// ParseConsistencyLevel converts a consistency level string to the corresponding ConsistencyLevel const
func ParseConsistencyLevel(level string) (ConsistencyLevel, error) {
switch strings.ToLower(level) {
case "any":
return ConsistencyLevelAny, nil
case "one":
return ConsistencyLevelOne, nil
case "quorum":
return ConsistencyLevelQuorum, nil
case "all":
return ConsistencyLevelAll, nil
default:
return 0, ErrInvalidConsistencyLevel
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,60 +0,0 @@
package models
import (
"hash/fnv"
"sort"
)
// Row represents a single row returned from the execution of a statement.
type Row struct {
Name string `json:"name,omitempty"`
Tags map[string]string `json:"tags,omitempty"`
Columns []string `json:"columns,omitempty"`
Values [][]interface{} `json:"values,omitempty"`
Err error `json:"err,omitempty"`
}
// SameSeries returns true if r contains values for the same series as o.
func (r *Row) SameSeries(o *Row) bool {
return r.tagsHash() == o.tagsHash() && r.Name == o.Name
}
// tagsHash returns a hash of tag key/value pairs.
func (r *Row) tagsHash() uint64 {
h := fnv.New64a()
keys := r.tagsKeys()
for _, k := range keys {
h.Write([]byte(k))
h.Write([]byte(r.Tags[k]))
}
return h.Sum64()
}
// tagKeys returns a sorted list of tag keys.
func (r *Row) tagsKeys() []string {
a := make([]string, 0, len(r.Tags))
for k := range r.Tags {
a = append(a, k)
}
sort.Strings(a)
return a
}
// Rows represents a collection of rows. Rows implements sort.Interface.
type Rows []*Row
func (p Rows) Len() int { return len(p) }
func (p Rows) Less(i, j int) bool {
// Sort by name first.
if p[i].Name != p[j].Name {
return p[i].Name < p[j].Name
}
// Sort by tag set hash. Tags don't have a meaningful sort order so we
// just compute a hash and sort by that instead. This allows the tests
// to receive rows in a predictable order every time.
return p[i].tagsHash() < p[j].tagsHash()
}
func (p Rows) Swap(i, j int) { p[i], p[j] = p[j], p[i] }

View File

@ -1,51 +0,0 @@
package models
// Helper time methods since parsing time can easily overflow and we only support a
// specific time range.
import (
"fmt"
"math"
"time"
)
var (
// MaxNanoTime is the maximum time that can be represented via int64 nanoseconds since the epoch.
MaxNanoTime = time.Unix(0, math.MaxInt64).UTC()
// MinNanoTime is the minumum time that can be represented via int64 nanoseconds since the epoch.
MinNanoTime = time.Unix(0, math.MinInt64).UTC()
// ErrTimeOutOfRange gets returned when time is out of the representable range using int64 nanoseconds since the epoch.
ErrTimeOutOfRange = fmt.Errorf("time outside range %s - %s", MinNanoTime, MaxNanoTime)
)
// SafeCalcTime safely calculates the time given. Will return error if the time is outside the
// supported range.
func SafeCalcTime(timestamp int64, precision string) (time.Time, error) {
mult := GetPrecisionMultiplier(precision)
if t, ok := safeSignedMult(timestamp, mult); ok {
return time.Unix(0, t).UTC(), nil
}
return time.Time{}, ErrTimeOutOfRange
}
// CheckTime checks that a time is within the safe range.
func CheckTime(t time.Time) error {
if t.Before(MinNanoTime) || t.After(MaxNanoTime) {
return ErrTimeOutOfRange
}
return nil
}
// Perform the multiplication and check to make sure it didn't overflow.
func safeSignedMult(a, b int64) (int64, bool) {
if a == 0 || b == 0 || a == 1 || b == 1 {
return a * b, true
}
if a == math.MinInt64 || b == math.MaxInt64 {
return 0, false
}
c := a * b
return c, c/b == a
}

View File

@ -1,45 +0,0 @@
package escape
import "bytes"
func Bytes(in []byte) []byte {
for b, esc := range Codes {
in = bytes.Replace(in, []byte{b}, esc, -1)
}
return in
}
func Unescape(in []byte) []byte {
i := 0
inLen := len(in)
var out []byte
for {
if i >= inLen {
break
}
if in[i] == '\\' && i+1 < inLen {
switch in[i+1] {
case ',':
out = append(out, ',')
i += 2
continue
case '"':
out = append(out, '"')
i += 2
continue
case ' ':
out = append(out, ' ')
i += 2
continue
case '=':
out = append(out, '=')
i += 2
continue
}
}
out = append(out, in[i])
i += 1
}
return out
}

View File

@ -1,34 +0,0 @@
package escape
import "strings"
var (
Codes = map[byte][]byte{
',': []byte(`\,`),
'"': []byte(`\"`),
' ': []byte(`\ `),
'=': []byte(`\=`),
}
codesStr = map[string]string{}
)
func init() {
for k, v := range Codes {
codesStr[string(k)] = string(v)
}
}
func UnescapeString(in string) string {
for b, esc := range codesStr {
in = strings.Replace(in, esc, b, -1)
}
return in
}
func String(in string) string {
for b, esc := range codesStr {
in = strings.Replace(in, b, esc, -1)
}
return in
}

View File

@ -1,2 +1,34 @@
all: PACKAGES=`find ./cmd -mindepth 1 -maxdepth 1 -type d`
go build ./... LIBRARYS=
all: clean man build
clean:
for p in $(PACKAGES); do rm -f `echo $${p}|cut -d/ -f3`{,.1,.1.gz}; done
build:
for p in $(PACKAGES); do go build -ldflags "-w -s" $${p}; done
linux:
for p in $(PACKAGES); do GOOS=linux go build -ldflags "-w -s" $${p}; done
install:
go install -ldflags "-w -s" ./...
test: lint
for p in $(PACKAGES) $(LIBRARYS); do go test -race -covermode=atomic $${p}; done
lint:
goimports -l $(PACKAGES) $(LIBRARYS)
gofmt -l $(PACKAGES) $(LIBRARYS)
errcheck $(PACKAGES) $(LIBRARYS)
golint $(PACKAGES) $(LIBRARYS)
go vet $(PACKAGES) $(LIBRARYS)
man:
script/build_manpages.sh ./
deps:
rm -rf Godeps vendor
godep save ./...
godep update ./...

View File

@ -1 +0,0 @@
worker: unifi

View File

@ -1,32 +1,20 @@
# Unifi # Unifi
Collect your Unifi client data every 15 seconds and send it to an InfluxDB instance. Collect your Unifi Controller Client data and send it to an InfluxDB instance.
![image](https://cloud.githubusercontent.com/assets/79995/19002122/6b81f928-86ff-11e6-8ab4-d67f943588f4.png) ![image](https://raw.githubusercontent.com/davidnewhall/unifi/master/grafana-unifi-dashboard.png)
## Deploying ## Deploying
The repository is ready for deployment on Heroku. Steps to deploy:
Clone the repository and using `.env.example` create your own `.env` file with your Unifi GUI and InfluxDB credentials. Clone the repository and using `.env.example` create your own `.env` file with your Unifi GUI and InfluxDB credentials.
Create your heroku application:
Set your environment variables before running:
``` ```
heroku create [name] source .env ; ./unifi-poller
``` ```
Set your environment variables before deploying: ## Copyright & License
```
heroku config:set $(cat .env | grep -v ^# | xargs)
```
Push to heroku:
```
git push heroku master
```
## Copyright
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](http://github.com/dewski/unifi/blob/master/MIT-LICENSE) for details.

View File

@ -1,11 +0,0 @@
{
"name": "unifi",
"description": "Data logger for Unifi controllers",
"keywords": [
"go",
],
"image": "heroku/go:1.6",
"mount_dir": "src/github.com/dewski/unifi",
"website": "https://github.com/dewski/unifi",
"repository": "https://github.com/dewski/unifi"
}

View File

@ -0,0 +1,51 @@
unifi-poller(1) -- Utility to poll Unifi Metrics and drop them into InfluxDB
===
## SYNOPSIS
`unifi-poller -c /usr/local/etc/unifi-poller.conf`
## DESCRIPTION
* This application polls a Unifi Controller API for Client and Device Metrics.
* The metrics are then stored in an InfluxDB instance.
## OPTIONS
`unifi-poller [-c <config file>] [-h] [-v]`
-c, --config <file_path>
Provide a configuration file (instead of the default).
-v, --version
Display version and exit.
-h, --help
Display usage and exit.
## GO DURATION
This application uses the Go Time Durations for a polling interval.
The format is an integer followed by a time unit. You may append multiple time units
to add them together. Some valid time units are:
`us` (microsecond)
`ns` (nanosecond)
`ms` (millisecond)
`s` (second)
`m` (minute)
`h` (hour)
Example Use: `1m`, `5h`, `100ms`, `17s`, `1s45ms`, `1m3s`
## AUTHOR
* Garrett Bjerkhoel (original code) ~ 2016
* David Newhall II (rewritten) ~ 4/20/2018
## LOCATION
* https://github.com/davidnewhall/unifi-poller
* /usr/local/bin/unifi-poller
* previously: https://github.com/dewski/unifi

View File

@ -1,7 +1,6 @@
package main package main
import ( import (
"log"
"strconv" "strconv"
"time" "time"
@ -46,6 +45,9 @@ type Client struct {
BytesR int64 `json:"bytes-r"` BytesR int64 `json:"bytes-r"`
Ccq int64 `json:"ccq"` Ccq int64 `json:"ccq"`
Channel int `json:"channel"` Channel int `json:"channel"`
DevCat int `json:"dev_cat"`
DevFamily int `json:"dev_family"`
DevID int `json:"dev_id"`
DpiStats []DpiStat `json:"dpi_stats"` DpiStats []DpiStat `json:"dpi_stats"`
DpiStatsLastUpdated int64 `json:"dpi_stats_last_updated"` DpiStatsLastUpdated int64 `json:"dpi_stats_last_updated"`
Essid string `json:"essid"` Essid string `json:"essid"`
@ -67,6 +69,8 @@ type Client struct {
Noise int64 `json:"noise"` Noise int64 `json:"noise"`
Note string `json:"note"` Note string `json:"note"`
Noted bool `json:"noted"` Noted bool `json:"noted"`
OsClass int `json:"os_class"`
OsName int `json:"os_name"`
Oui string `json:"oui"` Oui string `json:"oui"`
PowersaveEnabled bool `json:"powersave_enabled"` PowersaveEnabled bool `json:"powersave_enabled"`
QosPolicyApplied bool `json:"qos_policy_applied"` QosPolicyApplied bool `json:"qos_policy_applied"`
@ -94,10 +98,16 @@ type Client struct {
UserGroupID string `json:"usergroup_id"` UserGroupID string `json:"usergroup_id"`
UseFixedIP bool `json:"use_fixedip"` UseFixedIP bool `json:"use_fixedip"`
Vlan int `json:"vlan"` Vlan int `json:"vlan"`
WiredRxBytes int64 `json:"wired-rx_bytes"`
WiredRxBytesR int64 `json:"wired-rx_bytes-r"`
WiredRxPackets int64 `json:"wired-rx_packets"`
WiredTxBytes int64 `json:"wired-tx_bytes"`
WiredTxBytesR int64 `json:"wired-tx_bytes-r"`
WiredTxPackets int64 `json:"wired-tx_packets"`
} }
// Point generates a client's datapoint for InfluxDB. // Point generates a client's datapoint for InfluxDB.
func (c Client) Point() *influx.Point { func (c Client) Point() (*influx.Point, error) {
if c.Name == "" && c.Hostname != "" { if c.Name == "" && c.Hostname != "" {
c.Name = c.Hostname c.Name = c.Hostname
} else if c.Hostname == "" && c.Name != "" { } else if c.Hostname == "" && c.Name != "" {
@ -111,23 +121,23 @@ func (c Client) Point() *influx.Point {
"mac": c.Mac, "mac": c.Mac,
"user_id": c.UserID, "user_id": c.UserID,
"site_id": c.SiteID, "site_id": c.SiteID,
"ip": c.IP,
"fixed_ip": c.FixedIP,
"essid": c.Essid,
"bssid": c.Bssid,
"network": c.Network,
"network_id": c.NetworkID, "network_id": c.NetworkID,
"usergroup_id": c.UserGroupID, "usergroup_id": c.UserGroupID,
"ap_mac": c.ApMac, "ap_mac": c.ApMac,
"gw_mac": c.GwMac, "gw_mac": c.GwMac,
"sw_mac": c.SwMac, "sw_mac": c.SwMac,
"sw_port": strconv.Itoa(c.SwPort),
"oui": c.Oui, "oui": c.Oui,
"name": c.Name,
"hostname": c.Hostname,
"radio_name": c.RadioName, "radio_name": c.RadioName,
"radio": c.Radio, "radio": c.Radio,
"radio_proto": c.RadioProto, "radio_proto": c.RadioProto,
"name": c.Name,
"fixed_ip": c.FixedIP,
"sw_port": strconv.Itoa(c.SwPort),
"os_class": strconv.Itoa(c.OsClass),
"os_name": strconv.Itoa(c.OsName),
"dev_cat": strconv.Itoa(c.DevCat),
"dev_id": strconv.Itoa(c.DevID),
"dev_family": strconv.Itoa(c.DevFamily),
"authorized": strconv.FormatBool(c.Authorized), "authorized": strconv.FormatBool(c.Authorized),
"is_11r": strconv.FormatBool(c.Is11R), "is_11r": strconv.FormatBool(c.Is11R),
"is_wired": strconv.FormatBool(c.IsWired), "is_wired": strconv.FormatBool(c.IsWired),
@ -143,6 +153,10 @@ func (c Client) Point() *influx.Point {
"vlan": strconv.Itoa(c.Vlan), "vlan": strconv.Itoa(c.Vlan),
} }
fields := map[string]interface{}{ fields := map[string]interface{}{
"ip": c.IP,
"essid": c.Essid,
"bssid": c.Bssid,
"hostname": c.Hostname,
"dpi_stats_last_updated": c.DpiStatsLastUpdated, "dpi_stats_last_updated": c.DpiStatsLastUpdated,
"last_seen_by_uap": c.LastSeenByUAP, "last_seen_by_uap": c.LastSeenByUAP,
"last_seen_by_ugw": c.LastSeenByUGW, "last_seen_by_ugw": c.LastSeenByUGW,
@ -157,6 +171,7 @@ func (c Client) Point() *influx.Point {
"idle_time": c.IdleTime, "idle_time": c.IdleTime,
"last_seen": c.LastSeen, "last_seen": c.LastSeen,
"latest_assoc_time": c.LatestAssocTime, "latest_assoc_time": c.LatestAssocTime,
"network": c.Network,
"noise": c.Noise, "noise": c.Noise,
"note": c.Note, "note": c.Note,
"roam_count": c.RoamCount, "roam_count": c.RoamCount,
@ -172,12 +187,13 @@ func (c Client) Point() *influx.Point {
"tx_power": c.TxPower, "tx_power": c.TxPower,
"tx_rate": c.TxRate, "tx_rate": c.TxRate,
"uptime": c.Uptime, "uptime": c.Uptime,
"wired-rx_bytes": c.WiredRxBytes,
"wired-rx_bytes-r": c.WiredRxBytesR,
"wired-rx_packets": c.WiredRxPackets,
"wired-tx_bytes": c.WiredTxBytes,
"wired-tx_bytes-r": c.WiredTxBytesR,
"wired-tx_packets": c.WiredTxPackets,
} }
pt, err := influx.NewPoint("clients", tags, fields, time.Now()) return influx.NewPoint("clients", tags, fields, time.Now())
if err != nil {
log.Println("Error creating point:", err)
return nil
}
return pt
} }

View File

@ -16,12 +16,20 @@ const (
NetworkPath = "/api/s/default/rest/networkconf" NetworkPath = "/api/s/default/rest/networkconf"
// UserGroupPath contains usergroup configurations. // UserGroupPath contains usergroup configurations.
UserGroupPath = "/api/s/default/rest/usergroup" UserGroupPath = "/api/s/default/rest/usergroup"
// App defaults in case they're missing from the config.
defaultInterval = 30 * time.Second
defaultInfxDb = "unifi"
defaultInfxUser = "unifi"
defaultInfxPass = "unifi"
defaultInfxURL = "http://127.0.0.1:8086"
defaultUnifUser = "influx"
defaultUnifURL = "https://127.0.0.1:8443"
) )
// Config represents the data needed to poll a controller and report to influxdb. // Config represents the data needed to poll a controller and report to influxdb.
type Config struct { type Config struct {
Interval time.Duration `json:"interval",toml:"interval",yaml:"interval"` Interval time.Duration `json:"interval",toml:"interval",yaml:"interval"`
InfluxAddr string `json:"influx_addr",toml:"influx_addr",yaml:"influx_addr"` InfluxURL string `json:"influx_url",toml:"influx_addr",yaml:"influx_addr"`
InfluxUser string `json:"influx_user",toml:"influx_user",yaml:"influx_user"` InfluxUser string `json:"influx_user",toml:"influx_user",yaml:"influx_user"`
InfluxPass string `json:"influx_pass",toml:"influx_pass",yaml:"influx_pass"` InfluxPass string `json:"influx_pass",toml:"influx_pass",yaml:"influx_pass"`
InfluxDB string `json:"influx_db",toml:"influx_db",yaml:"influx_db"` InfluxDB string `json:"influx_db",toml:"influx_db",yaml:"influx_db"`

View File

@ -0,0 +1 @@
package main

View File

@ -4,7 +4,6 @@ import (
"bytes" "bytes"
"crypto/tls" "crypto/tls"
"encoding/json" "encoding/json"
"errors"
"io/ioutil" "io/ioutil"
"log" "log"
"net/http" "net/http"
@ -13,6 +12,7 @@ import (
"time" "time"
influx "github.com/influxdata/influxdb/client/v2" influx "github.com/influxdata/influxdb/client/v2"
"github.com/pkg/errors"
) )
func main() { func main() {
@ -20,16 +20,17 @@ func main() {
if err := config.AuthController(); err != nil { if err := config.AuthController(); err != nil {
log.Fatal(err) log.Fatal(err)
} }
log.Println("Successfully authenticated to Unifi Controller!") log.Println("Authenticated to Unifi Controller", config.UnifiBase, "as user", config.UnifiUser)
infdb, err := influx.NewHTTPClient(influx.HTTPConfig{ infdb, err := influx.NewHTTPClient(influx.HTTPConfig{
Addr: config.InfluxAddr, Addr: config.InfluxURL,
Username: config.InfluxUser, Username: config.InfluxUser,
Password: config.InfluxPass, Password: config.InfluxPass,
}) })
if err != nil { if err != nil {
log.Fatal(err) 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)
config.PollUnifiController(infdb) config.PollUnifiController(infdb)
} }
@ -37,21 +38,21 @@ func main() {
// GetConfig parses and returns our configuration data. // GetConfig parses and returns our configuration data.
func GetConfig() Config { func GetConfig() Config {
// TODO: A real config file. // TODO: A real config file.
var err error interval, err := time.ParseDuration(os.Getenv("INTERVAL"))
config := Config{ if err != nil {
InfluxAddr: os.Getenv("INFLUXDB_ADDR"), log.Println("Invalid Interval, defaulting to", defaultInterval)
interval = time.Duration(defaultInterval)
}
return Config{
InfluxURL: os.Getenv("INFLUXDB_URL"),
InfluxUser: os.Getenv("INFLUXDB_USERNAME"), InfluxUser: os.Getenv("INFLUXDB_USERNAME"),
InfluxPass: os.Getenv("INFLUXDB_PASSWORD"), InfluxPass: os.Getenv("INFLUXDB_PASSWORD"),
InfluxDB: os.Getenv("INFLUXDB_DATABASE"), InfluxDB: os.Getenv("INFLUXDB_DATABASE"),
UnifiUser: os.Getenv("UNIFI_USERNAME"), UnifiUser: os.Getenv("UNIFI_USERNAME"),
UnifiPass: os.Getenv("UNIFI_PASSWORD"), UnifiPass: os.Getenv("UNIFI_PASSWORD"),
UnifiBase: "https://" + os.Getenv("UNIFI_ADDR") + ":" + os.Getenv("UNIFI_PORT"), UnifiBase: "https://" + os.Getenv("UNIFI_ADDR") + ":" + os.Getenv("UNIFI_PORT"),
Interval: interval,
} }
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
} }
// AuthController creates a http.Client with authenticated cookies. // AuthController creates a http.Client with authenticated cookies.
@ -60,18 +61,19 @@ func (c *Config) AuthController() error {
json := `{"username": "` + c.UnifiUser + `","password": "` + c.UnifiPass + `"}` json := `{"username": "` + c.UnifiUser + `","password": "` + c.UnifiPass + `"}`
jar, err := cookiejar.New(nil) jar, err := cookiejar.New(nil)
if err != nil { if err != nil {
return err return errors.Wrap(err, "cookiejar.New(nil)")
} }
c.uniClient = &http.Client{ c.uniClient = &http.Client{
Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}, Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}},
Jar: jar, Jar: jar,
} }
if req, err := c.uniRequest(LoginPath, json); err != nil { if req, err := c.uniRequest(LoginPath, json); err != nil {
return err return errors.Wrap(err, "c.uniRequest(LoginPath, json)")
} else if resp, err := c.uniClient.Do(req); err != nil { } else if resp, err := c.uniClient.Do(req); err != nil {
return err return errors.Wrap(err, "c.uniClient.Do(req)")
} else if resp.StatusCode != http.StatusOK { } else if resp.StatusCode != http.StatusOK {
return errors.New("Error Authenticating with Unifi Controller") return errors.Errorf("authentication failed (%v): %v (status: %v/%v)",
c.UnifiUser, c.UnifiBase+LoginPath, resp.StatusCode, resp.Status)
} }
return nil return nil
} }
@ -94,7 +96,11 @@ func (c *Config) PollUnifiController(infdb influx.Client) {
} }
for _, client := range clients { for _, client := range clients {
bp.AddPoint(client.Point()) if pt, errr := client.Point(); errr != nil {
log.Println("client.Point():", errr)
} else {
bp.AddPoint(pt)
}
} }
if err = infdb.Write(bp); err != nil { if err = infdb.Write(bp); err != nil {
log.Println("infdb.Write(bp):", err) log.Println("infdb.Write(bp):", err)

View File

Before

Width:  |  Height:  |  Size: 909 KiB

After

Width:  |  Height:  |  Size: 909 KiB

View File

@ -0,0 +1,12 @@
#!/bin/bash
OUTPUT=$1
# This requires the installation of `ronn`: sudo gem install ronn
for f in cmd/*/README.md;do
# Strtip off cmd/ then strip off README to get the man-file name.
PKGNOCMD="${f#cmd/}"
PKG="${PKGNOCMD%/README.md}"
echo "Creating Man Page: ${f} -> ${OUTPUT}${PKG}.1.gz"
ronn < "$f" | gzip -9 > "${OUTPUT}${PKG}.1.gz"
done

View File

@ -1,9 +0,0 @@
#!/bin/bash
set -e
# Load the environment variables needed for testing
export $(cat .env | grep -v ^# | xargs)
go clean
go build -o unifi
./unifi

File diff suppressed because it is too large Load Diff