Skip to content
Snippets Groups Projects
Select Git revision
  • 7d077b7ebe48b1b9b28687cbc4561bbfacf00dcf
  • master default protected
2 results

main.go

Blame
  • main.go 11.98 KiB
    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
    	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},
    		),
    		volumeLunStatus: prometheus.NewDesc(
    			prometheus.BuildFQName(namespace, "volume", "lun_status"),
    			"Status of volume LUN.",
    			[]string{"volume", "lun", "state", "errors", "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.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
    		labelErrors := strconv.FormatFloat(volumeLunErrors, 'f', 0, 64)
    		if len(data) > 0 {
    			if (data[5] != "spares" && data[0] == "ONLINE") ||
    			   (data[5] == "spares" && data[0] == "AVAIL") {
    				volumeLunStatus = 1
    			}
    		}
    		ch <- prometheus.MustNewConstMetric(e.volumeLunStatus, prometheus.GaugeValue, volumeLunStatus,
    			volume, lun, data[0], labelErrors, 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))
    }