package main import ( "flag" "net/http" "github.com/prometheus/client_golang/prometheus/promhttp" "fmt" "os" "github.com/prometheus/common/log" "github.com/prometheus/common/version" "github.com/prometheus/client_golang/prometheus" "sync" "crypto/tls" "io/ioutil" "encoding/json" "bytes" "strconv" "strings" ) const ( namespace = "nexenta" // For Prometheus metrics. apiPath = "/rest/nms" ) var ( listenAddr = flag.String("listen-address", ":9457", "The address to listen on for HTTP requests.") metricsEndpoint = flag.String("metrics-endpoint", "/metrics", "Path under which to expose metrics.") apiHost = flag.String("host", "nexenta", "Nexenta API host.") apiPort = flag.String("port", "8457", "Nexenta API port.") apiUser = flag.String("user", "admin", "Nexenta API username.") apiPass = flag.String("password", "password", "Nexenta API password.") apiSsl = flag.Bool("ssl", false, "Use SSL for the Nexenta API.") insecure = flag.Bool("insecure", false, "Ignore server certificate if using https.") showVersion = flag.Bool("version", false, "Print version information.") ) type ApiRequest struct { Object string `json:"object"` Method string `json:"method"` Params []string `json:"params"` } type ApiResponse struct { TGFlash string `json:"tg_flash"` Result interface{} `json:"result"` Error ApiError `json:"error"` } type ApiError struct { Message string `json:"message"` } type VolumeStatus struct { State []string `json:"state"` Errors []string `json:"errors"` } type Sensor struct { Name string `json:"name"` Value string `json:"value"` State string `json:"state"` Dev string `json:"dev"` Units string `json:"units"` Type string `json:"type"` } type LicenseInfo struct { DaysLeft string `json:"license_days_left"` } type Exporter struct { URI string mutex sync.Mutex client *http.Client apiReachable float64 up *prometheus.Desc scrapeFailures prometheus.Counter volumeOnline *prometheus.Desc jbodStatus *prometheus.Desc jbodTemp *prometheus.Desc jbodVoltage *prometheus.Desc jbodSlotStatus *prometheus.Desc licenseDaysLeft *prometheus.Desc } func NewExporter(uri string) *Exporter { return &Exporter{ URI: uri, up: prometheus.NewDesc( prometheus.BuildFQName(namespace, "", "up"), "Is the Nexenta API reachable", nil, prometheus.Labels{"host":*apiHost}, ), scrapeFailures: prometheus.NewCounter(prometheus.CounterOpts{ Namespace: namespace, Name: "exporter_scrape_failures_total", Help: "Number of errors while scraping the Nexenta API", ConstLabels: prometheus.Labels{"host":*apiHost}, }), volumeOnline: prometheus.NewDesc( prometheus.BuildFQName(namespace, "volume", "online"), "Status of volume.", []string{"volume"}, prometheus.Labels{"host":*apiHost}, ), jbodStatus: prometheus.NewDesc( prometheus.BuildFQName(namespace, "jbod", "status"), "Status of JBOD.", []string{"jbod"}, prometheus.Labels{"host":*apiHost}, ), jbodTemp: prometheus.NewDesc( prometheus.BuildFQName(namespace, "jbod", "temp"), "Temperature of JBOD.", []string{"jbod", "name", "state"}, prometheus.Labels{"host":*apiHost}, ), jbodVoltage: prometheus.NewDesc( prometheus.BuildFQName(namespace, "jbod", "voltage"), "Voltage of JBOD.", []string{"jbod", "name", "state"}, prometheus.Labels{"host":*apiHost}, ), jbodSlotStatus: prometheus.NewDesc( prometheus.BuildFQName(namespace, "jbod", "slot_status"), "Status of JBOD slot.", []string{"jbod", "slot", "state"}, prometheus.Labels{"host":*apiHost}, ), licenseDaysLeft: prometheus.NewDesc( prometheus.BuildFQName(namespace, "license", "days_left"), "License days left.", nil, prometheus.Labels{"host":*apiHost}, ), client: &http.Client{ Transport: &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: *insecure}, }, }, } } func (e *Exporter) Describe(ch chan<- *prometheus.Desc) { ch <- e.up e.scrapeFailures.Describe(ch) ch <- e.volumeOnline ch <- e.jbodStatus ch <- e.jbodTemp ch <- e.jbodVoltage ch <- e.jbodSlotStatus } func (e *Exporter) Collect(ch chan<- prometheus.Metric) { e.mutex.Lock() // To protect metrics from concurrent collects. defer e.mutex.Unlock() err := e.collect(ch) if err != nil { log.Errorf("%v", err) } ch <- prometheus.MustNewConstMetric(e.up, prometheus.GaugeValue, e.apiReachable) return } func (e *Exporter) collect(ch chan<- prometheus.Metric) error { // Volumes volumes, err := e.getVolumes(ch) if err != nil { err = fmt.Errorf("getVolumes(): %v", err) } if len(volumes) > 0 { for _, volume := range volumes { err = e.getVolumeStatus(ch, volume) if err != nil { err = fmt.Errorf("getVolumeStatus(): %v", err) } } } // JBODs jbods, err := e.getJBODs(ch) if err != nil { err = fmt.Errorf("getJBODs(): %v", err) } if len(jbods) > 0 { for _, jbod := range jbods { err = e.getJBODSensors(ch, jbod) if err != nil { err = fmt.Errorf("getJBODSensors(): %v", err) } } } // License err = e.getLicenseInfo(ch) if err != nil { err = fmt.Errorf("getLicenseInfo(): %v", err) } return err } func (e *Exporter) getVolumes(ch chan<- prometheus.Metric) ([]string, error) { apiResponse, err := e.queryApi(ch, "volume", "get_names", []string{""}); if err != nil { return nil, err } apiResult, err := json.Marshal(apiResponse.Result); if err != nil { return nil, err } var volumes []string err = json.Unmarshal([]byte(apiResult), &volumes) return volumes, err } func (e *Exporter) getVolumeStatus(ch chan<- prometheus.Metric, volume string) error { apiResponse, err := e.queryApi(ch, "volume", "get_status", []string{volume}); if err != nil { return err } apiResult, err := json.Marshal(apiResponse.Result); if err != nil { return err } var volumeStatus = new(VolumeStatus) err = json.Unmarshal(apiResult, &volumeStatus); if err != nil { return err } var volumeOnline float64 = 0 if len(volumeStatus.State) > 0 { if volumeStatus.State[0] == "ONLINE" { volumeOnline = 1 } } ch <- prometheus.MustNewConstMetric(e.volumeOnline, prometheus.GaugeValue, volumeOnline, volume) return err } func (e *Exporter) getJBODs(ch chan<- prometheus.Metric) ([]string, error) { apiResponse, err := e.queryApi(ch, "jbod", "get_names", []string{""}); if err != nil { return nil, err } apiResult, err := json.Marshal(apiResponse.Result); if err != nil { return nil, err } var jbods []string err = json.Unmarshal([]byte(apiResult), &jbods) return jbods, err } func (e *Exporter) getJBODSensors(ch chan<- prometheus.Metric, jbod string) error { apiResponse, err := e.queryApi(ch, "jbod", "get_sensors", []string{jbod}); if err != nil { return err } apiResult, err := json.Marshal(apiResponse.Result); if err != nil { return err } var sensors []Sensor err = json.Unmarshal([]byte(apiResult), &sensors) for _, sensor := range sensors { var status float64 = 0 if sensor.Dev == "jbod" { switch sensor.Type { case "state": if sensor.State == "ok" { status = 1 } ch <- prometheus.MustNewConstMetric(e.jbodStatus, prometheus.GaugeValue, status, jbod) break case "temperature": value, _ := strconv.ParseFloat(sensor.Value, 64) ch <- prometheus.MustNewConstMetric(e.jbodTemp, prometheus.GaugeValue, value, jbod, sensor.Name, sensor.State) break case "voltage": value, _ := strconv.ParseFloat(sensor.Value, 64) ch <- prometheus.MustNewConstMetric(e.jbodVoltage, prometheus.GaugeValue, value, jbod, sensor.Name, sensor.State) break } } else { slot := strings.Split(sensor.Dev, ":") if sensor.State == "ok" { status = 1 } ch <- prometheus.MustNewConstMetric(e.jbodSlotStatus, prometheus.GaugeValue, status, jbod, slot[1], sensor.State) } } return err } func (e *Exporter) getLicenseInfo(ch chan<- prometheus.Metric) error { apiResponse, err := e.queryApi(ch, "appliance", "get_license_info", []string{}); if err != nil { return err } apiResult, err := json.Marshal(apiResponse.Result); if err != nil { return err } var licenseInfo LicenseInfo err = json.Unmarshal([]byte(apiResult), &licenseInfo) daysLeft, _ := strconv.ParseFloat(licenseInfo.DaysLeft, 64) ch <- prometheus.MustNewConstMetric(e.licenseDaysLeft, prometheus.GaugeValue, daysLeft) return err } func (e *Exporter) queryApi(ch chan<- prometheus.Metric, object string, method string, params []string) (*ApiResponse, error) { reqObject := &ApiRequest{ Object: object, Method: method, Params: params, } e.apiReachable = 0 reqJson, _ := json.Marshal(reqObject) resp, err := e.client.Post(e.URI, "application/json", bytes.NewBuffer(reqJson)) if err != nil { return nil, fmt.Errorf("Error scraping Nexenta API: %v", err) } e.apiReachable = 1 data, err := ioutil.ReadAll(resp.Body) resp.Body.Close() if resp.StatusCode != 200 { if err != nil { data = []byte(err.Error()) } e.scrapeFailures.Inc() e.scrapeFailures.Collect(ch) return nil, fmt.Errorf("Request Error: Status %s %s", resp.Status, data) } //log.Infof("response: %s", data) var apiResponse = new(ApiResponse) err = json.Unmarshal(data, &apiResponse); if err != nil { return nil, err } if apiResponse.Result == nil { e.scrapeFailures.Inc() e.scrapeFailures.Collect(ch) return nil, fmt.Errorf("API Error: %v", apiResponse.Error.Message) } return apiResponse, nil } func main() { flag.Parse() if *showVersion { fmt.Fprintln(os.Stdout, version.Print("nexenta_exporter")) os.Exit(0) } exporter := NewExporter("http://" + *apiUser + ":" + *apiPass + "@" + *apiHost + ":" + *apiPort + apiPath) prometheus.MustRegister(exporter) prometheus.MustRegister(version.NewCollector("nexenta_exporter")) // disable Go metrics prometheus.Unregister(prometheus.NewProcessCollector(os.Getpid(), "")) prometheus.Unregister(prometheus.NewGoCollector()) log.Infoln("Starting nexenta_exporter", version.Info()) log.Infoln("Build context", version.BuildContext()) log.Infof("Starting Server: %s", *listenAddr) http.Handle(*metricsEndpoint, promhttp.Handler()) http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(`<html> <head><title>Nexenta Exporter</title></head> <body> <h1>Nexenta Exporter</h1> <p><a href='` + *metricsEndpoint + `'>Metrics</a></p> </body> </html>`)) }) log.Fatal(http.ListenAndServe(*listenAddr, nil)) }