commit d30cb6f304f44454a2209e5c311267bbb49d69a9 Author: shoopea Date: Mon Oct 11 09:43:05 2021 +0800 initial commit diff --git a/backup.go b/backup.go new file mode 100644 index 0000000..3883095 --- /dev/null +++ b/backup.go @@ -0,0 +1,557 @@ +package main + +import ( + "bytes" + "encoding/csv" + "encoding/json" + "flag" + "fmt" + "io/ioutil" + "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 +} + +type BoxConfig struct { + Addr string `json:"addr"` + User string `json:"user"` + Key string `json:"key"` +} + +var ( + cfgFile = flag.String("config", "config.json", "config file") + schedFlag = flag.String("schedule", "", "specific schedule") + testFlag = flag.Bool("test", true, "test run") + debugFlag = flag.Bool("debug", true, "debug") + 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 +} + +//Load config from file +func (c *Config) Load() error { + b, err := ioutil.ReadFile(*cfgFile) + if err != nil { + if *debugFlag { + log.Printf("Config.Load : ioutil.ReadFile(%s) : %s", *cfgFile, err) + } + return err + } + + err = json.Unmarshal(b, &c) + if err != nil { + if *debugFlag { + log.Printf("Config.Load : json.Unmarshal : %s", err) + } + return err + } + + cfg.ssh = make(map[string]*SSHConfig) + for k, v := range c.Box { + s := &SSHConfig{ + logged: false, + name: k, + } + cfg.ssh[k] = s + keyRaw, err := ioutil.ReadFile(v.Key) + if err != nil { + if *debugFlag { + log.Printf("Config.Load : ioutil.ReadFile(%s) : %s", k, err) + } + return err + } + + key, err := ssh.ParseRawPrivateKey(keyRaw) + if err != nil { + if *debugFlag { + log.Printf("Config.Load : ssh.ParseRawPrivateKey(%s) : %s", k, err) + } + return err + } + + s.signer, err = ssh.NewSignerFromKey(key) + if err != nil { + if *debugFlag { + log.Printf("Config.Load : ssh.NewSignerFromKey(%s) : %s", k, err) + } + return err + } + + s.config = &ssh.ClientConfig{ + User: v.User, + Auth: []ssh.AuthMethod{ + ssh.PublicKeys(s.signer), + }, + HostKeyCallback: ssh.InsecureIgnoreHostKey(), + } + + s.client, err = ssh.Dial("tcp", v.Addr, s.config) + if err != nil { + if *debugFlag { + log.Printf("Config.Load : ssh.Dial(%s) : %s", k, err) + } + return err + } + + session, err := s.client.NewSession() + if err != nil { + if *debugFlag { + log.Printf("Config.Load : client.NewSession(%s) : %s", k, err) + } + return err + } + + var b bytes.Buffer + session.Stdout = &b + err = session.Run("/usr/bin/uname -a") + if err != nil { + if *debugFlag { + log.Printf("Config.Load : client.NewSession(%s) : %s", k, err) + } + return err + } + if *debugFlag { + log.Printf("Config.Load : logged into %s : %s", k, b.String()) + } + session.Close() + s.logged = true + + } + + for _, app := range c.Apps { + for _, src := range app.Sources { + if !src.Valid() { + return fmt.Errorf("Source not valid : %s", string(src)) + } + if _, ok := cfg.ssh[src.Box()]; !ok { + return fmt.Errorf("No box defined for source : %s", string(src)) + } + } + for _, dest := range app.Destinations { + if !dest.Valid() { + return fmt.Errorf("Destination not valid : %s", string(dest)) + } + if _, ok := cfg.ssh[dest.Box()]; !ok { + return fmt.Errorf("No box defined for destination : %s", string(dest)) + } + } + for val, before := range app.Before { + _, err = regexp.Compile(val) + if err != nil { + if *debugFlag { + log.Printf("Config.Load : invalid regex : %s", val) + } + return err + } + if !before.Valid() { + return fmt.Errorf("Before not valid : %s", string(before)) + } + if _, ok := cfg.ssh[before.Box()]; !ok { + return fmt.Errorf("No box defined for before : %s", string(before)) + } + } + for val, after := range app.After { + _, err = regexp.Compile(val) + if err != nil { + if *debugFlag { + log.Printf("Config.Load : invalid regex : %s", val) + } + return err + } + if !after.Valid() { + return fmt.Errorf("After not valid : %s", string(after)) + } + if _, ok := cfg.ssh[after.Box()]; !ok { + return fmt.Errorf("No box defined for after : %s", string(after)) + } + } + } + + return nil +} + +//Close config +func (c *Config) Close() error { + return nil +} + +func (s *SSHConfig) getSnapshotList() error { + if *debugFlag { + log.Printf("SSHConfig.getSnapshotList : 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.getSnapshotList : client.NewSession(%s) : %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 : session.Run(%s) : %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 : csvReader.ReadAll(%s) : %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 : Running %s(%s)", 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 : Error creating %s on %s", 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 : Error executing %s", string(v)) + } + return err + } + } + } + var refreshSnapshot map[string]bool + refreshSnapshot = make(map[string]bool) + for _, v := range a.Sources { + if *debugFlag { + log.Printf("RunAppSchedule : taking %s snapshot for %s", schedule, v.Path()) + } + err := cfg.ssh[v.Box()].exec("/usr/sbin/zfsnap snapshot -p '" + schedule + "-' -a " + cfg.Zfsnap[schedule] + " " + v.Path()) + if err != nil { + if *debugFlag { + log.Printf("RunAppSchedule : Error executing zfsnap on %s", string(v)) + } + return err + } + refreshSnapshot[v.Box()] = true + } + for k, _ := range refreshSnapshot { + if *debugFlag { + log.Printf("RunAppSchedule : refreshing snapshots for source %s", k) + } + err := cfg.ssh[k].getSnapshotList() + if err != nil { + if *debugFlag { + log.Printf("RunAppSchedule : Error getting snapshots on %s", k) + } + return err + } + } + for _, src := range a.Sources { + for _, dest := range a.Destinations { + if *debugFlag { + log.Printf("RunAppSchedule : Sending snapshots from %s to %s", string(src), string(dest)) + + } + } + } + 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 : Error executing %s on %s", v.Path(), v.Box()) + } + return err + } + } + } + return nil +} + +func main() { + flag.Parse() + + err := cfg.Load() + if err != nil { + log.Printf("Cannot load config (%s)", err) + 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) + 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 { + if *debugFlag { + log.Printf("RunSchedule : Start %s", schedule) + } + if _, ok := cfg.Zfsnap[schedule]; !ok { + return fmt.Errorf("No retention defined for %s schedule", schedule) + } + + 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 : Error running %s(%s)", app.Name, schedule) + } + return err + } + } + } + } + + return nil +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..2827e9a --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module gogs.siteop.biz/shoopea/backup + +go 1.16 + +require golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..a06bcfd --- /dev/null +++ b/go.sum @@ -0,0 +1,9 @@ +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=