package main import ( "github.com/namsral/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("api-host", "nexenta", "Nexenta API host.") apiPort = flag.String("api-port", "8457", "Nexenta API port.") apiUser = flag.String("api-user", "admin", "Nexenta API username.") apiPass = flag.String("api-password", "password", "Nexenta API password.") apiSsl = flag.Bool("api-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 jbodStatus *prometheus.Desc jbodTemp *prometheus.Desc jbodVoltage *prometheus.Desc jbodSlotStatus *prometheus.Desc licenseDaysLeft *prometheus.Desc volumeStatus *prometheus.Desc volumeLunErrors *prometheus.Desc volumeLunStatus *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}, }), 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}, ), volumeStatus: prometheus.NewDesc( prometheus.BuildFQName(namespace, "volume", "status"), "Status of volume.", []string{"volume", "state", "errors"}, prometheus.Labels{"host":*apiHost}, ), volumeLunErrors: prometheus.NewDesc( prometheus.BuildFQName(namespace, "volume", "lun_errors"), "Count of volume LUN errors.", []string{"volume", "lun", "read", "write", "checksum"}, prometheus.Labels{"host":*apiHost}, ), volumeLunStatus: prometheus.NewDesc( prometheus.BuildFQName(namespace, "volume", "lun_status"), "Status of volume LUN.", []string{"volume", "lun", "state", "group"}, 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.jbodStatus ch <- e.jbodTemp ch <- e.jbodVoltage ch <- e.jbodSlotStatus ch <- e.volumeStatus ch <- e.volumeLunErrors ch <- e.volumeLunStatus } 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 { return fmt.Errorf("getVolumes(): %v", err) } if len(volumes) > 0 { for _, volume := range volumes { err = e.getVolumeStatus(ch, volume) if err != nil { return fmt.Errorf("getVolumeStatus(): %v", err) } err = e.getVolumeLuns(ch, volume) if err != nil { return fmt.Errorf("getVolumeLuns(): %v", err) } } } // JBODs jbods, err := e.getJBODs(ch) if err != nil { return fmt.Errorf("getJBODs(): %v", err) } if len(jbods) > 0 { for _, jbod := range jbods { err = e.getJBODSensors(ch, jbod) if err != nil { return fmt.Errorf("getJBODSensors(): %v", err) } } } // License err = e.getLicenseInfo(ch) if err != nil { return 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.volumeStatus, prometheus.GaugeValue, volumeOnline, volume, volumeStatus.State[0], volumeStatus.Errors[0]) return err } func (e *Exporter) getVolumeLuns(ch chan<- prometheus.Metric, volume string) error { apiResponse, err := e.queryApi(ch, "volume", "get_luns", []string{volume}); if err != nil { return err } apiResult, err := json.Marshal(apiResponse.Result); if err != nil { return err } var volumeLuns map[string][]string err = json.Unmarshal(apiResult, &volumeLuns); if err != nil { return err } for lun, data := range volumeLuns { // data: 0=state, 1=err_read, 2=err_write, 3=err_chksum, 4=?, 5=group, 6=? var volumeLunStatus float64 = 0 var volumeLunErrors float64 = 0 errorsRead, _ := strconv.ParseFloat(data[1], 64) errorsWrite, _ := strconv.ParseFloat(data[2], 64) errorsChksum, _ := strconv.ParseFloat(data[3], 64) volumeLunErrors = errorsRead + errorsWrite + errorsChksum if len(data) > 0 { if (data[5] != "spares" && data[0] == "ONLINE") || (data[5] == "spares" && data[0] == "AVAIL") { volumeLunStatus = 1 } } ch <- prometheus.MustNewConstMetric(e.volumeLunErrors, prometheus.GaugeValue, volumeLunErrors, volume, lun, data[1], data[2], data[3]) ch <- prometheus.MustNewConstMetric(e.volumeLunStatus, prometheus.GaugeValue, volumeLunStatus, volume, lun, data[0], data[5]) } 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) } 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) } apiProtocol := "http" if *apiSsl { apiProtocol = "https" } exporter := NewExporter(apiProtocol + "://" + *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)) }