diff --git a/app.go b/app.go new file mode 100644 index 0000000..e476a4d --- /dev/null +++ b/app.go @@ -0,0 +1,576 @@ +package main + +import ( + "fmt" + "log" + "regexp" + "time" +) + +type AppConfig struct { + Name string `json:"name"` + Schedule []string `json:"schedule"` + Sources []Location `json:"src"` + Destinations []Location `json:"dest"` + Before map[string]Location `json:"before"` + After map[string]Location `json:"after"` +} + +func (a AppConfig) getTime() time.Time { + for _, v := range a.Sources { + return cfg.Box[v.Box()].ssh.now + } + for _, v := range a.Destinations { + return cfg.Box[v.Box()].ssh.now + } + return time.Now() +} + +func (a AppConfig) getSchedule() (string, error) { + var schedule string + if *debugFlag { + log.Printf("AppConfig.getSchedule : %s : Start", a.Name) + } + + refreshSnapshot := make(map[string]bool) + for _, v := range a.Sources { + refreshSnapshot[v.Box()] = true + } + for k, _ := range refreshSnapshot { + err := cfg.Box[k].ssh.getSnapshotList() + if err != nil { + if *debugFlag { + log.Printf("AppConfig.getSchedule : %s : getSnapshotList(%s) : %s", a.Name, k, err) + } + return "", err + } + } + + if *schedFlag != "" { + schedule = *schedFlag + } else if a.needYearlySnapshot() { + schedule = "yearly" + } else if a.needMonthlySnapshot() { + schedule = "monthly" + } else if a.needWeeklySnapshot() { + schedule = "weekly" + } else if a.needDailySnapshot() { + schedule = "daily" + } else if a.needHourlySnapshot() { + schedule = "hourly" + } else { + return schedule, nil + } + + if ret, ok := cfg.Zfsnap[schedule]; !ok { + return "", fmt.Errorf("no retention for %s", schedule) + } else { + re := regexp.MustCompile(`^([0-9]+[ymwdhMs]{1}|forever)$`) + if !re.MatchString(ret) { + return "", fmt.Errorf("wrong retention format for %s", schedule) + } + } + return schedule, nil +} + +func (a AppConfig) needYearlySnapshot() bool { + if *debugFlag { + log.Printf("AppConfig.needYearlySnapshot : %s : Start", a.Name) + } + ret := false + + // schedule enabled for app ? + for _, v := range a.Schedule { + if v == "yearly" { + ret = true + } + } + if !ret { + return false + } + + // finding out the timestamps existing + timeSource := make(map[string]map[time.Time]struct{}) + timeTotal := make(map[time.Time]struct{}) + re := regexp.MustCompile(`^yearly-(?P[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{2}.[0-9]{2}.[0-9]{2})--([0-9]+[ymwdhMs]{1}|forever)$`) + for _, src := range a.Sources { + timeSource[string(src)] = make(map[time.Time]struct{}) + for _, snap := range cfg.Box[src.Box()].ssh.snapshot { + if src.Path() == snap.Path() { + if re.MatchString(snap.Name()) { + dateString := re.ReplaceAllString(snap.Name(), "${Date}") + dateTime, err := time.Parse("2006-01-02_15.04.05", dateString) + if err != nil { + if *debugFlag { + log.Printf("AppConfig.needYearlySnapshot : %s : time.Parse(%s) : %s", a.Name, dateString, err) + } + } else { + timeSource[string(src)][dateTime] = struct{}{} + timeTotal[dateTime] = struct{}{} + } + } + } + } + } + + // cleaning up the available timestamps for common timestamps + for t, _ := range timeTotal { + for _, v := range timeSource { + if _, ok := v[t]; !ok { + delete(timeTotal, t) + } + } + } + + // finding an eligible timestamp + now := a.getTime() + for t, _ := range timeTotal { + if t.Year() == now.Year() { + return false + } + } + + // no timestamp => need the snapshot ! + + return true +} + +func (a AppConfig) needMonthlySnapshot() bool { + if *debugFlag { + log.Printf("AppConfig.needMonthlySnapshot : %s : Start", a.Name) + } + ret := false + + // schedule enabled for app ? + for _, v := range a.Schedule { + if v == "monthly" { + ret = true + } + } + if !ret { + return false + } + + // finding out the timestamps existing + timeSource := make(map[string]map[time.Time]struct{}) + timeTotal := make(map[time.Time]struct{}) + re := regexp.MustCompile(`^(yearly|monthly)-(?P[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{2}.[0-9]{2}.[0-9]{2})--([0-9]+[ymwdhMs]{1}|forever)$`) + for _, src := range a.Sources { + timeSource[string(src)] = make(map[time.Time]struct{}) + for _, snap := range cfg.Box[src.Box()].ssh.snapshot { + if src.Path() == snap.Path() { + if re.MatchString(snap.Name()) { + dateString := re.ReplaceAllString(snap.Name(), "${Date}") + dateTime, err := time.Parse("2006-01-02_15.04.05", dateString) + if err != nil { + if *debugFlag { + log.Printf("AppConfig.needYearlySnapshot : %s : time.Parse(%s) : %s", a.Name, dateString, err) + } + } else { + timeSource[string(src)][dateTime] = struct{}{} + timeTotal[dateTime] = struct{}{} + } + } + } + } + } + + // cleaning up the available timestamps for common timestamps + for t, _ := range timeTotal { + for _, v := range timeSource { + if _, ok := v[t]; !ok { + delete(timeTotal, t) + } + } + } + + // finding an eligible timestamp + now := a.getTime() + for t, _ := range timeTotal { + if t.Year() == now.Year() && t.Month() == now.Month() { + return false + } + } + + // no timestamp => need the snapshot ! + + return true +} + +func (a AppConfig) needWeeklySnapshot() bool { + if *debugFlag { + log.Printf("AppConfig.needWeeklySnapshot : %s : Start", a.Name) + } + ret := false + + // schedule enabled for app ? + for _, v := range a.Schedule { + if v == "weekly" { + ret = true + } + } + if !ret { + return false + } + + // finding out the timestamps existing + timeSource := make(map[string]map[time.Time]struct{}) + timeTotal := make(map[time.Time]struct{}) + re := regexp.MustCompile(`^(yearly|monthly|weekly)-(?P[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{2}.[0-9]{2}.[0-9]{2})--([0-9]+[ymwdhMs]{1}|forever)$`) + for _, src := range a.Sources { + timeSource[string(src)] = make(map[time.Time]struct{}) + for _, snap := range cfg.Box[src.Box()].ssh.snapshot { + if src.Path() == snap.Path() { + if re.MatchString(snap.Name()) { + dateString := re.ReplaceAllString(snap.Name(), "${Date}") + dateTime, err := time.Parse("2006-01-02_15.04.05", dateString) + if err != nil { + if *debugFlag { + log.Printf("AppConfig.needYearlySnapshot : %s : time.Parse(%s) : %s", a.Name, dateString, err) + } + } else { + timeSource[string(src)][dateTime] = struct{}{} + timeTotal[dateTime] = struct{}{} + } + } + } + } + } + + // cleaning up the available timestamps for common timestamps + for t, _ := range timeTotal { + for _, v := range timeSource { + if _, ok := v[t]; !ok { + delete(timeTotal, t) + } + } + } + + // finding an eligible timestamp + now := a.getTime() + nowYear, nowWeek := now.ISOWeek() + for t, _ := range timeTotal { + snapYear, snapWeek := t.ISOWeek() + if nowYear == snapYear && nowWeek == snapWeek { + return false + } + } + + // no timestamp => need the snapshot ! + + return true +} + +func (a AppConfig) needDailySnapshot() bool { + if *debugFlag { + log.Printf("AppConfig.needDailySnapshot : %s : Start", a.Name) + } + ret := false + + // schedule enabled for app ? + for _, v := range a.Schedule { + if v == "daily" { + ret = true + } + } + if !ret { + return false + } + + // finding out the timestamps existing + timeSource := make(map[string]map[time.Time]struct{}) + timeTotal := make(map[time.Time]struct{}) + re := regexp.MustCompile(`^(yearly|monthly|weekly|daily)-(?P[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{2}.[0-9]{2}.[0-9]{2})--([0-9]+[ymwdhMs]{1}|forever)$`) + for _, src := range a.Sources { + timeSource[string(src)] = make(map[time.Time]struct{}) + for _, snap := range cfg.Box[src.Box()].ssh.snapshot { + if src.Path() == snap.Path() { + if re.MatchString(snap.Name()) { + dateString := re.ReplaceAllString(snap.Name(), "${Date}") + dateTime, err := time.Parse("2006-01-02_15.04.05", dateString) + if err != nil { + if *debugFlag { + log.Printf("AppConfig.needYearlySnapshot : %s : time.Parse(%s) : %s", a.Name, dateString, err) + } + } else { + timeSource[string(src)][dateTime] = struct{}{} + timeTotal[dateTime] = struct{}{} + } + } + } + } + } + + // cleaning up the available timestamps for common timestamps + for t, _ := range timeTotal { + for _, v := range timeSource { + if _, ok := v[t]; !ok { + delete(timeTotal, t) + } + } + } + + // finding an eligible timestamp + now := a.getTime() + for t, _ := range timeTotal { + if t.Year() == now.Year() && t.Month() == now.Month() && t.Day() == now.Day() { + return false + } + } + + // no timestamp => need the snapshot ! + + return true +} + +func (a AppConfig) needHourlySnapshot() bool { + if *debugFlag { + log.Printf("AppConfig.needHourlySnapshot : %s : Start", a.Name) + } + ret := false + + // schedule enabled for app ? + for _, v := range a.Schedule { + if v == "hourly" { + ret = true + } + } + if !ret { + return false + } + + // finding out the timestamps existing + timeSource := make(map[string]map[time.Time]struct{}) + timeTotal := make(map[time.Time]struct{}) + re := regexp.MustCompile(`^(yearly|monthly|weekly|daily|hourly)-(?P[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{2}.[0-9]{2}.[0-9]{2})--([0-9]+[ymwdhMs]{1}|forever)$`) + for _, src := range a.Sources { + timeSource[string(src)] = make(map[time.Time]struct{}) + for _, snap := range cfg.Box[src.Box()].ssh.snapshot { + if src.Path() == snap.Path() { + if re.MatchString(snap.Name()) { + dateString := re.ReplaceAllString(snap.Name(), "${Date}") + dateTime, err := time.Parse("2006-01-02_15.04.05", dateString) + if err != nil { + if *debugFlag { + log.Printf("AppConfig.needYearlySnapshot : %s : time.Parse(%s) : %s", a.Name, dateString, err) + } + } else { + timeSource[string(src)][dateTime] = struct{}{} + timeTotal[dateTime] = struct{}{} + } + } + } + } + } + + // cleaning up the available timestamps for common timestamps + for t, _ := range timeTotal { + for _, v := range timeSource { + if _, ok := v[t]; !ok { + delete(timeTotal, t) + } + } + } + + // finding an eligible timestamp + now := a.getTime() + for t, _ := range timeTotal { + if t.Year() == now.Year() && t.Month() == now.Month() && t.Day() == now.Day() && t.Hour() == now.Hour() { + return false + } + } + + // no timestamp => need the snapshot ! + + return true +} + +func (a AppConfig) RunAppBackup() error { + if *debugFlag { + log.Printf("AppConfig.RunAppBackup : %s : Start", a.Name) + } + schedule, err := a.getSchedule() + if err != nil { + if *debugFlag { + log.Printf("AppConfig.RunAppBackup : %s : Error getting schedule : %s", a.Name, err) + } + return err + } + if schedule == "" { + return nil + } + + for _, src := range a.Sources { + if !cfg.Box[src.Box()].ssh.isZFS(src.Path()) { + return fmt.Errorf("No path %s on source", string(src)) + } + for _, dest := range a.Destinations { + if !cfg.Box[dest.Box()].ssh.isZFS(dest.Path() + "/" + src.Box() + "/" + src.Path()) { + err := cfg.Box[dest.Box()].ssh.createZFS(dest.Path() + "/" + src.Box() + "/" + src.Path()) + if err != nil { + if *debugFlag { + log.Printf("AppConfig.RunAppBackup : %s : Error creating %s on %s", a.Name, dest.Path()+"/"+src.Box()+"/"+src.Path(), dest.Box()) + } + return err + } + } + } + } + for k, v := range a.Before { + re := regexp.MustCompile(k) + if re.MatchString(schedule) { + err := cfg.Box[v.Box()].ssh.exec(v.Path()) + if err != nil { + if *debugFlag { + log.Printf("AppConfig.RunAppBackup : %s : Error executing %s", a.Name, string(v)) + } + return err + } + } + } + + refreshSnapshot := make(map[string]bool) + takeSnapshot := make(map[string]string) + delSnapshot := make(map[string]string) + for _, v := range a.Sources { + takeSnapshot[v.Box()] = takeSnapshot[v.Box()] + " " + v.Path() + refreshSnapshot[v.Box()] = true + for _, v2 := range a.Destinations { + delSnapshot[v2.Box()] = delSnapshot[v2.Box()] + " " + v2.Path() + "/" + v.Box() + "/" + v.Path() + } + } + for k, v := range takeSnapshot { + if *debugFlag { + log.Printf("AppConfig.RunAppBackup : %s : taking snapshot on %s for %s", a.Name, k, v) + } + err := cfg.Box[k].ssh.exec("/usr/sbin/zfsnap snapshot -p " + schedule + "- -a " + cfg.Zfsnap[schedule] + v) + if err != nil { + if *debugFlag { + log.Printf("AppConfig.RunAppBackup : %s : Error executing zfsnap on %s", a.Name, k) + } + return err + } + } + for _, v := range a.Destinations { + refreshSnapshot[v.Box()] = true + } + for k, _ := range refreshSnapshot { + if *debugFlag { + log.Printf("AppConfig.RunAppBackup : %s : refreshing snapshots for source %s", a.Name, k) + } + err := cfg.Box[k].ssh.getSnapshotList() + if err != nil { + if *debugFlag { + log.Printf("AppConfig.RunAppBackup : %s : Error getting snapshots on %s", a.Name, k) + } + return err + } + } + for _, src := range a.Sources { + for _, dest := range a.Destinations { + if *debugFlag { + log.Printf("AppConfig.RunAppBackup : %s : Sending snapshots from %s to %s", a.Name, string(src), string(dest)) + } + dLastSnapshot, err := cfg.Box[dest.Box()].ssh.getLastSnapshot(dest.Path() + "/" + src.Box() + "/" + src.Path()) + if err != nil { + if *debugFlag { + log.Printf("AppConfig.RunAppBackup : %s : No snapshot for %s on %s", a.Name, string(src), dest.Box()) + } + sFirstSnapshot, err := cfg.Box[src.Box()].ssh.getFirstSnapshot(src.Path()) + if err != nil { + if *debugFlag { + log.Printf("AppConfig.RunAppBackup : %s : No snapshot for %s", a.Name, string(src)) + } + return err + } + if *debugFlag { + log.Printf("AppConfig.RunAppBackup : %s : Initializing snapshot on %s from %s", a.Name, dest.Box(), string(sFirstSnapshot)) + } + err = cfg.Box[dest.Box()].ssh.exec("/usr/bin/ssh root@" + src.Box() + " /sbin/zfs send " + string(sFirstSnapshot) + " | /sbin/zfs recv -F " + dest.Path() + "/" + src.Box() + "/" + src.Path()) + if err != nil { + if *debugFlag { + log.Printf("AppConfig.RunAppBackup : %s : Initializing snapshot on %s from %s failed (%s)", a.Name, dest.Box(), string(sFirstSnapshot), err) + } + return err + } + var sCurrSnapshot Snapshot + sNextSnapshot := sFirstSnapshot + for !cfg.Box[src.Box()].ssh.isLastSnapshot(sNextSnapshot) { + sCurrSnapshot = sNextSnapshot + sNextSnapshot, err = cfg.Box[src.Box()].ssh.getNextSnapshot(sNextSnapshot) + if *debugFlag { + log.Printf("AppConfig.RunAppBackup : %s : Sending incrementally %s to %s", a.Name, string(sNextSnapshot), dest.Box()) + } + err = cfg.Box[dest.Box()].ssh.exec("/usr/bin/ssh root@" + src.Box() + " /sbin/zfs send -I " + string(sCurrSnapshot) + " " + string(sNextSnapshot) + " | /sbin/zfs recv " + dest.Path() + "/" + src.Box() + "/" + src.Path()) + if err != nil { + if *debugFlag { + log.Printf("AppConfig.RunAppBackup : %s : Sending snapshot on %s from %s failed (%s)", a.Name, dest.Box(), string(sNextSnapshot), err) + } + return err + } + } + if *debugFlag { + log.Printf("AppConfig.RunAppBackup : %s : All snapshots sent for %s", a.Name, string(src)) + } + + } else { + if *debugFlag { + log.Printf("AppConfig.RunAppBackup : %s : Last snapshot on %s is %s", a.Name, dest.Box(), string(dLastSnapshot)) + } + var sCurrSnapshot Snapshot + sNextSnapshot := Snapshot(string(dLastSnapshot)[len(string(dest))+2:]) + for !cfg.Box[src.Box()].ssh.isLastSnapshot(sNextSnapshot) { + sCurrSnapshot = sNextSnapshot + sNextSnapshot, err = cfg.Box[src.Box()].ssh.getNextSnapshot(sNextSnapshot) + if *debugFlag { + log.Printf("AppConfig.RunAppBackup : %s : Sending incrementally %s to %s", a.Name, string(sNextSnapshot), dest.Box()) + } + err = cfg.Box[dest.Box()].ssh.exec("/usr/bin/ssh root@" + src.Box() + " /sbin/zfs send -I " + string(sCurrSnapshot) + " " + string(sNextSnapshot) + " | /sbin/zfs recv " + dest.Path() + "/" + src.Box() + "/" + src.Path()) + if err != nil { + if *debugFlag { + log.Printf("AppConfig.RunAppBackup : %s : Sending snapshot on %s from %s failed (%s)", a.Name, dest.Box(), string(sNextSnapshot), err) + } + return err + } + } + } + } + } + for k, v := range takeSnapshot { + if *debugFlag { + log.Printf("AppConfig.RunAppBackup : %s : cleaning snapshot on %s for %s", a.Name, k, v) + } + err := cfg.Box[k].ssh.exec("/usr/sbin/zfsnap destroy -p hourly- -p daily- -p weekly- -p monthly- -p yearly- " + v) + if err != nil { + if *debugFlag { + log.Printf("AppConfig.RunAppBackup : %s : Error executing zfsnap on %s", a.Name, k) + } + return err + } + } + for k, v := range delSnapshot { + if *debugFlag { + log.Printf("AppConfig.RunAppBackup : %s : cleaning snapshot on %s for %s", a.Name, k, v) + } + err := cfg.Box[k].ssh.exec("/usr/sbin/zfsnap destroy -p hourly- -p daily- -p weekly- -p monthly- -p yearly- " + v) + if err != nil { + if *debugFlag { + log.Printf("AppConfig.RunAppBackup : %s : Error executing zfsnap on %s", a.Name, k) + } + return err + } + } + for k, v := range a.After { + re := regexp.MustCompile(k) + if re.MatchString(schedule) { + err := cfg.Box[v.Box()].ssh.exec(v.Path()) + if err != nil { + if *debugFlag { + log.Printf("AppConfig.RunAppBackup : %s : Error executing %s on %s", a.Name, v.Path(), v.Box()) + } + return err + } + } + } + return nil +} diff --git a/backup.go b/backup.go index d6584c3..1acae0c 100644 --- a/backup.go +++ b/backup.go @@ -2,7 +2,6 @@ package main import ( "bytes" - "encoding/csv" "encoding/json" "flag" "fmt" @@ -10,46 +9,22 @@ import ( "log" "os" "regexp" - "strings" - "time" "golang.org/x/crypto/ssh" ) type Config struct { - Zfsnap map[string]string `json:"zfsnap"` - Box map[string]BoxConfig `json:"box"` - Apps []AppConfig `json:apps` - ssh map[string]*SSHConfig -} - -type Location string - -type Snapshot string - -type AppConfig struct { - Name string `json:"name"` - Schedule []string `json:"schedule"` - Sources []Location `json:"src"` - Destinations []Location `json:"dest"` - Before map[string]Location `json:"before"` - After map[string]Location `json:"after"` -} - -type SSHConfig struct { - signer ssh.Signer - config *ssh.ClientConfig - client *ssh.Client - logged bool - name string - zfs map[string]string - snapshot []Snapshot + Zfsnap map[string]string `json:"zfsnap"` + Box map[string]*BoxConfig `json:"box"` + Apps []AppConfig `json:apps` + Timezone string `json:"timezone"` } type BoxConfig struct { Addr string `json:"addr"` User string `json:"user"` Key string `json:"key"` + ssh *SSHConfig } var ( @@ -60,38 +35,11 @@ var ( cfg Config ) -func (l Location) Box() string { - s := strings.Split(string(l), `:`) - return s[0] -} - -func (l Location) Path() string { - s := strings.Split(string(l), `:`) - return s[1] -} - -func (l Location) Valid() bool { - s := strings.Split(string(l), `:`) - return len(s) == 2 -} - -func (s Snapshot) Path() string { - s2 := strings.Split(string(s), `@`) - return s2[0] -} - -func (s Snapshot) Name() string { - s2 := strings.Split(string(s), `@`) - return s2[1] -} - -func (s Snapshot) Append(path string) Snapshot { - s2 := strings.Split(string(s), `@`) - return Snapshot(s2[0] + "/" + path + "@" + s2[1]) -} - //Load config from file func (c *Config) Load() error { + if *debugFlag { + log.Printf("SSHConfig.Load : Start") + } b, err := ioutil.ReadFile(*cfgFile) if err != nil { if *debugFlag { @@ -108,13 +56,12 @@ func (c *Config) Load() error { return err } - cfg.ssh = make(map[string]*SSHConfig) for k, v := range c.Box { s := &SSHConfig{ logged: false, name: k, } - cfg.ssh[k] = s + v.ssh = s keyRaw, err := ioutil.ReadFile(v.Key) if err != nil { if *debugFlag { @@ -165,7 +112,8 @@ func (c *Config) Load() error { var b bytes.Buffer session.Stdout = &b - err = session.Run("/usr/sbin/zfsnap --version") + + err = session.Run("TZ=\"" + cfg.Timezone + "\" /usr/sbin/zfsnap --version") if err != nil { if *debugFlag { log.Printf("Config.Load : client.NewSession(%s) : %s", k, err) @@ -177,7 +125,16 @@ func (c *Config) Load() error { } session.Close() s.logged = true + } + for _, box := range c.Box { + err = box.ssh.getTime() + if err != nil { + if *debugFlag { + log.Printf("Config.Load : ssh.getTime() : %s", err) + } + return err + } } for _, app := range c.Apps { @@ -185,7 +142,7 @@ func (c *Config) Load() error { if !src.Valid() { return fmt.Errorf("Source not valid : %s", string(src)) } - if _, ok := cfg.ssh[src.Box()]; !ok { + if _, ok := cfg.Box[src.Box()]; !ok { return fmt.Errorf("No box defined for source : %s", string(src)) } } @@ -193,7 +150,7 @@ func (c *Config) Load() error { if !dest.Valid() { return fmt.Errorf("Destination not valid : %s", string(dest)) } - if _, ok := cfg.ssh[dest.Box()]; !ok { + if _, ok := cfg.Box[dest.Box()]; !ok { return fmt.Errorf("No box defined for destination : %s", string(dest)) } } @@ -208,7 +165,7 @@ func (c *Config) Load() error { if !before.Valid() { return fmt.Errorf("Before not valid : %s", string(before)) } - if _, ok := cfg.ssh[before.Box()]; !ok { + if _, ok := cfg.Box[before.Box()]; !ok { return fmt.Errorf("No box defined for before : %s", string(before)) } } @@ -223,7 +180,7 @@ func (c *Config) Load() error { if !after.Valid() { return fmt.Errorf("After not valid : %s", string(after)) } - if _, ok := cfg.ssh[after.Box()]; !ok { + if _, ok := cfg.Box[after.Box()]; !ok { return fmt.Errorf("No box defined for after : %s", string(after)) } } @@ -237,449 +194,6 @@ func (c *Config) Close() error { return nil } -func (s *SSHConfig) getLastSnapshot(path string) (Snapshot, error) { - if *debugFlag { - log.Printf("SSHConfig.getLastSnapshot : Start %s:%s (%d snapshots)", s.name, path, len(s.snapshot)) - } - var last Snapshot - for _, v := range s.snapshot { - if v.Path() == path { - last = v - } else { - if len(string(last)) > 0 { - return last, nil - } - } - } - if len(string(last)) > 0 { - return last, nil - } - return last, fmt.Errorf("no snapshot") -} - -func (s *SSHConfig) isLastSnapshot(snapshot Snapshot) bool { - if *debugFlag { - log.Printf("SSHConfig.isLastSnapshot : Start %s:%s", s.name, string(snapshot)) - } - _, err := s.getNextSnapshot(snapshot) - if err != nil { - return true - } else { - return false - } -} - -func (s *SSHConfig) getFirstSnapshot(path string) (Snapshot, error) { - if *debugFlag { - log.Printf("SSHConfig.getFirstSnapshot : Start %s:%s", s.name, path) - } - var first Snapshot - for _, v := range s.snapshot { - if v.Path() == path { - first = v - return first, nil - } - } - return first, fmt.Errorf("no snapshot") -} - -func (s *SSHConfig) getNextSnapshot(snapshot Snapshot) (Snapshot, error) { - if *debugFlag { - log.Printf("SSHConfig.getNextSnapshot : Start %s:%s", s.name, string(snapshot)) - } - var next Snapshot - for id, v := range s.snapshot { - if v == snapshot { - if len(s.snapshot) > id+1 { - next = s.snapshot[id+1] - if next.Path() == snapshot.Path() { - return next, nil - } else { - return next, fmt.Errorf("no snapshot") - } - } else { - return next, fmt.Errorf("no snapshot") - } - } - } - return next, fmt.Errorf("no snapshot") -} - -func (s *SSHConfig) getSnapshotList() error { - if *debugFlag { - log.Printf("SSHConfig.getSnapshotList : %s : Start", s.name) - } - if !s.logged { - return fmt.Errorf("Client %s not logged in.", s.name) - } - - session, err := s.client.NewSession() - if err != nil { - if *debugFlag { - log.Printf("SSHConfig.getSnapshotList : %s : client.NewSession() : %s", s.name, err) - } - return err - } - - var b bytes.Buffer - session.Stdout = &b - err = session.Run("/usr/sbin/zfs list -H -t snapshot -o name") - if err != nil { - if *debugFlag { - log.Printf("SSHConfig.getSnapshotList : %s : session.Run() : %s", s.name, err) - } - return err - } - - s.snapshot = make([]Snapshot, 0) - - csvReader := csv.NewReader(&b) - csvReader.Comma = '\t' - csvReader.FieldsPerRecord = 1 - - csvData, err := csvReader.ReadAll() - if err != nil { - if *debugFlag { - log.Printf("SSHConfig.getSnapshotList : %s : csvReader.ReadAll() : %s", s.name, err) - } - return err - } - - for _, rec := range csvData { - s.snapshot = append(s.snapshot, Snapshot(rec[0])) - } - - if *debugFlag { - log.Printf("SSHConfig.getSnapshotList : %s : read %d zfs snapshots", s.name, len(s.snapshot)) - } - - session.Close() - - return nil -} - -func (s *SSHConfig) getZFSList() error { - if *debugFlag { - log.Printf("SSHConfig.getZFSList : Start %s", s.name) - } - if !s.logged { - return fmt.Errorf("Client %s not logged in.", s.name) - } - - session, err := s.client.NewSession() - if err != nil { - if *debugFlag { - log.Printf("SSHConfig.getZFSList : client.NewSession(%s) : %s", s.name, err) - } - return err - } - - var b bytes.Buffer - session.Stdout = &b - err = session.Run("/sbin/zfs list -H -o name,mountpoint") - if err != nil { - if *debugFlag { - log.Printf("SSHConfig.getZFSList : session.Run(%s) : %s", s.name, err) - } - return err - } - - s.zfs = make(map[string]string) - - csvReader := csv.NewReader(&b) - csvReader.Comma = '\t' - csvReader.FieldsPerRecord = 2 - - csvData, err := csvReader.ReadAll() - if err != nil { - if *debugFlag { - log.Printf("SSHConfig.getZFSList : csvReader.ReadAll(%s) : %s", s.name, err) - } - return err - } - - for _, rec := range csvData { - s.zfs[rec[0]] = rec[1] - } - - if *debugFlag { - log.Printf("SSHConfig.getZFSList : %s : read %d zfs file systems", s.name, len(s.zfs)) - } - - session.Close() - - return nil - -} - -func (s *SSHConfig) isZFS(path string) bool { - if *debugFlag { - log.Printf("SSHConfig.isZFS : Start %s:%s", s.name, path) - } - if len(s.zfs) == 0 { - err := s.getZFSList() - if err != nil { - if *debugFlag { - log.Printf("SSHConfig.isZFS : s.getZFSList(%s) : %s", s.name, err) - } - return false - } - } - _, ok := s.zfs[path] - if ok { - return true - } - return false -} - -func (s *SSHConfig) exec(cmd string) error { - if *debugFlag { - log.Printf("SSHConfig.exec : Start %s on %s", cmd, s.name) - } - - session, err := s.client.NewSession() - if err != nil { - if *debugFlag { - log.Printf("SSHConfig.exec : client(%s).NewSession(%s) : %s", s.name, cmd, err) - } - return err - } - - err = session.Run(cmd) - if err != nil { - if *debugFlag { - log.Printf("SSHConfig.exec : session(%s).Run(%s) : %s", s.name, cmd, err) - } - return err - } - - session.Close() - - return nil -} - -func (s *SSHConfig) createZFS(path string) error { - if *debugFlag { - log.Printf("SSHConfig.createZFS : Start %s:%s", s.name, path) - } - if len(s.zfs) == 0 { - err := s.getZFSList() - if err != nil { - if *debugFlag { - log.Printf("SSHConfig.createZFS : s.getZFSList(%s) : %s", s.name, err) - } - return err - } - } - p := strings.Split(path, `/`) - var base string - for _, d := range p { - if base == "" { - base = d - } else { - base = base + `/` + d - } - if _, ok := s.zfs[base]; !ok { - - if *debugFlag { - log.Printf("SSHConfig.createZFS : Creating %s:%s", s.name, base) - } - - err := s.exec("/sbin/zfs create -o mountpoint=none " + base) - if err != nil { - if *debugFlag { - log.Printf("SSHConfig.createZFS : s.exec(%s) : %s", s.name, err) - } - return err - } - - s.zfs[base] = "none" - } - } - - return nil - -} - -func (a AppConfig) RunAppSchedule(schedule string) error { - if *debugFlag { - log.Printf("RunAppSchedule(%s) : %s : Start", a.Name, schedule) - } - for _, src := range a.Sources { - if !cfg.ssh[src.Box()].isZFS(src.Path()) { - return fmt.Errorf("No path %s on source", string(src)) - } - for _, dest := range a.Destinations { - if !cfg.ssh[dest.Box()].isZFS(dest.Path() + "/" + src.Box() + "/" + src.Path()) { - err := cfg.ssh[dest.Box()].createZFS(dest.Path() + "/" + src.Box() + "/" + src.Path()) - if err != nil { - if *debugFlag { - log.Printf("RunAppSchedule(%s) : %s : Error creating %s on %s", a.Name, schedule, dest.Path()+"/"+src.Box()+"/"+src.Path(), dest.Box()) - } - return err - } - } - } - } - for k, v := range a.Before { - re := regexp.MustCompile(k) - if re.MatchString(schedule) { - err := cfg.ssh[v.Box()].exec(v.Path()) - if err != nil { - if *debugFlag { - log.Printf("RunAppSchedule(%s) : %s : Error executing %s", a.Name, schedule, string(v)) - } - return err - } - } - } - - refreshSnapshot := make(map[string]bool) - takeSnapshot := make(map[string]string) - delSnapshot := make(map[string]string) - for _, v := range a.Sources { - takeSnapshot[v.Box()] = takeSnapshot[v.Box()] + " " + v.Path() - refreshSnapshot[v.Box()] = true - for _, v2 := range a.Destinations { - delSnapshot[v2.Box()] = delSnapshot[v2.Box()] + " " + v2.Path() + "/" + v.Box() + "/" + v.Path() - } - } - for k, v := range takeSnapshot { - if *debugFlag { - log.Printf("RunAppSchedule(%s) : %s : taking snapshot on %s for %s", a.Name, schedule, k, v) - } - err := cfg.ssh[k].exec("/usr/sbin/zfsnap snapshot -a " + cfg.Zfsnap[schedule] + v) - if err != nil { - if *debugFlag { - log.Printf("RunAppSchedule(%s) : %s : Error executing zfsnap on %s", a.Name, schedule, k) - } - return err - } - } - for _, v := range a.Destinations { - refreshSnapshot[v.Box()] = true - } - for k, _ := range refreshSnapshot { - if *debugFlag { - log.Printf("RunAppSchedule(%s) : %s : refreshing snapshots for source %s", a.Name, schedule, k) - } - err := cfg.ssh[k].getSnapshotList() - if err != nil { - if *debugFlag { - log.Printf("RunAppSchedule(%s) : %s : Error getting snapshots on %s", a.Name, schedule, k) - } - return err - } - } - for _, src := range a.Sources { - for _, dest := range a.Destinations { - if *debugFlag { - log.Printf("RunAppSchedule(%s) : %s : Sending snapshots from %s to %s", a.Name, schedule, string(src), string(dest)) - } - dLastSnapshot, err := cfg.ssh[dest.Box()].getLastSnapshot(dest.Path() + "/" + src.Box() + "/" + src.Path()) - if err != nil { - if *debugFlag { - log.Printf("RunAppSchedule(%s) : %s : No snapshot for %s on %s", a.Name, schedule, string(src), dest.Box()) - } - sFirstSnapshot, err := cfg.ssh[src.Box()].getFirstSnapshot(src.Path()) - if err != nil { - if *debugFlag { - log.Printf("RunAppSchedule(%s) : %s : No snapshot for %s", a.Name, schedule, string(src)) - } - return err - } - if *debugFlag { - log.Printf("RunAppSchedule(%s) : %s : Initializing snapshot on %s from %s", a.Name, schedule, dest.Box(), string(sFirstSnapshot)) - } - err = cfg.ssh[dest.Box()].exec("/usr/bin/ssh root@" + src.Box() + " /sbin/zfs send " + string(sFirstSnapshot) + " | /sbin/zfs recv -F " + dest.Path() + "/" + src.Box() + "/" + src.Path()) - if err != nil { - if *debugFlag { - log.Printf("RunAppSchedule(%s) : %s : Initializing snapshot on %s from %s failed (%s)", a.Name, schedule, dest.Box(), string(sFirstSnapshot), err) - } - return err - } - var sCurrSnapshot Snapshot - sNextSnapshot := sFirstSnapshot - for !cfg.ssh[src.Box()].isLastSnapshot(sNextSnapshot) { - sCurrSnapshot = sNextSnapshot - sNextSnapshot, err = cfg.ssh[src.Box()].getNextSnapshot(sNextSnapshot) - if *debugFlag { - log.Printf("RunAppSchedule(%s) : %s : Sending incrementally %s to %s", a.Name, schedule, string(sNextSnapshot), dest.Box()) - } - err = cfg.ssh[dest.Box()].exec("/usr/bin/ssh root@" + src.Box() + " /sbin/zfs send -I " + string(sCurrSnapshot) + " " + string(sNextSnapshot) + " | /sbin/zfs recv " + dest.Path() + "/" + src.Box() + "/" + src.Path()) - if err != nil { - if *debugFlag { - log.Printf("RunAppSchedule(%s) : %s : Sending snapshot on %s from %s failed (%s)", a.Name, schedule, dest.Box(), string(sNextSnapshot), err) - } - return err - } - } - if *debugFlag { - log.Printf("RunAppSchedule(%s) : %s : All snapshots sent for %s", a.Name, schedule, string(src)) - } - - } else { - if *debugFlag { - log.Printf("RunAppSchedule(%s) : %s : Last snapshot on %s is %s", a.Name, schedule, dest.Box(), string(dLastSnapshot)) - } - var sCurrSnapshot Snapshot - sNextSnapshot := Snapshot(string(dLastSnapshot)[len(string(dest))+2:]) - for !cfg.ssh[src.Box()].isLastSnapshot(sNextSnapshot) { - sCurrSnapshot = sNextSnapshot - sNextSnapshot, err = cfg.ssh[src.Box()].getNextSnapshot(sNextSnapshot) - if *debugFlag { - log.Printf("RunAppSchedule(%s) : %s : Sending incrementally %s to %s", a.Name, schedule, string(sNextSnapshot), dest.Box()) - } - err = cfg.ssh[dest.Box()].exec("/usr/bin/ssh root@" + src.Box() + " /sbin/zfs send -I " + string(sCurrSnapshot) + " " + string(sNextSnapshot) + " | /sbin/zfs recv " + dest.Path() + "/" + src.Box() + "/" + src.Path()) - if err != nil { - if *debugFlag { - log.Printf("RunAppSchedule(%s) : %s : Sending snapshot on %s from %s failed (%s)", a.Name, schedule, dest.Box(), string(sNextSnapshot), err) - } - return err - } - } - } - } - } - for k, v := range takeSnapshot { - if *debugFlag { - log.Printf("RunAppSchedule(%s) : %s : cleaning snapshot on %s for %s", a.Name, schedule, k, v) - } - err := cfg.ssh[k].exec("/usr/sbin/zfsnap destroy" + v) - if err != nil { - if *debugFlag { - log.Printf("RunAppSchedule(%s) : %s : Error executing zfsnap on %s", a.Name, schedule, k) - } - return err - } - } - for k, v := range delSnapshot { - if *debugFlag { - log.Printf("RunAppSchedule(%s) : %s : cleaning snapshot on %s for %s", a.Name, schedule, k, v) - } - err := cfg.ssh[k].exec("/usr/sbin/zfsnap destroy" + v) - if err != nil { - if *debugFlag { - log.Printf("RunAppSchedule(%s) : %s : Error executing zfsnap on %s", a.Name, schedule, k) - } - return err - } - } - for k, v := range a.After { - re := regexp.MustCompile(k) - if re.MatchString(schedule) { - err := cfg.ssh[v.Box()].exec(v.Path()) - if err != nil { - if *debugFlag { - log.Printf("RunAppSchedule(%s) : %s : Error executing %s on %s", a.Name, schedule, v.Path(), v.Box()) - } - return err - } - } - } - return nil -} - func main() { flag.Parse() @@ -689,48 +203,26 @@ func main() { os.Exit(1) } - schedule := *schedFlag - if schedule == "" { - log.Printf("Main : Finding out schedule.") - now := time.Now() - if now.Day() == 1 && int(now.Month()) == 1 { - schedule = "yearly" - } else if now.Day() == 1 { - schedule = "monthly" - } else if now.Weekday().String() == "Monday" { - schedule = "weekly" - } else { - schedule = "daily" - } - } - - err = RunSchedule(schedule) + err = RunBackup() if err != nil { log.Printf("Cannot run schedule (%s)", err) os.Exit(1) } } -//RunSchedule run all backup targets where schedule is registered -func RunSchedule(schedule string) error { +//RunBackup run all backup targets where schedule is registered +func RunBackup() error { if *debugFlag { - log.Printf("RunSchedule(%s) : Start", schedule) - } - if _, ok := cfg.Zfsnap[schedule]; !ok { - return fmt.Errorf("No retention defined for %s schedule", schedule) + log.Printf("RunBackup() : Start") } for _, app := range cfg.Apps { - for _, schedName := range app.Schedule { - if schedName == schedule { - err := app.RunAppSchedule(schedule) - if err != nil { - if *debugFlag { - log.Printf("RunSchedule(%s) : Error running %s", schedule, app.Name) - } - return err - } + err := app.RunAppBackup() + if err != nil { + if *debugFlag { + log.Printf("RunBackup() : Error running %s", app.Name) } + return err } } diff --git a/location.go b/location.go new file mode 100644 index 0000000..3d8f8af --- /dev/null +++ b/location.go @@ -0,0 +1,20 @@ +package main + +import "strings" + +type Location string + +func (l Location) Box() string { + s := strings.Split(string(l), `:`) + return s[0] +} + +func (l Location) Path() string { + s := strings.Split(string(l), `:`) + return s[1] +} + +func (l Location) Valid() bool { + s := strings.Split(string(l), `:`) + return len(s) == 2 +} diff --git a/snapshot.go b/snapshot.go new file mode 100644 index 0000000..a8f65aa --- /dev/null +++ b/snapshot.go @@ -0,0 +1,20 @@ +package main + +import "strings" + +type Snapshot string + +func (s Snapshot) Path() string { + s2 := strings.Split(string(s), `@`) + return s2[0] +} + +func (s Snapshot) Name() string { + s2 := strings.Split(string(s), `@`) + return s2[1] +} + +func (s Snapshot) Append(path string) Snapshot { + s2 := strings.Split(string(s), `@`) + return Snapshot(s2[0] + "/" + path + "@" + s2[1]) +} diff --git a/ssh.go b/ssh.go new file mode 100644 index 0000000..3c20023 --- /dev/null +++ b/ssh.go @@ -0,0 +1,331 @@ +package main + +import ( + "bytes" + "encoding/csv" + "fmt" + "log" + "strings" + "time" + + "golang.org/x/crypto/ssh" +) + +type SSHConfig struct { + signer ssh.Signer + config *ssh.ClientConfig + client *ssh.Client + logged bool + name string + zfs map[string]string + snapshot []Snapshot + now time.Time +} + +func (s *SSHConfig) getLastSnapshot(path string) (Snapshot, error) { + if *debugFlag { + log.Printf("SSHConfig.getLastSnapshot : Start %s:%s (%d snapshots)", s.name, path, len(s.snapshot)) + } + var last Snapshot + for _, v := range s.snapshot { + if v.Path() == path { + last = v + } else { + if len(string(last)) > 0 { + return last, nil + } + } + } + if len(string(last)) > 0 { + return last, nil + } + return last, fmt.Errorf("no snapshot") +} + +func (s *SSHConfig) isLastSnapshot(snapshot Snapshot) bool { + if *debugFlag { + log.Printf("SSHConfig.isLastSnapshot : Start %s:%s", s.name, string(snapshot)) + } + _, err := s.getNextSnapshot(snapshot) + if err != nil { + return true + } else { + return false + } +} + +func (s *SSHConfig) getFirstSnapshot(path string) (Snapshot, error) { + if *debugFlag { + log.Printf("SSHConfig.getFirstSnapshot : Start %s:%s", s.name, path) + } + var first Snapshot + for _, v := range s.snapshot { + if v.Path() == path { + first = v + return first, nil + } + } + return first, fmt.Errorf("no snapshot") +} + +func (s *SSHConfig) getNextSnapshot(snapshot Snapshot) (Snapshot, error) { + if *debugFlag { + log.Printf("SSHConfig.getNextSnapshot : Start %s:%s", s.name, string(snapshot)) + } + var next Snapshot + for id, v := range s.snapshot { + if v == snapshot { + if len(s.snapshot) > id+1 { + next = s.snapshot[id+1] + if next.Path() == snapshot.Path() { + return next, nil + } else { + return next, fmt.Errorf("no snapshot") + } + } else { + return next, fmt.Errorf("no snapshot") + } + } + } + return next, fmt.Errorf("no snapshot") +} + +func (s *SSHConfig) getSnapshotList() error { + if *debugFlag { + log.Printf("SSHConfig.getSnapshotList : %s : Start", s.name) + } + if !s.logged { + return fmt.Errorf("Client %s not logged in.", s.name) + } + + session, err := s.client.NewSession() + if err != nil { + if *debugFlag { + log.Printf("SSHConfig.getSnapshotList : %s : client.NewSession() : %s", s.name, err) + } + return err + } + + var b bytes.Buffer + session.Stdout = &b + + err = session.Run("TZ=\"" + cfg.Timezone + "\" /usr/sbin/zfs list -H -t snapshot -o name") + if err != nil { + if *debugFlag { + log.Printf("SSHConfig.getSnapshotList : %s : session.Run() : %s", s.name, err) + } + return err + } + + s.snapshot = make([]Snapshot, 0) + + csvReader := csv.NewReader(&b) + csvReader.Comma = '\t' + csvReader.FieldsPerRecord = 1 + + csvData, err := csvReader.ReadAll() + if err != nil { + if *debugFlag { + log.Printf("SSHConfig.getSnapshotList : %s : csvReader.ReadAll() : %s", s.name, err) + } + return err + } + + for _, rec := range csvData { + s.snapshot = append(s.snapshot, Snapshot(rec[0])) + } + + if *debugFlag { + log.Printf("SSHConfig.getSnapshotList : %s : read %d zfs snapshots", s.name, len(s.snapshot)) + } + + session.Close() + + return nil +} + +func (s *SSHConfig) getZFSList() error { + if *debugFlag { + log.Printf("SSHConfig.getZFSList : %s : Start", s.name) + } + if !s.logged { + return fmt.Errorf("Client %s not logged in.", s.name) + } + + session, err := s.client.NewSession() + if err != nil { + if *debugFlag { + log.Printf("SSHConfig.getZFSList : %s : client.NewSession() : %s", s.name, err) + } + return err + } + + var b bytes.Buffer + session.Stdout = &b + + err = session.Run("TZ=\"" + cfg.Timezone + "\" /sbin/zfs list -H -o name,mountpoint") + if err != nil { + if *debugFlag { + log.Printf("SSHConfig.getZFSList : %s : session.Run() : %s", s.name, err) + } + return err + } + + s.zfs = make(map[string]string) + + csvReader := csv.NewReader(&b) + csvReader.Comma = '\t' + csvReader.FieldsPerRecord = 2 + + csvData, err := csvReader.ReadAll() + if err != nil { + if *debugFlag { + log.Printf("SSHConfig.getZFSList : %s : csvReader.ReadAll() : %s", s.name, err) + } + return err + } + + for _, rec := range csvData { + s.zfs[rec[0]] = rec[1] + } + + if *debugFlag { + log.Printf("SSHConfig.getZFSList : %s : read %d zfs file systems", s.name, len(s.zfs)) + } + + session.Close() + + return nil + +} + +func (s *SSHConfig) isZFS(path string) bool { + if *debugFlag { + log.Printf("SSHConfig.isZFS : Start %s:%s", s.name, path) + } + if len(s.zfs) == 0 { + err := s.getZFSList() + if err != nil { + if *debugFlag { + log.Printf("SSHConfig.isZFS : s.getZFSList(%s) : %s", s.name, err) + } + return false + } + } + _, ok := s.zfs[path] + if ok { + return true + } + return false +} + +func (s *SSHConfig) getTime() error { + if *debugFlag { + log.Printf("SSHConfig.getTime : %s : Start", s.name) + } + + session, err := s.client.NewSession() + if err != nil { + if *debugFlag { + log.Printf("SSHConfig.getTime : %s : client.NewSession() : %s", s.name, err) + } + return err + } + + var b bytes.Buffer + session.Stdout = &b + + err = session.Run("TZ=\"" + cfg.Timezone + "\" /usr/bin/date +\"%F %T\"") + if err != nil { + if *debugFlag { + log.Printf("SSHConfig.getTime : %s : session.Run() : %s", s.name, err) + } + return err + } + + s.now, err = time.Parse("2006-01-02 15:04:05\n", b.String()) + if err != nil { + if *debugFlag { + log.Printf("SSHConfig.getTime : %s : time.Parse() : %s", s.name, err) + } + return err + } + + if *debugFlag { + log.Printf("SSHConfig.getTime : %s : now is %s", s.name, s.now.String()) + } + + session.Close() + + return nil + +} + +func (s *SSHConfig) exec(cmd string) error { + if *debugFlag { + log.Printf("SSHConfig.exec : %s : Start %s", s.name, cmd) + } + + session, err := s.client.NewSession() + if err != nil { + if *debugFlag { + log.Printf("SSHConfig.exec : %s : client().NewSession(%s) : %s", s.name, cmd, err) + } + return err + } + + err = session.Run("TZ=\"" + cfg.Timezone + "\" " + cmd) + if err != nil { + if *debugFlag { + log.Printf("SSHConfig.exec : session(%s).Run(%s) : %s", s.name, cmd, err) + } + return err + } + + session.Close() + + return nil +} + +func (s *SSHConfig) createZFS(path string) error { + if *debugFlag { + log.Printf("SSHConfig.createZFS : Start %s:%s", s.name, path) + } + if len(s.zfs) == 0 { + err := s.getZFSList() + if err != nil { + if *debugFlag { + log.Printf("SSHConfig.createZFS : s.getZFSList(%s) : %s", s.name, err) + } + return err + } + } + p := strings.Split(path, `/`) + var base string + for _, d := range p { + if base == "" { + base = d + } else { + base = base + `/` + d + } + if _, ok := s.zfs[base]; !ok { + + if *debugFlag { + log.Printf("SSHConfig.createZFS : Creating %s:%s", s.name, base) + } + + err := s.exec("/sbin/zfs create -o mountpoint=none " + base) + if err != nil { + if *debugFlag { + log.Printf("SSHConfig.createZFS : s.exec(%s) : %s", s.name, err) + } + return err + } + + s.zfs[base] = "none" + } + } + + return nil + +}