backup/app.go

510 lines
15 KiB
Go

package main
import (
"errors"
"regexp"
"strings"
"time"
log "github.com/sirupsen/logrus"
)
type App struct {
name string
schedule map[string]struct{}
sources []Addr
destinations []Addr
before map[string]Addr
after map[string]Addr
}
func (c *Config) NewApp(name string, sources, destinations, schedule []string, before, after map[string]string) (*App, error) {
log.WithFields(log.Fields{"name": name}).Debugf("starting")
defer log.WithFields(log.Fields{"name": name}).Debugf("done")
a := &App{
name: name,
sources: make([]Addr, 0),
destinations: make([]Addr, 0),
schedule: make(map[string]struct{}, 0),
before: make(map[string]Addr),
after: make(map[string]Addr),
}
for _, v := range sources {
src := Addr(v)
if src.Box() == "" {
err := errors.New("source box incorrect")
log.WithFields(log.Fields{"app": name, "addr": v, "error": err}).Errorf("")
return nil, err
}
if _, ok := c.box[src.Box()]; !ok {
err := errors.New("source box doesn't exist")
log.WithFields(log.Fields{"app": name, "addr": v, "error": err}).Errorf("")
return nil, err
}
if src.Path() == "" {
err := errors.New("source path incorrect")
log.WithFields(log.Fields{"app": name, "addr": v, "error": err}).Errorf("")
return nil, err
}
a.sources = append(a.sources, src)
}
for _, v := range destinations {
dest := Addr(v)
if dest.Box() == "" {
err := errors.New("destination box incorrect")
log.WithFields(log.Fields{"app": name, "addr": v, "error": err}).Errorf("")
return nil, err
}
if _, ok := c.box[dest.Box()]; !ok {
err := errors.New("destination box doesn't exist")
log.WithFields(log.Fields{"app": name, "addr": v, "error": err}).Errorf("")
return nil, err
}
if dest.Path() == "" {
err := errors.New("destination path incorrect")
log.WithFields(log.Fields{"app": name, "addr": v, "error": err}).Errorf("")
return nil, err
}
a.destinations = append(a.destinations, dest)
}
for _, v := range schedule {
switch strings.ToLower(v) {
case "hourly":
a.schedule["hourly"] = struct{}{}
case "daily":
a.schedule["daily"] = struct{}{}
case "weekly":
a.schedule["weekly"] = struct{}{}
case "monthly":
a.schedule["monthly"] = struct{}{}
case "yearly":
a.schedule["yearly"] = struct{}{}
default:
err := errors.New("schedule incorrect")
log.WithFields(log.Fields{"app": name, "schedule": v, "error": err}).Errorf("")
return nil, err
}
}
for k, v := range before {
if _, err := regexp.Compile(k); err != nil {
log.WithFields(log.Fields{"app": name, "schedule": k, "addr": v, "call": "regexp.Compile", "attr": k, "error": err}).Errorf("")
return nil, err
}
script := Addr(v)
if script.Box() == "" {
err := errors.New("before box incorrect")
log.WithFields(log.Fields{"app": name, "schedule": k, "addr": v, "error": err}).Errorf("")
return nil, err
}
if script.Path() == "" {
err := errors.New("before path incorrect")
log.WithFields(log.Fields{"app": name, "schedule": k, "addr": v, "error": err}).Errorf("")
return nil, err
}
if _, ok := a.before[k]; ok {
err := errors.New("before already exists")
log.WithFields(log.Fields{"app": name, "schedule": k, "addr": v, "error": err}).Errorf("")
return nil, err
}
a.before[k] = script
}
for k, v := range after {
if _, err := regexp.Compile(k); err != nil {
log.WithFields(log.Fields{"app": name, "schedule": k, "addr": v, "call": "regexp.Compile", "attr": k, "error": err}).Errorf("")
return nil, err
}
script := Addr(v)
if script.Box() == "" {
err := errors.New("after box incorrect")
log.WithFields(log.Fields{"app": name, "schedule": k, "addr": v, "error": err}).Errorf("")
return nil, err
}
if script.Path() == "" {
err := errors.New("after path incorrect")
log.WithFields(log.Fields{"app": name, "schedule": k, "addr": v, "error": err}).Errorf("")
return nil, err
}
if _, ok := a.before[k]; ok {
err := errors.New("after already exists")
log.WithFields(log.Fields{"app": name, "schedule": k, "addr": v, "error": err}).Errorf("")
return nil, err
}
a.after[k] = script
}
return a, nil
}
func (a *App) Cleanup(now time.Time) error {
log.WithFields(log.Fields{"app": a.name}).Debugf("starting")
defer log.WithFields(log.Fields{"app": a.name}).Debugf("done")
for _, src := range a.sources {
for _, s := range cfg.box[src.Box()].zfs.filesystems[src.Path()].snapshots {
if !s.Valid() {
if err := s.Delete(); err != nil {
log.WithFields(log.Fields{"app": a.name, "now": now, "box": src.Box(), "snapshot": s.String(), "call": "Delete", "error": err}).Errorf("")
return err
}
} else if expired, err := s.Expired(now); err != nil {
log.WithFields(log.Fields{"app": a.name, "now": now, "box": src.Box(), "snapshot": s.String(), "call": "Expired", "error": err}).Errorf("")
return err
} else if expired {
if err := s.Delete(); err != nil {
log.WithFields(log.Fields{"app": a.name, "now": now, "box": src.Box(), "snapshot": s.String(), "call": "Delete", "error": err}).Errorf("")
return err
}
}
}
for _, dest := range a.destinations {
if cfg.box[dest.Box()].online {
dest2 := dest.Append("/" + src.Box() + "/" + src.Path())
for _, s := range cfg.box[dest2.Box()].zfs.filesystems[dest2.Path()].snapshots {
if !s.Valid() {
if err := s.Delete(); err != nil {
log.WithFields(log.Fields{"app": a.name, "now": now, "box": src.Box(), "snapshot": s.String(), "call": "Delete", "error": err}).Errorf("")
return err
}
} else if expired, err := s.Expired(now); err != nil {
log.WithFields(log.Fields{"app": a.name, "now": now, "box": dest.Box(), "snapshot": s.String(), "call": "Expired", "error": err}).Errorf("")
return err
} else if expired {
if err := s.Delete(); err != nil {
log.WithFields(log.Fields{"app": a.name, "now": now, "box": src.Box(), "snapshot": s.String(), "call": "Delete", "error": err}).Errorf("")
return err
}
}
}
}
}
}
return nil
}
func (a *App) SanityCheck() error {
log.WithFields(log.Fields{"app": a.name}).Debugf("starting")
defer log.WithFields(log.Fields{"app": a.name}).Debugf("done")
for _, src := range a.sources {
b := cfg.box[src.Box()]
if !b.online {
err := errors.New("source box offline")
log.WithFields(log.Fields{"app": a.name, "box": src.Box(), "error": err}).Errorf("")
return err
}
if _, ok := b.zfs.filesystems[src.Path()]; !ok {
err := errors.New("source path doesn't exist")
log.WithFields(log.Fields{"app": a.name, "box": src.Box(), "path": src.Path(), "error": err}).Errorf("")
return err
}
for _, s := range b.zfs.filesystems[src.Path()].snapshots {
if !s.Valid() {
if err := s.Delete(); err != nil {
log.WithFields(log.Fields{"app": a.name, "box": src.Box(), "snapshot": s.String(), "call": "Delete", "error": err}).Errorf("")
return err
}
}
}
}
onlineDestinations := 0
for _, dest := range a.destinations {
b := cfg.box[dest.Box()]
if b.online {
onlineDestinations++
for _, src := range a.sources {
dest2 := dest.Append("/" + src.Box() + "/" + src.Path())
if fs, ok := b.zfs.filesystems[dest2.Path()]; ok {
for _, s := range fs.snapshots {
if !s.Valid() {
if err := s.Delete(); err != nil {
log.WithFields(log.Fields{"app": a.name, "box": src.Box(), "snapshot": s.String(), "call": "Delete", "error": err}).Errorf("")
return err
}
}
}
}
}
}
}
if onlineDestinations == 0 {
err := errors.New("no destination box online")
log.WithFields(log.Fields{"app": a.name, "error": err}).Errorf("")
return err
}
return nil
}
func (a *App) RunSchedule(schedule string, now time.Time) error {
log.WithFields(log.Fields{"app": a.name, "schedule": schedule, "now": now}).Debugf("starting")
defer log.WithFields(log.Fields{"app": a.name, "schedule": schedule, "now": now}).Debugf("done")
snapshotName := SnapshotName(schedule, now)
log.WithFields(log.Fields{"app": a.name, "schedule": schedule, "now": now, "snapshot": snapshotName}).Debugf("snapshot name")
if err := a.RunBefore(schedule); err != nil {
log.WithFields(log.Fields{"app": a.name, "schedule": schedule, "now": now, "call": "RunBefore", "attr": schedule, "error": err}).Errorf("")
}
for _, src := range a.sources {
srcFs := cfg.box[src.Box()].zfs.filesystems[src.Path()]
if _, err := srcFs.TakeSnapshot(snapshotName); err != nil {
log.WithFields(log.Fields{"app": a.name, "schedule": schedule, "now": now, "call": "TakeSnapshot", "attr": snapshotName, "error": err}).Errorf("")
}
}
if err := a.RunAfter(schedule); err != nil {
log.WithFields(log.Fields{"app": a.name, "schedule": schedule, "now": now, "call": "RunAfter", "attr": schedule, "error": err}).Errorf("")
}
return nil
}
func (a *App) Run(now time.Time) (string, error) {
log.WithFields(log.Fields{"app": a.name, "now": now}).Debugf("starting")
defer log.WithFields(log.Fields{"app": a.name, "now": now}).Debugf("done")
if err := a.SanityCheck(); err != nil {
log.WithFields(log.Fields{"app": a.name, "now": now, "call": "SanityCheck", "error": err}).Errorf("")
return "", err
}
schedule, err := a.NextSchedule(now)
if err != nil {
log.WithFields(log.Fields{"app": a.name, "call": "NextSchedule", "error": err}).Errorf("")
return "", err
}
log.WithFields(log.Fields{"app": a.name, "now": now, "schedule": schedule}).Debugf("schedule")
if schedule != "" {
if err := a.RunSchedule(schedule, now); err != nil {
log.WithFields(log.Fields{"app": a.name, "call": "NextSchedule", "error": err}).Errorf("")
return "", err
}
}
for _, src := range a.sources {
if err := src.SetManaged(true); err != nil {
log.WithFields(log.Fields{"app": a.name, "call": "src.SetManaged", "error": err}).Errorf("")
return "", err
}
}
if err := a.Transfer(); err != nil {
log.WithFields(log.Fields{"app": a.name, "call": "Transfer", "error": err}).Errorf("")
return "", err
}
if err := a.Cleanup(now); err != nil {
log.WithFields(log.Fields{"app": a.name, "call": "Cleanup", "error": err}).Errorf("")
return "", err
}
return schedule, nil
}
func (a *App) NextSchedule(now time.Time) (string, error) {
log.WithFields(log.Fields{"app": a.name, "now": now}).Debugf("starting")
defer log.WithFields(log.Fields{"app": a.name, "now": now}).Debugf("done")
// get a list of all the common timestamps in sources snapshots
snapshots := make(map[string]map[time.Time]int)
for _, v := range a.sources {
log.WithFields(log.Fields{"app": a.name, "now": now, "box": v.Box(), "path": v.Path()}).Debugf("source")
b := cfg.box[v.Box()] // we tested the boxes
fs, ok := b.zfs.filesystems[v.Path()]
if !ok {
err := errors.New("path doesn't exist")
log.WithFields(log.Fields{"app": a.name, "now": now, "box": v.Box(), "path": v.Path(), "error": err}).Errorf("")
return "", err
}
log.WithFields(log.Fields{"app": a.name, "now": now, "box": v.Box(), "path": v.Path()}).Debugf("%d snapshots", len(fs.snapshots))
for _, v2 := range fs.snapshots {
if s, err := v2.Schedule(); err == nil {
snapshots2, ok := snapshots[s]
if !ok {
snapshots2 = make(map[time.Time]int)
}
if t, err := v2.Timestamp(); err == nil {
if t.After(now) {
err := errors.New("snapshot in the future")
log.WithFields(log.Fields{"app": a.name, "now": now, "source": v, "timestamp": t, "error": err}).Errorf("")
return "", err
}
if count, ok := snapshots2[t]; ok {
snapshots2[t] = count + 1
} else {
snapshots2[t] = 1
}
}
snapshots[s] = snapshots2
}
}
}
t := time.Unix(0, 0)
if _, ok := a.schedule["yearly"]; ok {
if s, ok := snapshots["yearly"]; ok {
for k, v := range s {
if k.After(t) && v == len(a.sources) {
t = k
}
}
}
if t.Year() < now.Year() {
return "yearly", nil
}
}
if _, ok := a.schedule["monthly"]; ok {
if s, ok := snapshots["monthly"]; ok {
for k, v := range s {
if k.After(t) && v == len(a.sources) {
t = k
}
}
}
if t.Year() < now.Year() || t.Month() != now.Month() {
return "monthly", nil
}
}
if _, ok := a.schedule["weekly"]; ok {
if s, ok := snapshots["weekly"]; ok {
for k, v := range s {
if k.After(t) && v == len(a.sources) {
t = k
}
}
}
ny, nw := now.ISOWeek()
ty, tw := t.ISOWeek()
if ty < ny || tw < nw {
return "weekly", nil
}
}
if _, ok := a.schedule["daily"]; ok {
if s, ok := snapshots["daily"]; ok {
for k, v := range s {
if k.After(t) && v == len(a.sources) {
t = k
}
}
}
if t.Year() < now.Year() || t.Month() != now.Month() || t.Day() < now.Day() {
return "daily", nil
}
}
if _, ok := a.schedule["hourly"]; ok {
if s, ok := snapshots["hourly"]; ok {
for k, v := range s {
if k.After(t) && v == len(a.sources) {
t = k
}
}
}
if t.Year() < now.Year() || t.Month() != now.Month() || t.Day() < now.Day() || t.Hour() < now.Hour() {
return "hourly", nil
}
}
return "", nil
}
func (a *App) RunBefore(schedule string) error {
log.WithFields(log.Fields{"app": a.name, "schedule": schedule}).Debugf("starting")
defer log.WithFields(log.Fields{"app": a.name, "schedule": schedule}).Debugf("done")
for k, v := range a.before {
re := regexp.MustCompile(k)
if re.MatchString(schedule) {
if _, err := v.Exec(); err != nil {
log.WithFields(log.Fields{"app": a.name, "schedule": schedule, "regex": k, "call": "Exec", "error": err}).Errorf("")
return err
}
}
}
return nil
}
func (a *App) RunAfter(schedule string) error {
log.WithFields(log.Fields{"app": a.name, "schedule": schedule}).Debugf("starting")
defer log.WithFields(log.Fields{"app": a.name, "schedule": schedule}).Debugf("done")
for k, v := range a.after {
re := regexp.MustCompile(k)
if re.MatchString(schedule) {
if _, err := v.Exec(); err != nil {
log.WithFields(log.Fields{"app": a.name, "schedule": schedule, "regex": k, "call": "Exec", "error": err}).Errorf("")
return err
}
}
}
return nil
}
func (a *App) Transfer() error {
log.WithFields(log.Fields{"app": a.name}).Debugf("starting")
defer log.WithFields(log.Fields{"app": a.name}).Debugf("done")
for _, src := range a.sources {
dests := make([]Addr, 0)
for _, dest := range a.destinations {
dest2 := dest.Append("/" + src.Box() + "/" + src.Path())
if dest2.Online() {
if err := dest2.Mkdir(); err != nil {
log.WithFields(log.Fields{"app": a.name, "call": "Mkdir", "attr": dest, "error": err}).Errorf("")
return err
}
if err := dest2.SetManaged(true); err != nil {
log.WithFields(log.Fields{"app": a.name, "call": "SetManaged", "src": src, "dest": dest, "error": err}).Errorf("")
return err
}
dests = append(dests, dest2)
}
}
if n, err := TransferZfs(src, dests); err != nil {
log.WithFields(log.Fields{"app": a.name, "call": "TransferZfs", "src": src, "dests": dests, "error": err}).Errorf("")
return err
} else if n > 0 {
if err := src.SetBackedUp(true); err != nil {
log.WithFields(log.Fields{"app": a.name, "call": "SetBackedUp", "src": src, "error": err}).Errorf("")
return err
}
}
}
return nil
}