commit c15d4128e3cb316c40dd1bf97a300c3993e06ba2 Author: Thomas Preisner Date: Mon Feb 21 00:12:06 2022 +0100 Initial commit diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..9b97c34 --- /dev/null +++ b/go.mod @@ -0,0 +1,11 @@ +module check_restic + +go 1.17 + +require github.com/pkg/sftp v1.13.4 + +require ( + github.com/kr/fs v0.1.0 // indirect + golang.org/x/crypto v0.0.0-20220214200702-86341886e292 // indirect + golang.org/x/sys v0.0.0-20220209214540-3681064d5158 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..6917ed2 --- /dev/null +++ b/go.sum @@ -0,0 +1,21 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/pkg/sftp v1.13.4 h1:Lb0RYJCmgUcBgZosfoi9Y9sbl6+LJgOIgk/2Y4YjMFg= +github.com/pkg/sftp v1.13.4/go.mod h1:LzqnAvaD5TWeNBsZpfKxSYn1MbjWwOsCIAFFJbpIsK8= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20220214200702-86341886e292 h1:f+lwQ+GtmgoY+A2YaQxlSOnDjXcQ7ZRLWOHbC6HtRqE= +golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220209214540-3681064d5158 h1:rm+CHSpPEEW2IsXUib1ThaHIjuBVZjxNgSKmBLFfD4c= +golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..f5f4ad2 --- /dev/null +++ b/main.go @@ -0,0 +1,137 @@ +package main + +import ( + "flag" + "fmt" + "os" + "os/exec" + "sort" + "time" + + "github.com/pkg/sftp" +) + +const ( + OK = 0 + WARNING = 1 + CRITICAL = 2 + UNKNOWN = 3 +) + +var ( + warning = flag.Duration("warning", -1, "return WARNING if the lastest snapshot is older than the specified number of hours") + critical = flag.Duration("critical", -1, "return CRITICAL if the lastest snapshot is older than the specified number of hours") + repoPath = flag.String("repository", "", "path to restic repository on sftp target") + sftpHost = flag.String("host", "", "ssh host to be used for sftp connection") + sftpUser = flag.String("user", "", "ssh user to be used for sftp connection") + sftpPort = flag.String("port", "22", "ssh port to be used for sftp connection") +) + +func parseArgs() error { + flag.Parse() + if *warning < 0 { + return fmt.Errorf("The option 'warning' needs to be set and greater than 0.") + } + if *critical < 0 { + return fmt.Errorf("The option 'critical' needs to be set and greater than 0.") + } + if *repoPath == "" { + return fmt.Errorf("The option 'repository' needs to be set.") + } + if *sftpHost == "" { + return fmt.Errorf("The option 'host' needs to be set.") + } + if *sftpUser == "" { + return fmt.Errorf("The option 'user' needs to be set.") + } + if *sftpPort == "" { + return fmt.Errorf("The option 'port' needs to be a valid port.") + } + return nil +} + +func getStatusStr(status int) string { + switch status { + case OK: + return "OK" + case WARNING: + return "WARNING" + case CRITICAL: + return "CRITICAL" + default: + return "UNKNOWN" + } +} + +func main() { + rc, msg := mainReturnWithStatus() + fmt.Printf("%s: %s\n", getStatusStr(rc), msg) + os.Exit(rc) +} + +func mainReturnWithStatus() (int, string) { + err := parseArgs() + if err != nil { + return UNKNOWN, err.Error() + } + + // Connect to a remote host and request the sftp subsystem via the 'ssh' + // command. This assumes that passwordless login is correctly configured. + cmd := exec.Command("ssh", *sftpHost, "-l", *sftpUser, "-p", *sftpPort, "-s", "sftp") + + // send errors from ssh to stderr + cmd.Stderr = os.Stderr + + // get stdin and stdout + wr, err := cmd.StdinPipe() + if err != nil { + return UNKNOWN, err.Error() + } + rd, err := cmd.StdoutPipe() + if err != nil { + return UNKNOWN, err.Error() + } + + // start the process + if err := cmd.Start(); err != nil { + return UNKNOWN, err.Error() + } + defer cmd.Wait() + + // open the SFTP session + client, err := sftp.NewClientPipe(rd, wr) + if err != nil { + return UNKNOWN, err.Error() + } + defer client.Close() + + // get a list of all snapshots in the restic repository + files, err := client.ReadDir(*repoPath + "/snapshots") + if err != nil { + return UNKNOWN, err.Error() + } + + if len(files) == 0 { + return CRITICAL, "no snapshots found" + } + + // sort snapshots by modtime + sort.Slice(files, func(a, b int) bool { + return files[b].ModTime().Before(files[a].ModTime()) + }) + + age := time.Now().Sub(files[0].ModTime()) + + // sanity check + if age < 0 { + return CRITICAL, "latest snapshot is in the future" + } + msg := fmt.Sprintf("latest snapshot created %s ago", age.Round(time.Second)) + if age > *critical { + return CRITICAL, msg + } else if age > *warning { + return WARNING, msg + } else { + return OK, msg + } +}