diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..57ca71e --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +g_*.go +/dist/ + +# Binaries for programs and plugins +ipmi-checker +*.exe +*.dll +*.so +*.dylib + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 +.glide/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ef84ad5 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 CrossEngage + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..bf205dd --- /dev/null +++ b/Makefile @@ -0,0 +1,25 @@ +APPNAME := ipmi-checker +DIST_DIR := ./dist +PLATFORMS := linux-386 linux-amd64 linux-arm + +RELEASE := $(shell git describe --all --always) + +build: + go generate + go get -v -t ./... + go test -v ./... + go build -v + + +dist: $(PLATFORMS) +$(PLATFORMS): + $(eval GOOS := $(firstword $(subst -, ,$@))) + $(eval GOARCH := $(lastword $(subst -, ,$@))) + mkdir -p $(DIST_DIR) + env GOOS=$(GOOS) GOARCH=$(GOARCH) go build -o $(DIST_DIR)/$(APPNAME).$(RELEASE).$@ + + +release: dist + go get github.com/tcnksm/ghr + if [ "x$$(git config --global --get github.token)" = "x" ]; then echo "Missing github.token in your git config"; fi + ghr -recreate -u crossengage $(RELEASE) dist/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..f30a39f --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +# ipmi-checker + +Simple tool to run ipmi-sel and print InfluxDB compatible output. + +This tool is meant to be used with Telegraf's `inputs.exec` plugin. + +The tool needs to have setuid to be able to run `ipmi-sel`. \ No newline at end of file diff --git a/event.go b/event.go new file mode 100644 index 0000000..c19ad12 --- /dev/null +++ b/event.go @@ -0,0 +1,70 @@ +package main + +import ( + "fmt" + "strings" + "time" +) + +var ( + errCouldNotParseCmdOut = "Unknown command output: %s" + errCouldNotParseTime = "Could not parse time: %s" +) + +type ipmiEvent struct { + ID string + Time time.Time + Sensor string + Type string + Level string + Message string + State int +} + +const ( + timeFmt = "Jan-02-2006T03:04:05" +) + +func newIPMIEvent(stdout string) (*ipmiEvent, error) { + parts := strings.Split(stdout, ",") + if len(parts) != 7 { + return nil, fmt.Errorf(errCouldNotParseCmdOut, stdout) + } + + inputTime := parts[1] + "T" + parts[2] + eventTime, err := time.Parse(timeFmt, inputTime) + if err != nil { + return nil, fmt.Errorf(errCouldNotParseTime, inputTime) + } + + event := &ipmiEvent{ + ID: parts[0], + Time: eventTime, + Sensor: parts[3], + Type: parts[4], + Level: parts[5], + Message: parts[6], + State: 1, + } + + return event, nil +} + +func (ev ipmiEvent) InfluxDB(checkName, hostname string) string { + return fmt.Sprintf( + `%s,host=%s,event_id=%s,error_level=%s,event_type=%s event_date="%s",event_time="%s",error_level="%s",sensor_name="%s",event_type="%s",error_message="%s",state=%d\n`, + checkName, hostname, ev.ID, ev.Level, ev.Type, ev.Time.Format("Jan-02-2006"), ev.Time.Format("03:04:05"), ev.Level, ev.Sensor, ev.Type, ev.Message, ev.State, + ) +} + +func newEmptyIPMIEvent() *ipmiEvent { + return &ipmiEvent{ + ID: "0", + Time: time.Now(), + Sensor: "OK", + Type: "OK", + Level: "OK", + Message: "No errors", + State: 1, + } +} diff --git a/event_test.go b/event_test.go new file mode 100644 index 0000000..fdc45c3 --- /dev/null +++ b/event_test.go @@ -0,0 +1,21 @@ +package main + +import "testing" +import "time" +import "github.com/stretchr/testify/assert" + +var ( + ipmiSelOutput = `1,Apr-22-2016,09:04:09,Sensor #255,Session Audit,Warning,Invalid Username of Password` +) + +func TestParsingIPMISelOutput(t *testing.T) { + ev, err := newIPMIEvent(ipmiSelOutput) + assert.Nil(t, err) + assert.NotNil(t, ev) + assert.Equal(t, "1", ev.ID) + assert.Equal(t, "Fri Apr 22 09:04:09 2016", ev.Time.Format(time.ANSIC)) + assert.Equal(t, "Sensor #255", ev.Sensor) + assert.Equal(t, "Session Audit", ev.Type) + assert.Equal(t, "Warning", ev.Level) + assert.Equal(t, "Invalid Username of Password", ev.Message) +} diff --git a/g_version.sh b/g_version.sh new file mode 100644 index 0000000..303ebef --- /dev/null +++ b/g_version.sh @@ -0,0 +1,3 @@ +#!/bin/bash +version=`git describe --all --always --dirty --long` +printf "package main\nconst (\nversion=\`$version\`\n)\n" | gofmt | tee g_version.go diff --git a/main.go b/main.go new file mode 100644 index 0000000..6e62c01 --- /dev/null +++ b/main.go @@ -0,0 +1,65 @@ +//go:generate bash ./g_version.sh +package main + +import ( + "bytes" + "fmt" + "log/syslog" + "os" + "os/exec" + "path" + "strings" + + "log" + + "gopkg.in/alecthomas/kingpin.v1" +) + +var ( + appName = path.Base(os.Args[0]) + app = kingpin.New(appName, "A command-line checker for IPMI checks using ipmi-sel, by CrossEngage") + checkName = app.Flag("name", "check name").Default(appName).String() + ipmiSel = app.Flag("ipmi-sel", "Path of ipmi-sel").Default("/usr/sbin/ipmi-sel").String() +) + +func main() { + app.Version(version) + kingpin.MustParse(app.Parse(os.Args[1:])) + + hostname, err := os.Hostname() + if err != nil { + log.Fatal(err) + } + + slog, err := syslog.New(syslog.LOG_NOTICE|syslog.LOG_DAEMON, appName) + if err != nil { + log.Fatal(err) + } + log.SetOutput(slog) + + cmd := exec.Command(*ipmiSel, "--output-event-state", "--comma-separated-output", "--no-header-output") + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + if err := cmd.Run(); err != nil { + log.Fatalf("Failed running `%s` with error `%s`\n", *ipmiSel, err) + } + + outStr, errStr := string(stdout.Bytes()), string(stderr.Bytes()) + slog.Debug(fmt.Sprintf("%s: stdout `%s`, stderr `%s`", *ipmiSel, outStr, errStr)) + + outStr = strings.TrimSpace(outStr) + lines := strings.Split(outStr, "\n") + if len(lines) >= 0 { + for _, line := range lines { + ev, err := newIPMIEvent(line) + if err != nil { + slog.Err(fmt.Sprintf("Could not parse `%s` stdout: `%s`", line, outStr)) + } + fmt.Println(ev.InfluxDB(*checkName, hostname)) + } + } else { + ev := newEmptyIPMIEvent() + fmt.Println(ev.InfluxDB(*checkName, hostname)) + } +}