properly set up loading config

develop
noerw 6 years ago
parent 8d87786c05
commit 4106d06493

@ -22,6 +22,6 @@ var checkBoxCmd = &cobra.Command{
Args: BoxIdValidator, Args: BoxIdValidator,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
cmd.SilenceUsage = true cmd.SilenceUsage = true
return checkAndNotify(args, defaultConf) return checkAndNotify(args)
}, },
} }

@ -0,0 +1,123 @@
package cmd
import (
"os"
"path"
"strings"
log "github.com/sirupsen/logrus"
"github.com/spf13/viper"
)
/**
* config file handling, as it is kinda broken in spf13/viper
* mostly copied from https://github.com/TheThingsNetwork/ttn/blob/f623a6a/ttnctl/util/config.go
*/
// GetConfigFile returns the location of the configuration file.
// It checks the following (in this order):
// the --config flag
// $XDG_CONFIG_HOME/osem_notify/config.yml (if $XDG_CONFIG_HOME is set)
// $HOME/.osem_notify.yml
func getConfigFile() string {
flag := viper.GetString("config")
xdg := os.Getenv("XDG_CONFIG_HOME")
if xdg != "" {
xdg = path.Join(xdg, "osem_notify", "config.yml")
}
home := os.Getenv("HOME")
homeyml := ""
homeyaml := ""
if home != "" {
homeyml = path.Join(home, ".osem_notify.yml")
homeyaml = path.Join(home, ".osem_notify.yaml")
}
try_files := []string{
flag,
xdg,
homeyml,
homeyaml,
}
// find a file that exists, and use that
for _, file := range try_files {
if file != "" {
if _, err := os.Stat(file); err == nil {
return file
}
}
}
// no file found, set up correct fallback
if os.Getenv("XDG_CONFIG_HOME") != "" {
return xdg
} else {
return homeyml
}
}
// initConfig reads in config file and ENV variables if set.
func initConfig() {
theConfig := cfgFile
if cfgFile == "" {
theConfig = getConfigFile()
}
viper.SetConfigType("yaml")
viper.SetConfigFile(theConfig)
viper.SetEnvPrefix("OSEM_NOTIFY") // keys only work in upper case
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_"))
viper.AutomaticEnv() // WARNING: OSEM_NOTIFIY_CONFIG will not be considered this way. but why should it..
// If a config file is found, read it in.
if _, err := os.Stat(theConfig); err == nil {
err := viper.ReadInConfig()
if err != nil {
log.Error("Error when reading config file:", err)
}
} else if cfgFile != "" {
log.Error("Specified config file not found!")
os.Exit(1)
}
validateConfig()
}
func validateConfig() {
transport := viper.GetString("defaultHealthchecks.notifications.transport")
if viper.GetBool("notify") && transport == "email" {
if len(viper.GetStringSlice("defaultHealthchecks.notifications.options.recipients")) == 0 {
log.Warn("No recipients set up for transport email")
}
emailRequired := []string{
viper.GetString("email.host"),
viper.GetString("email.port"),
viper.GetString("email.user"),
viper.GetString("email.pass"),
viper.GetString("email.from"),
}
for _, conf := range emailRequired {
if conf == "" {
log.Error("Default transport set as email, but missing email config")
os.Exit(1)
}
}
}
}
func printConfig() {
log.Debug("Using config:")
printKV("config file", viper.ConfigFileUsed())
for key, val := range viper.AllSettings() {
printKV(key, val)
}
}
func printKV(key, val interface{}) {
log.Debugf("%20s: %v", key, val)
}

