Skip to content

Commit 05afb52

Browse files
merge from branch status-file
1 parent 58ecde0 commit 05afb52

File tree

10 files changed

+467
-30
lines changed

10 files changed

+467
-30
lines changed

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,5 @@
2424

2525
# log files
2626
/*.log
27+
28+
status.json

README.md

+36
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ For the rest of the documentation, I'll be mostly showing examples using the TOM
6464
* [Examples of scheduling commands under Linux](#examples-of-scheduling-commands-under-linux)
6565
* [Examples of scheduling commands under macOS](#examples-of-scheduling-commands-under-macos)
6666
* [Changing schedule\-permission from user to system, or system to user](#changing-schedule-permission-from-user-to-system-or-system-to-user)
67+
* [Status file for easy monitoring](#status-file-for-easy-monitoring)
6768
* [Configuration file reference](#configuration-file-reference)
6869
* [Appendix](#appendix)
6970
* [Using resticprofile and systemd](#using-resticprofile-and-systemd)
@@ -1004,6 +1005,40 @@ This order is important:
10041005
- now you can change your permission (`user` to `system`, or `system` to `user`)
10051006
- `schedule` your updated profile
10061007
1008+
## Status file for easy monitoring
1009+
1010+
If you need to escalate the result of your backup to a monitoring system, you can definitely use the `run-after` and `run-after-fail` scripting.
1011+
1012+
But sometimes we just need something simple that a monitoring system can regularly check. For that matter, resticprofile can generate a simple JSON file with the details of the latest backup/forget/check command. I have a Zabbix agent checking this file once a day, and you can hook up any monitoring system that can load a JSON file.
1013+
1014+
In your profile, you simply need to add a new parameter, which is the location of your status file
1015+
1016+
```toml
1017+
[my-backup]
1018+
status-file = "backup-status.json"
1019+
```
1020+
1021+
Here's an example of a generated file, where you can see that the last check failed, whereas the last backup succeeded:
1022+
1023+
```json
1024+
{
1025+
"profiles": {
1026+
"my-backup": {
1027+
"backup": {
1028+
"success": true,
1029+
"time": "2020-07-31T23:54:00.401556+01:00",
1030+
"error": ""
1031+
},
1032+
"check": {
1033+
"success": false,
1034+
"time": "2020-07-31T23:47:22.311848+01:00",
1035+
"error": "exit status 1"
1036+
}
1037+
}
1038+
}
1039+
}
1040+
```
1041+
10071042
## Configuration file reference
10081043

10091044
`[global]`
@@ -1034,6 +1069,7 @@ Flags used by resticprofile only
10341069
* **run-before**: string OR list of strings
10351070
* **run-after**: string OR list of strings
10361071
* **run-after-fail**: string OR list of strings
1072+
* **status-file**: string
10371073

10381074
Flags passed to the restic command line
10391075

config/profile.go

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ type Profile struct {
2222
RunBefore []string `mapstructure:"run-before"`
2323
RunAfter []string `mapstructure:"run-after"`
2424
RunAfterFail []string `mapstructure:"run-after-fail"`
25+
StatusFile string `mapstructure:"status-file"`
2526
Environment map[string]string `mapstructure:"env"`
2627
Backup *BackupSection `mapstructure:"backup"`
2728
Retention *RetentionSection `mapstructure:"retention"`

examples/dev.yaml

+2-1
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,9 @@ root:
7575
- dev
7676

7777
self:
78-
initialize: true
78+
initialize: false
7979
inherit: default
80+
status-file: status.json
8081
backup:
8182
source: ./
8283
schedule:

go.mod

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ require (
2121
github.com/rhysd/go-github-selfupdate v1.2.2
2222
github.com/rickb777/date v1.12.3
2323
github.com/smartystreets/assertions v1.0.0 // indirect
24+
github.com/spf13/afero v1.3.2
2425
github.com/spf13/cast v1.3.1 // indirect
2526
github.com/spf13/jwalterweatherman v1.1.0 // indirect
2627
github.com/spf13/pflag v1.0.5

main.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import (
2525

2626
// These fields are populated by the goreleaser build
2727
var (
28-
version = "0.9.0"
28+
version = "0.9.1-dev"
2929
commit = ""
3030
date = ""
3131
builtBy = ""

status/profile.go

+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package status
2+
3+
import "time"
4+
5+
// Profile status
6+
type Profile struct {
7+
Backup *CommandStatus `json:"backup,omitempty"`
8+
Retention *CommandStatus `json:"retention,omitempty"`
9+
Check *CommandStatus `json:"check,omitempty"`
10+
}
11+
12+
func newProfile() *Profile {
13+
return &Profile{}
14+
}
15+
16+
// CommandStatus is the last command status
17+
type CommandStatus struct {
18+
Success bool `json:"success"`
19+
Time time.Time `json:"time"`
20+
Error string `json:"error"`
21+
}
22+
23+
// BackupSuccess indicates the last backup was successful
24+
func (p *Profile) BackupSuccess() *Profile {
25+
p.Backup = newSuccess()
26+
return p
27+
}
28+
29+
// BackupError sets the error of the last backup
30+
func (p *Profile) BackupError(err error) *Profile {
31+
p.Backup = newError(err)
32+
return p
33+
}
34+
35+
// RetentionSuccess indicates the last retention was successful
36+
func (p *Profile) RetentionSuccess() *Profile {
37+
p.Retention = newSuccess()
38+
return p
39+
}
40+
41+
// RetentionError sets the error of the last retention
42+
func (p *Profile) RetentionError(err error) *Profile {
43+
p.Retention = newError(err)
44+
return p
45+
}
46+
47+
// CheckSuccess indicates the last check was successful
48+
func (p *Profile) CheckSuccess() *Profile {
49+
p.Check = newSuccess()
50+
return p
51+
}
52+
53+
// CheckError sets the error of the last check
54+
func (p *Profile) CheckError(err error) *Profile {
55+
p.Check = newError(err)
56+
return p
57+
}
58+
59+
func newSuccess() *CommandStatus {
60+
return &CommandStatus{
61+
Success: true,
62+
Time: time.Now(),
63+
}
64+
}
65+
66+
func newError(err error) *CommandStatus {
67+
return &CommandStatus{
68+
Success: false,
69+
Time: time.Now(),
70+
Error: err.Error(),
71+
}
72+
}

status/status.go

+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package status
2+
3+
import (
4+
"encoding/json"
5+
"os"
6+
7+
"github.com/spf13/afero"
8+
)
9+
10+
// Status of last schedule profile
11+
type Status struct {
12+
fs afero.Fs
13+
filename string
14+
Profiles map[string]*Profile `json:"profiles"`
15+
}
16+
17+
// NewStatus returns a new blank status
18+
func NewStatus(fileName string) *Status {
19+
return &Status{
20+
fs: afero.NewOsFs(),
21+
filename: fileName,
22+
Profiles: make(map[string]*Profile),
23+
}
24+
}
25+
26+
// newAferoStatus returns a new blank status for unit test
27+
func newAferoStatus(fs afero.Fs, fileName string) *Status {
28+
return &Status{
29+
fs: fs,
30+
filename: fileName,
31+
Profiles: make(map[string]*Profile),
32+
}
33+
}
34+
35+
// Load existing status; does not complain if the file does not exists, or is not readable
36+
func (s *Status) Load() *Status {
37+
// we're not bothered if the status cannot be loaded
38+
file, err := s.fs.Open(s.filename)
39+
if err != nil {
40+
return s
41+
}
42+
defer file.Close()
43+
decoder := json.NewDecoder(file)
44+
_ = decoder.Decode(s)
45+
return s
46+
}
47+
48+
// Profile gets the profile from its name (it creates a blank new one if not exists)
49+
func (s *Status) Profile(name string) *Profile {
50+
if profile, ok := s.Profiles[name]; ok {
51+
return profile
52+
}
53+
profile := newProfile()
54+
s.Profiles[name] = profile
55+
return profile
56+
}
57+
58+
// Save current status to the file
59+
func (s *Status) Save() error {
60+
file, err := s.fs.OpenFile(s.filename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
61+
if err != nil {
62+
return err
63+
}
64+
defer file.Close()
65+
encoder := json.NewEncoder(file)
66+
err = encoder.Encode(s)
67+
if err != nil {
68+
return err
69+
}
70+
return nil
71+
}

0 commit comments

Comments
 (0)