...
 
Commits (7)
...@@ -3,3 +3,4 @@ script: ...@@ -3,3 +3,4 @@ script:
- go test -v - go test -v
before_install: before_install:
- export TZ=Europe/Berlin - export TZ=Europe/Berlin
- sudo apt-get install -y zsh
...@@ -94,6 +94,22 @@ Basic operation: ...@@ -94,6 +94,22 @@ Basic operation:
2016-09-14 Wednesday 20:32:29 (1s, 10m0s) 2016-09-14 Wednesday 20:32:29 (1s, 10m0s)
``` ```
E-Mail Notification
-------------------
If you add the `-notify` option to the run sub-command you will get an email
in case of a problem. Use it like this:
```
> snaprd run -notify root <other options...>
```
If snaprd has a severe problem it will stop execution and send an email to the
specified address, along with the last few lines of log output.
Sending happens through use of the standard mail(1) command, make sure your
system is configured accordingly.
Stopping Stopping
-------- --------
......
...@@ -53,6 +53,7 @@ type Config struct { ...@@ -53,6 +53,7 @@ type Config struct {
SchedFile string SchedFile string
MinPercSpace float64 MinPercSpace float64
MinGiBSpace int MinGiBSpace int
Notify string
} }
// WriteCache writes the global configuration to disk as a json file. // WriteCache writes the global configuration to disk as a json file.
...@@ -167,6 +168,9 @@ func loadConfig() (*Config, error) { ...@@ -167,6 +168,9 @@ func loadConfig() (*Config, error) {
flags.IntVar(&(config.MinGiBSpace), flags.IntVar(&(config.MinGiBSpace),
"minGbSpace", 0, "minGbSpace", 0,
"if set, keep at least x GiB of the snapshots filesystem free") "if set, keep at least x GiB of the snapshots filesystem free")
flags.StringVar(&(config.Notify),
"notify", "",
"specify an email address to send reports")
if err := flags.Parse(os.Args[2:]); err != nil { if err := flags.Parse(os.Args[2:]); err != nil {
return nil, err return nil, err
......
package main
import (
"fmt"
"io/ioutil"
"log"
"os/exec"
)
func FailureMail(exitCode int, logBuffer *RingIO) {
mail := fmt.Sprintf("snaprd exited with return value %d.\nLatest log output:\n\n%s",
exitCode, logBuffer.GetAsText())
subject := fmt.Sprintf("snaprd failure (origin: %s)", config.Origin)
SendMail(config.Notify, subject, mail)
}
func NotifyMail(to, msg string) {
SendMail(to, "snaprd notice", msg)
}
func SendMail(to, subject, msg string) {
sendmail := exec.Command("mail", "-s", subject, to)
stdin, err := sendmail.StdinPipe()
if err != nil {
log.Println(err)
return
}
stdout, err := sendmail.StdoutPipe()
if err != nil {
log.Println(err)
return
}
sendmail.Start()
stdin.Write([]byte(msg))
stdin.Write([]byte("\n"))
stdin.Close()
ioutil.ReadAll(stdout)
sendmail.Wait()
log.Printf("sending notification to %s done\n", to)
}
...@@ -9,6 +9,7 @@ import ( ...@@ -9,6 +9,7 @@ import (
"errors" "errors"
"flag" "flag"
"fmt" "fmt"
"io"
"log" "log"
"os" "os"
"os/signal" "os/signal"
...@@ -251,8 +252,9 @@ func subcmdList(cl clock) { ...@@ -251,8 +252,9 @@ func subcmdList(cl clock) {
} }
} }
func mainExitCode() int { func mainExitCode(logIO io.Writer) int {
logger = log.New(os.Stderr, "", log.Ldate|log.Ltime|log.Lshortfile) logger = log.New(logIO, "", log.Ldate|log.Ltime|log.Lshortfile)
log.SetOutput(logIO)
var err error var err error
if config, err = loadConfig(); err != nil || config == nil { if config, err = loadConfig(); err != nil || config == nil {
if err == flag.ErrHelp { if err == flag.ErrHelp {
...@@ -284,5 +286,10 @@ func mainExitCode() int { ...@@ -284,5 +286,10 @@ func mainExitCode() int {
} }
func main() { func main() {
os.Exit(mainExitCode()) rio := newRingIO(os.Stderr, 25, 100)
exitCode := mainExitCode(rio)
if exitCode != 0 && config.Notify != "" {
FailureMail(exitCode, rio)
}
os.Exit(exitCode)
} }
/* See the file "LICENSE.txt" for the full license governing this code. */
package main
import (
"bytes"
"io"
"sync"
)
type RingIO struct {
out io.Writer // the io we are proxying
maxLen int // max number of lines
maxElem int // max length of a line
mu *sync.Mutex
buf map[int][]byte // a map holding the lines
p int // points to the current item in the map
}
// newRingIO instantiates a new RingIO list, which satisfies the io.Writer
// out is an io.Writer that will write the output to the final destination.
// maxLen will be the maximum number of elements kept in the ring buffer. If
// this number is reached, for each Write() the first element will be removed
// before the new element is added.
// maxElem is the maximum size in bytes of an individual element of the list.
func newRingIO(out io.Writer, maxLen int, maxElem int) *RingIO {
return &RingIO{
out: out,
maxLen: maxLen,
maxElem: maxElem,
mu: new(sync.Mutex),
buf: make(map[int][]byte),
p: 0,
}
}
func (r *RingIO) Write(s []byte) (int, error) {
r.mu.Lock()
defer r.mu.Unlock()
var e []byte
// we need to copy the slice because the caller may be reusing it
c := make([]byte, len(s))
copy(c, s)
// write to the io.Writer we are proxying
r.out.Write(c)
ls := len(c)
// if needed, truncate the new entry to maxElem bytes and append a newline
if ls > r.maxElem {
e = append(c[0:r.maxElem], byte('\n'))
} else {
e = c
}
r.buf[r.p] = e
// reset the pointer if maxLen is reached
if r.p < r.maxLen-1 {
r.p += 1
} else {
r.p = 0
}
return len(c), nil
}
// GetAll returns all elements of the ring buffer as a slice of byte slices
func (r *RingIO) GetAll() [][]byte {
r.mu.Lock()
defer r.mu.Unlock()
var ret [][]byte
// return buf, but starting from where the pointer currently points to
for i := r.p; i < r.maxLen; i += 1 {
ret = append(ret, r.buf[i])
}
for i := 0; i < r.p; i += 1 {
ret = append(ret, r.buf[i])
}
return ret
}
// GetAsText concatenates all buffered lines into one byte slice
func (r *RingIO) GetAsText() []byte {
var b bytes.Buffer
for _, l := range r.GetAll() {
b.Write(l)
}
return b.Bytes()
}
/* See the file "LICENSE.txt" for the full license governing this code. */
package main
import (
"bytes"
"reflect"
"testing"
)
type rioTestPair struct {
params [2]int
in [][]byte
out []byte
}
func TestRingIO(t *testing.T) {
tests := []rioTestPair{
{
[2]int{3, 12},
[][]byte{
[]byte("a string\n"),
[]byte("another string\n"),
[]byte("something\n"),
[]byte("else\n"),
},
[]byte("another stri\nsomething\nelse\n"),
},
{
[2]int{2, 10},
[][]byte{
[]byte("a string\n"),
[]byte("another string\n"),
[]byte("something\n"),
[]byte("else\n"),
},
[]byte("something\nelse\n"),
},
{
[2]int{2, 4},
[][]byte{
[]byte("a string"),
[]byte("test1"),
[]byte("test2"),
},
[]byte("test\ntest\n"),
},
{
[2]int{100, 100},
[][]byte{
[]byte("a string"),
[]byte("test1"),
[]byte("test2"),
},
[]byte("a stringtest1test2"),
},
}
var buf bytes.Buffer
for _, tp := range tests {
rio := newRingIO(&buf, tp.params[0], tp.params[1])
for _, l := range tp.in {
rio.Write(l)
}
got := rio.GetAsText()
wanted := tp.out
if !reflect.DeepEqual(got, wanted) {
t.Errorf("wanted:\n>>>\n%s\n<<<\ngot:\n>>>\n%s\n<<<", wanted, got)
}
}
}
...@@ -5,8 +5,10 @@ ...@@ -5,8 +5,10 @@
package main package main
import ( import (
"bufio"
"errors" "errors"
"fmt" "fmt"
"io"
"log" "log"
"os" "os"
"os/exec" "os/exec"
...@@ -60,13 +62,27 @@ func createRsyncCommand(sn *snapshot, base *snapshot) *exec.Cmd { ...@@ -60,13 +62,27 @@ func createRsyncCommand(sn *snapshot, base *snapshot) *exec.Cmd {
// error channel the caller can receive a return status from. // error channel the caller can receive a return status from.
func runRsyncCommand(cmd *exec.Cmd) (chan error, error) { func runRsyncCommand(cmd *exec.Cmd) (chan error, error) {
var err error var err error
cmd.Stdout = os.Stdout stdout, err := cmd.StdoutPipe()
cmd.Stderr = os.Stderr if err != nil {
return nil, err
}
stderr, err := cmd.StderrPipe()
if err != nil {
return nil, err
}
debugf("starting rsync command") debugf("starting rsync command")
err = cmd.Start() err = cmd.Start()
if err != nil { if err != nil {
return nil, err return nil, err
} }
multi := io.MultiReader(stdout, stderr)
in := bufio.NewScanner(multi)
for in.Scan() {
log.Printf("(rsync) %s", in.Text())
}
if err := in.Err(); err != nil {
log.Printf("error scanning rsync output: %s", err)
}
done := make(chan error) done := make(chan error)
go func() { go func() {
time.Sleep(time.Second) time.Sleep(time.Second)
......
...@@ -3,6 +3,8 @@ ...@@ -3,6 +3,8 @@
package main package main
import ( import (
"os"
"path/filepath"
"reflect" "reflect"
"testing" "testing"
"time" "time"
...@@ -27,3 +29,40 @@ func TestCreateRsyncCommand(t *testing.T) { ...@@ -27,3 +29,40 @@ func TestCreateRsyncCommand(t *testing.T) {
t.Errorf("wanted %v, got %v", wanted, got) t.Errorf("wanted %v, got %v", wanted, got)
} }
} }
func TestFakeRsyncOk(t *testing.T) {
var testSnapshots = snapshotList{
{time.Unix(1400337531, 0), time.Unix(1400338693, 0), stateComplete},
{time.Unix(1400534523, 0), time.Unix(0, 0), stateIncomplete},
}
var config = config
config.repository = "/tmp/snaprd_dest"
mockRepository()
config.ReadCache()
dir, _ := os.Getwd()
config.RsyncPath = filepath.Join(dir, "fake_rsync")
config.RsyncOpts.Set("--fake_exit=24")
_, err := createSnapshot(testSnapshots[0])
got := err
if got != nil {
t.Errorf("createSnapshot() returned an error, but it shouldn't: %v", got)
}
}
func TestFakeRsyncFail(t *testing.T) {
var testSnapshots = snapshotList{
{time.Unix(1400337531, 0), time.Unix(1400338693, 0), stateComplete},
{time.Unix(1400534523, 0), time.Unix(0, 0), stateIncomplete},
}
var config = config
config.repository = "/tmp/snaprd_dest"
config.ReadCache()
dir, _ := os.Getwd()
config.RsyncPath = filepath.Join(dir, "fake_rsync")
config.RsyncOpts.Set("--fake_exit=3")
_, err := createSnapshot(testSnapshots[0])
got := err
if got == nil {
t.Errorf("createSnapshot() succeded, but it should have failed: %v", got)
}
}