@ -2,7 +2,6 @@ package cmd
import ( import (
"os" "os"
"strings"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -10,38 +9,19 @@ import (
) )
var rootCmd = &cobra.Command{ var rootCmd = &cobra.Command{
Use: "osem_notify", Use: "osem_notify",
Long: "Run healthchecks and send notifications for boxes on opensensemap.org", Short: "Root command displaying help",
Long: "Run healthchecks and send notifications for boxes on opensensemap.org",
PersistentPreRunE: func(cmd *cobra.Command, args []string) error { PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
// set up config environment FIXME: cannot open / write file?!
viper.SetConfigType("json")
viper.SetConfigFile(".osem_notify")
viper.AddConfigPath("$HOME")
viper.AddConfigPath(".")
// // If a config file is found, read it in.
// if _, err := os.Stat(path.Join(os.Getenv("HOME"), ".osem_notify.yml")); err == nil {
// err := viper.ReadInConfig()
// if err != nil {
// fmt.Println("Error when reading config file:", err)
// }
// }
viper.SetEnvPrefix("osem_notify")
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_"))
viper.AutomaticEnv()
// set up logger // set up logger
log.SetOutput(os.Stdout) log.SetOutput(os.Stdout)
switch logLevel { if viper.GetBool("debug") {
case "debug":
log.SetLevel(log.DebugLevel) log.SetLevel(log.DebugLevel)
case "info": printConfig()
} else {
log.SetLevel(log.InfoLevel) log.SetLevel(log.InfoLevel)
case "warn":
log.SetLevel(log.WarnLevel)
case "error":
log.SetLevel(log.ErrorLevel)
} }
switch logFormat { switch viper.Get("logformat") {
case "json": case "json":
log.SetFormatter(&log.JSONFormatter{}) log.SetFormatter(&log.JSONFormatter{})
} }
@ -53,19 +33,33 @@ var rootCmd = &cobra.Command{
}, },
} }
var ( // accessed in initConfig(), as it is initialized before config is loaded (sic)
shouldNotify bool var cfgFile string
logLevel string
logFormat string
)
func init() { func init() {
rootCmd.PersistentFlags().StringVarP(&logLevel, "log-level", "", "info", "log level, can be one of debug, info, warn, error") var (
rootCmd.PersistentFlags().StringVarP(&logFormat, "log-format", "", "plain", "log format, can be plain or json") shouldNotify bool
debug bool
logFormat string
)
cobra.OnInitialize(initConfig)
rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "path to config file (default $HOME/.osem_notify.yml)")
rootCmd.PersistentFlags().StringVarP(&logFormat, "logformat", "l", "plain", "log format, can be plain or json")
rootCmd.PersistentFlags().BoolVarP(&debug, "debug", "d", false, "enable verbose logging")
rootCmd.PersistentFlags().BoolVarP(&shouldNotify, "notify", "n", false, "if set, will send out notifications.\nOtherwise results are printed to stdout only") rootCmd.PersistentFlags().BoolVarP(&shouldNotify, "notify", "n", false, "if set, will send out notifications.\nOtherwise results are printed to stdout only")
viper.BindPFlags(rootCmd.PersistentFlags()) // let flags override config
} }
func Execute() { func Execute() {
// generate documentation
// err := doc.GenMarkdownTree(rootCmd, "./doc")
// if err != nil {
// log.Fatal(err)
// }
if err := rootCmd.Execute(); err != nil { if err := rootCmd.Execute(); err != nil {
os.Exit(1) os.Exit(1)
} }

@ -5,6 +5,7 @@ import (
"regexp" "regexp"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/viper"
"../core" "../core"
) )
@ -13,29 +14,6 @@ import (
* shared functionality between watch and check * shared functionality between watch and check
*/ */
// TODO: actually to be read from arg / file
var defaultConf = &core.NotifyConfig{
Notifications: core.TransportConfig{
Transport: "email",
Options: core.EmailNotifier{
[]string{"test@nroo.de"},
"notify@nroo.de",
},
},
Events: []core.NotifyEvent{
core.NotifyEvent{
Type: "measurement_age",
Target: "all",
Threshold: "15m",
},
core.NotifyEvent{
Type: "measurement_suspicious",
Target: "all",
},
},
}
func isValidBoxId(boxId string) bool { func isValidBoxId(boxId string) bool {
// boxIds are UUIDs // boxIds are UUIDs
r := regexp.MustCompile("^[0-9a-fA-F]{24}$") r := regexp.MustCompile("^[0-9a-fA-F]{24}$")
@ -54,15 +32,21 @@ func BoxIdValidator(cmd *cobra.Command, args []string) error {
return nil return nil
} }
func checkAndNotify(boxIds []string, defaultNotifyConf *core.NotifyConfig) error { func checkAndNotify(boxIds []string) error {
results, err := core.CheckBoxes(boxIds, defaultConf) defaultNotifyConf := &core.NotifyConfig{}
err := viper.UnmarshalKey("defaultHealthchecks", defaultNotifyConf)
if err != nil {
return err
}
results, err := core.CheckBoxes(boxIds, defaultNotifyConf)
if err != nil { if err != nil {
return err return err
} }
results.Log() results.Log()
if shouldNotify { if viper.GetBool("notify") {
return results.SendNotifications() return results.SendNotifications()
} }
return nil return nil

@ -32,13 +32,13 @@ var watchBoxesCmd = &cobra.Command{
}, },
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
cmd.SilenceUsage = true cmd.SilenceUsage = true
err := checkAndNotify(args, defaultConf) err := checkAndNotify(args)
if err != nil { if err != nil {
return err return err
} }
for { for {
<-ticker <-ticker
err = checkAndNotify(args, defaultConf) err = checkAndNotify(args)
if err != nil { if err != nil {
return err return err
} }

@ -7,34 +7,34 @@ import (
) )
const ( const (
CheckOk = "OK" CheckOk = "OK"
CheckErr = "FAILED" CheckErr = "FAILED"
eventMeasurementAge = "measurement_age" eventMeasurementAge = "measurement_age"
eventMeasurementValMin = "measurement_min" eventMeasurementValMin = "measurement_min"
eventMeasurementValMax = "measurement_max" eventMeasurementValMax = "measurement_max"
eventMeasurementValSuspicious = "measurement_suspicious" eventMeasurementValFaulty = "measurement_faulty"
eventTargetAll = "all" // if event.Target is this value, all sensors will be checked eventTargetAll = "all" // if event.Target is this value, all sensors will be checked
) )
type checkType = struct{ description string } type checkType = struct{ description string }
var checkTypes = map[string]checkType{ var checkTypes = map[string]checkType{
eventMeasurementAge: checkType{"No measurement from %s since %s"}, eventMeasurementAge: checkType{"No measurement from %s since %s"},
eventMeasurementValMin: checkType{"Sensor %s reads low value of %s"}, eventMeasurementValMin: checkType{"Sensor %s reads low value of %s"},
eventMeasurementValMax: checkType{"Sensor %s reads high value of %s"}, eventMeasurementValMax: checkType{"Sensor %s reads high value of %s"},
eventMeasurementValSuspicious: checkType{"Sensor %s reads presumably faulty value of %s"}, eventMeasurementValFaulty: checkType{"Sensor %s reads presumably faulty value of %s"},
} }
type SuspiciousValue struct { type FaultyValue struct {
sensor string sensor string
val float64 val float64
} }
var suspiciousVals = map[SuspiciousValue]bool{ var faultyVals = map[FaultyValue]bool{
SuspiciousValue{sensor: "BMP280", val: 0.0}: true, FaultyValue{sensor: "BMP280", val: 0.0}: true,
SuspiciousValue{sensor: "HDC1008", val: 0.0}: true, FaultyValue{sensor: "HDC1008", val: 0.0}: true,
SuspiciousValue{sensor: "HDC1008", val: -40}: true, FaultyValue{sensor: "HDC1008", val: -40}: true,
SuspiciousValue{sensor: "SDS 011", val: 0.0}: true, FaultyValue{sensor: "SDS 011", val: 0.0}: true,
} }
type NotifyEvent struct { type NotifyEvent struct {
@ -71,20 +71,23 @@ func (box Box) RunChecks() ([]CheckResult, error) {
var results = []CheckResult{} var results = []CheckResult{}
for _, event := range box.NotifyConf.Events { for _, event := range box.NotifyConf.Events {
target := event.Target
for _, s := range box.Sensors { for _, s := range box.Sensors {
// if a sensor never measured anything, thats ok. checks would fail anyway // if a sensor never measured anything, thats ok. checks would fail anyway
if s.LastMeasurement == nil { if s.LastMeasurement == nil {
continue continue
} }
if target == eventTargetAll || target == s.Id { // a validator must set these values
var (
status = CheckOk
target = s.Id
value string
)
if event.Target == eventTargetAll || event.Target == s.Id {
switch event.Type { switch event.Type {
case eventMeasurementAge: case eventMeasurementAge:
// check if age of lastMeasurement is within threshold
status := CheckOk
thresh, err := time.ParseDuration(event.Threshold) thresh, err := time.ParseDuration(event.Threshold)
if err != nil { if err != nil {
return nil, err return nil, err
@ -93,16 +96,9 @@ func (box Box) RunChecks() ([]CheckResult, error) {
status = CheckErr status = CheckErr
} }
results = append(results, CheckResult{ value = s.LastMeasurement.Date.String()
Threshold: event.Threshold,
Event: event.Type,
Target: s.Id,
Value: s.LastMeasurement.Date.String(),
Status: status,
})
case eventMeasurementValMin, eventMeasurementValMax: case eventMeasurementValMin, eventMeasurementValMax:
status := CheckOk
thresh, err := strconv.ParseFloat(event.Threshold, 64) thresh, err := strconv.ParseFloat(event.Threshold, 64)
if err != nil { if err != nil {
return nil, err return nil, err
@ -116,40 +112,34 @@ func (box Box) RunChecks() ([]CheckResult, error) {
status = CheckErr status = CheckErr
} }
results = append(results, CheckResult{ value = s.LastMeasurement.Value
Threshold: event.Threshold,
Event: event.Type,
Target: s.Id,
Value: s.LastMeasurement.Value,
Status: status,
})
case eventMeasurementValSuspicious:
status := CheckOk
case eventMeasurementValFaulty:
val, err := strconv.ParseFloat(s.LastMeasurement.Value, 64) val, err := strconv.ParseFloat(s.LastMeasurement.Value, 64)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if suspiciousVals[SuspiciousValue{ if faultyVals[FaultyValue{
sensor: s.Type, sensor: s.Type,
val: val, val: val,
}] { }] {
status = CheckErr status = CheckErr
} }
results = append(results, CheckResult{ value = s.LastMeasurement.Value
Threshold: event.Threshold,
Event: event.Type,
Target: s.Id,
Value: s.LastMeasurement.Value,
Status: status,
})
} }
results = append(results, CheckResult{
Threshold: event.Threshold,
Event: event.Type,
Target: target,
Value: value,
Status: status,
})
} }
} }
} }
// must return ALL events to enable Notifier to clear previous notifications
return results, nil return results, nil
} }

@ -6,6 +6,8 @@ import (
"net/smtp" "net/smtp"
"strings" "strings"
"time" "time"
"github.com/spf13/viper"
) )
var notifiers = map[string]AbstractNotifier{ var notifiers = map[string]AbstractNotifier{
@ -23,21 +25,20 @@ type Notification struct {
subject string subject string
} }
// box config required for the EmailNotifier
type EmailNotifier struct { type EmailNotifier struct {
Recipients []string Recipients []string
FromAddress string
} }
func (n EmailNotifier) New(config interface{}) (AbstractNotifier, error) { func (n EmailNotifier) New(config interface{}) (AbstractNotifier, error) {
res, ok := config.(EmailNotifier) res, ok := config.(EmailNotifier)
if !ok || res.Recipients == nil || res.FromAddress == "" { if !ok || res.Recipients == nil {
return nil, errors.New("Invalid EmailNotifier options") return nil, errors.New("Invalid EmailNotifier options")
} }
return EmailNotifier{ return EmailNotifier{
Recipients: res.Recipients, Recipients: res.Recipients,
FromAddress: res.FromAddress,
}, nil }, nil
} }
@ -55,24 +56,22 @@ func (n EmailNotifier) ComposeNotification(box *Box, checks []CheckResult) Notif
} }
func (n EmailNotifier) Submit(notification Notification) error { func (n EmailNotifier) Submit(notification Notification) error {
// Set up authentication information. TODO: load from config
auth := smtp.PlainAuth( auth := smtp.PlainAuth(
"", "",
"USERNAME", viper.GetString("email.user"),
"PASSWORD", viper.GetString("email.pass"),
"SERVER", viper.GetString("email.host"),
) )
fromAddress := "EXAMPLE@EXAMPLE.COM" from := viper.GetString("email.from")
from := fmt.Sprintf("openSenseMap Notifier <%s>", fromAddress) body := fmt.Sprintf("From: openSenseMap Notifier <%s>\nSubject: %s\nContent-Type: text/plain; charset=\"utf-8\"\n\n%s", from, notification.subject, notification.body)
body := fmt.Sprintf("From: %s\nSubject: %s\nContent-Type: text/plain; charset=\"utf-8\"\n\n%s", from, notification.subject, notification.body)
// Connect to the server, authenticate, set the sender and recipient, // Connect to the server, authenticate, set the sender and recipient,
// and send the email all in one step. // and send the email all in one step.
err := smtp.SendMail( err := smtp.SendMail(
"smtp.gmx.de:25", fmt.Sprintf("%s:%s", viper.GetString("email.host"), viper.GetString("email.port")),
auth, auth,
fromAddress, from,
n.Recipients, n.Recipients,
[]byte(body), []byte(body),
) )

Loading…
Cancel
Save