diff --git a/config.example.yaml b/config.example.yaml index a3032ce..87a7bba 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -166,7 +166,6 @@ notifications: # username: duplicity-backup # emoji: :package: # mondash: -# board: yourboardurl +# board: https://mondash.org/yourboardurl # token: yoursecrettoken -# metric: duplicity backup (%h) -# instance: https://mondash.org/ +# freshness: 3600 diff --git a/configfile.go b/configfile.go index d3f64a4..90b1602 100644 --- a/configfile.go +++ b/configfile.go @@ -58,10 +58,9 @@ type configFile struct { Emoji string `yaml:"emoji"` } `yaml:"slack"` MonDash struct { - Board string `yaml:"board"` - Token string `yaml:"token"` - Metric string `yaml:"metric"` - Instance string `yaml:"instance"` + BoardURL string `yaml:"board"` + Token string `yaml:"token"` + Freshness int64 `yaml:"freshness"` } `yaml:"mondash"` } `yaml:"notifications"` } diff --git a/main.go b/main.go index a1c6eb2..1e58e5c 100644 --- a/main.go +++ b/main.go @@ -114,6 +114,11 @@ func main() { defer lock.Unlock() if err := execute(config, argv[1:]); err != nil { + if err := config.Notify(false, fmt.Errorf("Could not create backup: %s", err)); err != nil { + logf("[ERR] Error sending notifications: %s", err) + } else { + logf("[INF] Notifications sent") + } return } @@ -121,24 +126,41 @@ func main() { logf("++++ Starting removal of old backups") if err := execute(config, []string{"__remove_old"}); err != nil { + if err := config.Notify(false, fmt.Errorf("Could not cleanup backup: %s", err)); err != nil { + logf("[ERR] Error sending notifications: %s", err) + } else { + logf("[INF] Notifications sent") + } + return } } + if err := config.Notify(true, nil); err != nil { + logf("[ERR] Error sending notifications: %s", err) + } else { + logf("[INF] Notifications sent") + } + logf("++++ Backup finished successfully") } func execute(config *configFile, argv []string) error { var ( - err error - commandLine, env []string + err error + commandLine, tmpEnv []string ) - commandLine, env, err = config.GenerateCommand(argv, cfg.RestoreTime) + commandLine, tmpEnv, err = config.GenerateCommand(argv, cfg.RestoreTime) if err != nil { logf("[ERR] %s", err) return err } + env := envListToMap(os.Environ()) + for k, v := range envListToMap(tmpEnv) { + env[k] = v + } + // Ensure duplicity is talking to us commandLine = append([]string{"-v3"}, commandLine...) @@ -154,7 +176,7 @@ func execute(config *configFile, argv []string) error { cmd := exec.Command(duplicityBinary, commandLine...) cmd.Stdout = output cmd.Stderr = output - cmd.Env = env + cmd.Env = envMapToList(env) err = cmd.Run() logf("%s", output.String()) @@ -166,3 +188,24 @@ func execute(config *configFile, argv []string) error { return err } + +func envListToMap(list []string) map[string]string { + out := map[string]string{} + for _, entry := range list { + if len(entry) == 0 || entry[0] == '#' { + continue + } + + parts := strings.SplitN(entry, "=", 2) + out[parts[0]] = parts[1] + } + return out +} + +func envMapToList(envMap map[string]string) []string { + out := []string{} + for k, v := range envMap { + out = append(out, k+"="+v) + } + return out +} diff --git a/notification.go b/notification.go new file mode 100644 index 0000000..b8c7afa --- /dev/null +++ b/notification.go @@ -0,0 +1,132 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" +) + +func (c *configFile) Notify(success bool, err error) error { + errs := []error{} + + for _, n := range []func(bool, error) error{ + c.notifyMonDash, + c.notifySlack, + } { + if e := n(success, err); e != nil { + errs = append(errs, e) + } + } + + if len(errs) == 0 { + return nil + } else { + estr := "" + for _, e := range errs { + if e == nil { + continue + } + + estr = fmt.Sprintf("%s\n- %s", estr, e) + } + return fmt.Errorf("%d notifiers failed:%s", len(errs), estr) + } +} + +type mondashResult struct { + Title string `json:"title"` + Description string `json:"description"` + Status string `json:"status"` + Freshness int64 `json:"freshness"` + IgnoreMAD bool `json:"ignore_mad"` + HideMAD bool `json:"hide_mad"` +} + +func (c *configFile) notifyMonDash(success bool, err error) error { + if c.Notifications.MonDash.BoardURL == "" { + return nil + } + + monitoringResult := mondashResult{ + Title: fmt.Sprintf("duplicity-backup on %s", c.Hostname), + Freshness: c.Notifications.MonDash.Freshness, + IgnoreMAD: true, + HideMAD: true, + } + + if success { + monitoringResult.Status = "OK" + monitoringResult.Description = "Backup succeeded" + } else { + monitoringResult.Status = "Critical" + monitoringResult.Description = fmt.Sprintf("Backup failed: %s", err) + } + + buf := bytes.NewBuffer([]byte{}) + if err := json.NewEncoder(buf).Encode(monitoringResult); err != nil { + return err + } + + url := fmt.Sprintf("%s/duplicity-%s", + c.Notifications.MonDash.BoardURL, + c.Hostname, + ) + + req, _ := http.NewRequest(http.MethodPut, url, buf) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", c.Notifications.MonDash.Token) + res, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + defer res.Body.Close() + + if res.StatusCode != 200 { + return fmt.Errorf("Received unexpected status code: %d", res.StatusCode) + } + + return nil +} + +type slackResult struct { + Username string `json:"username,omitempty"` + Channel string `json:"channel,omitempty"` + Icon string `json:"icon_emoji,omitempty"` + Text string `json:"text"` +} + +func (c *configFile) notifySlack(success bool, err error) error { + if c.Notifications.Slack.HookURL == "" { + return nil + } + + text := "Backup succeeded" + if !success { + text = fmt.Sprintf("Backup failed: %s", err) + } + + sr := slackResult{ + Username: c.Notifications.Slack.Username, + Channel: c.Notifications.Slack.Channel, + Icon: c.Notifications.Slack.Emoji, + Text: text, + } + + buf := bytes.NewBuffer([]byte{}) + if err := json.NewEncoder(buf).Encode(sr); err != nil { + return err + } + + res, err := http.Post(c.Notifications.Slack.HookURL, "application/json", buf) + if err != nil { + return err + } + defer res.Body.Close() + + if res.StatusCode != 200 { + return fmt.Errorf("Received unexpected status code: %d", res.StatusCode) + } + + return nil +}