diff --git a/cmd/config.go b/cmd/config.go index 4dde920..0602929 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -1,10 +1,10 @@ package cmd import ( - "errors" "fmt" "github.com/jinzhu/copier" "github.com/mitchellh/mapstructure" + "github.com/pkg/errors" "github.com/zrepl/zrepl/jobrun" "github.com/zrepl/zrepl/rpc" "github.com/zrepl/zrepl/sshbytestream" @@ -24,6 +24,8 @@ var ( JobSectionPull string = "pull" JobSectionPrune string = "prune" JobSectionAutosnap string = "autosnap" + JobSectionPullACL string = "pull_acl" + JobSectionSinks string = "sink" ) type Remote struct { @@ -51,7 +53,7 @@ type SSHTransport struct { type Push struct { jobName string // for use with jobrun package To *Remote - Filter zfs.DatasetFilter + Filter DatasetMapFilter InitialReplPolicy InitialReplPolicy RepeatStrategy jobrun.RepeatStrategy } @@ -65,7 +67,7 @@ type Pull struct { type Prune struct { jobName string // for use with jobrun package - DatasetFilter zfs.DatasetFilter + DatasetFilter DatasetMapFilter SnapshotFilter zfs.FilesystemVersionFilter RetentionPolicy *RetentionGrid // TODO abstract interface to support future policies? Repeat jobrun.RepeatStrategy @@ -75,7 +77,7 @@ type Autosnap struct { jobName string // for use with jobrun package Prefix string Interval jobrun.RepeatStrategy - DatasetFilter zfs.DatasetFilter + DatasetFilter DatasetMapFilter } type Config struct { @@ -95,10 +97,12 @@ func ParseConfig(path string) (config Config, err error) { var bytes []byte if bytes, err = ioutil.ReadFile(path); err != nil { + err = errors.WithStack(err) return } if err = yaml.Unmarshal(bytes, &c); err != nil { + err = errors.WithStack(err) return } @@ -118,28 +122,57 @@ func parseMain(root map[string]interface{}) (c Config, err error) { return } - if c.Pushs, err = parsePushs(root["pushs"], remoteLookup); err != nil { + if c.Pushs, err = parsePushs(root[JobSectionPush], remoteLookup); err != nil { return } - if c.Pulls, err = parsePulls(root["pulls"], remoteLookup); err != nil { + if c.Pulls, err = parsePulls(root[JobSectionPull], remoteLookup); err != nil { return } - if c.Sinks, err = parseSinks(root["sinks"]); err != nil { + if c.Sinks, err = parseSinks(root[JobSectionSinks]); err != nil { return } - if c.PullACLs, err = parsePullACLs(root["pull_acls"]); err != nil { + if c.PullACLs, err = parsePullACLs(root[JobSectionPullACL]); err != nil { return } - if c.Prunes, err = parsePrunes(root["prune"]); err != nil { + if c.Prunes, err = parsePrunes(root[JobSectionPrune]); err != nil { return } - if c.Autosnaps, err = parseAutosnaps(root["autosnap"]); err != nil { + if c.Autosnaps, err = parseAutosnaps(root[JobSectionAutosnap]); err != nil { return } return } +func (c *Config) resolveJobName(jobname string) (i interface{}, err error) { + s := strings.SplitN(jobname, ".", 2) + if len(s) != 2 { + return nil, fmt.Errorf("invalid job name syntax (section.name)") + } + section, name := s[0], s[1] + var ok bool + switch section { + case JobSectionAutosnap: + i, ok = c.Autosnaps[name] + case JobSectionPush: + i, ok = c.Pushs[name] + case JobSectionPull: + i, ok = c.Pulls[name] + case JobSectionPrune: + i, ok = c.Prunes[name] + case JobSectionPullACL: + i, ok = c.PullACLs[name] + case JobSectionSinks: + i, ok = c.Sinks[name] + default: + return nil, fmt.Errorf("invalid section name: %s", section) + } + if !ok { + return nil, fmt.Errorf("cannot find job '%s' in section '%s'", name, section) + } + return i, nil +} + func fullJobName(section, name string) (full string, err error) { if len(name) < 1 { err = fmt.Errorf("job name not set") @@ -470,7 +503,7 @@ func parsePrunes(m interface{}) (rets map[string]*Prune, err error) { asList := make(map[string]map[string]interface{}, 0) if err = mapstructure.Decode(m, &asList); err != nil { - return + return nil, errors.Wrap(err, "mapstructure error") } rets = make(map[string]*Prune, len(asList)) diff --git a/cmd/sampleconf/zrepl.yml b/cmd/sampleconf/zrepl.yml index d683caf..85a4c68 100644 --- a/cmd/sampleconf/zrepl.yml +++ b/cmd/sampleconf/zrepl.yml @@ -7,7 +7,7 @@ remotes: port: 22 identity_file: /etc/zrepl/identities/offsite_backups -pushs: +push: offsite: to: offsite_backups @@ -17,7 +17,7 @@ pushs: "tank/usr/home<": ok, } -pulls: +pull: offsite: from: offsite_backups @@ -35,7 +35,7 @@ pulls: "tank/usr/home":"mirrorpool/foo/bar" } -sinks: +sink: db1: mapping: { @@ -61,7 +61,7 @@ sinks: } -pull_acls: +pull_acl: office_backup: filter: { diff --git a/cmd/test.go b/cmd/test.go new file mode 100644 index 0000000..0e0a28f --- /dev/null +++ b/cmd/test.go @@ -0,0 +1,91 @@ +package cmd + +import ( + "os" + + "github.com/spf13/cobra" + "github.com/zrepl/zrepl/zfs" +) + +var testCmd = &cobra.Command{ + Use: "test", + Short: "test configuration", +} + +var testConfigSyntaxCmd = &cobra.Command{ + Use: "config", + Short: "test if config file can be parsed", + Run: doTestConfig, +} + +var testDatasetMapFilter = &cobra.Command{ + Use: "pattern jobtype.name test/zfs/dataset/path", + Short: "test dataset mapping / filter specified in config", + Example: ` zrepl test pattern prune.clean_backups tank/backups/legacyscript/foo`, + Run: doTestDatasetMapFilter, +} + +func init() { + RootCmd.AddCommand(testCmd) + testCmd.AddCommand(testConfigSyntaxCmd) + testCmd.AddCommand(testDatasetMapFilter) +} + +func doTestConfig(cmd *cobra.Command, args []string) { + log.Printf("config ok") + return +} + +func doTestDatasetMapFilter(cmd *cobra.Command, args []string) { + if len(args) != 2 { + log.Printf("specify job name as first postitional argument, test input as second") + log.Printf(cmd.UsageString()) + os.Exit(1) + } + n, i := args[0], args[1] + jobi, err := conf.resolveJobName(n) + if err != nil { + log.Printf("%s", err) + os.Exit(1) + } + + var mf DatasetMapFilter + switch j := jobi.(type) { + case *Autosnap: + mf = j.DatasetFilter + case *Prune: + mf = j.DatasetFilter + case *Pull: + mf = j.Mapping + case *Push: + mf = j.Filter + case DatasetMapFilter: + mf = j + default: + panic("incomplete implementation") + } + + ip, err := zfs.NewDatasetPath(i) + if err != nil { + log.Printf("cannot parse test input as ZFS dataset path: %s", err) + os.Exit(1) + } + + if mf.filterOnly { + pass, err := mf.Filter(ip) + if err != nil { + log.Printf("error evaluating filter: %s", err) + os.Exit(1) + } + log.Printf("filter result: %v", pass) + } else { + res, err := mf.Map(ip) + if err != nil { + log.Printf("error evaluating mapping: %s", err) + os.Exit(1) + } + log.Printf("%s => %s", ip.ToString(), res.ToString()) + + } + +}