From 6a60050e9f37d6641d987d6d7913d8f4c37cef83 Mon Sep 17 00:00:00 2001 From: Feng_Qi Date: Fri, 29 Jan 2021 21:35:12 +0800 Subject: [PATCH] init --- .gitignore | 5 + README.md | 3 +- config/address/address.go | 142 +++++++++++ config/config.go | 79 ++++++ config/const.go | 3 + config/logger.go | 27 +++ control | 122 ++++++++++ funcs/core/clients.go | 51 ++++ funcs/core/common.go | 28 +++ funcs/core/dataobj.go | 451 +++++++++++++++++++++++++++++++++++ funcs/core/push.go | 131 ++++++++++ funcs/cron.go | 150 ++++++++++++ funcs/models/common.go | 62 +++++ funcs/models/esxi.go | 187 +++++++++++++++ funcs/models/extend.go | 57 +++++ funcs/models/performance.go | 39 +++ funcs/models/vm.go | 189 +++++++++++++++ funcs/models/vsphere_test.go | 331 +++++++++++++++++++++++++ funcs/report.go | 99 ++++++++ gitversion | 1 + go.mod | 14 ++ go.sum | 345 +++++++++++++++++++++++++++ http/health.go | 20 ++ http/middleware/logger.go | 295 +++++++++++++++++++++++ http/middleware/recovery.go | 160 +++++++++++++ http/route.go | 18 ++ http/start.go | 61 +++++ install.sh | 4 + main.go | 103 ++++++++ service/vsphere.service | 20 ++ 30 files changed, 3196 insertions(+), 1 deletion(-) create mode 100644 .gitignore create mode 100644 config/address/address.go create mode 100644 config/config.go create mode 100644 config/const.go create mode 100644 config/logger.go create mode 100644 control create mode 100644 funcs/core/clients.go create mode 100644 funcs/core/common.go create mode 100644 funcs/core/dataobj.go create mode 100644 funcs/core/push.go create mode 100644 funcs/cron.go create mode 100644 funcs/models/common.go create mode 100644 funcs/models/esxi.go create mode 100644 funcs/models/extend.go create mode 100644 funcs/models/performance.go create mode 100644 funcs/models/vm.go create mode 100644 funcs/models/vsphere_test.go create mode 100644 funcs/report.go create mode 100644 gitversion create mode 100644 go.mod create mode 100644 go.sum create mode 100644 http/health.go create mode 100644 http/middleware/logger.go create mode 100644 http/middleware/recovery.go create mode 100644 http/route.go create mode 100644 http/start.go create mode 100644 install.sh create mode 100644 main.go create mode 100644 service/vsphere.service diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..844af67 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +*.exe +vsphere-mon +etc/vsphere.local.yml +etc/address.local.yml +logs/ diff --git a/README.md b/README.md index e4deb52..499095f 100644 --- a/README.md +++ b/README.md @@ -1 +1,2 @@ -# vsphere-mon \ No newline at end of file +# vsphere-mon + diff --git a/config/address/address.go b/config/address/address.go new file mode 100644 index 0000000..8d25081 --- /dev/null +++ b/config/address/address.go @@ -0,0 +1,142 @@ +package address + +import ( + "fmt" + "os" + "path" + "strconv" + "strings" + "sync" + + "github.com/toolkits/pkg/file" + "github.com/toolkits/pkg/runner" +) + +type Module struct { + HTTP string `yaml:"http"` + RPC string `yaml:"rpc"` + Addresses []string `yaml:"addresses"` +} + +var ( + lock sync.Once + mods map[string]Module +) + +func GetHTTPListen(mod string) string { + return getMod(mod).HTTP +} + +func GetHTTPPort(mod string) int { + return convPort(mod, getMod(mod).HTTP, "http") +} + +func GetRPCListen(mod string) string { + return getMod(mod).RPC +} + +func GetRPCPort(mod string) int { + return convPort(mod, getMod(mod).RPC, "rpc") +} + +func convPort(module, listen, portType string) int { + splitChar := ":" + if IsIPv6(listen) { + splitChar = "]:" + } + port, err := strconv.Atoi(strings.Split(listen, splitChar)[1]) + if err != nil { + fmt.Printf("%s.%s invalid", module, portType) + os.Exit(1) + } + + return port +} + +func GetHTTPAddresses(mod string) []string { + modConf := getMod(mod) + + count := len(modConf.Addresses) + if count == 0 { + return []string{} + } + + port := convPort(mod, modConf.HTTP, "http") + + addresses := make([]string, count) + for i := 0; i < count; i++ { + addresses[i] = fmt.Sprintf("%s:%d", modConf.Addresses[i], port) + } + + return addresses +} + +func GetAddresses(mod string) []string { + modConf := getMod(mod) + return modConf.Addresses +} + +func GetRPCAddresses(mod string) []string { + modConf := getMod(mod) + + count := len(modConf.Addresses) + if count == 0 { + return []string{} + } + + port := convPort(mod, modConf.RPC, "rpc") + + addresses := make([]string, count) + for i := 0; i < count; i++ { + addresses[i] = fmt.Sprintf("%s:%d", modConf.Addresses[i], port) + } + + return addresses +} + +func getMod(modKey string) Module { + lock.Do(func() { + parseConf() + }) + + mod, has := mods[modKey] + if !has { + fmt.Printf("module(%s) configuration section not found", modKey) + os.Exit(1) + } + + return mod +} + +func parseConf() { + conf := getConf() + + var c map[string]Module + err := file.ReadYaml(conf, &c) + if err != nil { + fmt.Println("cannot parse file:", conf) + os.Exit(1) + } + + mods = c +} + +func getConf() string { + conf := path.Join(runner.Cwd, "etc", "address.local.yml") + if file.IsExist(conf) { + return conf + } + + conf = path.Join(runner.Cwd, "etc", "address.yml") + if file.IsExist(conf) { + return conf + } + + fmt.Println("configuration file address.[local.]yml not found") + os.Exit(1) + return "" +} + +func IsIPv6(address string) bool { + return strings.Count(address, ":") >= 2 +} diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..8f40508 --- /dev/null +++ b/config/config.go @@ -0,0 +1,79 @@ +package config + +import ( + "bytes" + "fmt" + "sync" + + "github.com/spf13/viper" + "github.com/toolkits/pkg/file" +) + +type ConfYaml struct { + Logger LoggerSection `yaml:"logger"` + Interval int64 `yaml:"interval"` + Report ReportSection `yaml:"report"` + Vspheres []VsphereSection `yaml:"vspheres"` + Performance PerfSection `yaml:"performance"` +} + +type ReportSection struct { + Token string `yaml:"token"` + Cate string `yaml:"cate"` + UniqKey string `yaml:"uniqkey"` + Tenant string `yaml:"tenant"` +} + +type VsphereSection struct { + Addr string `yaml:"addr"` + User string `yaml:"user"` + Pwd string `yaml:"pwd"` + EsxiPerf bool `yaml:"esxiperf"` + VM bool `yaml:"vm"` + Nid string `yaml:"nid"` + VmList []string `yaml:"vmlist"` + VmPerf bool `yaml:"vmperf"` + VmPerfList []string `yaml:"vmperflist"` +} + +type PerfSection struct { + Esxi []string `yaml:"esxi"` + VM []string `yaml:"vm"` +} + +var ( + Config *ConfYaml + lock = new(sync.RWMutex) + Endpoint string + Cwd string +) + +// Get configuration file +func Get() *ConfYaml { + lock.RLock() + defer lock.RUnlock() + return Config +} + +func Parse(conf string) error { + bs, err := file.ReadBytes(conf) + if err != nil { + return fmt.Errorf("cannot read yml[%s]: %v", conf, err) + } + + lock.Lock() + defer lock.Unlock() + + viper.SetConfigType("yaml") + err = viper.ReadConfig(bytes.NewBuffer(bs)) + if err != nil { + return fmt.Errorf("cannot read yml[%s]: %v", conf, err) + } + + err = viper.Unmarshal(&Config) + if err != nil { + return fmt.Errorf("Unmarshal %v", err) + } + + return nil +} diff --git a/config/const.go b/config/const.go new file mode 100644 index 0000000..99835f8 --- /dev/null +++ b/config/const.go @@ -0,0 +1,3 @@ +package config + +const Version = "0.1.0" diff --git a/config/logger.go b/config/logger.go new file mode 100644 index 0000000..43c1d05 --- /dev/null +++ b/config/logger.go @@ -0,0 +1,27 @@ +package config + +import ( + "fmt" + "os" + + "github.com/toolkits/pkg/logger" +) + +type LoggerSection struct { + Dir string `json:"dir"` + Level string `json:"level"` + KeepHours uint `json:"keepHours"` +} + +func InitLog(l LoggerSection) { + + lb, err := logger.NewFileBackend(l.Dir) + if err != nil { + fmt.Println("cannot init logger:", err) + os.Exit(1) + } + lb.SetRotateByHour(true) + lb.SetKeepHours(l.KeepHours) + + logger.SetLogging(l.Level, lb) +} diff --git a/control b/control new file mode 100644 index 0000000..6087b24 --- /dev/null +++ b/control @@ -0,0 +1,122 @@ +#!/bin/bash + +# release version + +CWD=$(cd $(dirname $0)/; pwd) +cd $CWD + +start() +{ + mod="vsphere" + + binfile=${mod}-mon + + if [ ! -f $binfile ]; then + echo "file[$binfile] not found" + exit 1 + fi + + if [ $(ps aux|grep -v grep|grep -v control|grep "$binfile" -c) -gt 0 ]; then + echo "${mod} already started" + return + fi + + mkdir -p logs/ + nohup $CWD/$binfile &> logs/stdout.log & + + for((i=1;i<=15;i++)); do + if [ $(ps aux|grep -v grep|grep -v control|grep "$binfile" -c) -gt 0 ]; then + echo "${mod} started" + return + fi + sleep 0.2 + done + + echo "cannot start ${mod}" + exit 1 +} + +stop() +{ + mod="vsphere" + + binfile=${mod}-mon + + if [ $(ps aux|grep -v grep|grep -v control|grep "$binfile" -c) -eq 0 ]; then + echo "${mod} already stopped" + return + fi + + ps aux|grep -v grep|grep -v control|grep "$binfile"|awk '{print $2}'|xargs kill + for((i=1;i<=15;i++)); do + if [ $(ps aux|grep -v grep|grep -v control|grep "$binfile" -c) -eq 0 ]; then + echo "${mod} stopped" + return + fi + sleep 0.2 + done + + echo "cannot stop $mod" + exit 1 +} + +restart() +{ + mod="vsphere" + stop $mod + start $mod + + status +} + +status() +{ + ps aux|grep -v grep|grep "vsphere-mon" +} + +build() +{ + go build + if [ $? -ne 0 ]; then + exit $? + fi + mod="vsphere" + + binfile=${mod}-mon + ./$binfile -v +} + +pack() +{ + build + git log -1 --pretty=%h > gitversion + mod="vsphere" + binfile=${mod}-mon + version=`./$binfile -v | cut -d " " -f2` + file_list="control install.sh etc service $binfile" + echo "...tar $binfile-$version.tar.gz <= $file_list" + tar zcf $binfile-$version.tar.gz gitversion $file_list +} + +case "$1" in + start) + start $2 + ;; + stop) + stop $2 + ;; + restart) + restart $2 + ;; + status) + status + ;; + build) + build + ;; + pack) + pack + ;; + *) + usage +esac diff --git a/funcs/core/clients.go b/funcs/core/clients.go new file mode 100644 index 0000000..789c32d --- /dev/null +++ b/funcs/core/clients.go @@ -0,0 +1,51 @@ +package core + +import ( + "net/rpc" + "sync" +) + +type RpcClientContainer struct { + M map[string]*rpc.Client + sync.RWMutex +} + +var rpcClients *RpcClientContainer + +func InitRpcClients() { + rpcClients = &RpcClientContainer{ + M: make(map[string]*rpc.Client), + } +} + +func (rcc *RpcClientContainer) Get(addr string) *rpc.Client { + rcc.RLock() + defer rcc.RUnlock() + + client, has := rcc.M[addr] + if !has { + return nil + } + + return client +} + +// Put 返回的bool表示affected,确实把自己塞进去了 +func (rcc *RpcClientContainer) Put(addr string, client *rpc.Client) bool { + rcc.Lock() + defer rcc.Unlock() + + oc, has := rcc.M[addr] + if has && oc != nil { + return false + } + + rcc.M[addr] = client + return true +} + +func (rcc *RpcClientContainer) Del(addr string) { + rcc.Lock() + defer rcc.Unlock() + delete(rcc.M, addr) +} diff --git a/funcs/core/common.go b/funcs/core/common.go new file mode 100644 index 0000000..6a7a121 --- /dev/null +++ b/funcs/core/common.go @@ -0,0 +1,28 @@ +package core + +//NewMetricValue decorate metric object,return new metric with tags +func NewMetricValue(endpoint, metric string, val interface{}, dataType string, tags map[string]string) *MetricValue { + mv := MetricValue{ + Endpoint: endpoint, + Metric: metric, + ValueUntyped: val, + CounterType: dataType, + TagsMap: map[string]string{}, + } + + for k, v := range tags { + mv.TagsMap[k] = v + } + + return &mv +} + +//GaugeValue Gauge type +func GaugeValue(endpoint, metric string, val interface{}, tags map[string]string) *MetricValue { + return NewMetricValue(endpoint, metric, val, GAUGE, tags) +} + +//CounterValue Gauge type +func CounterValue(endpoint, metric string, val interface{}, tags map[string]string) *MetricValue { + return NewMetricValue(endpoint, metric, val, COUNTER, tags) +} diff --git a/funcs/core/dataobj.go b/funcs/core/dataobj.go new file mode 100644 index 0000000..1bbc489 --- /dev/null +++ b/funcs/core/dataobj.go @@ -0,0 +1,451 @@ +package core + +import ( + "bytes" + "fmt" + "sort" + "strconv" + "strings" + "sync" +) + +const ( + GAUGE = "GAUGE" + COUNTER = "COUNTER" + SUBTRACT = "SUBTRACT" + DERIVE = "DERIVE" + SPLIT = "/" +) + +const ( + MachineDep = 1 + MachineIndep = 2 +) + +type MetricValue struct { + Nid string `json:"nid"` + Metric string `json:"metric"` + Endpoint string `json:"endpoint"` + Timestamp int64 `json:"timestamp"` + Step int64 `json:"step"` + ValueUntyped interface{} `json:"value"` + Value float64 `json:"-"` + CounterType string `json:"counterType"` + Tags string `json:"tags"` + TagsMap map[string]string `json:"tagsMap"` //保留2种格式,方便后端组件使用 + Extra string `json:"extra"` +} + +var bufferPool = sync.Pool{ + New: func() interface{} { + return new(bytes.Buffer) + }, +} + +func (m *MetricValue) PK() string { + ret := bufferPool.Get().(*bytes.Buffer) + ret.Reset() + defer bufferPool.Put(ret) + + if m.TagsMap == nil || len(m.TagsMap) == 0 { + ret.WriteString(m.Endpoint) + ret.WriteString(SPLIT) + ret.WriteString(m.Metric) + + return ret.String() + } + ret.WriteString(m.Endpoint) + ret.WriteString(SPLIT) + ret.WriteString(m.Metric) + ret.WriteString(SPLIT) + ret.WriteString(SortedTags(m.TagsMap)) + return ret.String() +} + +func (m *MetricValue) CheckValidity(now int64) (err error) { + if m == nil { + err = fmt.Errorf("item is nil") + return + } + + if m.Nid == "" && m.Endpoint == "" { + err = fmt.Errorf("nid or endpoint should not be empty") + return + } + + if m.Nid != "" { + m.Endpoint = NidToEndpoint(m.Nid) + } + + if m.Metric == "" { + err = fmt.Errorf("metric should not be empty") + return + } + + // 检测保留字 + reservedWords := "[\\t] [\\r] [\\n] [,] [ ] [=]" + if HasReservedWords(m.Metric) { + err = fmt.Errorf("metric:%s contains reserved words: %s", m.Metric, reservedWords) + return + } + if HasReservedWords(m.Endpoint) { + err = fmt.Errorf("endpoint:%s contains reserved words: %s", m.Endpoint, reservedWords) + return + } + + if m.CounterType == "" { + m.CounterType = GAUGE + } + + if m.CounterType != GAUGE && m.CounterType != COUNTER && m.CounterType != SUBTRACT { + err = fmt.Errorf("wrong counter type") + return + } + + if m.ValueUntyped == "" { + err = fmt.Errorf("value is nil") + return + } + + if m.Step <= 0 { + err = fmt.Errorf("step should larger than 0") + return + } + + if len(m.TagsMap) == 0 { + m.TagsMap, err = SplitTagsString(m.Tags) + if err != nil { + return + } + } + + if len(m.TagsMap) > 20 { + err = fmt.Errorf("tagkv count is too large > 20") + } + + if len(m.Metric) > 128 { + err = fmt.Errorf("len(m.Metric) is too large") + return + } + + for k, v := range m.TagsMap { + delete(m.TagsMap, k) + k = filterString(k) + v = filterString(v) + if len(k) == 0 || len(v) == 0 { + err = fmt.Errorf("tag key and value should not be empty") + return + } + + m.TagsMap[k] = v + } + + m.Tags = SortedTags(m.TagsMap) + if len(m.Tags) > 512 { + err = fmt.Errorf("len(m.Tags) is too large") + return + } + + //时间超前5分钟则报错 + if m.Timestamp-now > 300 { + err = fmt.Errorf("point timestamp:%d is ahead of now:%d", m.Timestamp, now) + return + } + + if m.Timestamp <= 0 { + m.Timestamp = now + } + + m.Timestamp = alignTs(m.Timestamp, int64(m.Step)) + + valid := true + var vv float64 + + switch cv := m.ValueUntyped.(type) { + case string: + vv, err = strconv.ParseFloat(cv, 64) + if err != nil { + valid = false + } + case float64: + vv = cv + case uint64: + vv = float64(cv) + case int64: + vv = float64(cv) + case int: + vv = float64(cv) + default: + valid = false + } + + if !valid { + err = fmt.Errorf("value [%v] is illegal", m.Value) + return + } + + m.Value = vv + return +} + +func HasReservedWords(str string) bool { + idx := strings.IndexFunc(str, func(r rune) bool { + return r == '\t' || + r == '\r' || + r == '\n' || + r == ',' || + r == ' ' || + r == '=' + }) + return idx != -1 +} + +func filterString(str string) string { + if -1 == strings.IndexFunc(str, + func(r rune) bool { + return r == '\t' || + r == '\r' || + r == '\n' || + r == ',' || + r == ' ' || + r == '=' + }) { + + return str + } + + return strings.Map(func(r rune) rune { + if r == '\t' || + r == '\r' || + r == '\n' || + r == ',' || + r == ' ' || + r == '=' { + return '_' + } + return r + }, str) + + return str +} + +func SortedTags(tags map[string]string) string { + if tags == nil { + return "" + } + + size := len(tags) + if size == 0 { + return "" + } + + ret := bufferPool.Get().(*bytes.Buffer) + ret.Reset() + defer bufferPool.Put(ret) + + if size == 1 { + for k, v := range tags { + ret.WriteString(k) + ret.WriteString("=") + ret.WriteString(v) + } + return ret.String() + } + + keys := make([]string, size) + i := 0 + for k := range tags { + keys[i] = k + i++ + } + sort.Strings(keys) + + for j, key := range keys { + ret.WriteString(key) + ret.WriteString("=") + ret.WriteString(tags[key]) + if j != size-1 { + ret.WriteString(",") + } + } + + return ret.String() +} + +func SplitTagsString(s string) (tags map[string]string, err error) { + tags = make(map[string]string) + + s = strings.Replace(s, " ", "", -1) + if s == "" { + return + } + + tagSlice := strings.Split(s, ",") + for _, tag := range tagSlice { + tagPair := strings.SplitN(tag, "=", 2) + if len(tagPair) == 2 { + tags[tagPair[0]] = tagPair[1] + } else { + err = fmt.Errorf("bad tag %s", tag) + return + } + } + + return +} + +func DictedTagstring(s string) map[string]string { + if s == "" { + return map[string]string{} + } + s = strings.Replace(s, " ", "", -1) + + result := make(map[string]string) + tags := strings.Split(s, ",") + for _, tag := range tags { + pair := strings.SplitN(tag, "=", 2) + if len(pair) == 2 { + result[pair[0]] = pair[1] + } + } + + return result +} + +func PKWithCounter(endpoint, counter string) string { + ret := bufferPool.Get().(*bytes.Buffer) + ret.Reset() + defer bufferPool.Put(ret) + + ret.WriteString(endpoint) + ret.WriteString("/") + ret.WriteString(counter) + + return ret.String() +} + +func GetCounter(metric, tag string, tagMap map[string]string) (counter string, err error) { + if tagMap == nil { + tagMap, err = SplitTagsString(tag) + if err != nil { + return + } + } + + tagStr := SortedTags(tagMap) + counter = PKWithTags(metric, tagStr) + return +} + +func PKWithTags(metric, tags string) string { + ret := bufferPool.Get().(*bytes.Buffer) + ret.Reset() + defer bufferPool.Put(ret) + + if tags == "" { + ret.WriteString(metric) + return ret.String() + } + ret.WriteString(metric) + ret.WriteString("/") + ret.WriteString(tags) + return ret.String() +} + +func PKWhitEndpointAndTags(endpoint, metric, tags string) string { + ret := bufferPool.Get().(*bytes.Buffer) + ret.Reset() + defer bufferPool.Put(ret) + + if tags == "" { + ret.WriteString(endpoint) + ret.WriteString("/") + ret.WriteString(metric) + + return ret.String() + } + ret.WriteString(endpoint) + ret.WriteString("/") + ret.WriteString(metric) + ret.WriteString("/") + ret.WriteString(tags) + return ret.String() +} + +// e.g. tcp.port.listen or proc.num +type BuiltinMetric struct { + Metric string + Tags string +} + +func (bm *BuiltinMetric) String() string { + return fmt.Sprintf("%s/%s", bm.Metric, bm.Tags) +} + +type BuiltinMetricRequest struct { + Ty int + IP string + Checksum string +} + +type BuiltinMetricResponse struct { + Metrics []*BuiltinMetric + Checksum string + Timestamp int64 + ErrCode int +} + +func (br *BuiltinMetricResponse) String() string { + return fmt.Sprintf( + "", + br.Metrics, + br.Checksum, + br.Timestamp, + ) +} + +type BuiltinMetricSlice []*BuiltinMetric + +func (bm BuiltinMetricSlice) Len() int { + return len(bm) +} +func (bm BuiltinMetricSlice) Swap(i, j int) { + bm[i], bm[j] = bm[j], bm[i] +} +func (bm BuiltinMetricSlice) Less(i, j int) bool { + return bm[i].String() < bm[j].String() +} + +func alignTs(ts int64, period int64) int64 { + return ts - ts%period +} + +// code == 0 => success +// code == 1 => bad request +type SimpleRpcResponse struct { + Code int `json:"code"` +} + +type NullRpcRequest struct { +} + +type TransferResp struct { + Msg string + Total int + Invalid int + Latency int64 +} + +func (t *TransferResp) String() string { + s := fmt.Sprintf("TransferResp total=%d, err_invalid=%d, latency=%dms", + t.Total, t.Invalid, t.Latency) + if t.Msg != "" { + s = fmt.Sprintf("%s, msg=%s", s, t.Msg) + } + return s +} + +func NidToEndpoint(nid string) string { + endpoint := "__nid__" + nid + "__" + return endpoint +} diff --git a/funcs/core/push.go b/funcs/core/push.go new file mode 100644 index 0000000..8ab3c69 --- /dev/null +++ b/funcs/core/push.go @@ -0,0 +1,131 @@ +package core + +import ( + "bufio" + "fmt" + "io" + "math/rand" + "net" + "net/rpc" + "reflect" + "time" + + "github.com/toolkits/pkg/logger" + "github.com/ugorji/go/codec" + + "github.com/shanghai-edu/vsphere-mon/config/address" +) + +func Push(metricItems []*MetricValue) error { + var err error + var items []*MetricValue + now := time.Now().Unix() + + for _, item := range metricItems { + logger.Debugf("->recv:%+v", item) + if err = item.CheckValidity(now); err != nil { + msg := fmt.Errorf("metric:%v err:%v", item, err) + logger.Warning(msg) + // 如果数据有问题,直接跳过吧,比如mymon采集的到的数据,其实只有一个有问题,剩下的都没问题 + continue + } + logger.Debugf("push item: %+v", item) + items = append(items, item) + } + + addrs := address.GetRPCAddresses("transfer") + count := len(addrs) + retry := 0 + for { + for _, i := range rand.Perm(count) { + addr := addrs[i] + reply, err := rpcCall(addr, items) + if err != nil { + logger.Error(err) + continue + } else { + if reply.Msg != "ok" { + err = fmt.Errorf("some item push err: %s", reply.Msg) + logger.Error(err) + } + return err + } + } + + time.Sleep(time.Millisecond * 500) + + retry += 1 + if retry == 3 { + break + } + } + + return err +} + +func rpcCall(addr string, items []*MetricValue) (TransferResp, error) { + var reply TransferResp + var err error + + client := rpcClients.Get(addr) + if client == nil { + client, err = rpcClient(addr) + if err != nil { + return reply, err + } + affected := rpcClients.Put(addr, client) + if !affected { + defer func() { + // 我尝试把自己这个client塞进map失败,说明已经有一个client塞进去了,那我自己用完了就关闭 + client.Close() + }() + + } + } + + timeout := time.Duration(8) * time.Second + done := make(chan error, 1) + + go func() { + err := client.Call("Transfer.Push", items, &reply) + done <- err + }() + + select { + case <-time.After(timeout): + logger.Warningf("rpc call timeout, transfer addr: %s\n", addr) + rpcClients.Put(addr, nil) + client.Close() + return reply, fmt.Errorf("%s rpc call timeout", addr) + case err := <-done: + if err != nil { + rpcClients.Del(addr) + client.Close() + return reply, fmt.Errorf("%s rpc call done, but fail: %v", addr, err) + } + } + + return reply, nil +} + +func rpcClient(addr string) (*rpc.Client, error) { + conn, err := net.DialTimeout("tcp", addr, time.Second*3) + if err != nil { + err = fmt.Errorf("dial transfer %s fail: %v", addr, err) + logger.Error(err) + return nil, err + } + + var bufConn = struct { + io.Closer + *bufio.Reader + *bufio.Writer + }{conn, bufio.NewReader(conn), bufio.NewWriter(conn)} + + var mh codec.MsgpackHandle + mh.MapType = reflect.TypeOf(map[string]interface{}(nil)) + + rpcCodec := codec.MsgpackSpecRpc.ClientCodec(bufConn, &mh) + client := rpc.NewClientWithCodec(rpcCodec) + return client, nil +} diff --git a/funcs/cron.go b/funcs/cron.go new file mode 100644 index 0000000..04cac2b --- /dev/null +++ b/funcs/cron.go @@ -0,0 +1,150 @@ +package funcs + +import ( + "context" + "net/url" + "time" + + "github.com/toolkits/pkg/logger" + + "github.com/shanghai-edu/vsphere-mon/config" + "github.com/shanghai-edu/vsphere-mon/funcs/core" + "github.com/shanghai-edu/vsphere-mon/funcs/models" + "github.com/vmware/govmomi" + "github.com/vmware/govmomi/vim25/soap" +) + +func Collect() { + sec := config.Get().Interval + for _, vcConfig := range config.Get().Vspheres { + go collect(vcConfig, config.Get().Performance, sec) + } +} + +func collect(vcConfig config.VsphereSection, perfConfig config.PerfSection, sec int64) { + t := time.NewTicker(time.Second * time.Duration(sec)) + defer t.Stop() + for { + collectOnce(vcConfig, perfConfig, sec) + <-t.C + } +} + +func collectOnce(vcConfig config.VsphereSection, perfConfig config.PerfSection, sec int64) { + stime := time.Now().Unix() + ts := time.Now().Unix() + + u, err := soap.ParseURL(vcConfig.Addr) + if err != nil { + logger.Errorf("parse vcenter failed, %v", err) + collectVsphere(sec, ts, vcConfig.Nid, models.VcenterAlive(false, vcConfig.Addr)) + return + } + u.User = url.UserPassword(vcConfig.User, vcConfig.Pwd) + ctx := context.Background() + c, err := govmomi.NewClient(ctx, u, true) + if err != nil { + logger.Errorf("create vmomi client failed, %v", err) + collectVsphere(sec, ts, vcConfig.Nid, models.VcenterAlive(false, vcConfig.Addr)) + return + } + defer c.Logout(ctx) + collectVsphere(sec, ts, vcConfig.Nid, models.VcenterAlive(true, vcConfig.Addr)) + + //collect esxi + esxiList, err := models.EsxiList(ctx, c) + if err != nil { + logger.Errorf("get esxi list failed, %v", err) + return + } + for _, esxi := range esxiList { + if err := report(esxi); err != nil { + logger.Errorf("report esxi failed, %v", err) + return + } + collectEsxi(sec, ts, models.EsxiPower(esxi)) + collectEsxi(sec, ts, models.EsxiStatus(esxi)) + collectEsxi(sec, ts, models.EsxiUptime(esxi)) + collectEsxi(sec, ts, models.EsxiCPU(esxi)) + collectEsxi(sec, ts, models.EsxiMem(esxi)) + net, err := models.EsxiNet(ctx, c, esxi) + if err != nil { + logger.Warningf("get esxi net failed: %v", err) + } else { + collectEsxi(sec, ts, net) + } + disk, err := models.EsxiDisk(ctx, c, esxi) + if err != nil { + logger.Warningf("get esxi disk failed: %v", err) + } else { + collectEsxi(sec, ts, disk) + } + //collect esxi extend performance + if vcConfig.EsxiPerf { + perf, err := models.EsxiExtend(ctx, c, esxi, perfConfig.Esxi) + if err != nil { + logger.Warningf("get esxi perfomance failed: %v", err) + } else { + collectEsxi(sec, ts, perf) + } + } + } + //collect vm + if vcConfig.VM { + vms, err := models.VsphereVirtualMachines(ctx, c) + if err != nil { + logger.Errorf("get vm list failed, %v", err) + return + } + vmList := models.GenVirtualMachinesByName(vms, vcConfig.VmList) + perfVms := models.GenVirtualMachinesByName(vms, vcConfig.VmPerfList) + for _, vm := range vmList { + collectVsphere(sec, ts, vcConfig.Nid, models.VmPower(vm)) + collectVsphere(sec, ts, vcConfig.Nid, models.VmStatus(vm)) + collectVsphere(sec, ts, vcConfig.Nid, models.VmUptime(vm)) + collectVsphere(sec, ts, vcConfig.Nid, models.VmCPU(vm)) + collectVsphere(sec, ts, vcConfig.Nid, models.VmMem(vm)) + } + + //collect vm extend perfomance + if vcConfig.VmPerf { + for _, vm := range perfVms { + perf, err := models.VmExtend(ctx, c, vm, perfConfig.VM) + if err != nil { + logger.Warningf("get vm perfomance failed: %v", err) + } else { + collectVsphere(sec, ts, vcConfig.Nid, perf) + } + } + } + } + etime := time.Now().Unix() + logger.Infof("vc %s have been collected, time:%d s", vcConfig.Addr, etime-stime) +} + +func collectEsxi(sec, ts int64, items []*core.MetricValue) { + if items == nil || len(items) == 0 { + return + } + metricValues := []*core.MetricValue{} + for _, item := range items { + item.Step = sec + item.Timestamp = ts + metricValues = append(metricValues, item) + } + core.Push(metricValues) +} + +func collectVsphere(sec, ts int64, nid string, items []*core.MetricValue) { + if items == nil || len(items) == 0 { + return + } + metricValues := []*core.MetricValue{} + for _, item := range items { + item.Step = sec + item.Timestamp = ts + item.Nid = nid + metricValues = append(metricValues, item) + } + core.Push(metricValues) +} diff --git a/funcs/models/common.go b/funcs/models/common.go new file mode 100644 index 0000000..ca586a3 --- /dev/null +++ b/funcs/models/common.go @@ -0,0 +1,62 @@ +package models + +import ( + "context" + + "github.com/vmware/govmomi" + "github.com/vmware/govmomi/performance" +) + +//MetricPerf vsphere performance +type MetricPerf struct { + Metric string `json:"metric"` + Value int64 `json:"value"` + Instance string `json:"instance"` +} + +//DatastoreWithURL datastore with url +type DatastoreWithURL struct { + Datastore string + URL string +} + +//CounterWithID get all counter list(Key:counter name,Value:counter id) +func CounterWithID(ctx context.Context, c *govmomi.Client) (map[string]int32, error) { + CounterNameID := make(map[string]int32) + m := performance.NewManager(c.Client) + p, err := m.CounterInfoByKey(ctx) + if err != nil { + return nil, err + } + for _, cc := range p { + CounterNameID[cc.Name()] = cc.Key + } + return CounterNameID, nil +} + +//DsWithURL get all datastore with URL +func DsWithURL(ctx context.Context, c *govmomi.Client) ([]DatastoreWithURL, error) { + dss, err := VsphereDatastores(ctx, c) + if err != nil { + return nil, err + } + datastoreWithURL := []DatastoreWithURL{} + if dss != nil { + for _, ds := range dss { + datastoreWithURL = append(datastoreWithURL, DatastoreWithURL{Datastore: ds.Summary.Name, URL: ds.Summary.Url}) + } + } + return datastoreWithURL, nil +} + +//CounterIDByName get counter key by counter name +func CounterIDByName(CounterNameID map[string]int32, Name []string) []int32 { + IDList := make([]int32, 0) + for _, eachName := range Name { + ID, exit := CounterNameID[eachName] + if exit { + IDList = append(IDList, ID) + } + } + return IDList +} diff --git a/funcs/models/esxi.go b/funcs/models/esxi.go new file mode 100644 index 0000000..e389ac8 --- /dev/null +++ b/funcs/models/esxi.go @@ -0,0 +1,187 @@ +package models + +import ( + "context" + + "github.com/shanghai-edu/vsphere-mon/funcs/core" + "github.com/vmware/govmomi" + "github.com/vmware/govmomi/property" + "github.com/vmware/govmomi/view" + "github.com/vmware/govmomi/vim25/mo" +) + +//EsxiList get esxi list +func EsxiList(ctx context.Context, c *govmomi.Client) (esxiList []mo.HostSystem, err error) { + m := view.NewManager(c.Client) + v, err := m.CreateContainerView(ctx, c.ServiceContent.RootFolder, []string{"HostSystem"}, true) + if err != nil { + return + } + defer v.Destroy(ctx) + + err = v.Retrieve(ctx, []string{"HostSystem"}, []string{"summary", "datastore"}, &esxiList) + return +} + +//EsxiAlive power status +func EsxiPower(esxi mo.HostSystem) []*core.MetricValue { + /* + 1.0: poweredOff + 2.0: poweredOn + 3.0: standBy + 4.0: unknown + */ + switch esxi.Summary.Runtime.PowerState { + case "poweredOff": + return []*core.MetricValue{core.GaugeValue(esxi.Summary.Config.Name, "esxi.power", 1.0, nil)} + case "poweredOn": + return []*core.MetricValue{core.GaugeValue(esxi.Summary.Config.Name, "esxi.power", 2.0, nil)} + case "standBy": + return []*core.MetricValue{core.GaugeValue(esxi.Summary.Config.Name, "esxi.power", 3.0, nil)} + case "unknown": + return []*core.MetricValue{core.GaugeValue(esxi.Summary.Config.Name, "esxi.power", 4.0, nil)} + default: + return []*core.MetricValue{core.GaugeValue(esxi.Summary.Config.Name, "esxi.power", 4.0, nil)} + } +} + +//EsxiStatus The Status enumeration defines a general "health" value for a managed entity. +func EsxiStatus(esxi mo.HostSystem) []*core.MetricValue { + /* + 1.0: gray,The status is unknown. + 2.0: green,The entity is OK. + 3.0: red,The entity definitely has a problem. + 4.0: yellow,The entity might have a problem. + */ + switch esxi.Summary.OverallStatus { + case "gray": + return []*core.MetricValue{core.GaugeValue(esxi.Summary.Config.Name, "esxi.status", 1.0, nil)} + case "green": + return []*core.MetricValue{core.GaugeValue(esxi.Summary.Config.Name, "esxi.status", 2.0, nil)} + case "red": + return []*core.MetricValue{core.GaugeValue(esxi.Summary.Config.Name, "esxi.status", 3.0, nil)} + case "yellow": + return []*core.MetricValue{core.GaugeValue(esxi.Summary.Config.Name, "esxi.status", 4.0, nil)} + default: + return []*core.MetricValue{core.GaugeValue(esxi.Summary.Config.Name, "esxi.status", 1.0, nil)} + } +} + +//EsxiUptime uptime +func EsxiUptime(esxi mo.HostSystem) []*core.MetricValue { + return []*core.MetricValue{core.GaugeValue(esxi.Summary.Config.Name, "esxi.uptime", int64(esxi.Summary.QuickStats.Uptime), nil)} +} + +//EsxiCPU cpu metrics +func EsxiCPU(esxi mo.HostSystem) []*core.MetricValue { + var total = int64(esxi.Summary.Hardware.CpuMhz) * int64(esxi.Summary.Hardware.NumCpuCores) + perf := []*core.MetricValue{} + if total > 0 { + usePercentCPU := core.GaugeValue(esxi.Summary.Config.Name, "cpu.util", float64(esxi.Summary.QuickStats.OverallCpuUsage)/float64(total)*100, nil) + perf = append(perf, usePercentCPU) + freePercentCPU := core.GaugeValue(esxi.Summary.Config.Name, "cpu.idle", (float64(total)-float64(esxi.Summary.QuickStats.OverallCpuUsage))/float64(total)*100, nil) + perf = append(perf, freePercentCPU) + } + + return perf +} + +//EsxiMem mem metrics +func EsxiMem(esxi mo.HostSystem) []*core.MetricValue { + var total = esxi.Summary.Hardware.MemorySize + var free = int64(esxi.Summary.Hardware.MemorySize) - (int64(esxi.Summary.QuickStats.OverallMemoryUsage) * 1024 * 1024) + + totalMem := core.GaugeValue(esxi.Summary.Config.Name, "mem.bytes.total", total, nil) + useMem := core.GaugeValue(esxi.Summary.Config.Name, "mem.bytes.used", int64(esxi.Summary.QuickStats.OverallMemoryUsage)*1024*1024, nil) + freeMem := core.GaugeValue(esxi.Summary.Config.Name, "mem.bytes.free", free, nil) + perf := []*core.MetricValue{totalMem, useMem, freeMem} + if total > 0 { + usedMemPer := core.GaugeValue(esxi.Summary.Config.Name, "mem.bytes.used.percent", float64(total-free)/float64(total)*100, nil) + perf = append(perf, usedMemPer) + } + + return perf +} + +//EsxiNet net metrics +func EsxiNet(ctx context.Context, c *govmomi.Client, esxi mo.HostSystem) ([]*core.MetricValue, error) { + var netPerf []*core.MetricValue + counterNameID, err := CounterWithID(ctx, c) + if err != nil { + return nil, err + } + var EsxiNetExtend = []string{"net.transmitted.average", "net.received.average"} + extendID := CounterIDByName(counterNameID, EsxiNetExtend) + for _, k := range extendID { + metricPerf, err := Performance(ctx, c, esxi.Self, k) + if err != nil { + return nil, err + } + for _, each := range metricPerf { + var tags = map[string]string{} + if each.Metric == "net.received.average" { + if each.Instance == "" { + netPerf = append(netPerf, core.GaugeValue(esxi.Summary.Config.Name, "net.in.bits.total", each.Value*1024*8, tags)) + } else { + tags["iface"] = each.Instance + netPerf = append(netPerf, core.GaugeValue(esxi.Summary.Config.Name, "net.in.bits", each.Value*1024*8, tags)) + } + } + if each.Metric == "net.transmitted.average" { + if each.Instance == "" { + netPerf = append(netPerf, core.GaugeValue(esxi.Summary.Config.Name, "net.out.bits.total", each.Value*1024*8, tags)) + } else { + tags["iface"] = each.Instance + netPerf = append(netPerf, core.GaugeValue(esxi.Summary.Config.Name, "net.out.bits", each.Value*1024*8, tags)) + } + } + } + + } + return netPerf, nil +} + +//EsxiDisk disk metrics +func EsxiDisk(ctx context.Context, c *govmomi.Client, esxi mo.HostSystem) ([]*core.MetricValue, error) { + var diskPerf []*core.MetricValue + pc := property.DefaultCollector(c.Client) + dss := []mo.Datastore{} + err := pc.Retrieve(ctx, esxi.Datastore, []string{"summary"}, &dss) + if err != nil { + return nil, err + } + var ( + freeAll int64 + totalAll int64 + usedAll int64 + ) + for _, ds := range dss { + var tags = map[string]string{} + tags["datastore="] = ds.Summary.Name + var free = ds.Summary.FreeSpace + var total = ds.Summary.Capacity + var used = total - free + freeAll += free + totalAll += total + usedAll += used + + diskPerf = append(diskPerf, core.GaugeValue(esxi.Summary.Config.Name, "dsik.bytes.free", free, tags)) + diskPerf = append(diskPerf, core.GaugeValue(esxi.Summary.Config.Name, "disk.bytes.total", total, tags)) + diskPerf = append(diskPerf, core.GaugeValue(esxi.Summary.Config.Name, "disk.bytes.used", used, tags)) + if total > 0 { + usedPercent := float64(used) / float64(total) * 100 + diskPerf = append(diskPerf, core.GaugeValue(esxi.Summary.Config.Name, "disk.bytes.used.Percent", usedPercent, tags)) + } + + } + + diskPerf = append(diskPerf, core.GaugeValue(esxi.Summary.Config.Name, "disk.cap.free", freeAll, nil)) + diskPerf = append(diskPerf, core.GaugeValue(esxi.Summary.Config.Name, "disk.cap.total", totalAll, nil)) + diskPerf = append(diskPerf, core.GaugeValue(esxi.Summary.Config.Name, "disk.cap.used", usedAll, nil)) + if totalAll > 0 { + usedAllPercent := float64(usedAll) / float64(totalAll) * 100 + diskPerf = append(diskPerf, core.GaugeValue(esxi.Summary.Config.Name, "disk.cap.used.percent", usedAllPercent, nil)) + } + + return diskPerf, nil +} diff --git a/funcs/models/extend.go b/funcs/models/extend.go new file mode 100644 index 0000000..c72d428 --- /dev/null +++ b/funcs/models/extend.go @@ -0,0 +1,57 @@ +package models + +import ( + "context" + "strings" + + "github.com/shanghai-edu/vsphere-mon/funcs/core" + "github.com/vmware/govmomi" + "github.com/vmware/govmomi/vim25/mo" +) + +func EsxiExtend(ctx context.Context, c *govmomi.Client, esxi mo.HostSystem, extend []string) ([]*core.MetricValue, error) { + counterNameID, err := CounterWithID(ctx, c) + if err != nil { + return nil, err + } + extendID := CounterIDByName(counterNameID, extend) + perf := []*core.MetricValue{} + for _, k := range extendID { + metricPerf, err := Performance(ctx, c, esxi.Self, k) + if err == nil { + for _, each := range metricPerf { + var tags = map[string]string{} + if each.Instance != "" { + sp := strings.Split(each.Metric, ".") + tags[sp[0]] = each.Instance + } + perf = append(perf, core.GaugeValue(esxi.Summary.Config.Name, each.Metric, each.Value, tags)) + } + } + } + return perf, nil +} + +func VmExtend(ctx context.Context, c *govmomi.Client, vm mo.VirtualMachine, extend []string) ([]*core.MetricValue, error) { + counterNameID, err := CounterWithID(ctx, c) + if err != nil { + return nil, err + } + extendID := CounterIDByName(counterNameID, extend) + perf := []*core.MetricValue{} + for _, k := range extendID { + metricPerf, err := Performance(ctx, c, vm.Self, k) + if err == nil { + for _, each := range metricPerf { + var tags = map[string]string{} + if each.Instance != "" { + sp := strings.Split(each.Metric, ".") + tags[sp[0]] = each.Instance + } + tags["name"] = vm.Summary.Config.Name + perf = append(perf, core.GaugeValue("", each.Metric, each.Value, tags)) + } + } + } + return perf, nil +} diff --git a/funcs/models/performance.go b/funcs/models/performance.go new file mode 100644 index 0000000..e6778e0 --- /dev/null +++ b/funcs/models/performance.go @@ -0,0 +1,39 @@ +package models + +import ( + "context" + + "github.com/vmware/govmomi" + "github.com/vmware/govmomi/performance" + + "github.com/vmware/govmomi/vim25/types" +) + +// Performance get counter value by counter key +func Performance(ctx context.Context, c *govmomi.Client, MOR types.ManagedObjectReference, counterID int32) ([]*MetricPerf, error) { + pm := performance.NewManager(c.Client) + var pQS = perfQuerySpec(MOR, counterID) + counterKVL, err := pm.Query(ctx, pQS) + var metricPerf = make([]*MetricPerf, 0) + + if err == nil { + counterKV, err := pm.ToMetricSeries(ctx, counterKVL) + if (err == nil) && (counterKV != nil) { + for _, eachCounter := range counterKV[0].Value { + metricPerf = append(metricPerf, &MetricPerf{Metric: eachCounter.Name, Value: eachCounter.Value[0], Instance: eachCounter.Instance}) + } + return metricPerf, nil + } + } + return []*MetricPerf{}, err +} + +func perfQuerySpec(MOR types.ManagedObjectReference, counterID int32) []types.PerfQuerySpec { + var Rqs types.PerfQuerySpec + Rqs.Entity = MOR + Rqs.MaxSample = 1 + Rqs.Format = "normal" + Rqs.IntervalId = 20 + Rqs.MetricId = []types.PerfMetricId{types.PerfMetricId{CounterId: counterID, Instance: "*"}} + return []types.PerfQuerySpec{Rqs} +} diff --git a/funcs/models/vm.go b/funcs/models/vm.go new file mode 100644 index 0000000..296b80d --- /dev/null +++ b/funcs/models/vm.go @@ -0,0 +1,189 @@ +package models + +import ( + "context" + + "github.com/shanghai-edu/vsphere-mon/funcs/core" + "github.com/vmware/govmomi" + "github.com/vmware/govmomi/view" + "github.com/vmware/govmomi/vim25/mo" +) + +func VcenterAlive(alive bool, vcAddr string) []*core.MetricValue { + tags := map[string]string{} + tags["vcenter"] = vcAddr + if alive { + return []*core.MetricValue{core.GaugeValue("", "vcenter.alive", 1, tags)} + } else { + return []*core.MetricValue{core.GaugeValue("", "vcenter.alive", 0, tags)} + } +} + +func VsphereDatastores(ctx context.Context, c *govmomi.Client) ([]mo.Datastore, error) { + m := view.NewManager(c.Client) + v, err := m.CreateContainerView(ctx, c.ServiceContent.RootFolder, []string{"Datastore"}, true) + if err != nil { + return nil, err + } + defer v.Destroy(ctx) + + var dss []mo.Datastore + if err = v.Retrieve(ctx, []string{"Datastore"}, []string{"summary"}, &dss); err != nil { + return nil, err + } + + if le := len(dss); le != 0 { + return dss, nil + } + return nil, nil +} + +//DatastoreMetrics datastore metrics +func DatastoreMetrics(ctx context.Context, c *govmomi.Client) (L []*core.MetricValue, err error) { + dss, err := VsphereDatastores(ctx, c) + if err != nil { + return + } + if dss != nil { + for _, ds := range dss { + var tags = map[string]string{} + tags["ds"] = ds.Summary.Name + tags["fstype"] = ds.Summary.Type + L = append(L, core.GaugeValue("", "datastore.bytes.total", ds.Summary.Capacity, tags)) + L = append(L, core.GaugeValue("", "datastore.bytes.free", ds.Summary.FreeSpace, tags)) + L = append(L, core.GaugeValue("", "datastore.bytes.used", ds.Summary.Capacity-ds.Summary.FreeSpace, tags)) + if ds.Summary.Capacity > 0 { + usedPercent := float64(ds.Summary.Capacity-ds.Summary.FreeSpace) / float64(ds.Summary.Capacity) * 100 + L = append(L, core.GaugeValue("", "datastore.used.percent", usedPercent, tags)) + } + + } + } + return +} + +func VsphereVirtualMachines(ctx context.Context, c *govmomi.Client) ([]mo.VirtualMachine, error) { + m := view.NewManager(c.Client) + v, err := m.CreateContainerView(ctx, c.ServiceContent.RootFolder, []string{"VirtualMachine"}, true) + if err != nil { + return nil, err + } + defer v.Destroy(ctx) + + var vms []mo.VirtualMachine + if err = v.Retrieve(ctx, []string{"VirtualMachine"}, []string{"summary"}, &vms); err != nil { + return nil, err + } + + if le := len(vms); le != 0 { + return vms, nil + } + return nil, nil +} + +func GenVirtualMachinesByName(vms []mo.VirtualMachine, names []string) (resVms []mo.VirtualMachine) { + if len(names) == 0 { + resVms = vms + return + } + for _, vm := range vms { + if inSliceStr(vm.Summary.Config.Name, names) { + resVms = append(resVms, vm) + } + } + return +} + +func inSliceStr(str string, slice []string) bool { + for _, s := range slice { + if str == s { + return true + } + } + return false +} + +//VmAlive power status +func VmPower(vm mo.VirtualMachine) []*core.MetricValue { + /* + 1.0: poweredOff + 2.0: poweredOn + 3.0: standBy + 4.0: unknown + */ + var tags = map[string]string{} + tags["name"] = vm.Summary.Config.Name + + switch vm.Summary.Runtime.PowerState { + case "poweredOff": + return []*core.MetricValue{core.GaugeValue("", "vm.power", 1.0, tags)} + case "poweredOn": + return []*core.MetricValue{core.GaugeValue("", "vm.power", 2.0, tags)} + case "standBy": + return []*core.MetricValue{core.GaugeValue("", "vm.power", 3.0, tags)} + case "unknown": + return []*core.MetricValue{core.GaugeValue("", "vm.power", 4.0, tags)} + default: + return []*core.MetricValue{core.GaugeValue("", "vm.power", 4.0, tags)} + } +} + +//VmStatus The Status enumeration defines a general "health" value for a managed entity. +func VmStatus(vm mo.VirtualMachine) []*core.MetricValue { + /* + 1.0: gray,The status is unknown. + 2.0: green,The entity is OK. + 3.0: red,The entity definitely has a problem. + 4.0: yellow,The entity might have a problem. + */ + var tags = map[string]string{} + tags["name"] = vm.Summary.Config.Name + switch vm.Summary.OverallStatus { + case "gray": + return []*core.MetricValue{core.GaugeValue("", "vm.status", 1.0, tags)} + case "green": + return []*core.MetricValue{core.GaugeValue("", "vm.status", 2.0, tags)} + case "red": + return []*core.MetricValue{core.GaugeValue("", "vm.status", 3.0, tags)} + case "yellow": + return []*core.MetricValue{core.GaugeValue("", "vm.status", 4.0, tags)} + default: + return []*core.MetricValue{core.GaugeValue("", "vm.status", 1.0, tags)} + } +} + +//VmUptime uptime +func VmUptime(vm mo.VirtualMachine) []*core.MetricValue { + var tags = map[string]string{} + tags["name"] = vm.Summary.Config.Name + return []*core.MetricValue{core.GaugeValue("", "vm.uptime", int64(vm.Summary.QuickStats.UptimeSeconds), tags)} +} + +//VmCPU cpu metrics +func VmCPU(vm mo.VirtualMachine) []*core.MetricValue { + var tags = map[string]string{} + tags["name"] = vm.Summary.Config.Name + perf := []*core.MetricValue{} + if vm.Summary.Runtime.MaxCpuUsage > 0 { + usePercentCPU := core.GaugeValue("", "cpu.util", float64(vm.Summary.QuickStats.OverallCpuUsage)/float64(vm.Summary.Runtime.MaxCpuUsage)*100, tags) + freePercentCPU := core.GaugeValue("", "cpu.idle", (float64(vm.Summary.Runtime.MaxCpuUsage)-float64(vm.Summary.QuickStats.OverallCpuUsage))/float64(vm.Summary.Runtime.MaxCpuUsage)*100, tags) + perf = append(perf, usePercentCPU) + perf = append(perf, freePercentCPU) + } + return perf +} + +//VmMem mem metrics +func VmMem(vm mo.VirtualMachine) []*core.MetricValue { + var tags = map[string]string{} + tags["name"] = vm.Summary.Config.Name + totalMem := core.GaugeValue("", "mem.bytes.total", int64(vm.Summary.Runtime.MaxMemoryUsage)*1024*1024, tags) + guestUseMem := core.GaugeValue("", "mem.bytes.guest.used", int64(vm.Summary.QuickStats.GuestMemoryUsage)*1024*1024, tags) + hostUsedMem := core.GaugeValue("", "mem.bytes.host.used", int64(vm.Summary.QuickStats.HostMemoryUsage)*1024*1024, tags) + perf := []*core.MetricValue{totalMem, guestUseMem, hostUsedMem} + if vm.Summary.Runtime.MaxMemoryUsage > 0 { + usedMemPer := core.GaugeValue("", "mem.bytes.guest.used.percent", float64(vm.Summary.QuickStats.GuestMemoryUsage)/float64(vm.Summary.Runtime.MaxMemoryUsage)*100, tags) + perf = append(perf, usedMemPer) + } + return perf +} diff --git a/funcs/models/vsphere_test.go b/funcs/models/vsphere_test.go new file mode 100644 index 0000000..ffba628 --- /dev/null +++ b/funcs/models/vsphere_test.go @@ -0,0 +1,331 @@ +package models + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "testing" + + "github.com/vmware/govmomi/vim25/mo" + "github.com/vmware/govmomi/vim25/soap" + + "github.com/vmware/govmomi" +) + +const ( + USER = "administrator@vsphere.local" + PASS = "password" + VcAddr = "https://1.1.1.1/sdk" + VcIP = "1.1.1.1" +) + +var ctx = context.Background() +var c *govmomi.Client +var esxiList []mo.HostSystem +var vmList []mo.VirtualMachine +var vmnames = []string{"n9e-v3-centos7-92.18", "VSAN-VC", "splunk-demo1-10.26"} + +func init() { + u, err := soap.ParseURL(VcAddr) + if err != nil { + fmt.Println(err) + return + } + u.User = url.UserPassword(USER, PASS) + c, err = govmomi.NewClient(ctx, u, true) + + if err != nil { + fmt.Println(err) + return + } +} +func Test_Esxi(t *testing.T) { + if c.IsVC() { + t.Log("connection successful!") + } + var err error + esxiList, err = EsxiList(ctx, c) + + if err != nil { + t.Error(err) + return + } + bs, _ := json.Marshal(esxiList[0]) + t.Log(string(bs)) + +} + +func Test_EsxiPower(t *testing.T) { + for _, esxi := range esxiList { + res := EsxiPower(esxi) + for _, r := range res { + bs, _ := json.Marshal(r) + t.Log(string(bs)) + } + + } +} + +func Test_EsxiStatus(t *testing.T) { + for _, esxi := range esxiList { + res := EsxiStatus(esxi) + for _, r := range res { + bs, _ := json.Marshal(r) + t.Log(string(bs)) + } + } +} + +func Test_EsxiUptime(t *testing.T) { + for _, esxi := range esxiList { + res := EsxiUptime(esxi) + for _, r := range res { + bs, _ := json.Marshal(r) + t.Log(string(bs)) + } + } +} + +func Test_EsxiMem(t *testing.T) { + for _, esxi := range esxiList { + res := EsxiMem(esxi) + for _, r := range res { + bs, _ := json.Marshal(r) + t.Log(string(bs)) + } + } +} + +func Test_EsxiNet(t *testing.T) { + for _, esxi := range esxiList { + res, err := EsxiNet(ctx, c, esxi) + if err != nil { + t.Error(err) + return + } + for _, r := range res { + bs, _ := json.Marshal(r) + t.Log(string(bs)) + } + } +} + +func Test_EsxiDisk(t *testing.T) { + for _, esxi := range esxiList { + res, err := EsxiDisk(ctx, c, esxi) + if err != nil { + t.Error(err) + return + } + for _, r := range res { + bs, _ := json.Marshal(r) + t.Log(string(bs)) + } + } +} + +func Test_EsxiExtend(t *testing.T) { + extend := []string{ + "cpu.coreUtilization.average", + "cpu.costop.summation", + "cpu.demand.average", + "cpu.idle.summation", + "cpu.latency.average", + "cpu.readiness.average", + "cpu.ready.summation", + "cpu.swapwait.summation", + "cpu.usage.average", + "cpu.usagemhz.average", + "cpu.used.summation", + "cpu.utilization.average", + "cpu.wait.summation", + "disk.deviceReadLatency.average", + "disk.deviceWriteLatency.average", + "disk.kernelReadLatency.average", + "disk.kernelWriteLatency.average", + "disk.numberReadAveraged.average", + "disk.numberWriteAveraged.average", + "disk.read.average", + "disk.totalReadLatency.average", + "disk.totalWriteLatency.average", + "disk.write.average", + "mem.active.average", + "mem.latency.average", + "mem.state.latest", + "mem.swapin.average", + "mem.swapinRate.average", + "mem.swapout.average", + "mem.swapoutRate.average", + "mem.totalCapacity.average", + "mem.usage.average", + "mem.vmmemctl.average", + "net.bytesRx.average", + "net.bytesTx.average", + "net.droppedRx.summation", + "net.droppedTx.summation", + "net.errorsRx.summation", + "net.errorsTx.summation", + "net.usage.average", + "power.power.average", + "storageAdapter.numberReadAveraged.average", + "storageAdapter.numberWriteAveraged.average", + "storageAdapter.read.average", + "storageAdapter.write.average", + "sys.uptime.latest", + } + for _, esxi := range esxiList { + res, err := EsxiExtend(ctx, c, esxi, extend) + if err != nil { + t.Error(err) + return + } + for _, r := range res { + bs, _ := json.Marshal(r) + t.Log(string(bs)) + } + } +} + +func Test_VsphereDatastores(t *testing.T) { + res, err := VsphereDatastores(ctx, c) + if err != nil { + t.Error(err) + return + } + for _, r := range res { + bs, _ := json.Marshal(r) + t.Log(string(bs)) + } +} + +func Test_DatastoreMetrics(t *testing.T) { + res, err := DatastoreMetrics(ctx, c) + if err != nil { + t.Error(err) + return + } + for _, r := range res { + bs, _ := json.Marshal(r) + t.Log(string(bs)) + } +} + +func Test_VsphereVirtualMachines(t *testing.T) { + var err error + vmList, err = VsphereVirtualMachines(ctx, c) + if err != nil { + t.Error(err) + return + } +} + +func Test_VmPower(t *testing.T) { + for _, vm := range vmList { + res := VmPower(vm) + for _, r := range res { + bs, _ := json.Marshal(r) + t.Log(string(bs)) + } + } +} + +func Test_VmStatus(t *testing.T) { + for _, vm := range vmList { + res := VmStatus(vm) + for _, r := range res { + bs, _ := json.Marshal(r) + t.Log(string(bs)) + } + } +} +func Test_VmUptime(t *testing.T) { + for _, vm := range vmList { + res := VmUptime(vm) + for _, r := range res { + bs, _ := json.Marshal(r) + t.Log(string(bs)) + } + } +} +func Test_VmCpu(t *testing.T) { + for _, vm := range vmList { + res := VmCPU(vm) + for _, r := range res { + bs, _ := json.Marshal(r) + t.Log(string(bs)) + } + } +} +func Test_VmMem(t *testing.T) { + for _, vm := range vmList { + res := VmMem(vm) + for _, r := range res { + bs, _ := json.Marshal(r) + t.Log(string(bs)) + } + } +} + +func Test_VmExtend(t *testing.T) { + + extend := []string{ + "cpu.demand.average", + "cpu.idle.summation", + "cpu.latency.average", + "cpu.readiness.average", + "cpu.ready.summation", + "cpu.run.summation", + "cpu.usagemhz.average", + "cpu.used.summation", + "cpu.wait.summation", + "mem.active.average", + "mem.granted.average", + "mem.latency.average", + "mem.swapin.average", + "mem.swapinRate.average", + "mem.swapout.average", + "mem.swapoutRate.average", + "mem.usage.average", + "mem.vmmemctl.average", + "net.bytesRx.average", + "net.bytesTx.average", + "net.droppedRx.summation", + "net.droppedTx.summation", + "net.usage.average", + "power.power.average", + "virtualDisk.numberReadAveraged.average", + "virtualDisk.numberWriteAveraged.average", + "virtualDisk.read.average", + "virtualDisk.readOIO.latest", + "virtualDisk.throughput.usage.average", + "virtualDisk.totalReadLatency.average", + "virtualDisk.totalWriteLatency.average", + "virtualDisk.write.average", + "virtualDisk.writeOIO.latest", + "sys.uptime.latest", + } + vms := GenVirtualMachinesByName(vmList, vmnames) + for _, vm := range vms { + bs, _ := json.Marshal(vm) + t.Log(string(bs)) + res, err := VmExtend(ctx, c, vm, extend) + if err != nil { + t.Error(err) + return + } + for _, r := range res { + bs, _ := json.Marshal(r) + t.Log(string(bs)) + } + } +} + +func Test_Logout(t *testing.T) { + err := c.Logout(ctx) + if err != nil { + t.Error(err) + return + } + t.Log("logout success!") +} diff --git a/funcs/report.go b/funcs/report.go new file mode 100644 index 0000000..8598197 --- /dev/null +++ b/funcs/report.go @@ -0,0 +1,99 @@ +package funcs + +import ( + "fmt" + "math/rand" + + "sort" + + "time" + + "github.com/toolkits/pkg/net/httplib" + "github.com/toolkits/pkg/str" + + "github.com/shanghai-edu/vsphere-mon/config" + "github.com/shanghai-edu/vsphere-mon/config/address" + + "github.com/vmware/govmomi/vim25/mo" +) + +type hostRegisterForm struct { + SN string `json:"sn"` + IP string `json:"ip"` + Ident string `json:"ident"` + Name string `json:"name"` + Cate string `json:"cate"` + UniqKey string `json:"uniqkey"` + Fields map[string]string `json:"fields"` + Digest string `json:"digest"` +} + +type errRes struct { + Err string `json:"err"` +} + +func getEsxiSn(esxi mo.HostSystem) (sn string) { + for _, IdentifyingInfo := range esxi.Summary.Hardware.OtherIdentifyingInfo { + if IdentifyingInfo.IdentifierType.GetElementDescription().Label == "Serial number tag" { + sn = IdentifyingInfo.IdentifierValue + return + } + } + if sn == "" { + sn = esxi.Summary.Hardware.Uuid + } + return +} + +func report(esxi mo.HostSystem) error { + + fields := map[string]string{ + "cpu": fmt.Sprintf("%d", esxi.Summary.Hardware.NumCpuCores), + "mem": fmt.Sprintf("%.2fG", float64(esxi.Summary.Hardware.MemorySize)/float64(1024*1024*1024)), + "model": esxi.Summary.Hardware.Model, + "version": esxi.Summary.Config.Product.FullName, + "tenant": config.Get().Report.Tenant, + } + + form := hostRegisterForm{ + SN: getEsxiSn(esxi), + IP: esxi.Summary.Config.Name, + Ident: esxi.Summary.Config.Name, + Name: esxi.Summary.Config.Name, + Cate: config.Get().Report.Cate, + UniqKey: config.Get().Report.UniqKey, + Fields: fields, + } + + content := form.SN + form.IP + form.Ident + form.Name + form.Cate + form.UniqKey + var keys []string + for key := range fields { + keys = append(keys, key) + } + sort.Strings(keys) + + for _, key := range keys { + content += fields[key] + } + + form.Digest = str.MD5(content) + + servers := address.GetHTTPAddresses("ams") + for _, i := range rand.Perm(len(servers)) { + url := fmt.Sprintf("http://%s/v1/ams-ce/hosts/register", servers[i]) + + var body errRes + err := httplib.Post(url).JSONBodyQuiet(form).Header("X-Srv-Token", config.Config.Report.Token).SetTimeout(time.Second * 5).ToJSON(&body) + if err != nil { + return fmt.Errorf("curl %s fail: %v", url, err) + } + + if body.Err != "" { + return fmt.Errorf(body.Err) + } + + return nil + } + + return fmt.Errorf("all server instance is dead") +} diff --git a/gitversion b/gitversion new file mode 100644 index 0000000..f4694d9 --- /dev/null +++ b/gitversion @@ -0,0 +1 @@ +d6bb045 diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..86e1286 --- /dev/null +++ b/go.mod @@ -0,0 +1,14 @@ +module github.com/shanghai-edu/vsphere-mon + +go 1.15 + +require ( + github.com/gin-contrib/pprof v1.3.0 + github.com/gin-gonic/gin v1.6.3 + github.com/mattn/go-isatty v0.0.12 + github.com/spf13/viper v1.7.1 + github.com/toolkits/pkg v1.1.3 + github.com/ugorji/go/codec v1.2.3 + github.com/vmware/govmomi v0.24.0 + go.uber.org/automaxprocs v1.3.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..72ba2ae --- /dev/null +++ b/go.sum @@ -0,0 +1,345 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-xdr v0.0.0-20161123171359-e6a2ba005892/go.mod h1:CTDl0pzVzE5DEzZhPfvhY/9sPFMQIxaJ9VAMs9AagrE= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gin-contrib/pprof v1.3.0 h1:G9eK6HnbkSqDZBYbzG4wrjCsA4e+cvYAHUZw6W+W9K0= +github.com/gin-contrib/pprof v1.3.0/go.mod h1:waMjT1H9b179t3CxuG1cV3DHpga6ybizwfBaM5OXaB0= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.6.2/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= +github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14= +github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= +github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v0.0.0-20170306145142-6a5e28554805/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= +github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= +github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= +github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/paulstuart/ping v0.0.0-20140925212352-0345a9703e43 h1:KdVgTa60PIJyle4rgRiRDMLU4rBPlvP9/Mig8tka7Ow= +github.com/paulstuart/ping v0.0.0-20140925212352-0345a9703e43/go.mod h1:Mqu1lFC2j84abm0x+RLoyj/uWj4iuO6mI/yabMSjX6Q= +github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/shanghai-edu/n9e-probe v0.0.0-20210120052415-947427790b5b h1:1lhVm+DRNZ9VbIzEG151tuuyD3JcOgYdV40MszT0Hbc= +github.com/shanghai-edu/n9e-probe v0.0.0-20210120052415-947427790b5b/go.mod h1:HljHrHqiH+dIdfkvQFxVlbx8V85I2mLtlLjOR/0Xrbo= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/viper v1.7.1 h1:pM5oEahlgWv/WnHXpgbKz7iLIxRf65tye2Ci+XFK5sk= +github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= +github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/toolkits/pkg v1.1.3 h1:cjZMz9hmuTv4v7ivYERA9mWJCLKyr8JMd4S+CL/YzMM= +github.com/toolkits/pkg v1.1.3/go.mod h1:ge83E8FQqUnFk+2wtVtZ8kvbmoSjE1l8FP3f+qmR0fY= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go v1.2.2/go.mod h1:bitgyERdV7L7Db/Z5gfd5v2NQMNhhiFiZwpgMw2SP7k= +github.com/ugorji/go v1.2.3 h1:WbFSXLxDFKVN69Sk8t+XHGzVCD7R8UoAATR8NqZgTbk= +github.com/ugorji/go v1.2.3/go.mod h1:5l8GZ8hZvmL4uMdy+mhCO1LjswGRYco9Q3HfuisB21A= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +github.com/ugorji/go/codec v1.2.2/go.mod h1:OM8g7OAy52uYl3Yk+RE/3AS1nXFn1Wh4PPLtupCxbuU= +github.com/ugorji/go/codec v1.2.3 h1:/mVYEV+Jo3IZKeA5gBngN0AvNnQltEDkR+eQikkWQu0= +github.com/ugorji/go/codec v1.2.3/go.mod h1:5FxzDJIgeiWJZslYHPj+LS1dq1ZBQVelZFnjsFGI/Uc= +github.com/vmware/govmomi v0.24.0 h1:G7YFF6unMTG3OY25Dh278fsomVTKs46m2ENlEFSbmbs= +github.com/vmware/govmomi v0.24.0/go.mod h1:Y+Wq4lst78L85Ge/F8+ORXIWiKYqaro1vhAulACy9Lc= +github.com/vmware/vmw-guestinfo v0.0.0-20170707015358-25eff159a728/go.mod h1:x9oS4Wk2s2u4tS29nEaDLdzvuHdB19CvSGJjPgkZJNk= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/automaxprocs v1.3.0 h1:II28aZoGdaglS5vVNnspf28lnZpXScxtIozx1lAjdb0= +go.uber.org/automaxprocs v1.3.0/go.mod h1:9CWT6lKIep8U41DDaPiH6eFscnTyjfTANNQNx6LrIcA= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= +gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= diff --git a/http/health.go b/http/health.go new file mode 100644 index 0000000..cc49f06 --- /dev/null +++ b/http/health.go @@ -0,0 +1,20 @@ +package http + +import ( + "fmt" + "os" + + "github.com/gin-gonic/gin" +) + +func ping(c *gin.Context) { + c.String(200, "pong") +} + +func addr(c *gin.Context) { + c.String(200, c.Request.RemoteAddr) +} + +func pid(c *gin.Context) { + c.String(200, fmt.Sprintf("%d", os.Getpid())) +} diff --git a/http/middleware/logger.go b/http/middleware/logger.go new file mode 100644 index 0000000..d8f49d4 --- /dev/null +++ b/http/middleware/logger.go @@ -0,0 +1,295 @@ +package middleware + +import ( + "bytes" + "fmt" + "io" + "io/ioutil" + "net/http" + "os" + "time" + + "github.com/gin-gonic/gin" + "github.com/mattn/go-isatty" + "github.com/toolkits/pkg/logger" +) + +type consoleColorModeValue int + +const ( + autoColor consoleColorModeValue = iota + disableColor + forceColor +) + +var ( + green = string([]byte{27, 91, 57, 55, 59, 52, 50, 109}) + white = string([]byte{27, 91, 57, 48, 59, 52, 55, 109}) + yellow = string([]byte{27, 91, 57, 48, 59, 52, 51, 109}) + red = string([]byte{27, 91, 57, 55, 59, 52, 49, 109}) + blue = string([]byte{27, 91, 57, 55, 59, 52, 52, 109}) + magenta = string([]byte{27, 91, 57, 55, 59, 52, 53, 109}) + cyan = string([]byte{27, 91, 57, 55, 59, 52, 54, 109}) + reset = string([]byte{27, 91, 48, 109}) + consoleColorMode = autoColor +) + +// LoggerConfig defines the config for Logger middleware. +type LoggerConfig struct { + // Optional. Default value is gin.defaultLogFormatter + Formatter LogFormatter + + // Output is a writer where logs are written. + // Optional. Default value is gin.DefaultWriter. + Output io.Writer + + // SkipPaths is a url path array which logs are not written. + // Optional. + SkipPaths []string +} + +// LogFormatter gives the signature of the formatter function passed to LoggerWithFormatter +type LogFormatter func(params LogFormatterParams) string + +// LogFormatterParams is the structure any formatter will be handed when time to log comes +type LogFormatterParams struct { + Request *http.Request + + // TimeStamp shows the time after the server returns a response. + TimeStamp time.Time + // StatusCode is HTTP response code. + StatusCode int + // Latency is how much time the server cost to process a certain request. + Latency time.Duration + // ClientIP equals Context's ClientIP method. + ClientIP string + // Method is the HTTP method given to the request. + Method string + // Path is a path the client requests. + Path string + // ErrorMessage is set if error has occurred in processing the request. + ErrorMessage string + // isTerm shows whether does gin's output descriptor refers to a terminal. + isTerm bool + // BodySize is the size of the Response Body + BodySize int + // Keys are the keys set on the request's context. + Keys map[string]interface{} +} + +// StatusCodeColor is the ANSI color for appropriately logging http status code to a terminal. +func (p *LogFormatterParams) StatusCodeColor() string { + code := p.StatusCode + + switch { + case code >= http.StatusOK && code < http.StatusMultipleChoices: + return green + case code >= http.StatusMultipleChoices && code < http.StatusBadRequest: + return white + case code >= http.StatusBadRequest && code < http.StatusInternalServerError: + return yellow + default: + return red + } +} + +// MethodColor is the ANSI color for appropriately logging http method to a terminal. +func (p *LogFormatterParams) MethodColor() string { + method := p.Method + + switch method { + case "GET": + return blue + case "POST": + return cyan + case "PUT": + return yellow + case "DELETE": + return red + case "PATCH": + return green + case "HEAD": + return magenta + case "OPTIONS": + return white + default: + return reset + } +} + +// ResetColor resets all escape attributes. +func (p *LogFormatterParams) ResetColor() string { + return reset +} + +// IsOutputColor indicates whether can colors be outputted to the log. +func (p *LogFormatterParams) IsOutputColor() bool { + return consoleColorMode == forceColor || (consoleColorMode == autoColor && p.isTerm) +} + +// defaultLogFormatter is the default log format function Logger middleware uses. +var defaultLogFormatter = func(param LogFormatterParams) string { + var statusColor, methodColor, resetColor string + if param.IsOutputColor() { + statusColor = param.StatusCodeColor() + methodColor = param.MethodColor() + resetColor = param.ResetColor() + } + + if param.Latency > time.Minute { + // Truncate in a golang < 1.8 safe way + param.Latency = param.Latency - param.Latency%time.Second + } + return fmt.Sprintf("[GIN] |%s %3d %s| %13v | %15s |%s %-7s %s %s\n%s", + statusColor, param.StatusCode, resetColor, + param.Latency, + param.ClientIP, + methodColor, param.Method, resetColor, + param.Path, + param.ErrorMessage, + ) +} + +// DisableConsoleColor disables color output in the console. +func DisableConsoleColor() { + consoleColorMode = disableColor +} + +// ForceConsoleColor force color output in the console. +func ForceConsoleColor() { + consoleColorMode = forceColor +} + +// ErrorLogger returns a handlerfunc for any error type. +func ErrorLogger() gin.HandlerFunc { + return ErrorLoggerT(gin.ErrorTypeAny) +} + +// ErrorLoggerT returns a handlerfunc for a given error type. +func ErrorLoggerT(typ gin.ErrorType) gin.HandlerFunc { + return func(c *gin.Context) { + c.Next() + errors := c.Errors.ByType(typ) + if len(errors) > 0 { + c.JSON(-1, errors) + } + } +} + +// Logger instances a Logger middleware that will write the logs to gin.DefaultWriter. +// By default gin.DefaultWriter = os.Stdout. +func Logger() gin.HandlerFunc { + return LoggerWithConfig(LoggerConfig{}) +} + +// LoggerWithFormatter instance a Logger middleware with the specified log format function. +func LoggerWithFormatter(f LogFormatter) gin.HandlerFunc { + return LoggerWithConfig(LoggerConfig{ + Formatter: f, + }) +} + +// LoggerWithWriter instance a Logger middleware with the specified writer buffer. +// Example: os.Stdout, a file opened in write mode, a socket... +func LoggerWithWriter(out io.Writer, notlogged ...string) gin.HandlerFunc { + return LoggerWithConfig(LoggerConfig{ + Output: out, + SkipPaths: notlogged, + }) +} + +// LoggerWithConfig instance a Logger middleware with config. +func LoggerWithConfig(conf LoggerConfig) gin.HandlerFunc { + formatter := conf.Formatter + if formatter == nil { + formatter = defaultLogFormatter + } + + out := conf.Output + if out == nil { + out = os.Stdout + } + + notlogged := conf.SkipPaths + + isTerm := true + + if w, ok := out.(*os.File); !ok || os.Getenv("TERM") == "dumb" || + (!isatty.IsTerminal(w.Fd()) && !isatty.IsCygwinTerminal(w.Fd())) { + isTerm = false + } + + var skip map[string]struct{} + + if length := len(notlogged); length > 0 { + skip = make(map[string]struct{}, length) + + for _, path := range notlogged { + skip[path] = struct{}{} + } + } + + return func(c *gin.Context) { + // Start timer + start := time.Now() + path := c.Request.URL.Path + raw := c.Request.URL.RawQuery + + var ( + rdr1 io.ReadCloser + rdr2 io.ReadCloser + ) + + if c.Request.Method != "GET" { + buf, _ := ioutil.ReadAll(c.Request.Body) + rdr1 = ioutil.NopCloser(bytes.NewBuffer(buf)) + rdr2 = ioutil.NopCloser(bytes.NewBuffer(buf)) + + c.Request.Body = rdr2 + } + + // Process request + c.Next() + + // Log only when path is not being skipped + if _, ok := skip[path]; !ok { + param := LogFormatterParams{ + Request: c.Request, + isTerm: isTerm, + Keys: c.Keys, + } + + // Stop timer + param.TimeStamp = time.Now() + param.Latency = param.TimeStamp.Sub(start) + + param.ClientIP = c.ClientIP() + param.Method = c.Request.Method + param.StatusCode = c.Writer.Status() + param.ErrorMessage = c.Errors.ByType(gin.ErrorTypePrivate).String() + + param.BodySize = c.Writer.Size() + + if raw != "" { + path = path + "?" + raw + } + + param.Path = path + + // fmt.Fprint(out, formatter(param)) + logger.Info(formatter(param)) + + if c.Request.Method != "GET" { + logger.Info(readBody(rdr1)) + } + } + } +} + +func readBody(reader io.Reader) string { + buf := new(bytes.Buffer) + buf.ReadFrom(reader) + + s := buf.String() + return s +} diff --git a/http/middleware/recovery.go b/http/middleware/recovery.go new file mode 100644 index 0000000..7c1d511 --- /dev/null +++ b/http/middleware/recovery.go @@ -0,0 +1,160 @@ +package middleware + +// Copyright 2014 Manu Martinez-Almeida. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +import ( + "bytes" + "fmt" + "io" + "io/ioutil" + "log" + "net" + "net/http" + "net/http/httputil" + "os" + "runtime" + "strings" + "time" + + "github.com/gin-gonic/gin" + "github.com/toolkits/pkg/errors" +) + +var ( + dunno = []byte("???") + centerDot = []byte("·") + dot = []byte(".") + slash = []byte("/") +) + +// Recovery returns a middleware that recovers from any panics and writes a 500 if there was one. +func Recovery() gin.HandlerFunc { + return RecoveryWithWriter(gin.DefaultErrorWriter) +} + +// RecoveryWithWriter returns a middleware for a given writer that recovers from any panics and writes a 500 if there was one. +func RecoveryWithWriter(out io.Writer) gin.HandlerFunc { + var logger *log.Logger + if out != nil { + logger = log.New(out, "\n\n\x1b[31m", log.LstdFlags) + } + return func(c *gin.Context) { + defer func() { + if err := recover(); err != nil { + // custom error + if e, ok := err.(errors.PageError); ok { + c.JSON(200, gin.H{"err": e.Message}) + c.Abort() + return + } + + // Check for a broken connection, as it is not really a + // condition that warrants a panic stack trace. + var brokenPipe bool + if ne, ok := err.(*net.OpError); ok { + if se, ok := ne.Err.(*os.SyscallError); ok { + if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") { + brokenPipe = true + } + } + } + if logger != nil { + stack := stack(3) + httpRequest, _ := httputil.DumpRequest(c.Request, false) + headers := strings.Split(string(httpRequest), "\r\n") + for idx, header := range headers { + current := strings.Split(header, ":") + if current[0] == "Authorization" { + headers[idx] = current[0] + ": *" + } + } + if brokenPipe { + logger.Printf("%s\n%s%s", err, string(httpRequest), reset) + } else if gin.IsDebugging() { + logger.Printf("[Recovery] %s panic recovered:\n%s\n%s\n%s%s", + timeFormat(time.Now()), strings.Join(headers, "\r\n"), err, stack, reset) + } else { + logger.Printf("[Recovery] %s panic recovered:\n%s\n%s%s", + timeFormat(time.Now()), err, stack, reset) + } + } + + // If the connection is dead, we can't write a status to it. + if brokenPipe { + c.Error(err.(error)) // nolint: errcheck + c.Abort() + } else { + c.AbortWithStatus(http.StatusInternalServerError) + } + } + }() + c.Next() + } +} + +// stack returns a nicely formatted stack frame, skipping skip frames. +func stack(skip int) []byte { + buf := new(bytes.Buffer) // the returned data + // As we loop, we open files and read them. These variables record the currently + // loaded file. + var lines [][]byte + var lastFile string + for i := skip; ; i++ { // Skip the expected number of frames + pc, file, line, ok := runtime.Caller(i) + if !ok { + break + } + // Print this much at least. If we can't find the source, it won't show. + fmt.Fprintf(buf, "%s:%d (0x%x)\n", file, line, pc) + if file != lastFile { + data, err := ioutil.ReadFile(file) + if err != nil { + continue + } + lines = bytes.Split(data, []byte{'\n'}) + lastFile = file + } + fmt.Fprintf(buf, "\t%s: %s\n", function(pc), source(lines, line)) + } + return buf.Bytes() +} + +// source returns a space-trimmed slice of the n'th line. +func source(lines [][]byte, n int) []byte { + n-- // in stack trace, lines are 1-indexed but our array is 0-indexed + if n < 0 || n >= len(lines) { + return dunno + } + return bytes.TrimSpace(lines[n]) +} + +// function returns, if possible, the name of the function containing the PC. +func function(pc uintptr) []byte { + fn := runtime.FuncForPC(pc) + if fn == nil { + return dunno + } + name := []byte(fn.Name()) + // The name includes the path name to the package, which is unnecessary + // since the file name is already included. Plus, it has center dots. + // That is, we see + // runtime/debug.*T·ptrmethod + // and want + // *T.ptrmethod + // Also the package path might contains dot (e.g. code.google.com/...), + // so first eliminate the path prefix + if lastSlash := bytes.LastIndex(name, slash); lastSlash >= 0 { + name = name[lastSlash+1:] + } + if period := bytes.Index(name, dot); period >= 0 { + name = name[period+1:] + } + name = bytes.Replace(name, centerDot, dot, -1) + return name +} + +func timeFormat(t time.Time) string { + return t.Format("2006/01/02 - 15:04:05") +} diff --git a/http/route.go b/http/route.go new file mode 100644 index 0000000..64af153 --- /dev/null +++ b/http/route.go @@ -0,0 +1,18 @@ +package http + +import ( + "github.com/gin-contrib/pprof" + "github.com/gin-gonic/gin" +) + +func Config(r *gin.Engine) { + // collector apis, compatible with open-falcon + v1 := r.Group("/v1") + { + v1.GET("/ping", ping) + v1.GET("/pid", pid) + v1.GET("/addr", addr) + } + + pprof.Register(r, "/debug/pprof") +} diff --git a/http/start.go b/http/start.go new file mode 100644 index 0000000..bb2c3bf --- /dev/null +++ b/http/start.go @@ -0,0 +1,61 @@ +package http + +import ( + "context" + "fmt" + "net/http" + "os" + "time" + + "github.com/gin-gonic/gin" + + "github.com/shanghai-edu/vsphere-mon/config/address" + "github.com/shanghai-edu/vsphere-mon/http/middleware" +) + +var srv = &http.Server{ + ReadTimeout: 10 * time.Second, + WriteTimeout: 10 * time.Second, + MaxHeaderBytes: 1 << 20, +} + +func Start() { + recoveryMid := middleware.Recovery() + + gin.SetMode(gin.ReleaseMode) + middleware.DisableConsoleColor() + + r := gin.New() + r.Use(recoveryMid) + + Config(r) + + srv.Addr = address.GetHTTPListen("vsphere-mon") + srv.Handler = r + + go func() { + fmt.Println("http.listening:", srv.Addr) + if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + fmt.Printf("listening %s occur error: %s\n", srv.Addr, err) + os.Exit(3) + } + }() +} + +// Shutdown http server +func Shutdown() { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := srv.Shutdown(ctx); err != nil { + fmt.Println("cannot shutdown http server:", err) + os.Exit(2) + } + + // catching ctx.Done(). timeout of 5 seconds. + select { + case <-ctx.Done(): + fmt.Println("shutdown http server timeout of 5 seconds.") + default: + fmt.Println("http server stopped") + } +} diff --git a/install.sh b/install.sh new file mode 100644 index 0000000..c006458 --- /dev/null +++ b/install.sh @@ -0,0 +1,4 @@ +cp service/vsphere.service /usr/lib/systemd/system/ +systemctl daemon-reload +systemctl start vsphere-mon +systemctl enable vsphere-mon diff --git a/main.go b/main.go new file mode 100644 index 0000000..3f60692 --- /dev/null +++ b/main.go @@ -0,0 +1,103 @@ +package main + +import ( + "flag" + "fmt" + "os" + "os/signal" + "syscall" + + "github.com/shanghai-edu/vsphere-mon/config" + "github.com/shanghai-edu/vsphere-mon/funcs" + "github.com/shanghai-edu/vsphere-mon/funcs/core" + "github.com/shanghai-edu/vsphere-mon/http" + + "github.com/toolkits/pkg/file" + "github.com/toolkits/pkg/logger" + "github.com/toolkits/pkg/runner" +) + +var ( + vers *bool + help *bool + conf *string +) + +func init() { + vers = flag.Bool("v", false, "display the version.") + help = flag.Bool("h", false, "print this help.") + conf = flag.String("f", "", "specify configuration file.") + flag.Parse() + + if *vers { + fmt.Println("version:", config.Version) + os.Exit(0) + } + + if *help { + flag.Usage() + os.Exit(0) + } +} + +func main() { + aconf() + pconf() + start() + + cfg := config.Get() + config.InitLog(cfg.Logger) + + core.InitRpcClients() + funcs.Collect() + + http.Start() + ending() +} + +// auto detect configuration file +func aconf() { + if *conf != "" && file.IsExist(*conf) { + return + } + + *conf = "etc/vsphere.local.yml" + if file.IsExist(*conf) { + return + } + + *conf = "etc/vsphere.yml" + if file.IsExist(*conf) { + return + } + + fmt.Println("no configuration file for vsphere-mon") + os.Exit(1) +} + +// parse configuration file +func pconf() { + if err := config.Parse(*conf); err != nil { + fmt.Println("cannot parse configuration file:", err) + os.Exit(1) + } +} + +func ending() { + c := make(chan os.Signal, 1) + signal.Notify(c, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) + select { + case <-c: + fmt.Printf("stop signal caught, stopping... pid=%d\n", os.Getpid()) + } + + logger.Close() + http.Shutdown() + fmt.Println("vsphere-mon stopped successfully") +} + +func start() { + runner.Init() + fmt.Println("vsphere-mon start, use configuration file:", *conf) + fmt.Println("runner.cwd:", runner.Cwd) +} diff --git a/service/vsphere.service b/service/vsphere.service new file mode 100644 index 0000000..ba82a7a --- /dev/null +++ b/service/vsphere.service @@ -0,0 +1,20 @@ +[Unit] +Description=vsphere-mon +After=network-online.target +Wants=network-online.target + +[Service] +# modify when deploy in prod env +User=root +Group=root + +Type=simple +ExecStart=/home/vsphere-mon/vsphere-mon +WorkingDirectory=/home/vsphere-mon + +Restart=always +RestartSec=1 +StartLimitInterval=0 + +[Install] +WantedBy=multi-user.target