...
 
Commits (7)
......@@ -3,3 +3,4 @@ script:
- go test -v
before_install:
- export TZ=Europe/Berlin
- sudo apt-get install -y zsh
......@@ -94,6 +94,22 @@ Basic operation:
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
--------
......
......@@ -53,6 +53,7 @@ type Config struct {
SchedFile string
MinPercSpace float64
MinGiBSpace int
Notify string
}
// WriteCache writes the global configuration to disk as a json file.
......@@ -167,6 +168,9 @@ func loadConfig() (*Config, error) {
flags.IntVar(&(config.MinGiBSpace),
"minGbSpace", 0,
"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 {
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 (
"errors"
"flag"
"fmt"
"io"
"log"
"os"
"os/signal"
......@@ -251,8 +252,9 @@ func subcmdList(cl clock) {
}
}
func mainExitCode() int {
logger = log.New(os.Stderr, "", log.Ldate|log.Ltime|log.Lshortfile)
func mainExitCode(logIO io.Writer) int {
logger = log.New(logIO, "", log.Ldate|log.Ltime|log.Lshortfile)
log.SetOutput(logIO)
var err error
if config, err = loadConfig(); err != nil || config == nil {
if err == flag.ErrHelp {
......@@ -284,5 +286,10 @@ func mainExitCode() int {
}
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 @@
package main
import (
"bufio"
"errors"
"fmt"
"io"
"log"
"os"
"os/exec"
......@@ -60,13 +62,27 @@ func createRsyncCommand(sn *snapshot, base *snapshot) *exec.Cmd {
// error channel the caller can receive a return status from.
func runRsyncCommand(cmd *exec.Cmd) (chan error, error) {
var err error
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
stdout, err := cmd.StdoutPipe()
if err != nil {
return nil, err
}
stderr, err := cmd.StderrPipe()
if err != nil {
return nil, err
}
debugf("starting rsync command")
err = cmd.Start()
if err != nil {
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)
go func() {
time.Sleep(time.Second)
......
......@@ -3,6 +3,8 @@
package main
import (
"os"
"path/filepath"
"reflect"
"testing"
"time"
......@@ -27,3 +29,40 @@ func TestCreateRsyncCommand(t *testing.T) {
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)
}
}