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 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 { dest = dest.Append("/" + src.Box() + "/" + src.Path()) for _, s := range cfg.box[dest.Box()].zfs.filesystems[dest.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()) for _, s := range b.zfs.filesystems[dest2.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 } } } } } } 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) 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 } } 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 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 "month", 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 "month", 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 { for _, dest := range a.destinations { dest := dest.Append("/" + src.Box() + "/" + src.Path()) if dest.Online() { if err := dest.Mkdir(); err != nil { log.WithFields(log.Fields{"app": a.name, "call": "Mkdir", "attr": dest, "error": err}).Errorf("") return err } if err := TransferZfs(src, dest); err != nil { log.WithFields(log.Fields{"app": a.name, "call": "TransferZfs", "src": src, "dest": dest, "error": err}).Errorf("") return err } } } } return nil }