mirror of
https://github.com/zrepl/zrepl.git
synced 2025-01-09 15:58:33 +01:00
implement automatic snapshotting feature
This commit is contained in:
parent
8c8a6ee905
commit
655b3ab55f
42
cmd/autosnap.go
Normal file
42
cmd/autosnap.go
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/zrepl/zrepl/zfs"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AutosnapContext struct {
|
||||||
|
Autosnap Autosnap
|
||||||
|
}
|
||||||
|
|
||||||
|
func doAutosnap(ctx AutosnapContext, log Logger) (err error) {
|
||||||
|
|
||||||
|
snap := ctx.Autosnap
|
||||||
|
|
||||||
|
filesystems, err := zfs.ZFSListMapping(snap.DatasetFilter)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot filter datasets: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
suffix := time.Now().In(time.UTC).Format("20060102_150405_000")
|
||||||
|
snapname := fmt.Sprintf("%s%s", snap.Prefix, suffix)
|
||||||
|
|
||||||
|
hadError := false
|
||||||
|
|
||||||
|
for _, fs := range filesystems { // optimization: use recursive snapshots / channel programs here
|
||||||
|
log.Printf("snapshotting filesystem %s@%s", fs, snapname)
|
||||||
|
err := zfs.ZFSSnapshot(fs, snapname, false)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error snapshotting %s: %s", fs, err)
|
||||||
|
hadError = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if hadError {
|
||||||
|
err = fmt.Errorf("errors occurred during autosnap, check logs for details")
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
}
|
@ -67,6 +67,13 @@ type Prune struct {
|
|||||||
RetentionPolicy *RetentionGrid // TODO abstract interface to support future policies?
|
RetentionPolicy *RetentionGrid // TODO abstract interface to support future policies?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Autosnap struct {
|
||||||
|
Name string
|
||||||
|
Prefix string
|
||||||
|
Interval jobrun.RepeatStrategy
|
||||||
|
DatasetFilter zfs.DatasetMapping
|
||||||
|
}
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Pools []Pool
|
Pools []Pool
|
||||||
Pushs []Push
|
Pushs []Push
|
||||||
@ -74,6 +81,7 @@ type Config struct {
|
|||||||
Sinks []ClientMapping
|
Sinks []ClientMapping
|
||||||
PullACLs []ClientMapping
|
PullACLs []ClientMapping
|
||||||
Prunes []Prune
|
Prunes []Prune
|
||||||
|
Autosnaps []Autosnap
|
||||||
}
|
}
|
||||||
|
|
||||||
func ParseConfig(path string) (config Config, err error) {
|
func ParseConfig(path string) (config Config, err error) {
|
||||||
@ -122,6 +130,9 @@ func parseMain(root map[string]interface{}) (c Config, err error) {
|
|||||||
if c.Prunes, err = parsePrunes(root["prune"]); err != nil {
|
if c.Prunes, err = parsePrunes(root["prune"]); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if c.Autosnaps, err = parseAutosnaps(root["autosnap"]); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -624,3 +635,63 @@ func parseSnapshotFilter(fm map[string]string) (snapFilter zfs.FilesystemVersion
|
|||||||
snapFilter = prefixSnapshotFilter{prefix}
|
snapFilter = prefixSnapshotFilter{prefix}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseAutosnaps(m interface{}) (snaps []Autosnap, err error) {
|
||||||
|
|
||||||
|
asList := make([]map[string]interface{}, 0)
|
||||||
|
if err = mapstructure.Decode(m, &asList); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
snaps = make([]Autosnap, len(asList))
|
||||||
|
|
||||||
|
for i, e := range asList {
|
||||||
|
if snaps[i], err = parseAutosnap(e); err != nil {
|
||||||
|
err = fmt.Errorf("cannot parse autonsap job #%d: %s", i+1, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseAutosnap(m map[string]interface{}) (a Autosnap, err error) {
|
||||||
|
|
||||||
|
var i struct {
|
||||||
|
Name string
|
||||||
|
Prefix string
|
||||||
|
Interval string
|
||||||
|
DatasetFilter map[string]string `mapstructure:"dataset_filter"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = mapstructure.Decode(m, &i); err != nil {
|
||||||
|
err = fmt.Errorf("structure unfit: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
a.Name = i.Name
|
||||||
|
|
||||||
|
if len(i.Prefix) < 1 {
|
||||||
|
err = fmt.Errorf("prefix must not be empty")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
a.Prefix = i.Prefix
|
||||||
|
|
||||||
|
var interval time.Duration
|
||||||
|
if interval, err = time.ParseDuration(i.Interval); err != nil {
|
||||||
|
err = fmt.Errorf("cannot parse interval: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
a.Interval = &jobrun.PeriodicRepeatStrategy{interval}
|
||||||
|
|
||||||
|
if len(i.DatasetFilter) == 0 {
|
||||||
|
err = fmt.Errorf("dataset_filter not specified")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if a.DatasetFilter, err = parseComboMapping(i.DatasetFilter); err != nil {
|
||||||
|
err = fmt.Errorf("cannot parse dataset filter: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
47
cmd/main.go
47
cmd/main.go
@ -119,6 +119,13 @@ func main() {
|
|||||||
cli.BoolFlag{Name: "n", Usage: "simulation (dry run)"},
|
cli.BoolFlag{Name: "n", Usage: "simulation (dry run)"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "autosnap",
|
||||||
|
Action: cmdAutosnap,
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
cli.StringFlag{Name: "job"},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
app.Run(os.Args)
|
app.Run(os.Args)
|
||||||
@ -306,6 +313,7 @@ func cmdPrune(c *cli.Context) error {
|
|||||||
log.Printf("Prune job failed with error: %s", err)
|
log.Printf("Prune job failed with error: %s", err)
|
||||||
}
|
}
|
||||||
log.Printf("\n")
|
log.Printf("\n")
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -313,6 +321,45 @@ func cmdPrune(c *cli.Context) error {
|
|||||||
if jobFailed {
|
if jobFailed {
|
||||||
return cli.NewExitError("At least one job failed with an error. Check log for details.", 1)
|
return cli.NewExitError("At least one job failed with an error. Check log for details.", 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func cmdAutosnap(c *cli.Context) error {
|
||||||
|
|
||||||
|
log := defaultLog
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
runner.Start()
|
||||||
|
}()
|
||||||
|
|
||||||
|
log.Printf("autosnap...")
|
||||||
|
|
||||||
|
for i := range conf.Autosnaps {
|
||||||
|
|
||||||
|
snap := conf.Autosnaps[i]
|
||||||
|
|
||||||
|
if !c.IsSet("job") || (c.IsSet("job") && c.String("job") == snap.Name) {
|
||||||
|
|
||||||
|
job := jobrun.Job{
|
||||||
|
Name: fmt.Sprintf("autosnap.%s", snap.Name),
|
||||||
|
RepeatStrategy: snap.Interval,
|
||||||
|
RunFunc: func(log jobrun.Logger) error {
|
||||||
|
log.Printf("doing autosnap: %v", snap)
|
||||||
|
ctx := AutosnapContext{snap}
|
||||||
|
return doAutosnap(ctx, log)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
runner.AddJob(job)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -97,3 +97,25 @@ prune:
|
|||||||
snapshot_filter: {
|
snapshot_filter: {
|
||||||
prefix: zrepl_
|
prefix: zrepl_
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- name: hfbak_prune # cleans up after hfbak autosnap job
|
||||||
|
policy: grid
|
||||||
|
grid: 1x1min(keep=all)
|
||||||
|
dataset_filter: {
|
||||||
|
"pool1*": ok
|
||||||
|
}
|
||||||
|
snapshot_filter: {
|
||||||
|
prefix: zrepl_hfbak_
|
||||||
|
}
|
||||||
|
|
||||||
|
autosnap:
|
||||||
|
|
||||||
|
- name: hfbak
|
||||||
|
prefix: zrepl_hfbak_
|
||||||
|
interval: 1s
|
||||||
|
dataset_filter: {
|
||||||
|
"pool1*": ok
|
||||||
|
}
|
||||||
|
# prune: hfbak_prune
|
||||||
|
# future versions may inline the retention policy here, but for now,
|
||||||
|
# pruning has to be triggered manually (it's safe to run autosnap + prune in parallel)
|
||||||
|
23
zfs/zfs.go
23
zfs/zfs.go
@ -204,3 +204,26 @@ func ZFSDestroy(dataset string) (err error) {
|
|||||||
return
|
return
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ZFSSnapshot(fs DatasetPath, name string, recursive bool) (err error) {
|
||||||
|
|
||||||
|
snapname := fmt.Sprintf("%s@%s", fs.ToString(), name)
|
||||||
|
cmd := exec.Command(ZFS_BINARY, "snapshot", snapname)
|
||||||
|
|
||||||
|
stderr := bytes.NewBuffer(make([]byte, 0, 1024))
|
||||||
|
cmd.Stderr = stderr
|
||||||
|
|
||||||
|
if err = cmd.Start(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = cmd.Wait(); err != nil {
|
||||||
|
err = ZFSError{
|
||||||
|
Stderr: stderr.Bytes(),
|
||||||
|
WaitErr: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user