package freeipmi import ( "bytes" "crypto/rand" "encoding/csv" "encoding/hex" "fmt" "math" "os" "os/exec" "path/filepath" "regexp" "strconv" "strings" "syscall" ) var ( ipmiDCMICurrentPowerRegex = regexp.MustCompile(`^Current Power\s*:\s*(?P[0-9.]*)\s*Watts.*`) ipmiChassisPowerRegex = regexp.MustCompile(`^System Power\s*:\s(?P.*)`) ipmiSELEntriesRegex = regexp.MustCompile(`^Number of log entries\s*:\s(?P[0-9.]*)`) ipmiSELFreeSpaceRegex = regexp.MustCompile(`^Free space remaining\s*:\s(?P[0-9.]*)\s*bytes.*`) bmcInfoFirmwareRevisionRegex = regexp.MustCompile(`^Firmware Revision\s*:\s*(?P[0-9.]*).*`) bmcInfoSystemFirmwareVersionRegex = regexp.MustCompile(`^System Firmware Version\s*:\s*(?P[0-9.]*).*`) bmcInfoManufacturerIDRegex = regexp.MustCompile(`^Manufacturer ID\s*:\s*(?P.*)`) ) // Result represents the outcome of a call to one of the FreeIPMI tools. // It can be used with other functions in this package to extract data. type Result struct { output []byte err error } // SensorData represents the reading of a single sensor. type SensorData struct { ID int64 Name string Type string State string Value float64 Unit string Event string } // Logger interface is used to enable logging in async functions that cannot return errors. // The main use case is using a prometheus.log logger, which satisfies this interface. type Logger interface { Debugf(string, ...interface{}) Errorf(string, ...interface{}) } // EscapePassword escapes a password so that the result is suitable for usage in a // FreeIPMI config file. func EscapePassword(password string) string { return strings.Replace(password, "#", "\\#", -1) } func pipeName() string { randBytes := make([]byte, 16) rand.Read(randBytes) return filepath.Join(os.TempDir(), "ipmi_exporter-"+hex.EncodeToString(randBytes)) } func contains(s []int64, elm int64) bool { for _, a := range s { if a == elm { return true } } return false } func getValue(ipmiOutput []byte, regex *regexp.Regexp) (string, error) { for _, line := range strings.Split(string(ipmiOutput), "\n") { match := regex.FindStringSubmatch(line) if match == nil { continue } for i, name := range regex.SubexpNames() { if name != "value" { continue } return match[i], nil } } return "", fmt.Errorf("could not find value in output: %s", string(ipmiOutput)) } func freeipmiConfigPipe(config string, logger Logger) (string, error) { content := []byte(config) pipe := pipeName() err := syscall.Mkfifo(pipe, 0600) if err != nil { return "", err } go func(file string, data []byte) { f, err := os.OpenFile(file, os.O_WRONLY|os.O_CREATE|os.O_APPEND, os.ModeNamedPipe) if err != nil { logger.Errorf("Error opening pipe: %s", err) } if _, err := f.Write(data); err != nil { logger.Errorf("Error writing config to pipe: %s", err) } f.Close() }(pipe, content) return pipe, nil } func Execute(cmd string, args []string, config string, target string, logger Logger) Result { pipe, err := freeipmiConfigPipe(config, logger) if err != nil { return Result{nil, err} } defer func() { if err := os.Remove(pipe); err != nil { logger.Errorf("Error deleting named pipe: %s", err) } }() args = append(args, "--config-file", pipe) if target != "" { args = append(args, "-h", target) } else { target = "[local]" } logger.Debugf("Executing %s %v", cmd, args) out, err := exec.Command(cmd, args...).CombinedOutput() if err != nil { err = fmt.Errorf("error running %s: %s", cmd, err) } return Result{out, err} } func GetSensorData(ipmiOutput Result, excludeSensorIds []int64) ([]SensorData, error) { var result []SensorData if ipmiOutput.err != nil { return result, fmt.Errorf("%s: %s", ipmiOutput.err, ipmiOutput.output) } r := csv.NewReader(bytes.NewReader(ipmiOutput.output)) fields, err := r.ReadAll() if err != nil { return result, err } for _, line := range fields { var data SensorData data.ID, err = strconv.ParseInt(line[0], 10, 64) if err != nil { return result, err } if contains(excludeSensorIds, data.ID) { continue } data.Name = line[1] data.Type = line[2] data.State = line[3] value := line[4] if value != "N/A" { data.Value, err = strconv.ParseFloat(value, 64) if err != nil { return result, err } } else { data.Value = math.NaN() } data.Unit = line[5] data.Event = strings.Trim(line[6], "'") result = append(result, data) } return result, err } func GetCurrentPowerConsumption(ipmiOutput Result) (float64, error) { if ipmiOutput.err != nil { return -1, fmt.Errorf("%s: %s", ipmiOutput.err, ipmiOutput.output) } value, err := getValue(ipmiOutput.output, ipmiDCMICurrentPowerRegex) if err != nil { return -1, err } return strconv.ParseFloat(value, 64) } func GetChassisPowerState(ipmiOutput Result) (float64, error) { if ipmiOutput.err != nil { return -1, fmt.Errorf("%s: %s", ipmiOutput.err, ipmiOutput.output) } value, err := getValue(ipmiOutput.output, ipmiChassisPowerRegex) if err != nil { return -1, err } if value == "on" { return 1, err } return 0, err } func GetBMCInfoFirmwareRevision(ipmiOutput Result) (string, error) { // Workaround for an issue described here: https://github.com/soundcloud/ipmi_exporter/issues/57 // The command may fail, but produce usable output (minus the system firmware revision). // Try to recover gracefully from that situation by first trying to parse the output, and only // raise the initial error if that also fails. value, err := getValue(ipmiOutput.output, bmcInfoFirmwareRevisionRegex) if err != nil { if ipmiOutput.err != nil { return "", fmt.Errorf("%s: %s", ipmiOutput.err, ipmiOutput.output) } } return value, err } func GetBMCInfoManufacturerID(ipmiOutput Result) (string, error) { // Workaround for an issue described here: https://github.com/soundcloud/ipmi_exporter/issues/57 // The command may fail, but produce usable output (minus the system firmware revision). // Try to recover gracefully from that situation by first trying to parse the output, and only // raise the initial error if that also fails. value, err := getValue(ipmiOutput.output, bmcInfoManufacturerIDRegex) if err != nil { if ipmiOutput.err != nil { return "", fmt.Errorf("%s: %s", ipmiOutput.err, ipmiOutput.output) } } return value, err } func GetBMCInfoSystemFirmwareVersion(ipmiOutput Result) (string, error) { if ipmiOutput.err != nil { return "", fmt.Errorf("%s: %s", ipmiOutput.err, ipmiOutput.output) } return getValue(ipmiOutput.output, bmcInfoSystemFirmwareVersionRegex) } func GetSELInfoEntriesCount(ipmiOutput Result) (float64, error) { if ipmiOutput.err != nil { return -1, fmt.Errorf("%s: %s", ipmiOutput.err, ipmiOutput.output) } value, err := getValue(ipmiOutput.output, ipmiSELEntriesRegex) if err != nil { return -1, err } return strconv.ParseFloat(value, 64) } func GetSELInfoFreeSpace(ipmiOutput Result) (float64, error) { if ipmiOutput.err != nil { return -1, fmt.Errorf("%s: %s", ipmiOutput.err, ipmiOutput.output) } value, err := getValue(ipmiOutput.output, ipmiSELFreeSpaceRegex) if err != nil { return -1, err } return strconv.ParseFloat(value, 64) } func GetRawOctets(ipmiOutput Result) ([]string, error) { if ipmiOutput.err != nil { return nil, fmt.Errorf("%s: %s", ipmiOutput.err, ipmiOutput.output) } strOutput := strings.Trim(string(ipmiOutput.output), " \r\n") if !strings.HasPrefix(strOutput, "rcvd: ") { return nil, fmt.Errorf("unexpected raw response: %s", strOutput) } octects := strings.Split(strOutput[6:], " ") return octects, nil }