package main import ( "bytes" "encoding/csv" "errors" "fmt" "regexp" "sort" "strings" "sync" log "github.com/sirupsen/logrus" ) type BoxZfs struct { filesystems map[string]*ZfsFs box *Box online bool mx sync.Mutex } type ZfsFs struct { path string managed bool backedUp bool zfs *BoxZfs snapshots map[string]*ZfsSnapshot srcApps []*App destApps []*App mx sync.Mutex } type ZfsSnapshot struct { name string fs *ZfsFs } func (z *BoxZfs) Open() error { log.WithFields(log.Fields{"name": z.box.name}).Debugf("starting") defer log.WithFields(log.Fields{"name": z.box.name}).Debugf("done") z.mx.Lock() defer z.mx.Unlock() if z.online { return nil } z.filesystems = make(map[string]*ZfsFs) zfsList, err := z.box.Exec("zfs list -H -t filesystem -o name,mountpoint") if err != nil { log.WithFields(log.Fields{"name": z.box.name, "call": "Exec", "attr": "zfs list -H -t filesystem -o name,mountpoint", "error": err}).Errorf("") return err } csvReader := csv.NewReader(bytes.NewBufferString(zfsList)) csvReader.Comma = '\t' csvReader.FieldsPerRecord = 2 csvData, err := csvReader.ReadAll() if err != nil { log.WithFields(log.Fields{"name": z.box.name, "call": "csvReader.ReadAll", "error": err}).Errorf("") return err } for _, rec := range csvData { log.WithFields(log.Fields{"name": z.box.name, "zfs-name": rec[0], "zfs-mount": rec[1]}).Debugf("zfs list -t filesystem") if rec[1] != "legacy" { fs := &ZfsFs{ path: rec[0], zfs: z, snapshots: make(map[string]*ZfsSnapshot), } z.filesystems[rec[0]] = fs log.WithFields(log.Fields{"name": z.box.name, "fs": rec[0]}).Infof("new filesystem") } } log.WithFields(log.Fields{"name": z.box.name, "call": "csvReader.ReadAll"}).Infof("") zfsList, err = z.box.Exec("zfs list -H -t snapshot -o name") if err != nil { log.WithFields(log.Fields{"name": z.box.name, "call": "Exec", "attr": "zfs list -H -t snapshot -o name", "error": err}).Errorf("") return err } csvReader = csv.NewReader(bytes.NewBufferString(zfsList)) csvReader.Comma = '\t' csvReader.FieldsPerRecord = 1 csvData, err = csvReader.ReadAll() if err != nil { log.WithFields(log.Fields{"name": z.box.name, "call": "csvReader.ReadAll", "error": err}).Errorf("") return err } for _, rec := range csvData { log.WithFields(log.Fields{"name": z.box.name, "zfs-snapshot": rec[0]}).Debugf("zfs list -t snapshot") s := strings.Split(rec[0], `@`) if fs, ok := z.filesystems[s[0]]; ok { snap := &ZfsSnapshot{ name: s[1], fs: fs, } fs.snapshots[s[1]] = snap log.WithFields(log.Fields{"name": z.box.name, "fs": s[0], "snapshot": s[1]}).Infof("new snapshot") } } zfsList, err = z.box.Exec("zfs get -H -o name,value " + zfsManagedPropertyName) if err != nil { log.WithFields(log.Fields{"name": z.box.name, "call": "Exec", "attr": "zfs get -H -o name,value,source " + zfsManagedPropertyName, "error": err}).Errorf("") return err } csvReader = csv.NewReader(bytes.NewBufferString(zfsList)) csvReader.Comma = '\t' csvReader.FieldsPerRecord = 2 csvData, err = csvReader.ReadAll() if err != nil { log.WithFields(log.Fields{"name": z.box.name, "call": "csvReader.ReadAll", "error": err}).Errorf("") return err } for _, rec := range csvData { log.WithFields(log.Fields{"name": z.box.name, "zfs-fs": rec[0], "zfs-value": rec[1]}).Debugf("zfs get " + zfsManagedPropertyName) if fs, ok := z.filesystems[rec[0]]; ok { if rec[1] == "+" { fs.managed = true log.WithFields(log.Fields{"name": z.box.name, "zfs-fs": rec[0], "zfs-value": rec[1]}).Infof("managed fs") } } } z.online = true return nil } func (z *BoxZfs) Close() error { log.WithFields(log.Fields{"name": z.box.name}).Debugf("starting") defer log.WithFields(log.Fields{"name": z.box.name}).Debugf("done") z.mx.Lock() defer z.mx.Unlock() for _, fs := range z.filesystems { fs.mx.Lock() defer fs.mx.Unlock() } z.online = false return nil } func (z *BoxZfs) Mkdir(path string) error { log.WithFields(log.Fields{"name": z.box.name}).Debugf("starting") defer log.WithFields(log.Fields{"name": z.box.name}).Debugf("done") if !z.online { err := errors.New("zfs offline") log.WithFields(log.Fields{"name": z.box.name, "error": err}).Errorf("") return err } z.mx.Lock() defer z.mx.Unlock() b := z.box if !b.online { err := errors.New("box offline") log.WithFields(log.Fields{"name": z.box.name, "error": err}).Errorf("") return err } if _, ok := z.filesystems[path]; ok { return nil } if _, err := b.Exec(fmt.Sprintf("zfs create -p %s", path)); err != nil { log.WithFields(log.Fields{"name": z.box.name, "call": "Exec", "error": err}).Errorf("") return err } newPath := "" for _, p := range strings.Split(path, "/") { if newPath == "" { newPath = p } else { newPath = newPath + "/" + p } if _, ok := z.filesystems[newPath]; !ok { fs := &ZfsFs{ path: newPath, managed: false, zfs: z, snapshots: make(map[string]*ZfsSnapshot), srcApps: make([]*App, 0), destApps: make([]*App, 0), } z.filesystems[newPath] = fs } } return nil } func (fs *ZfsFs) TakeSnapshot(name string) (*ZfsSnapshot, error) { log.WithFields(log.Fields{"box": fs.zfs.box.name, "fs": fs.path, "name": name}).Debugf("starting") defer log.WithFields(log.Fields{"box": fs.zfs.box.name, "fs": fs.path, "name": name}).Debugf("done") if !fs.zfs.online { err := errors.New("zfs offline") log.WithFields(log.Fields{"box": fs.zfs.box.name, "fs": fs.path, "name": name, "error": err}).Errorf("") return nil, err } re := regexp.MustCompile(`^[a-zA-Z0-9\-\._]{1,255}$`) if !re.MatchString(name) { err := errors.New("unsupported name") log.WithFields(log.Fields{"box": fs.zfs.box.name, "fs": fs.path, "name": name, "error": err}).Errorf("") return nil, err } fs.mx.Lock() defer fs.mx.Unlock() if _, ok := fs.snapshots[name]; ok { err := errors.New("already exists") log.WithFields(log.Fields{"box": fs.zfs.box.name, "fs": fs.path, "name": name, "error": err}).Errorf("") return nil, err } if _, err := fs.zfs.box.Exec("zfs snapshot " + fs.path + "@" + name); err != nil { log.WithFields(log.Fields{"box": fs.zfs.box.name, "fs": fs.path, "name": name, "error": err}).Errorf("") return nil, err } s := &ZfsSnapshot{ name: name, fs: fs, } fs.snapshots[name] = s return s, nil } func (fs *ZfsFs) DelSnapshot(name string) error { log.WithFields(log.Fields{"box": fs.zfs.box.name, "fs": fs.path, "name": name}).Debugf("starting") defer log.WithFields(log.Fields{"box": fs.zfs.box.name, "fs": fs.path, "name": name}).Debugf("done") if !fs.zfs.online { err := errors.New("zfs offline") log.WithFields(log.Fields{"box": fs.zfs.box.name, "fs": fs.path, "name": name, "error": err}).Errorf("") return err } fs.mx.Lock() defer fs.mx.Unlock() if _, ok := fs.snapshots[name]; !ok { err := errors.New("doesn't exist") log.WithFields(log.Fields{"box": fs.zfs.box.name, "fs": fs.path, "name": name, "error": err}).Errorf("") return err } log.WithFields(log.Fields{"box": fs.zfs.box.name, "fs": fs.path, "name": name}).Debugf("zfs destroy " + fs.path + "@" + name) if _, err := fs.zfs.box.Exec("zfs destroy " + fs.path + "@" + name); err != nil { log.WithFields(log.Fields{"box": fs.zfs.box.name, "fs": fs.path, "name": name, "error": err}).Errorf("") return err } delete(fs.snapshots, name) return nil } func (fs *ZfsFs) AddSnapshot(s *ZfsSnapshot) error { log.WithFields(log.Fields{"box": fs.zfs.box.name, "fs": fs.path, "name": s.name}).Debugf("starting") defer log.WithFields(log.Fields{"box": fs.zfs.box.name, "fs": fs.path, "name": s.name}).Debugf("done") if !fs.zfs.online { err := errors.New("zfs offline") log.WithFields(log.Fields{"box": fs.zfs.box.name, "fs": fs.path, "name": s.name, "error": err}).Errorf("") return err } fs.mx.Lock() defer fs.mx.Unlock() if _, ok := fs.snapshots[s.name]; ok { err := errors.New("already exist") log.WithFields(log.Fields{"box": fs.zfs.box.name, "fs": fs.path, "name": s.name, "error": err}).Errorf("") return err } fs.snapshots[s.name] = s return nil } func (fs *ZfsFs) ValidSnapshots() []*ZfsSnapshot { tab := make([]*ZfsSnapshot, 0) for _, s := range fs.snapshots { if s.Valid() { tab = append(tab, s) } } sort.Slice(tab, func(i, j int) bool { ti, _ := tab[i].Timestamp() tj, _ := tab[j].Timestamp() return ti.Before(tj) }) return tab }