package cmd import ( "context" "fmt" "github.com/zrepl/zrepl/zfs" "time" ) type Pruner struct { task *Task Now time.Time DryRun bool DatasetFilter zfs.DatasetFilter SnapshotPrefix string PrunePolicy PrunePolicy } type PruneResult struct { Filesystem *zfs.DatasetPath All []zfs.FilesystemVersion Keep []zfs.FilesystemVersion Remove []zfs.FilesystemVersion } func (p *Pruner) filterFilesystems() (filesystems []*zfs.DatasetPath, stop bool) { p.task.Enter("filter_fs") defer p.task.Finish() filesystems, err := zfs.ZFSListMapping(p.DatasetFilter) if err != nil { p.task.Log().WithError(err).Error("error applying filesystem filter") return nil, true } if len(filesystems) <= 0 { p.task.Log().Info("no filesystems matching filter") return nil, true } return filesystems, false } func (p *Pruner) filterVersions(fs *zfs.DatasetPath) (fsversions []zfs.FilesystemVersion, stop bool) { p.task.Enter("filter_versions") defer p.task.Finish() log := p.task.Log().WithField(logFSField, fs.ToString()) filter := NewPrefixFilter(p.SnapshotPrefix) fsversions, err := zfs.ZFSListFilesystemVersions(fs, filter) if err != nil { log.WithError(err).Error("error listing filesytem versions") return nil, true } if len(fsversions) == 0 { log.WithField("prefix", p.SnapshotPrefix).Info("no filesystem versions matching prefix") return nil, true } return fsversions, false } func (p *Pruner) pruneFilesystem(fs *zfs.DatasetPath) (r PruneResult, valid bool) { p.task.Enter("prune_fs") defer p.task.Finish() log := p.task.Log().WithField(logFSField, fs.ToString()) fsversions, stop := p.filterVersions(fs) if stop { return } p.task.Enter("prune_policy") keep, remove, err := p.PrunePolicy.Prune(fs, fsversions) p.task.Finish() if err != nil { log.WithError(err).Error("error evaluating prune policy") return } log.WithField("fsversions", fsversions). WithField("keep", keep). WithField("remove", remove). Debug("prune policy debug dump") r = PruneResult{fs, fsversions, keep, remove} makeFields := func(v zfs.FilesystemVersion) (fields map[string]interface{}) { fields = make(map[string]interface{}) fields["version"] = v.ToAbsPath(fs) timeSince := v.Creation.Sub(p.Now) fields["age_ns"] = timeSince const day time.Duration = 24 * time.Hour days := timeSince / day remainder := timeSince % day fields["age_str"] = fmt.Sprintf("%dd%s", days, remainder) return } for _, v := range remove { fields := makeFields(v) log.WithFields(fields).Info("destroying version") // echo what we'll do and exec zfs destroy if not dry run // TODO special handling for EBUSY (zfs hold) // TODO error handling for clones? just echo to cli, skip over, and exit with non-zero status code (we're idempotent) if !p.DryRun { p.task.Enter("destroy") err := zfs.ZFSDestroyFilesystemVersion(fs, v) p.task.Finish() if err != nil { log.WithFields(fields).WithError(err).Error("error destroying version") } } } return r, true } func (p *Pruner) Run(ctx context.Context) (r []PruneResult, err error) { p.task.Enter("run") defer p.task.Finish() if p.DryRun { p.task.Log().Info("doing dry run") } filesystems, stop := p.filterFilesystems() if stop { return } r = make([]PruneResult, 0, len(filesystems)) for _, fs := range filesystems { res, ok := p.pruneFilesystem(fs) if ok { r = append(r, res) } } return }