node_exporter/collector/megacli.go

237 lines
5.5 KiB
Go

// Copyright 2015 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// +build !nomegacli
package collector
import (
"bufio"
"flag"
"io"
"os/exec"
"strconv"
"strings"
"github.com/prometheus/client_golang/prometheus"
)
const (
defaultMegaCli = "megacli"
adapterHeaderSep = "================"
)
var (
megacliCommand = flag.String("collector.megacli.command", defaultMegaCli, "Command to run megacli.")
)
type megaCliCollector struct {
cli string
driveTemperature *prometheus.GaugeVec
driveCounters *prometheus.CounterVec
drivePresence *prometheus.GaugeVec
}
func init() {
Factories["megacli"] = NewMegaCliCollector
}
// Takes a prometheus registry and returns a new Collector exposing
// RAID status through megacli.
func NewMegaCliCollector() (Collector, error) {
return &megaCliCollector{
cli: *megacliCommand,
driveTemperature: prometheus.NewGaugeVec(prometheus.GaugeOpts{
Namespace: Namespace,
Name: "megacli_drive_temperature_celsius",
Help: "megacli: drive temperature",
}, []string{"enclosure", "slot"}),
driveCounters: prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: Namespace,
Name: "megacli_drive_count",
Help: "megacli: drive error and event counters",
}, []string{"enclosure", "slot", "type"}),
drivePresence: prometheus.NewGaugeVec(prometheus.GaugeOpts{
Namespace: Namespace,
Name: "megacli_adapter_disk_presence",
Help: "megacli: disk presence per adapter",
}, []string{"type"}),
}, nil
}
func (c *megaCliCollector) Update(ch chan<- prometheus.Metric) (err error) {
err = c.updateAdapter()
if err != nil {
return err
}
err = c.updateDisks()
c.driveTemperature.Collect(ch)
c.driveCounters.Collect(ch)
c.drivePresence.Collect(ch)
return err
}
func parseMegaCliDisks(r io.Reader) (map[int]map[int]map[string]string, error) {
var (
stats = map[int]map[int]map[string]string{}
scanner = bufio.NewScanner(r)
curEnc = -1
curSlot = -1
)
for scanner.Scan() {
var err error
text := strings.TrimSpace(scanner.Text())
parts := strings.SplitN(text, ":", 2)
if len(parts) != 2 { // Adapter #X
continue
}
key := strings.TrimSpace(parts[0])
value := strings.TrimSpace(parts[1])
switch {
case key == "Enclosure Device ID":
curEnc, err = strconv.Atoi(value)
if err != nil {
return nil, err
}
case key == "Slot Number":
curSlot, err = strconv.Atoi(value)
if err != nil {
return nil, err
}
case curSlot != -1 && curEnc != -1:
if _, ok := stats[curEnc]; !ok {
stats[curEnc] = map[int]map[string]string{}
}
if _, ok := stats[curEnc][curSlot]; !ok {
stats[curEnc][curSlot] = map[string]string{}
}
stats[curEnc][curSlot][key] = value
}
}
return stats, nil
}
func parseMegaCliAdapter(r io.Reader) (map[string]map[string]string, error) {
var (
raidStats = map[string]map[string]string{}
scanner = bufio.NewScanner(r)
header = ""
last = ""
)
for scanner.Scan() {
text := strings.TrimSpace(scanner.Text())
if text == adapterHeaderSep {
header = last
raidStats[header] = map[string]string{}
continue
}
last = text
if header == "" { // skip Adapter #X and separator
continue
}
parts := strings.SplitN(text, ":", 2)
if len(parts) != 2 { // these section never include anything we are interested in
continue
}
key := strings.TrimSpace(parts[0])
value := strings.TrimSpace(parts[1])
raidStats[header][key] = value
}
return raidStats, nil
}
func (c *megaCliCollector) updateAdapter() error {
cmd := exec.Command(c.cli, "-AdpAllInfo", "-aALL")
pipe, err := cmd.StdoutPipe()
if err != nil {
return err
}
if err := cmd.Start(); err != nil {
return err
}
stats, err := parseMegaCliAdapter(pipe)
if err != nil {
return err
}
if err := cmd.Wait(); err != nil {
return err
}
for k, v := range stats["Device Present"] {
value, err := strconv.ParseFloat(v, 64)
if err != nil {
return err
}
c.drivePresence.WithLabelValues(k).Set(value)
}
return nil
}
func (c *megaCliCollector) updateDisks() error {
var counters = []string{"Media Error Count", "Other Error Count", "Predictive Failure Count"}
cmd := exec.Command(c.cli, "-PDList", "-aALL")
pipe, err := cmd.StdoutPipe()
if err != nil {
return err
}
if err := cmd.Start(); err != nil {
return err
}
stats, err := parseMegaCliDisks(pipe)
if err != nil {
return err
}
if err := cmd.Wait(); err != nil {
return err
}
for enc, encStats := range stats {
for slot, slotStats := range encStats {
encStr := strconv.Itoa(enc)
slotStr := strconv.Itoa(slot)
tStr := slotStats["Drive Temperature"]
if strings.Index(tStr, "C") > 0 {
tStr = tStr[:strings.Index(tStr, "C")]
t, err := strconv.ParseFloat(tStr, 64)
if err != nil {
return err
}
c.driveTemperature.WithLabelValues(encStr, slotStr).Set(t)
}
for _, i := range counters {
counter, err := strconv.ParseFloat(slotStats[i], 64)
if err != nil {
return err
}
c.driveCounters.WithLabelValues(encStr, slotStr, i).Set(counter)
}
}
}
return nil
}