Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b86bbda7fe | |||
| 2fbd9d8f8c | |||
| 18e101a04e | |||
| e594421322 | |||
| 0d4bfda2fb | |||
| b83c026cdc | |||
| 46caf31075 | |||
| 2b9d696b49 | |||
| 70afff6e3b | |||
| 01bbda13e5 | |||
| c5a8f6635f | |||
| 6f441c55dc | |||
| 7b34d6cba5 | |||
| 70f9c6482f | |||
| aed6149c8c | |||
| 0834a184b8 | |||
| f61295f76c | |||
| 0920a40751 | |||
| e0b5bd75f8 | |||
| 96e188d7c4 |
@@ -66,6 +66,7 @@ wrapup-and-checksum:
|
|||||||
-acf $(NOARCH_TARBALL) \
|
-acf $(NOARCH_TARBALL) \
|
||||||
$(ARTIFACTDIR)/docs/html \
|
$(ARTIFACTDIR)/docs/html \
|
||||||
$(ARTIFACTDIR)/bash_completion \
|
$(ARTIFACTDIR)/bash_completion \
|
||||||
|
$(ARTIFACTDIR)/_zrepl.zsh_completion \
|
||||||
$(ARTIFACTDIR)/go_env.txt \
|
$(ARTIFACTDIR)/go_env.txt \
|
||||||
dist \
|
dist \
|
||||||
config/samples
|
config/samples
|
||||||
@@ -160,7 +161,7 @@ platformtest: # do not track dependency on platformtest-bin to allow build of pl
|
|||||||
$(ZREPL_PLATFORMTEST_ARGS)
|
$(ZREPL_PLATFORMTEST_ARGS)
|
||||||
|
|
||||||
##################### NOARCH #####################
|
##################### NOARCH #####################
|
||||||
.PHONY: noarch $(ARTIFACTDIR)/bash_completion $(ARTIFACTDIR)/go_env.txt docs docs-clean
|
.PHONY: noarch $(ARTIFACTDIR)/bash_completion $(ARTIFACTDIR)/_zrepl.zsh_completion $(ARTIFACTDIR)/go_env.txt docs docs-clean
|
||||||
|
|
||||||
|
|
||||||
$(ARTIFACTDIR):
|
$(ARTIFACTDIR):
|
||||||
@@ -168,12 +169,16 @@ $(ARTIFACTDIR):
|
|||||||
$(ARTIFACTDIR)/docs: $(ARTIFACTDIR)
|
$(ARTIFACTDIR)/docs: $(ARTIFACTDIR)
|
||||||
mkdir -p "$@"
|
mkdir -p "$@"
|
||||||
|
|
||||||
noarch: $(ARTIFACTDIR)/bash_completion $(ARTIFACTDIR)/go_env.txt docs
|
noarch: $(ARTIFACTDIR)/bash_completion $(ARTIFACTDIR)/_zrepl.zsh_completion $(ARTIFACTDIR)/go_env.txt docs
|
||||||
# pass
|
# pass
|
||||||
|
|
||||||
$(ARTIFACTDIR)/bash_completion:
|
$(ARTIFACTDIR)/bash_completion:
|
||||||
$(MAKE) zrepl-bin GOOS=$(GOHOSTOS) GOARCH=$(GOHOSTARCH)
|
$(MAKE) zrepl-bin GOOS=$(GOHOSTOS) GOARCH=$(GOHOSTARCH)
|
||||||
artifacts/zrepl-$(GOHOSTOS)-$(GOHOSTARCH) bashcomp "$@"
|
artifacts/zrepl-$(GOHOSTOS)-$(GOHOSTARCH) gencompletion bash "$@"
|
||||||
|
|
||||||
|
$(ARTIFACTDIR)/_zrepl.zsh_completion:
|
||||||
|
$(MAKE) zrepl-bin GOOS=$(GOHOSTOS) GOARCH=$(GOHOSTARCH)
|
||||||
|
artifacts/zrepl-$(GOHOSTOS)-$(GOHOSTARCH) gencompletion zsh "$@"
|
||||||
|
|
||||||
$(ARTIFACTDIR)/go_env.txt:
|
$(ARTIFACTDIR)/go_env.txt:
|
||||||
$(GO_ENV_VARS) $(GO) env > $@
|
$(GO_ENV_VARS) $(GO) env > $@
|
||||||
|
|||||||
+45
-19
@@ -19,29 +19,55 @@ var rootCmd = &cobra.Command{
|
|||||||
Short: "One-stop ZFS replication solution",
|
Short: "One-stop ZFS replication solution",
|
||||||
}
|
}
|
||||||
|
|
||||||
var bashcompCmd = &cobra.Command{
|
func init() {
|
||||||
Use: "bashcomp path/to/out/file",
|
rootCmd.PersistentFlags().StringVar(&rootArgs.configPath, "config", "", "config file path")
|
||||||
Short: "generate bash completions",
|
}
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
if len(args) != 1 {
|
var genCompletionCmd = &cobra.Command{
|
||||||
fmt.Fprintf(os.Stderr, "specify exactly one positional agument\n")
|
Use: "gencompletion",
|
||||||
err := cmd.Usage()
|
Short: "generate shell auto-completions",
|
||||||
if err != nil {
|
}
|
||||||
panic(err)
|
|
||||||
}
|
type completionCmdInfo struct {
|
||||||
os.Exit(1)
|
genFunc func(outpath string) error
|
||||||
}
|
help string
|
||||||
if err := rootCmd.GenBashCompletionFile(args[0]); err != nil {
|
}
|
||||||
fmt.Fprintf(os.Stderr, "error generating bash completion: %s", err)
|
|
||||||
os.Exit(1)
|
var completionCmdMap = map[string]completionCmdInfo{
|
||||||
}
|
"zsh": {
|
||||||
|
rootCmd.GenZshCompletionFile,
|
||||||
|
" save to file `_zrepl` in your zsh's $fpath",
|
||||||
|
},
|
||||||
|
"bash": {
|
||||||
|
rootCmd.GenBashCompletionFile,
|
||||||
|
" save to a path and source that path in your .bashrc",
|
||||||
},
|
},
|
||||||
Hidden: true,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
rootCmd.PersistentFlags().StringVar(&rootArgs.configPath, "config", "", "config file path")
|
for sh, info := range completionCmdMap {
|
||||||
rootCmd.AddCommand(bashcompCmd)
|
sh, info := sh, info
|
||||||
|
genCompletionCmd.AddCommand(&cobra.Command{
|
||||||
|
Use: fmt.Sprintf("%s path/to/out/file", sh),
|
||||||
|
Short: fmt.Sprintf("generate %s completions", sh),
|
||||||
|
Example: info.help,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
if len(args) != 1 {
|
||||||
|
fmt.Fprintf(os.Stderr, "specify exactly one positional agument\n")
|
||||||
|
err := cmd.Usage()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if err := info.genFunc(args[0]); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "error generating %s completion: %s", sh, err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
rootCmd.AddCommand(genCompletionCmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
type Subcommand struct {
|
type Subcommand struct {
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
package client
|
|
||||||
|
|
||||||
import "github.com/zrepl/zrepl/cli"
|
|
||||||
|
|
||||||
var holdsCmdCreate = &cli.Subcommand{
|
|
||||||
Use: "create",
|
|
||||||
NoRequireConfig: true,
|
|
||||||
Short: `create zrepl-managed holds and boomkmarks (for debugging & development only!)`,
|
|
||||||
SetupSubcommands: func() []*cli.Subcommand {
|
|
||||||
return []*cli.Subcommand{
|
|
||||||
holdsCmdCreateStepHold,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
+1
-1
@@ -211,7 +211,7 @@ func doMigrateReplicationCursorFS(ctx context.Context, v1CursorJobs []job.Job, f
|
|||||||
}
|
}
|
||||||
fmt.Printf("identified owning job %q\n", owningJob.Name())
|
fmt.Printf("identified owning job %q\n", owningJob.Name())
|
||||||
|
|
||||||
bookmarks, err := zfs.ZFSListFilesystemVersions(fs, zfs.ListFilesystemVersionsOptions{
|
bookmarks, err := zfs.ZFSListFilesystemVersions(ctx, fs, zfs.ListFilesystemVersionsOptions{
|
||||||
Types: zfs.Bookmarks,
|
Types: zfs.Bookmarks,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -14,15 +14,15 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
HoldsCmd = &cli.Subcommand{
|
ZFSAbstractionsCmd = &cli.Subcommand{
|
||||||
Use: "holds",
|
Use: "zfs-abstraction",
|
||||||
Short: "manage holds & step bookmarks",
|
Short: "manage abstractions that zrepl builds on top of ZFS",
|
||||||
SetupSubcommands: func() []*cli.Subcommand {
|
SetupSubcommands: func() []*cli.Subcommand {
|
||||||
return []*cli.Subcommand{
|
return []*cli.Subcommand{
|
||||||
holdsCmdList,
|
zabsCmdList,
|
||||||
holdsCmdReleaseAll,
|
zabsCmdReleaseAll,
|
||||||
holdsCmdReleaseStale,
|
zabsCmdReleaseStale,
|
||||||
holdsCmdCreate,
|
zabsCmdCreate,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -30,7 +30,7 @@ var (
|
|||||||
|
|
||||||
// a common set of CLI flags that map to the fields of an
|
// a common set of CLI flags that map to the fields of an
|
||||||
// endpoint.ListZFSHoldsAndBookmarksQuery
|
// endpoint.ListZFSHoldsAndBookmarksQuery
|
||||||
type holdsFilterFlags struct {
|
type zabsFilterFlags struct {
|
||||||
Filesystems FilesystemsFilterFlag
|
Filesystems FilesystemsFilterFlag
|
||||||
Job JobIDFlag
|
Job JobIDFlag
|
||||||
Types AbstractionTypesFlag
|
Types AbstractionTypesFlag
|
||||||
@@ -38,7 +38,7 @@ type holdsFilterFlags struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// produce a query from the CLI flags
|
// produce a query from the CLI flags
|
||||||
func (f holdsFilterFlags) Query() (endpoint.ListZFSHoldsAndBookmarksQuery, error) {
|
func (f zabsFilterFlags) Query() (endpoint.ListZFSHoldsAndBookmarksQuery, error) {
|
||||||
q := endpoint.ListZFSHoldsAndBookmarksQuery{
|
q := endpoint.ListZFSHoldsAndBookmarksQuery{
|
||||||
FS: f.Filesystems.FlagValue(),
|
FS: f.Filesystems.FlagValue(),
|
||||||
What: f.Types.FlagValue(),
|
What: f.Types.FlagValue(),
|
||||||
@@ -48,7 +48,7 @@ func (f holdsFilterFlags) Query() (endpoint.ListZFSHoldsAndBookmarksQuery, error
|
|||||||
return q, q.Validate()
|
return q, q.Validate()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *holdsFilterFlags) registerHoldsFilterFlags(s *pflag.FlagSet, verb string) {
|
func (f *zabsFilterFlags) registerZabsFilterFlags(s *pflag.FlagSet, verb string) {
|
||||||
// Note: the default value is defined in the .FlagValue methods
|
// Note: the default value is defined in the .FlagValue methods
|
||||||
s.Var(&f.Filesystems, "fs", fmt.Sprintf("only %s holds on the specified filesystem [default: all filesystems] [comma-separated list of <dataset-pattern>:<ok|!> pairs]", verb))
|
s.Var(&f.Filesystems, "fs", fmt.Sprintf("only %s holds on the specified filesystem [default: all filesystems] [comma-separated list of <dataset-pattern>:<ok|!> pairs]", verb))
|
||||||
s.Var(&f.Job, "job", fmt.Sprintf("only %s holds created by the specified job [default: any job]", verb))
|
s.Var(&f.Job, "job", fmt.Sprintf("only %s holds created by the specified job [default: any job]", verb))
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import "github.com/zrepl/zrepl/cli"
|
||||||
|
|
||||||
|
var zabsCmdCreate = &cli.Subcommand{
|
||||||
|
Use: "create",
|
||||||
|
NoRequireConfig: true,
|
||||||
|
Short: `create zrepl ZFS abstractions (mostly useful for debugging & development, users should not need to use this command)`,
|
||||||
|
SetupSubcommands: func() []*cli.Subcommand {
|
||||||
|
return []*cli.Subcommand{
|
||||||
|
zabsCmdCreateStepHold,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -12,28 +12,28 @@ import (
|
|||||||
"github.com/zrepl/zrepl/zfs"
|
"github.com/zrepl/zrepl/zfs"
|
||||||
)
|
)
|
||||||
|
|
||||||
var holdsCreateStepHoldFlags struct {
|
var zabsCreateStepHoldFlags struct {
|
||||||
target string
|
target string
|
||||||
jobid JobIDFlag
|
jobid JobIDFlag
|
||||||
}
|
}
|
||||||
|
|
||||||
var holdsCmdCreateStepHold = &cli.Subcommand{
|
var zabsCmdCreateStepHold = &cli.Subcommand{
|
||||||
Use: "step",
|
Use: "step",
|
||||||
Run: doHoldsCreateStep,
|
Run: doZabsCreateStep,
|
||||||
NoRequireConfig: true,
|
NoRequireConfig: true,
|
||||||
Short: `create a step hold or bookmark`,
|
Short: `create a step hold or bookmark`,
|
||||||
SetupFlags: func(f *pflag.FlagSet) {
|
SetupFlags: func(f *pflag.FlagSet) {
|
||||||
f.StringVarP(&holdsCreateStepHoldFlags.target, "target", "t", "", "snapshot to be held / bookmark to be held")
|
f.StringVarP(&zabsCreateStepHoldFlags.target, "target", "t", "", "snapshot to be held / bookmark to be held")
|
||||||
f.VarP(&holdsCreateStepHoldFlags.jobid, "jobid", "j", "jobid for which the hold is installed")
|
f.VarP(&zabsCreateStepHoldFlags.jobid, "jobid", "j", "jobid for which the hold is installed")
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func doHoldsCreateStep(sc *cli.Subcommand, args []string) error {
|
func doZabsCreateStep(sc *cli.Subcommand, args []string) error {
|
||||||
if len(args) > 0 {
|
if len(args) > 0 {
|
||||||
return errors.New("subcommand takes no arguments")
|
return errors.New("subcommand takes no arguments")
|
||||||
}
|
}
|
||||||
|
|
||||||
f := &holdsCreateStepHoldFlags
|
f := &zabsCreateStepHoldFlags
|
||||||
|
|
||||||
fs, _, _, err := zfs.DecomposeVersionString(f.target)
|
fs, _, _, err := zfs.DecomposeVersionString(f.target)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -16,23 +16,23 @@ import (
|
|||||||
"github.com/zrepl/zrepl/util/chainlock"
|
"github.com/zrepl/zrepl/util/chainlock"
|
||||||
)
|
)
|
||||||
|
|
||||||
var holdsListFlags struct {
|
var zabsListFlags struct {
|
||||||
Filter holdsFilterFlags
|
Filter zabsFilterFlags
|
||||||
Json bool
|
Json bool
|
||||||
}
|
}
|
||||||
|
|
||||||
var holdsCmdList = &cli.Subcommand{
|
var zabsCmdList = &cli.Subcommand{
|
||||||
Use: "list",
|
Use: "list",
|
||||||
Run: doHoldsList,
|
Short: `list zrepl ZFS abstractions`,
|
||||||
|
Run: doZabsList,
|
||||||
NoRequireConfig: true,
|
NoRequireConfig: true,
|
||||||
Short: "list holds and bookmarks",
|
|
||||||
SetupFlags: func(f *pflag.FlagSet) {
|
SetupFlags: func(f *pflag.FlagSet) {
|
||||||
holdsListFlags.Filter.registerHoldsFilterFlags(f, "list")
|
zabsListFlags.Filter.registerZabsFilterFlags(f, "list")
|
||||||
f.BoolVar(&holdsListFlags.Json, "json", false, "emit JSON")
|
f.BoolVar(&zabsListFlags.Json, "json", false, "emit JSON")
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func doHoldsList(sc *cli.Subcommand, args []string) error {
|
func doZabsList(sc *cli.Subcommand, args []string) error {
|
||||||
var err error
|
var err error
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
@@ -40,7 +40,7 @@ func doHoldsList(sc *cli.Subcommand, args []string) error {
|
|||||||
return errors.New("this subcommand takes no positional arguments")
|
return errors.New("this subcommand takes no positional arguments")
|
||||||
}
|
}
|
||||||
|
|
||||||
q, err := holdsListFlags.Filter.Query()
|
q, err := zabsListFlags.Filter.Query()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "invalid filter specification on command line")
|
return errors.Wrap(err, "invalid filter specification on command line")
|
||||||
}
|
}
|
||||||
@@ -62,7 +62,7 @@ func doHoldsList(sc *cli.Subcommand, args []string) error {
|
|||||||
for a := range abstractions {
|
for a := range abstractions {
|
||||||
func() {
|
func() {
|
||||||
defer line.Lock().Unlock()
|
defer line.Lock().Unlock()
|
||||||
if holdsListFlags.Json {
|
if zabsListFlags.Json {
|
||||||
enc.SetIndent("", " ")
|
enc.SetIndent("", " ")
|
||||||
if err := enc.Encode(abstractions); err != nil {
|
if err := enc.Encode(abstractions); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
@@ -15,35 +15,35 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// shared between release-all and release-step
|
// shared between release-all and release-step
|
||||||
var holdsReleaseFlags struct {
|
var zabsReleaseFlags struct {
|
||||||
Filter holdsFilterFlags
|
Filter zabsFilterFlags
|
||||||
Json bool
|
Json bool
|
||||||
DryRun bool
|
DryRun bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func registerHoldsReleaseFlags(s *pflag.FlagSet) {
|
func registerZabsReleaseFlags(s *pflag.FlagSet) {
|
||||||
holdsReleaseFlags.Filter.registerHoldsFilterFlags(s, "release")
|
zabsReleaseFlags.Filter.registerZabsFilterFlags(s, "release")
|
||||||
s.BoolVar(&holdsReleaseFlags.Json, "json", false, "emit json instead of pretty-printed")
|
s.BoolVar(&zabsReleaseFlags.Json, "json", false, "emit json instead of pretty-printed")
|
||||||
s.BoolVar(&holdsReleaseFlags.DryRun, "dry-run", false, "do a dry-run")
|
s.BoolVar(&zabsReleaseFlags.DryRun, "dry-run", false, "do a dry-run")
|
||||||
}
|
}
|
||||||
|
|
||||||
var holdsCmdReleaseAll = &cli.Subcommand{
|
var zabsCmdReleaseAll = &cli.Subcommand{
|
||||||
Use: "release-all",
|
Use: "release-all",
|
||||||
Run: doHoldsReleaseAll,
|
Run: doZabsReleaseAll,
|
||||||
NoRequireConfig: true,
|
NoRequireConfig: true,
|
||||||
Short: `(DANGEROUS) release all zrepl-managed holds and bookmarks, mostly useful for uninstalling zrepl`,
|
Short: `(DANGEROUS) release ALL zrepl ZFS abstractions (mostly useful for uninstalling zrepl completely or for "de-zrepl-ing" a filesystem)`,
|
||||||
SetupFlags: registerHoldsReleaseFlags,
|
SetupFlags: registerZabsReleaseFlags,
|
||||||
}
|
}
|
||||||
|
|
||||||
var holdsCmdReleaseStale = &cli.Subcommand{
|
var zabsCmdReleaseStale = &cli.Subcommand{
|
||||||
Use: "release-stale",
|
Use: "release-stale",
|
||||||
Run: doHoldsReleaseStale,
|
Run: doZabsReleaseStale,
|
||||||
NoRequireConfig: true,
|
NoRequireConfig: true,
|
||||||
Short: `release stale zrepl-managed holds and boomkarks (useful if zrepl has a bug and doesn't do it by itself)`,
|
Short: `release stale zrepl ZFS abstractions (useful if zrepl has a bug and does not do it by itself)`,
|
||||||
SetupFlags: registerHoldsReleaseFlags,
|
SetupFlags: registerZabsReleaseFlags,
|
||||||
}
|
}
|
||||||
|
|
||||||
func doHoldsReleaseAll(sc *cli.Subcommand, args []string) error {
|
func doZabsReleaseAll(sc *cli.Subcommand, args []string) error {
|
||||||
var err error
|
var err error
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
@@ -51,7 +51,7 @@ func doHoldsReleaseAll(sc *cli.Subcommand, args []string) error {
|
|||||||
return errors.New("this subcommand takes no positional arguments")
|
return errors.New("this subcommand takes no positional arguments")
|
||||||
}
|
}
|
||||||
|
|
||||||
q, err := holdsReleaseFlags.Filter.Query()
|
q, err := zabsReleaseFlags.Filter.Query()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "invalid filter specification on command line")
|
return errors.Wrap(err, "invalid filter specification on command line")
|
||||||
}
|
}
|
||||||
@@ -65,10 +65,10 @@ func doHoldsReleaseAll(sc *cli.Subcommand, args []string) error {
|
|||||||
// proceed anyways with rest of abstractions
|
// proceed anyways with rest of abstractions
|
||||||
}
|
}
|
||||||
|
|
||||||
return doHoldsRelease_Common(ctx, abstractions)
|
return doZabsRelease_Common(ctx, abstractions)
|
||||||
}
|
}
|
||||||
|
|
||||||
func doHoldsReleaseStale(sc *cli.Subcommand, args []string) error {
|
func doZabsReleaseStale(sc *cli.Subcommand, args []string) error {
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
@@ -77,7 +77,7 @@ func doHoldsReleaseStale(sc *cli.Subcommand, args []string) error {
|
|||||||
return errors.New("this subcommand takes no positional arguments")
|
return errors.New("this subcommand takes no positional arguments")
|
||||||
}
|
}
|
||||||
|
|
||||||
q, err := holdsReleaseFlags.Filter.Query()
|
q, err := zabsReleaseFlags.Filter.Query()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "invalid filter specification on command line")
|
return errors.Wrap(err, "invalid filter specification on command line")
|
||||||
}
|
}
|
||||||
@@ -87,13 +87,13 @@ func doHoldsReleaseStale(sc *cli.Subcommand, args []string) error {
|
|||||||
return err // context clear by invocation of command
|
return err // context clear by invocation of command
|
||||||
}
|
}
|
||||||
|
|
||||||
return doHoldsRelease_Common(ctx, stalenessInfo.Stale)
|
return doZabsRelease_Common(ctx, stalenessInfo.Stale)
|
||||||
}
|
}
|
||||||
|
|
||||||
func doHoldsRelease_Common(ctx context.Context, destroy []endpoint.Abstraction) error {
|
func doZabsRelease_Common(ctx context.Context, destroy []endpoint.Abstraction) error {
|
||||||
|
|
||||||
if holdsReleaseFlags.DryRun {
|
if zabsReleaseFlags.DryRun {
|
||||||
if holdsReleaseFlags.Json {
|
if zabsReleaseFlags.Json {
|
||||||
m, err := json.MarshalIndent(destroy, "", " ")
|
m, err := json.MarshalIndent(destroy, "", " ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
@@ -121,7 +121,7 @@ func doHoldsRelease_Common(ctx context.Context, destroy []endpoint.Abstraction)
|
|||||||
|
|
||||||
for res := range outcome {
|
for res := range outcome {
|
||||||
hadErr = hadErr || res.DestroyErr != nil
|
hadErr = hadErr || res.DestroyErr != nil
|
||||||
if holdsReleaseFlags.Json {
|
if zabsReleaseFlags.Json {
|
||||||
err := enc.Encode(res)
|
err := enc.Encode(res)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
colorErr.Fprintf(os.Stderr, "cannot marshal there were errors in destroying the abstractions")
|
colorErr.Fprintf(os.Stderr, "cannot marshal there were errors in destroying the abstractions")
|
||||||
@@ -5,7 +5,11 @@ jobs:
|
|||||||
type: tcp
|
type: tcp
|
||||||
listen: "0.0.0.0:8888"
|
listen: "0.0.0.0:8888"
|
||||||
clients: {
|
clients: {
|
||||||
"192.168.122.123" : "client1"
|
"192.168.122.123" : "mysql01",
|
||||||
|
"192.168.122.42" : "mx01",
|
||||||
|
"2001:0db8:85a3::8a2e:0370:7334": "gateway",
|
||||||
|
"10.23.42.0/24": "cluster-*",
|
||||||
|
"fde4:8dba:82e1::/64": "san-*",
|
||||||
}
|
}
|
||||||
filesystems: {
|
filesystems: {
|
||||||
"<": true,
|
"<": true,
|
||||||
|
|||||||
@@ -485,7 +485,7 @@ var findSyncPointFSNoFilesystemVersionsErr = fmt.Errorf("no filesystem versions"
|
|||||||
|
|
||||||
func findSyncPointFSNextOptimalSnapshotTime(l Logger, now time.Time, interval time.Duration, prefix string, d *zfs.DatasetPath) (time.Time, error) {
|
func findSyncPointFSNextOptimalSnapshotTime(l Logger, now time.Time, interval time.Duration, prefix string, d *zfs.DatasetPath) (time.Time, error) {
|
||||||
|
|
||||||
fsvs, err := zfs.ZFSListFilesystemVersions(d, zfs.ListFilesystemVersionsOptions{
|
fsvs, err := zfs.ZFSListFilesystemVersions(context.TODO(), d, zfs.ListFilesystemVersionsOptions{
|
||||||
Types: zfs.Snapshots,
|
Types: zfs.Snapshots,
|
||||||
ShortnamePrefix: prefix,
|
ShortnamePrefix: prefix,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -62,7 +62,9 @@ Actual changelog:
|
|||||||
* |bugfix| |docs| snapshotting: clarify sync-up behavior and warn about filesystems
|
* |bugfix| |docs| snapshotting: clarify sync-up behavior and warn about filesystems
|
||||||
that will not be snapshotted until the sync-up phase is over
|
that will not be snapshotted until the sync-up phase is over
|
||||||
* |docs| Document new replication features in the :ref:`config overview <overview-how-replication-works>` and :repomasterlink:`replication/design.md`.
|
* |docs| Document new replication features in the :ref:`config overview <overview-how-replication-works>` and :repomasterlink:`replication/design.md`.
|
||||||
|
* |feature| documented subcommand to generate ``bash`` and ``zsh`` completions
|
||||||
* **[MAINTAINER NOTICE]** New platform tests in this version, please make sure you run them for your distro!
|
* **[MAINTAINER NOTICE]** New platform tests in this version, please make sure you run them for your distro!
|
||||||
|
* **[MAINTAINER NOTICE]** Please add the shell completions to the zrepl packages.
|
||||||
|
|
||||||
0.2.1
|
0.2.1
|
||||||
-----
|
-----
|
||||||
|
|||||||
@@ -52,8 +52,15 @@ Serve
|
|||||||
listen: ":8888"
|
listen: ":8888"
|
||||||
listen_freebind: true # optional, default false
|
listen_freebind: true # optional, default false
|
||||||
clients: {
|
clients: {
|
||||||
"192.168.122.123" : "mysql01"
|
"192.168.122.123" : "mysql01",
|
||||||
"192.168.122.123" : "mx01"
|
"192.168.122.42" : "mx01",
|
||||||
|
"2001:0db8:85a3::8a2e:0370:7334": "gateway",
|
||||||
|
|
||||||
|
# CIDR masks require a '*' in the client identity string
|
||||||
|
# that is expanded to the client's IP address
|
||||||
|
|
||||||
|
"10.23.42.0/24": "cluster-*"
|
||||||
|
"fde4:8dba:82e1::/64": "san-*"
|
||||||
}
|
}
|
||||||
...
|
...
|
||||||
|
|
||||||
|
|||||||
+10
-129
@@ -1,138 +1,19 @@
|
|||||||
.. _binary releases: https://github.com/zrepl/zrepl/releases
|
|
||||||
|
|
||||||
.. _installation:
|
.. _installation_toc:
|
||||||
|
|
||||||
|
************
|
||||||
Installation
|
Installation
|
||||||
============
|
************
|
||||||
|
|
||||||
.. TIP::
|
.. TIP::
|
||||||
|
|
||||||
Note: check out the :ref:`tutorial` if you want a first impression of zrepl.
|
Note: check out the :ref:`tutorial` if you want a first impression of zrepl.
|
||||||
|
|
||||||
User Privileges
|
.. toctree::
|
||||||
---------------
|
|
||||||
|
|
||||||
It is possible to run zrepl as an unprivileged user in combination with
|
installation/user-privileges
|
||||||
`ZFS delegation <https://www.freebsd.org/doc/handbook/zfs-zfs-allow.html>`_.
|
installation/packages
|
||||||
Also, there is the possibility to run it in a jail on FreeBSD by delegating a dataset to the jail.
|
installation/apt-repos
|
||||||
However, until we get around documenting those setups, you will have to run zrepl as root or experiment yourself :)
|
installation/compile-from-source
|
||||||
|
installation/freebsd-jail-with-iocage
|
||||||
Packages
|
installation/what-next
|
||||||
--------
|
|
||||||
|
|
||||||
zrepl source releases are signed & tagged by the author in the git repository.
|
|
||||||
Your OS vendor may provide binary packages of zrepl through the package manager.
|
|
||||||
Additionally, `binary releases`_ are provided on GitHub.
|
|
||||||
The following list may be incomplete, feel free to submit a PR with an update:
|
|
||||||
|
|
||||||
.. list-table::
|
|
||||||
:header-rows: 1
|
|
||||||
|
|
||||||
* - OS / Distro
|
|
||||||
- Install Command
|
|
||||||
- Link
|
|
||||||
* - FreeBSD
|
|
||||||
- ``pkg install zrepl``
|
|
||||||
- `<https://www.freshports.org/sysutils/zrepl/>`_
|
|
||||||
* - MacOS
|
|
||||||
- ``brew install zrepl``
|
|
||||||
- Available on `homebrew <https://brew.sh>`_
|
|
||||||
* - Arch Linux
|
|
||||||
- ``yay install zrepl``
|
|
||||||
- Available on `AUR <https://aur.archlinux.org/packages/zrepl>`_
|
|
||||||
* - Fedora
|
|
||||||
- ``dnf install zrepl``
|
|
||||||
- Available on `COPR <https://copr.fedorainfracloud.org/coprs/poettlerric/zrepl/>`_
|
|
||||||
* - CentOS/RHEL
|
|
||||||
- ``yum install zrepl``
|
|
||||||
- Available on `COPR <https://copr.fedorainfracloud.org/coprs/poettlerric/zrepl/>`_
|
|
||||||
* - Debian + Ubuntu
|
|
||||||
- ``apt install zrepl``
|
|
||||||
- APT repository config :ref:`see below <installation-apt-repos>`
|
|
||||||
* - OmniOS
|
|
||||||
- ``pkg install zrepl``
|
|
||||||
- Available since `r151030 <https://pkg.omniosce.org/r151030/extra/en/search.shtml?token=zrepl&action=Search>`_
|
|
||||||
* - Void Linux
|
|
||||||
- ``xbps-install zrepl``
|
|
||||||
- Available since `a88a2a4 <https://github.com/void-linux/void-packages/commit/a88a2a4d7bf56072dadf61ab56b8424e39155890>`_
|
|
||||||
* - Others
|
|
||||||
-
|
|
||||||
- Use `binary releases`_ or build from source.
|
|
||||||
|
|
||||||
.. _installation-apt-repos:
|
|
||||||
|
|
||||||
Debian / Ubuntu APT repositories
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
We maintain APT repositories for Debian, Ubuntu and derivatives.
|
|
||||||
The fingerprint of the signing key is ``E101 418F D3D6 FBCB 9D65 A62D 7086 99FC 5F2E BF16``.
|
|
||||||
It is available at `<https://zrepl.cschwarz.com/apt/apt-key.asc>`_ .
|
|
||||||
Please open an issue `in the packaging repository <https://github.com/zrepl/debian-binary-packaging>`_ if you encounter any issues with the repository.
|
|
||||||
|
|
||||||
The following snippet configure the repository for your Debian or Ubuntu release:
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
apt update && apt install curl gnupg lsb-release; \
|
|
||||||
ARCH="$(dpkg --print-architecture)"; \
|
|
||||||
CODENAME="$(lsb_release -i -s | tr '[:upper:]' '[:lower:]') $(lsb_release -c -s | tr '[:upper:]' '[:lower:]')"; \
|
|
||||||
echo "Using Distro and Codename: $CODENAME"; \
|
|
||||||
(curl https://zrepl.cschwarz.com/apt/apt-key.asc | apt-key add -) && \
|
|
||||||
(echo "deb [arch=$ARCH] https://zrepl.cschwarz.com/apt/$CODENAME main" > /etc/apt/sources.list.d/zrepl.list) && \
|
|
||||||
apt update
|
|
||||||
|
|
||||||
|
|
||||||
.. NOTE::
|
|
||||||
|
|
||||||
Until zrepl reaches 1.0, all APT repositories will be updated to the latest zrepl release immediately.
|
|
||||||
This includes breaking changes between zrepl versions.
|
|
||||||
Use ``apt-mark hold zrepl`` to prevent upgrades of zrepl.
|
|
||||||
|
|
||||||
Compile From Source
|
|
||||||
~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
Producing a release requires **Go 1.11** or newer and **Python 3** + **pip3** + ``docs/requirements.txt`` for the Sphinx documentation.
|
|
||||||
A tutorial to install Go is available over at `golang.org <https://golang.org/doc/install>`_.
|
|
||||||
Python and pip3 should probably be installed via your distro's package manager.
|
|
||||||
|
|
||||||
Alternatively, you can use the Docker build process:
|
|
||||||
it is used to produce the official zrepl `binary releases`_
|
|
||||||
and serves as a reference for build dependencies and procedure:
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
git clone https://github.com/zrepl/zrepl.git && \
|
|
||||||
cd zrepl && \
|
|
||||||
sudo docker build -t zrepl_build -f build.Dockerfile . && \
|
|
||||||
sudo docker run -it --rm \
|
|
||||||
-v "${PWD}:/src" \
|
|
||||||
--user "$(id -u):$(id -g)" \
|
|
||||||
zrepl_build make release
|
|
||||||
|
|
||||||
Alternatively, you can install build dependencies on your local system and then build in your ``$GOPATH``:
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
mkdir -p "${GOPATH}/src/github.com/zrepl/zrepl"
|
|
||||||
git clone https://github.com/zrepl/zrepl.git "${GOPATH}/src/github.com/zrepl/zrepl"
|
|
||||||
cd "${GOPATH}/src/github.com/zrepl/zrepl"
|
|
||||||
python3 -m venv3
|
|
||||||
source venv3/bin/activate
|
|
||||||
./lazy.sh devsetup
|
|
||||||
make release
|
|
||||||
|
|
||||||
The Python venv is used for the documentation build dependencies.
|
|
||||||
If you just want to build the zrepl binary, leave it out and use `./lazy.sh godep` instead.
|
|
||||||
Either way, all build results are located in the ``artifacts/`` directory.
|
|
||||||
|
|
||||||
.. NOTE::
|
|
||||||
|
|
||||||
It is your job to install the appropriate binary in the zrepl users's ``$PATH``, e.g. ``/usr/local/bin/zrepl``.
|
|
||||||
Otherwise, the examples in the :ref:`tutorial` may need to be adjusted.
|
|
||||||
|
|
||||||
What next?
|
|
||||||
----------
|
|
||||||
|
|
||||||
Read the :ref:`configuration chapter<configuration_toc>` and then continue with the :ref:`usage chapter<usage>`.
|
|
||||||
|
|
||||||
**Reminder**: If you want a quick introduction, please read the :ref:`tutorial`.
|
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
|
||||||
|
.. _installation-apt-repos:
|
||||||
|
|
||||||
|
Debian / Ubuntu APT repositories
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
We maintain APT repositories for Debian, Ubuntu and derivatives.
|
||||||
|
The fingerprint of the signing key is ``E101 418F D3D6 FBCB 9D65 A62D 7086 99FC 5F2E BF16``.
|
||||||
|
It is available at `<https://zrepl.cschwarz.com/apt/apt-key.asc>`_ .
|
||||||
|
Please open an issue `in the packaging repository <https://github.com/zrepl/debian-binary-packaging>`_ if you encounter any issues with the repository.
|
||||||
|
|
||||||
|
The following snippet configure the repository for your Debian or Ubuntu release:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
apt update && apt install curl gnupg lsb-release; \
|
||||||
|
ARCH="$(dpkg --print-architecture)"; \
|
||||||
|
CODENAME="$(lsb_release -i -s | tr '[:upper:]' '[:lower:]') $(lsb_release -c -s | tr '[:upper:]' '[:lower:]')"; \
|
||||||
|
echo "Using Distro and Codename: $CODENAME"; \
|
||||||
|
(curl https://zrepl.cschwarz.com/apt/apt-key.asc | apt-key add -) && \
|
||||||
|
(echo "deb [arch=$ARCH] https://zrepl.cschwarz.com/apt/$CODENAME main" > /etc/apt/sources.list.d/zrepl.list) && \
|
||||||
|
apt update
|
||||||
|
|
||||||
|
|
||||||
|
.. NOTE::
|
||||||
|
|
||||||
|
Until zrepl reaches 1.0, all APT repositories will be updated to the latest zrepl release immediately.
|
||||||
|
This includes breaking changes between zrepl versions.
|
||||||
|
Use ``apt-mark hold zrepl`` to prevent upgrades of zrepl.
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
.. _binary releases: https://github.com/zrepl/zrepl/releases
|
||||||
|
|
||||||
|
.. _installation-compile-from-source:
|
||||||
|
|
||||||
|
Compile From Source
|
||||||
|
~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Producing a release requires **Go 1.11** or newer and **Python 3** + **pip3** + ``docs/requirements.txt`` for the Sphinx documentation.
|
||||||
|
A tutorial to install Go is available over at `golang.org <https://golang.org/doc/install>`_.
|
||||||
|
Python and pip3 should probably be installed via your distro's package manager.
|
||||||
|
|
||||||
|
Alternatively, you can use the Docker build process:
|
||||||
|
it is used to produce the official zrepl `binary releases`_
|
||||||
|
and serves as a reference for build dependencies and procedure:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
git clone https://github.com/zrepl/zrepl.git && \
|
||||||
|
cd zrepl && \
|
||||||
|
sudo docker build -t zrepl_build -f build.Dockerfile . && \
|
||||||
|
sudo docker run -it --rm \
|
||||||
|
-v "${PWD}:/src" \
|
||||||
|
--user "$(id -u):$(id -g)" \
|
||||||
|
zrepl_build make release
|
||||||
|
|
||||||
|
Alternatively, you can install build dependencies on your local system and then build in your ``$GOPATH``:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
mkdir -p "${GOPATH}/src/github.com/zrepl/zrepl"
|
||||||
|
git clone https://github.com/zrepl/zrepl.git "${GOPATH}/src/github.com/zrepl/zrepl"
|
||||||
|
cd "${GOPATH}/src/github.com/zrepl/zrepl"
|
||||||
|
python3 -m venv3
|
||||||
|
source venv3/bin/activate
|
||||||
|
./lazy.sh devsetup
|
||||||
|
make release
|
||||||
|
|
||||||
|
The Python venv is used for the documentation build dependencies.
|
||||||
|
If you just want to build the zrepl binary, leave it out and use `./lazy.sh godep` instead.
|
||||||
|
Either way, all build results are located in the ``artifacts/`` directory.
|
||||||
|
|
||||||
|
.. NOTE::
|
||||||
|
|
||||||
|
It is your job to install the appropriate binary in the zrepl users's ``$PATH``, e.g. ``/usr/local/bin/zrepl``.
|
||||||
|
Otherwise, the examples in the :ref:`tutorial` may need to be adjusted.
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
.. include:: ../global.rst.inc
|
||||||
|
|
||||||
|
.. _installation-freebsd-jail-with-iocage:
|
||||||
|
|
||||||
|
FreeBSD Jail With iocage
|
||||||
|
========================
|
||||||
|
|
||||||
|
|
||||||
|
This tutorial shows how zrepl can be installed on FreeBSD, or FreeNAS in a jail using iocage.
|
||||||
|
While this tutorial focuses on using iocage, much of the setup would be similar
|
||||||
|
using a different jail manager.
|
||||||
|
|
||||||
|
.. NOTE::
|
||||||
|
|
||||||
|
From a security perspective, just keep in mind that ``zfs send``/``recv`` was never designed with
|
||||||
|
jails in mind, an attacker could probably crash the receive-side kernel or worse induce stateful
|
||||||
|
damage to the receive-side pool if they were able to get access to the jail.
|
||||||
|
|
||||||
|
The jail doesn't provide security benefits, but only management ones.
|
||||||
|
|
||||||
|
Requirements
|
||||||
|
------------
|
||||||
|
|
||||||
|
A dataset that will be delegated to the jail needs to be created if one does not already exist.
|
||||||
|
For the tutorial ``tank/zrepl`` will be used.
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
zfs create -o mountpoint=none tank/zrepl
|
||||||
|
|
||||||
|
The only software requirements on the host system are ``iocage``, which can be installed
|
||||||
|
from ports or packages.
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
pkg install py37-iocage
|
||||||
|
|
||||||
|
.. NOTE::
|
||||||
|
|
||||||
|
By default ``iocage`` will "activate" on first use which will set up some defaults such as
|
||||||
|
which pool will be used. To activate ``iocage`` manually the ``iocage activate`` command can be used.
|
||||||
|
|
||||||
|
Jail Creation
|
||||||
|
-------------
|
||||||
|
|
||||||
|
There are two options for jail creation using FreeBSD.
|
||||||
|
|
||||||
|
1. Manually set up the jail from scratch
|
||||||
|
2. Create the jail using the ``zrepl`` plugin. On FreeNAS this is possible from the user interface using the community index.
|
||||||
|
|
||||||
|
Manual Jail
|
||||||
|
###########
|
||||||
|
|
||||||
|
Create a jail, using the same release as the host, called ``zrepl`` that will be automatically started at boot.
|
||||||
|
The jail will have ``tank/zrepl`` delegated into it.
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
iocage create --release "$(freebsd-version -k | cut -d '-' -f '1,2')" --name zrepl \
|
||||||
|
boot=on nat=1 \
|
||||||
|
jail_zfs=on \
|
||||||
|
jail_zfs_dataset=zrepl \
|
||||||
|
jail_zfs_mountpoint='none'
|
||||||
|
|
||||||
|
Enter the jail:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
iocage console zrepl
|
||||||
|
|
||||||
|
Install ``zrepl``
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
pkg update && pkg upgrade
|
||||||
|
pkg install zrepl
|
||||||
|
|
||||||
|
Create the log file ``/var/log/zrepl.log``
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
touch /var/log/zrepl.log && service newsyslog restart
|
||||||
|
|
||||||
|
Tell syslogd to redirect facility local0 to the ``zrepl.log`` file:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
service syslogd reload
|
||||||
|
|
||||||
|
Enable the zrepl daemon to start automatically at boot:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
sysrc zrepl_enable="YES"
|
||||||
|
|
||||||
|
|
||||||
|
Plugin
|
||||||
|
######
|
||||||
|
|
||||||
|
When using the plugin, ``zrepl`` will be installed for you in a jail using the following ``iocage`` properties.
|
||||||
|
|
||||||
|
* ``nat=1``
|
||||||
|
* ``jail_zfs=on``
|
||||||
|
* ``jail_zfs_mountpoint=none``
|
||||||
|
|
||||||
|
Additionally the delegated dataset should be specified upon creation, and optionally start on boot can be set.
|
||||||
|
This can also be done from the FreeNAS webui.
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
fetch https://raw.githubusercontent.com/ix-plugin-hub/iocage-plugin-index/master/zrepl.json -o /tmp/zrepl.json
|
||||||
|
iocage fetch -P /tmp/zrepl.json --name zrepl jail_zfs_dataset=zrepl boot=on
|
||||||
|
|
||||||
|
Configuration
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Now ``zrepl`` can be configured.
|
||||||
|
|
||||||
|
Enter the jail.
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
iocage console zrepl
|
||||||
|
|
||||||
|
Modify the ``/usr/local/etc/zrepl/zrepl.yml`` configuration file.
|
||||||
|
|
||||||
|
.. TIP::
|
||||||
|
|
||||||
|
Note: check out the :ref:`tutorial` for examples of a ``sink`` job.
|
||||||
|
|
||||||
|
Now ``zrepl`` can be started.
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
service zrepl start
|
||||||
|
|
||||||
|
Summary
|
||||||
|
-------
|
||||||
|
|
||||||
|
Congratulations, you have a working jail!
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
.. _installation-packages:
|
||||||
|
|
||||||
|
.. _binary releases: https://github.com/zrepl/zrepl/releases
|
||||||
|
|
||||||
|
Packages
|
||||||
|
--------
|
||||||
|
|
||||||
|
zrepl source releases are signed & tagged by the author in the git repository.
|
||||||
|
Your OS vendor may provide binary packages of zrepl through the package manager.
|
||||||
|
Additionally, `binary releases`_ are provided on GitHub.
|
||||||
|
The following list may be incomplete, feel free to submit a PR with an update:
|
||||||
|
|
||||||
|
.. list-table::
|
||||||
|
:header-rows: 1
|
||||||
|
|
||||||
|
* - OS / Distro
|
||||||
|
- Install Command
|
||||||
|
- Link
|
||||||
|
* - FreeBSD
|
||||||
|
- ``pkg install zrepl``
|
||||||
|
- `<https://www.freshports.org/sysutils/zrepl/>`_
|
||||||
|
|
||||||
|
:ref:`installation-freebsd-jail-with-iocage`
|
||||||
|
* - FreeNAS
|
||||||
|
-
|
||||||
|
- :ref:`installation-freebsd-jail-with-iocage`
|
||||||
|
* - MacOS
|
||||||
|
- ``brew install zrepl``
|
||||||
|
- Available on `homebrew <https://brew.sh>`_
|
||||||
|
* - Arch Linux
|
||||||
|
- ``yay install zrepl``
|
||||||
|
- Available on `AUR <https://aur.archlinux.org/packages/zrepl>`_
|
||||||
|
* - Fedora
|
||||||
|
- ``dnf install zrepl``
|
||||||
|
- Available on `COPR <https://copr.fedorainfracloud.org/coprs/poettlerric/zrepl/>`_
|
||||||
|
* - CentOS/RHEL
|
||||||
|
- ``yum install zrepl``
|
||||||
|
- Available on `COPR <https://copr.fedorainfracloud.org/coprs/poettlerric/zrepl/>`_
|
||||||
|
* - Debian + Ubuntu
|
||||||
|
- ``apt install zrepl``
|
||||||
|
- APT repository config :ref:`see below <installation-apt-repos>`
|
||||||
|
* - OmniOS
|
||||||
|
- ``pkg install zrepl``
|
||||||
|
- Available since `r151030 <https://pkg.omniosce.org/r151030/extra/en/search.shtml?token=zrepl&action=Search>`_
|
||||||
|
* - Void Linux
|
||||||
|
- ``xbps-install zrepl``
|
||||||
|
- Available since `a88a2a4 <https://github.com/void-linux/void-packages/commit/a88a2a4d7bf56072dadf61ab56b8424e39155890>`_
|
||||||
|
* - Others
|
||||||
|
-
|
||||||
|
- Use `binary releases`_ or build from source.
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
.. _installation-user-privileges:
|
||||||
|
|
||||||
|
User Privileges
|
||||||
|
---------------
|
||||||
|
|
||||||
|
It is possible to run zrepl as an unprivileged user in combination with
|
||||||
|
`ZFS delegation <https://www.freebsd.org/doc/handbook/zfs-zfs-allow.html>`_.
|
||||||
|
Also, there is the possibility to run it in a jail on FreeBSD by delegating a dataset to the jail.
|
||||||
|
|
||||||
|
.. TIP::
|
||||||
|
|
||||||
|
Note: check out the :ref:`installation-freebsd-jail-with-iocage` for FreeBSD jail setup instructions.
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
.. _installation-what-next:
|
||||||
|
|
||||||
|
What next?
|
||||||
|
----------
|
||||||
|
|
||||||
|
Read the :ref:`configuration chapter<configuration_toc>` and then continue with the :ref:`usage chapter<usage>`.
|
||||||
|
|
||||||
|
**Reminder**: If you want a quick introduction, please read the :ref:`tutorial`.
|
||||||
+1
-1
@@ -47,7 +47,7 @@ We can model this situation as two jobs:
|
|||||||
Install zrepl
|
Install zrepl
|
||||||
-------------
|
-------------
|
||||||
|
|
||||||
Follow the :ref:`OS-specific installation instructions <installation>` and come back here.
|
Follow the :ref:`OS-specific installation instructions <installation_toc>` and come back here.
|
||||||
|
|
||||||
Generate TLS Certificates
|
Generate TLS Certificates
|
||||||
-------------------------
|
-------------------------
|
||||||
|
|||||||
+2
-2
@@ -38,8 +38,8 @@ CLI Overview
|
|||||||
* - ``zrepl migrate``
|
* - ``zrepl migrate``
|
||||||
- | perform on-disk state / ZFS property migrations
|
- | perform on-disk state / ZFS property migrations
|
||||||
| (see :ref:`changelog <changelog>` for details)
|
| (see :ref:`changelog <changelog>` for details)
|
||||||
* - ``zrepl holds``
|
* - ``zrepl zfs-abstractions``
|
||||||
- list and remove holds and step bookmarks created by zrepl (see :ref:`overview <replication-cursor-and-last-received-hold>` )
|
- list and remove zrepl's abstractions on top of ZFS, e.g. holds and step bookmarks (see :ref:`overview <replication-cursor-and-last-received-hold>` )
|
||||||
|
|
||||||
.. _usage-zrepl-daemon:
|
.. _usage-zrepl-daemon:
|
||||||
|
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ func (s *Sender) ListFilesystemVersions(ctx context.Context, r *pdu.ListFilesyst
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
fsvs, err := zfs.ZFSListFilesystemVersions(lp, zfs.ListFilesystemVersionsOptions{})
|
fsvs, err := zfs.ZFSListFilesystemVersions(ctx, lp, zfs.ListFilesystemVersionsOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -648,7 +648,7 @@ func (s *Receiver) ListFilesystemVersions(ctx context.Context, req *pdu.ListFile
|
|||||||
}
|
}
|
||||||
// TODO share following code with sender
|
// TODO share following code with sender
|
||||||
|
|
||||||
fsvs, err := zfs.ZFSListFilesystemVersions(lp, zfs.ListFilesystemVersionsOptions{})
|
fsvs, err := zfs.ZFSListFilesystemVersions(ctx, lp, zfs.ListFilesystemVersionsOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -467,7 +467,7 @@ func (e ListAbstractionsErrors) Error() string {
|
|||||||
}
|
}
|
||||||
msgs := make([]string, len(e))
|
msgs := make([]string, len(e))
|
||||||
for i := range e {
|
for i := range e {
|
||||||
msgs[i] = e.Error()
|
msgs[i] = e[i].Error()
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("list endpoint abstractions: multiple errors:\n%s", strings.Join(msgs, "\n"))
|
return fmt.Sprintf("list endpoint abstractions: multiple errors:\n%s", strings.Join(msgs, "\n"))
|
||||||
}
|
}
|
||||||
@@ -573,7 +573,7 @@ func listAbstractionsImplFS(ctx context.Context, fs string, query *ListZFSHoldsA
|
|||||||
whatTypes[zfs.Snapshot] = true
|
whatTypes[zfs.Snapshot] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fsvs, err := zfs.ZFSListFilesystemVersions(fsp, zfs.ListFilesystemVersionsOptions{
|
fsvs, err := zfs.ZFSListFilesystemVersions(ctx, fsp, zfs.ListFilesystemVersionsOptions{
|
||||||
Types: whatTypes,
|
Types: whatTypes,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -731,6 +731,9 @@ func listStaleFiltering(abs []Abstraction, sinceBound *CreateTXGRangeBound) *Sta
|
|||||||
}
|
}
|
||||||
stepFirstNotStaleCandidates := make(map[fsAndJobId]stepFirstNotStaleCandidate) // empty map => will always return nil
|
stepFirstNotStaleCandidates := make(map[fsAndJobId]stepFirstNotStaleCandidate) // empty map => will always return nil
|
||||||
for _, a := range abs {
|
for _, a := range abs {
|
||||||
|
if a.GetJobID() == nil {
|
||||||
|
continue // already put those into always-live list noJobId in above loop
|
||||||
|
}
|
||||||
key := fsAndJobId{a.GetFS(), *a.GetJobID()}
|
key := fsAndJobId{a.GetFS(), *a.GetJobID()}
|
||||||
c := stepFirstNotStaleCandidates[key]
|
c := stepFirstNotStaleCandidates[key]
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ require (
|
|||||||
github.com/onsi/gomega v1.7.0 // indirect
|
github.com/onsi/gomega v1.7.0 // indirect
|
||||||
github.com/pkg/errors v0.8.1
|
github.com/pkg/errors v0.8.1
|
||||||
github.com/pkg/profile v1.2.1
|
github.com/pkg/profile v1.2.1
|
||||||
github.com/problame/go-netssh v0.0.0-20191209123953-18d8aa6923c7
|
github.com/problame/go-netssh v0.0.0-20200601114649-26439f9f0dc5
|
||||||
github.com/prometheus/client_golang v1.2.1
|
github.com/prometheus/client_golang v1.2.1
|
||||||
github.com/sergi/go-diff v1.0.1-0.20180205163309-da645544ed44 // go1.12 thinks it needs this
|
github.com/sergi/go-diff v1.0.1-0.20180205163309-da645544ed44 // go1.12 thinks it needs this
|
||||||
github.com/spf13/cobra v0.0.2
|
github.com/spf13/cobra v0.0.2
|
||||||
|
|||||||
@@ -208,6 +208,8 @@ github.com/problame/go-netssh v0.0.0-20191026123024-f34099f4f6b1 h1:HH8yzlaZq/A8
|
|||||||
github.com/problame/go-netssh v0.0.0-20191026123024-f34099f4f6b1/go.mod h1:JRs3nyBjvOv/9YHC+Vlw9z1Jfzl5s5t0BNnTzmclj1c=
|
github.com/problame/go-netssh v0.0.0-20191026123024-f34099f4f6b1/go.mod h1:JRs3nyBjvOv/9YHC+Vlw9z1Jfzl5s5t0BNnTzmclj1c=
|
||||||
github.com/problame/go-netssh v0.0.0-20191209123953-18d8aa6923c7 h1:Oik8tLrLhoMq4fduDRuNNOAQ+q0M0dXkjAYLUf4+mAc=
|
github.com/problame/go-netssh v0.0.0-20191209123953-18d8aa6923c7 h1:Oik8tLrLhoMq4fduDRuNNOAQ+q0M0dXkjAYLUf4+mAc=
|
||||||
github.com/problame/go-netssh v0.0.0-20191209123953-18d8aa6923c7/go.mod h1:Ba6RaFpK1+IHWs1wLZ6sCBuXkt4iAVLeOG5GinXHusM=
|
github.com/problame/go-netssh v0.0.0-20191209123953-18d8aa6923c7/go.mod h1:Ba6RaFpK1+IHWs1wLZ6sCBuXkt4iAVLeOG5GinXHusM=
|
||||||
|
github.com/problame/go-netssh v0.0.0-20200601114649-26439f9f0dc5 h1:1eSrO2WAeSXjMol8WRKW9LekL1252VIMaKQwWkmEdvs=
|
||||||
|
github.com/problame/go-netssh v0.0.0-20200601114649-26439f9f0dc5/go.mod h1:Ba6RaFpK1+IHWs1wLZ6sCBuXkt4iAVLeOG5GinXHusM=
|
||||||
github.com/prometheus/client_golang v0.0.0-20180410130117-e11c6ff8170b/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
github.com/prometheus/client_golang v0.0.0-20180410130117-e11c6ff8170b/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||||
@@ -263,6 +265,7 @@ github.com/spf13/viper v1.0.2/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7Sr
|
|||||||
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
|
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ func init() {
|
|||||||
cli.AddSubcommand(client.PprofCmd)
|
cli.AddSubcommand(client.PprofCmd)
|
||||||
cli.AddSubcommand(client.TestCmd)
|
cli.AddSubcommand(client.TestCmd)
|
||||||
cli.AddSubcommand(client.MigrateCmd)
|
cli.AddSubcommand(client.MigrateCmd)
|
||||||
cli.AddSubcommand(client.HoldsCmd)
|
cli.AddSubcommand(client.ZFSAbstractionsCmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ func (c *Context) Logf(format string, args ...interface{}) {
|
|||||||
|
|
||||||
func (c *Context) Errorf(format string, args ...interface{}) {
|
func (c *Context) Errorf(format string, args ...interface{}) {
|
||||||
GetLog(c).Printf(format, args...)
|
GetLog(c).Printf(format, args...)
|
||||||
|
c.FailNow()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Context) FailNow() {
|
func (c *Context) FailNow() {
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ func ListFilesystemVersionsTypeFilteringAndPrefix(t *platformtest.Context) {
|
|||||||
fs := fmt.Sprintf("%s/foo bar", t.RootDataset)
|
fs := fmt.Sprintf("%s/foo bar", t.RootDataset)
|
||||||
|
|
||||||
// no options := all types
|
// no options := all types
|
||||||
vs, err := zfs.ZFSListFilesystemVersions(mustDatasetPath(fs), zfs.ListFilesystemVersionsOptions{})
|
vs, err := zfs.ZFSListFilesystemVersions(t, mustDatasetPath(fs), zfs.ListFilesystemVersionsOptions{})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, []string{
|
require.Equal(t, []string{
|
||||||
"#blup 1", "#bookfoo 1", "#bookfoo 2", "#foo 1", "#foo 2",
|
"#blup 1", "#bookfoo 1", "#bookfoo 2", "#foo 1", "#foo 2",
|
||||||
@@ -51,21 +51,21 @@ func ListFilesystemVersionsTypeFilteringAndPrefix(t *platformtest.Context) {
|
|||||||
}, versionRelnamesSorted(vs))
|
}, versionRelnamesSorted(vs))
|
||||||
|
|
||||||
// just snapshots
|
// just snapshots
|
||||||
vs, err = zfs.ZFSListFilesystemVersions(mustDatasetPath(fs), zfs.ListFilesystemVersionsOptions{
|
vs, err = zfs.ZFSListFilesystemVersions(t, mustDatasetPath(fs), zfs.ListFilesystemVersionsOptions{
|
||||||
Types: zfs.Snapshots,
|
Types: zfs.Snapshots,
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, []string{"@ foo with leading whitespace", "@blup 1", "@foo 1", "@foo 2"}, versionRelnamesSorted(vs))
|
require.Equal(t, []string{"@ foo with leading whitespace", "@blup 1", "@foo 1", "@foo 2"}, versionRelnamesSorted(vs))
|
||||||
|
|
||||||
// just bookmarks
|
// just bookmarks
|
||||||
vs, err = zfs.ZFSListFilesystemVersions(mustDatasetPath(fs), zfs.ListFilesystemVersionsOptions{
|
vs, err = zfs.ZFSListFilesystemVersions(t, mustDatasetPath(fs), zfs.ListFilesystemVersionsOptions{
|
||||||
Types: zfs.Bookmarks,
|
Types: zfs.Bookmarks,
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, []string{"#blup 1", "#bookfoo 1", "#bookfoo 2", "#foo 1", "#foo 2"}, versionRelnamesSorted(vs))
|
require.Equal(t, []string{"#blup 1", "#bookfoo 1", "#bookfoo 2", "#foo 1", "#foo 2"}, versionRelnamesSorted(vs))
|
||||||
|
|
||||||
// just with prefix foo
|
// just with prefix foo
|
||||||
vs, err = zfs.ZFSListFilesystemVersions(mustDatasetPath(fs), zfs.ListFilesystemVersionsOptions{
|
vs, err = zfs.ZFSListFilesystemVersions(t, mustDatasetPath(fs), zfs.ListFilesystemVersionsOptions{
|
||||||
ShortnamePrefix: "foo",
|
ShortnamePrefix: "foo",
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -82,7 +82,7 @@ func ListFilesystemVersionsZeroExistIsNotAnError(t *platformtest.Context) {
|
|||||||
|
|
||||||
fs := fmt.Sprintf("%s/foo bar", t.RootDataset)
|
fs := fmt.Sprintf("%s/foo bar", t.RootDataset)
|
||||||
|
|
||||||
vs, err := zfs.ZFSListFilesystemVersions(mustDatasetPath(fs), zfs.ListFilesystemVersionsOptions{})
|
vs, err := zfs.ZFSListFilesystemVersions(t, mustDatasetPath(fs), zfs.ListFilesystemVersionsOptions{})
|
||||||
require.Empty(t, vs)
|
require.Empty(t, vs)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
dsne, ok := err.(*zfs.DatasetDoesNotExist)
|
dsne, ok := err.(*zfs.DatasetDoesNotExist)
|
||||||
@@ -98,7 +98,7 @@ func ListFilesystemVersionsFilesystemNotExist(t *platformtest.Context) {
|
|||||||
|
|
||||||
nonexistentFS := fmt.Sprintf("%s/not existent", t.RootDataset)
|
nonexistentFS := fmt.Sprintf("%s/not existent", t.RootDataset)
|
||||||
|
|
||||||
vs, err := zfs.ZFSListFilesystemVersions(mustDatasetPath(nonexistentFS), zfs.ListFilesystemVersionsOptions{})
|
vs, err := zfs.ZFSListFilesystemVersions(t, mustDatasetPath(nonexistentFS), zfs.ListFilesystemVersionsOptions{})
|
||||||
require.Empty(t, vs)
|
require.Empty(t, vs)
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
t.Logf("err = %T\n%s", err, err)
|
t.Logf("err = %T\n%s", err, err)
|
||||||
@@ -141,7 +141,7 @@ func ListFilesystemVersionsUserrefs(t *platformtest.Context) {
|
|||||||
|
|
||||||
fs := fmt.Sprintf("%s/foo bar", t.RootDataset)
|
fs := fmt.Sprintf("%s/foo bar", t.RootDataset)
|
||||||
|
|
||||||
vs, err := zfs.ZFSListFilesystemVersions(mustDatasetPath(fs), zfs.ListFilesystemVersionsOptions{})
|
vs, err := zfs.ZFSListFilesystemVersions(t, mustDatasetPath(fs), zfs.ListFilesystemVersionsOptions{})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
type expectation struct {
|
type expectation struct {
|
||||||
|
|||||||
@@ -602,7 +602,7 @@ func (s *Step) doReplication(ctx context.Context) error {
|
|||||||
|
|
||||||
fs := s.parent.Path
|
fs := s.parent.Path
|
||||||
|
|
||||||
log := getLogger(ctx)
|
log := getLogger(ctx).WithField("filesystem", fs)
|
||||||
sr := s.buildSendRequest(false)
|
sr := s.buildSendRequest(false)
|
||||||
|
|
||||||
log.Debug("initiate send request")
|
log.Debug("initiate send request")
|
||||||
|
|||||||
@@ -11,39 +11,6 @@ import (
|
|||||||
"github.com/zrepl/zrepl/util/tcpsock"
|
"github.com/zrepl/zrepl/util/tcpsock"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ipMapEntry struct {
|
|
||||||
ip net.IP
|
|
||||||
ident string
|
|
||||||
}
|
|
||||||
|
|
||||||
type ipMap struct {
|
|
||||||
entries []ipMapEntry
|
|
||||||
}
|
|
||||||
|
|
||||||
func ipMapFromConfig(clients map[string]string) (*ipMap, error) {
|
|
||||||
entries := make([]ipMapEntry, 0, len(clients))
|
|
||||||
for clientIPString, clientIdent := range clients {
|
|
||||||
clientIP := net.ParseIP(clientIPString)
|
|
||||||
if clientIP == nil {
|
|
||||||
return nil, errors.Errorf("cannot parse client IP %q", clientIPString)
|
|
||||||
}
|
|
||||||
if err := transport.ValidateClientIdentity(clientIdent); err != nil {
|
|
||||||
return nil, errors.Wrapf(err, "invalid client identity for IP %q", clientIPString)
|
|
||||||
}
|
|
||||||
entries = append(entries, ipMapEntry{clientIP, clientIdent})
|
|
||||||
}
|
|
||||||
return &ipMap{entries: entries}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *ipMap) Get(ip net.IP) (string, error) {
|
|
||||||
for _, e := range m.entries {
|
|
||||||
if e.ip.Equal(ip) {
|
|
||||||
return e.ident, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "", errors.Errorf("no identity mapping for client IP %s", ip)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TCPListenerFactoryFromConfig(c *config.Global, in *config.TCPServe) (transport.AuthenticatedListenerFactory, error) {
|
func TCPListenerFactoryFromConfig(c *config.Global, in *config.TCPServe) (transport.AuthenticatedListenerFactory, error) {
|
||||||
clientMap, err := ipMapFromConfig(in.Clients)
|
clientMap, err := ipMapFromConfig(in.Clients)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -69,10 +36,13 @@ func (f *TCPAuthListener) Accept(ctx context.Context) (*transport.AuthConn, erro
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
clientIP := nc.RemoteAddr().(*net.TCPAddr).IP
|
clientAddr := &net.IPAddr{
|
||||||
clientIdent, err := f.clientMap.Get(clientIP)
|
IP: nc.RemoteAddr().(*net.TCPAddr).IP,
|
||||||
|
Zone: nc.RemoteAddr().(*net.TCPAddr).Zone,
|
||||||
|
}
|
||||||
|
clientIdent, err := f.clientMap.Get(clientAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
transport.GetLogger(ctx).WithField("ip", clientIP).Error("client IP not in client map")
|
transport.GetLogger(ctx).WithField("ipaddr", clientAddr).Error("client IP not in client map")
|
||||||
nc.Close()
|
nc.Close()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,166 @@
|
|||||||
|
package tcp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/zrepl/zrepl/transport"
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ipMapEntry struct {
|
||||||
|
subnet *net.IPNet
|
||||||
|
// ident is always not empty
|
||||||
|
ident string
|
||||||
|
// zone may be empty (e.g. for IPv4)
|
||||||
|
zone string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ipMap struct {
|
||||||
|
entries []*ipMapEntry
|
||||||
|
}
|
||||||
|
|
||||||
|
func ipMapFromConfig(clients map[string]string) (*ipMap, error) {
|
||||||
|
|
||||||
|
entries := make([]*ipMapEntry, 0, len(clients))
|
||||||
|
for clientInput, clientIdent := range clients {
|
||||||
|
userIPMapEntry, err := newIPMapEntry(clientInput, clientIdent)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "cannot not parse %q:%q", clientInput, clientIdent)
|
||||||
|
}
|
||||||
|
|
||||||
|
entries = append(entries, userIPMapEntry)
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Sort(byPrefixlen(entries))
|
||||||
|
|
||||||
|
return &ipMap{entries: entries}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *ipMap) Get(ipAddr *net.IPAddr) (string, error) {
|
||||||
|
for _, e := range m.entries {
|
||||||
|
if e.zone != ipAddr.Zone {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if e.subnet.Contains(ipAddr.IP) {
|
||||||
|
return zfsDatasetPathComponentCompatibleRepresentation(e.ident, ipAddr), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", errors.Errorf("no identity mapping for client IP: %s%%%s", ipAddr.IP, ipAddr.Zone)
|
||||||
|
}
|
||||||
|
|
||||||
|
var ipv6FullySpecifiedMask = bytes.Repeat([]byte{0xff}, net.IPv6len)
|
||||||
|
|
||||||
|
func newIPMapEntry(input string, ident string) (*ipMapEntry, error) {
|
||||||
|
ip := input
|
||||||
|
var zone string
|
||||||
|
|
||||||
|
ipZoneSplit := strings.SplitN(input, "%", 2)
|
||||||
|
if len(ipZoneSplit) > 1 {
|
||||||
|
ip = ipZoneSplit[0]
|
||||||
|
zone = ipZoneSplit[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
_, subnet, err := net.ParseCIDR(ip)
|
||||||
|
if err != nil {
|
||||||
|
// expect full IP, no '*' placeholder expansion
|
||||||
|
|
||||||
|
if strings.Count(ident, "*") != 0 {
|
||||||
|
return nil, fmt.Errorf("non-CIDR matches must not contain '*' placeholder")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := transport.ValidateClientIdentity(ident); err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "invalid client identity %q for IP %q", ident, ip)
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedIP := net.ParseIP(ip)
|
||||||
|
if parsedIP == nil {
|
||||||
|
return nil, errors.Wrapf(err, "invalid client address %q", ip)
|
||||||
|
}
|
||||||
|
parsedIP = parsedIP.To16()
|
||||||
|
|
||||||
|
return &ipMapEntry{
|
||||||
|
subnet: &net.IPNet{
|
||||||
|
IP: parsedIP,
|
||||||
|
Mask: ipv6FullySpecifiedMask,
|
||||||
|
},
|
||||||
|
zone: zone,
|
||||||
|
ident: ident,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// expect CIDR and '*' placeholder expansion
|
||||||
|
|
||||||
|
if strings.Count(ident, "*") != 1 {
|
||||||
|
err = fmt.Errorf("CIDRs require 1 IP placeholder")
|
||||||
|
return nil, errors.Wrapf(err, "invalid client identity %q for IP %q", ident, ip)
|
||||||
|
}
|
||||||
|
|
||||||
|
longestIPAddr := net.IPAddr{
|
||||||
|
IP: net.IP(bytes.Repeat([]byte{0xff}, net.IPv6len)),
|
||||||
|
Zone: strings.Repeat("i", unix.IFNAMSIZ),
|
||||||
|
}
|
||||||
|
longestIdent := zfsDatasetPathComponentCompatibleRepresentation(ident, &longestIPAddr)
|
||||||
|
|
||||||
|
if err := transport.ValidateClientIdentity(longestIdent); err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "invalid client identity for IP %q", ip)
|
||||||
|
}
|
||||||
|
|
||||||
|
ones, _ := subnet.Mask.Size()
|
||||||
|
preExpansionAddrlen := len(subnet.IP) * 8
|
||||||
|
expanded := subnet.IP.To16()
|
||||||
|
postExpansionAddrlen := len(expanded) * 8
|
||||||
|
return &ipMapEntry{
|
||||||
|
subnet: &net.IPNet{
|
||||||
|
IP: expanded,
|
||||||
|
Mask: net.CIDRMask(postExpansionAddrlen-preExpansionAddrlen+ones, postExpansionAddrlen),
|
||||||
|
},
|
||||||
|
zone: zone,
|
||||||
|
ident: ident,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func zfsDatasetPathComponentCompatibleRepresentation(identityWithWildcard string, addr *net.IPAddr) string {
|
||||||
|
// If a Zone exists we append it after the IP using a "-" because "%"
|
||||||
|
// is a zfs dataset forbidden char.
|
||||||
|
if addr.Zone != "" {
|
||||||
|
return strings.Replace(identityWithWildcard, "*", addr.IP.String()+"-"+addr.Zone, 1)
|
||||||
|
}
|
||||||
|
// newIPMapEntry validates that the line contains exactly one "*"
|
||||||
|
return strings.Replace(identityWithWildcard, "*", addr.IP.String(), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ipMapEntry) String() string {
|
||||||
|
return fmt.Sprintf("&ipMapEntry{subnet=%q, ident=%q, zone=%q}", e.subnet.String(), e.ident, e.zone)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ipMapEntry) PrefixLen() int {
|
||||||
|
ones, bits := e.subnet.Mask.Size()
|
||||||
|
if bits != net.IPv6len*8 {
|
||||||
|
panic(fmt.Sprintf("impl error: we represent all addresses as 16byte internally ones=%v bits=%v: %s", ones, bits, e))
|
||||||
|
}
|
||||||
|
return ones
|
||||||
|
}
|
||||||
|
|
||||||
|
// newtype to support sorting by prefixlength
|
||||||
|
type byPrefixlen []*ipMapEntry
|
||||||
|
|
||||||
|
func (m byPrefixlen) Len() int { return len(m) }
|
||||||
|
func (m byPrefixlen) Less(i, j int) bool {
|
||||||
|
|
||||||
|
if m[i].PrefixLen() != m[j].PrefixLen() {
|
||||||
|
return m[i].PrefixLen() > m[j].PrefixLen()
|
||||||
|
}
|
||||||
|
|
||||||
|
addrCmp := bytes.Compare(m[i].subnet.IP.To16(), m[j].subnet.IP.To16())
|
||||||
|
if addrCmp != 0 {
|
||||||
|
return addrCmp < 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Compare(m[i].zone, m[j].zone) < 0
|
||||||
|
}
|
||||||
|
func (m byPrefixlen) Swap(i, j int) { m[i], m[j] = m[j], m[i] }
|
||||||
@@ -0,0 +1,204 @@
|
|||||||
|
package tcp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/kr/pretty"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestIPMap(t *testing.T) {
|
||||||
|
type testCaseExpect struct {
|
||||||
|
expectNoMapping bool
|
||||||
|
expectIdent string
|
||||||
|
}
|
||||||
|
type testCase struct {
|
||||||
|
name string
|
||||||
|
config map[string]string
|
||||||
|
expectInitErr bool
|
||||||
|
expect map[string]testCaseExpect
|
||||||
|
}
|
||||||
|
|
||||||
|
cases := []testCase{
|
||||||
|
{
|
||||||
|
name: "regular ips",
|
||||||
|
expectInitErr: false,
|
||||||
|
config: map[string]string{
|
||||||
|
"192.168.123.234": "ident1",
|
||||||
|
"192.168.20.23": "ident2",
|
||||||
|
},
|
||||||
|
expect: map[string]testCaseExpect{
|
||||||
|
"192.168.123.234": {expectIdent: "ident1"},
|
||||||
|
"192.168.20.23": {expectIdent: "ident2"},
|
||||||
|
"192.168.20.10": {expectNoMapping: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "full wildcard",
|
||||||
|
expectInitErr: false,
|
||||||
|
config: map[string]string{
|
||||||
|
"0.0.0.0/0": "*",
|
||||||
|
"0::/0": "*",
|
||||||
|
},
|
||||||
|
expect: map[string]testCaseExpect{
|
||||||
|
"10.123.234.24": {expectIdent: "10.123.234.24"},
|
||||||
|
// '[' and ']' are forbiddenin dataset names
|
||||||
|
"fe80::23:42": {expectIdent: "fe80::23:42"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "longest prefix matching",
|
||||||
|
expectInitErr: false,
|
||||||
|
config: map[string]string{
|
||||||
|
"10.1.2.3": "specific-host",
|
||||||
|
"10.1.2.0/24": "subnet-one-two-*",
|
||||||
|
"10.1.1.0/24": "subnet-one-one-*",
|
||||||
|
"10.1.0.0/24": "subnet-one-zero-*",
|
||||||
|
"10.1.0.0/16": "subnet-one-*",
|
||||||
|
"fde4:8dba:82e1:1::1": "v6-48-1-specialhost",
|
||||||
|
"fde4:8dba:82e1::/48": "v6-48-*",
|
||||||
|
"fde4:8dba:82e1:1::/64": "v6-64-1-*",
|
||||||
|
"fde4:8dba:82e1:2::/64": "v6-64-2-*",
|
||||||
|
},
|
||||||
|
expect: map[string]testCaseExpect{
|
||||||
|
"10.1.2.3": {expectIdent: "specific-host"},
|
||||||
|
"10.1.2.1": {expectIdent: "subnet-one-two-10.1.2.1"},
|
||||||
|
"10.1.1.1": {expectIdent: "subnet-one-one-10.1.1.1"},
|
||||||
|
"10.1.0.23": {expectIdent: "subnet-one-zero-10.1.0.23"},
|
||||||
|
"10.1.3.1": {expectIdent: "subnet-one-10.1.3.1"},
|
||||||
|
"10.2.1.1": {expectNoMapping: true},
|
||||||
|
"fde4:8dba:82e1:1::1": {expectIdent: "v6-48-1-specialhost"},
|
||||||
|
"fde4:8dba:82e1:23::1": {expectIdent: "v6-48-fde4:8dba:82e1:23::1"},
|
||||||
|
"fde4:8dba:82e1:1::2": {expectIdent: "v6-64-1-fde4:8dba:82e1:1::2"},
|
||||||
|
"fde4:8dba:82e1:2::1": {expectIdent: "v6-64-2-fde4:8dba:82e1:2::1"},
|
||||||
|
"fde4:8dba:82e2::1": {expectNoMapping: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "different prefixes, mixed ipv4 ipv6, with interface ids",
|
||||||
|
expectInitErr: false,
|
||||||
|
config: map[string]string{
|
||||||
|
"192.168.23.0/24": "db-*",
|
||||||
|
"192.168.23.23": "db-twentythree",
|
||||||
|
"192.168.42.0/24": "web-*",
|
||||||
|
"10.1.4.0/24": "my-*-server",
|
||||||
|
"2001:0db8:85a3:0000:0000:8a2e:0370:7334": "aspecifichost",
|
||||||
|
"2001:0db8:85a3:0000:0000:8a2e:0370:7334%eth1": "aspecifichost",
|
||||||
|
"fe80::/16%eth1": "san-*",
|
||||||
|
"fde4:8dba:82e1::/64": "sub64-*",
|
||||||
|
},
|
||||||
|
expect: map[string]testCaseExpect{
|
||||||
|
"10.1.2.3": {expectNoMapping: true},
|
||||||
|
"192.168.23.1": {expectIdent: "db-192.168.23.1"},
|
||||||
|
"192.168.23.23": {expectIdent: "db-twentythree"},
|
||||||
|
"192.168.023.001": {expectIdent: "db-192.168.23.1"},
|
||||||
|
"10.1.4.5": {expectIdent: "my-10.1.4.5-server"},
|
||||||
|
|
||||||
|
// normalization
|
||||||
|
"192.168.42.1": {expectIdent: "web-192.168.42.1"},
|
||||||
|
"192.168.042.001": {expectIdent: "web-192.168.42.1"},
|
||||||
|
// v6 matching
|
||||||
|
"fe80::23:42%eth1": {expectIdent: "san-fe80::23:42-eth1"},
|
||||||
|
"fe80::23:42%eth2": {expectNoMapping: true},
|
||||||
|
|
||||||
|
// v6 subnet matching
|
||||||
|
"fde4:8dba:82e1::1": {expectIdent: "sub64-fde4:8dba:82e1::1"},
|
||||||
|
// v6 subnet matching with suffix that matches another allowed IPv4
|
||||||
|
"fde4:8dba:82e1::c0a8:1717": {expectIdent: "sub64-fde4:8dba:82e1::c0a8:1717"},
|
||||||
|
|
||||||
|
"2001:0db8:85a3:0000:0000:8a2e:0370:7334": {expectIdent: "aspecifichost"},
|
||||||
|
"2001:0db8:85a3:0000:0000:8a2e:0370:7334%eth1": {expectIdent: "aspecifichost"},
|
||||||
|
"2001:0db8:85a3:0000:0000:8a2e:0370:7334%eth2": {expectNoMapping: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid user input: non ip or cidr",
|
||||||
|
expectInitErr: true,
|
||||||
|
config: map[string]string{
|
||||||
|
"jimmy": "db",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid user input: v4 ip with an identity containing *",
|
||||||
|
expectInitErr: true,
|
||||||
|
config: map[string]string{
|
||||||
|
"192.168.1.2": "db-*",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid user input: v4 subnet without an identity containing *",
|
||||||
|
expectInitErr: true,
|
||||||
|
config: map[string]string{
|
||||||
|
"192.168.1.0/24": "db-",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid user input: v6 ip with an identity containing *",
|
||||||
|
expectInitErr: true,
|
||||||
|
config: map[string]string{
|
||||||
|
"2001:0db8:85a3:0000:0000:8a2e:0370:7334": "aspecifichost*",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid user input: v6 subnet without identity containing *",
|
||||||
|
expectInitErr: true,
|
||||||
|
config: map[string]string{
|
||||||
|
"fe80::/16%eth1": "db-",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid user input with subnet match: client identity with forbidden zfs dataset name char @",
|
||||||
|
expectInitErr: true,
|
||||||
|
config: map[string]string{
|
||||||
|
"fe80::/16": "db@-*",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid user input with IP match: client identity with forbidden zfs dataset name char @",
|
||||||
|
expectInitErr: true,
|
||||||
|
config: map[string]string{
|
||||||
|
"fe80::1": "db@foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range cases {
|
||||||
|
c := cases[i]
|
||||||
|
t.Run(c.name, func(t *testing.T) {
|
||||||
|
pretty.Fprintf(os.Stderr, "running %#v\n", c)
|
||||||
|
m, err := ipMapFromConfig(c.config)
|
||||||
|
if c.expectInitErr {
|
||||||
|
require.Error(t, err)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, m)
|
||||||
|
}
|
||||||
|
for input, expect := range c.expect {
|
||||||
|
// reuse newIPMapEntry to parse test case input
|
||||||
|
// "test" is not used during testing but must not be empty.
|
||||||
|
ipMapEntry, _ := newIPMapEntry(input, "test")
|
||||||
|
ones, bits := ipMapEntry.subnet.Mask.Size()
|
||||||
|
require.Equal(t, bits, net.IPv6len*8, "and we know ipMapEntry always expands its IPs to 16bytes")
|
||||||
|
require.Equal(t, ones, net.IPv6len*8, "test case addresses must be fully specified")
|
||||||
|
require.NotNil(t, ipMapEntry)
|
||||||
|
ident, err := m.Get(&net.IPAddr{
|
||||||
|
IP: ipMapEntry.subnet.IP,
|
||||||
|
Zone: ipMapEntry.zone,
|
||||||
|
})
|
||||||
|
if expect.expectNoMapping {
|
||||||
|
assert.Empty(t, ident)
|
||||||
|
} else {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, expect.expectIdent, ident)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPackageNetAssumptions(t *testing.T) {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -4,10 +4,10 @@ package transport
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"net"
|
"net"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
"github.com/zrepl/zrepl/logger"
|
"github.com/zrepl/zrepl/logger"
|
||||||
"github.com/zrepl/zrepl/rpc/dataconn/timeoutconn"
|
"github.com/zrepl/zrepl/rpc/dataconn/timeoutconn"
|
||||||
"github.com/zrepl/zrepl/zfs"
|
"github.com/zrepl/zrepl/zfs"
|
||||||
@@ -55,13 +55,10 @@ type Connecter interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// A client identity must be a single component in a ZFS filesystem path
|
// A client identity must be a single component in a ZFS filesystem path
|
||||||
func ValidateClientIdentity(in string) (err error) {
|
func ValidateClientIdentity(in string) error {
|
||||||
path, err := zfs.NewDatasetPath(in)
|
err := zfs.ComponentNamecheck(in)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return errors.Wrap(err, "client identity must be usable as a single dataset path component")
|
||||||
}
|
|
||||||
if path.Length() != 1 {
|
|
||||||
return errors.New("client identity must be a single path component (not empty, no '/')")
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -206,13 +206,13 @@ func (o *ListFilesystemVersionsOptions) matches(v FilesystemVersion) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// returned versions are sorted by createtxg FIXME drop sort by createtxg requirement
|
// returned versions are sorted by createtxg FIXME drop sort by createtxg requirement
|
||||||
func ZFSListFilesystemVersions(fs *DatasetPath, options ListFilesystemVersionsOptions) (res []FilesystemVersion, err error) {
|
func ZFSListFilesystemVersions(ctx context.Context, fs *DatasetPath, options ListFilesystemVersionsOptions) (res []FilesystemVersion, err error) {
|
||||||
listResults := make(chan ZFSListResult)
|
listResults := make(chan ZFSListResult)
|
||||||
|
|
||||||
promTimer := prometheus.NewTimer(prom.ZFSListFilesystemVersionDuration.WithLabelValues(fs.ToString()))
|
promTimer := prometheus.NewTimer(prom.ZFSListFilesystemVersionDuration.WithLabelValues(fs.ToString()))
|
||||||
defer promTimer.ObserveDuration()
|
defer promTimer.ObserveDuration()
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
go ZFSListChan(ctx, listResults,
|
go ZFSListChan(ctx, listResults,
|
||||||
[]string{"name", "guid", "createtxg", "creation", "userrefs"},
|
[]string{"name", "guid", "createtxg", "creation", "userrefs"},
|
||||||
|
|||||||
+1
-1
@@ -1084,7 +1084,7 @@ func ZFSRecv(ctx context.Context, fs string, v *ZFSSendArgVersion, streamCopier
|
|||||||
if opts.RollbackAndForceRecv {
|
if opts.RollbackAndForceRecv {
|
||||||
// destroy all snapshots before `recv -F` because `recv -F`
|
// destroy all snapshots before `recv -F` because `recv -F`
|
||||||
// does not perform a rollback unless `send -R` was used (which we assume hasn't been the case)
|
// does not perform a rollback unless `send -R` was used (which we assume hasn't been the case)
|
||||||
snaps, err := ZFSListFilesystemVersions(fsdp, ListFilesystemVersionsOptions{
|
snaps, err := ZFSListFilesystemVersions(ctx, fsdp, ListFilesystemVersionsOptions{
|
||||||
Types: Snapshots,
|
Types: Snapshots,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
+54
-8
@@ -18,10 +18,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Cmd struct {
|
type Cmd struct {
|
||||||
cmd *exec.Cmd
|
cmd *exec.Cmd
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
mtx sync.RWMutex
|
mtx sync.RWMutex
|
||||||
startedAt, waitReturnedAt time.Time
|
startedAt, waitStartedAt, waitReturnedAt time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
func CommandContext(ctx context.Context, name string, arg ...string) *Cmd {
|
func CommandContext(ctx context.Context, name string, arg ...string) *Cmd {
|
||||||
@@ -119,19 +119,65 @@ func (c *Cmd) startPost(err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Cmd) waitPre() {
|
func (c *Cmd) waitPre() {
|
||||||
waitPreLogging(c, time.Now())
|
now := time.Now()
|
||||||
|
|
||||||
|
// ignore duplicate waits
|
||||||
|
c.mtx.Lock()
|
||||||
|
// ignore duplicate waits
|
||||||
|
if !c.waitStartedAt.IsZero() {
|
||||||
|
c.mtx.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.waitStartedAt = now
|
||||||
|
c.mtx.Unlock()
|
||||||
|
|
||||||
|
waitPreLogging(c, now)
|
||||||
|
}
|
||||||
|
|
||||||
|
type usage struct {
|
||||||
|
total_secs, system_secs, user_secs float64
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Cmd) waitPost(err error) {
|
func (c *Cmd) waitPost(err error) {
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
|
|
||||||
c.mtx.Lock()
|
c.mtx.Lock()
|
||||||
|
// ignore duplicate waits
|
||||||
|
if !c.waitReturnedAt.IsZero() {
|
||||||
|
c.mtx.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
c.waitReturnedAt = now
|
c.waitReturnedAt = now
|
||||||
c.mtx.Unlock()
|
c.mtx.Unlock()
|
||||||
|
|
||||||
waitPostReport(c, now)
|
// build usage
|
||||||
waitPostLogging(c, err, now)
|
var u usage
|
||||||
waitPostPrometheus(c, err, now)
|
{
|
||||||
|
var s *os.ProcessState
|
||||||
|
if err == nil {
|
||||||
|
s = c.cmd.ProcessState
|
||||||
|
} else if ee, ok := err.(*exec.ExitError); ok {
|
||||||
|
s = ee.ProcessState
|
||||||
|
}
|
||||||
|
|
||||||
|
if s == nil {
|
||||||
|
u = usage{
|
||||||
|
total_secs: c.Runtime().Seconds(),
|
||||||
|
system_secs: -1,
|
||||||
|
user_secs: -1,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
u = usage{
|
||||||
|
total_secs: c.Runtime().Seconds(),
|
||||||
|
system_secs: s.SystemTime().Seconds(),
|
||||||
|
user_secs: s.UserTime().Seconds(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
waitPostReport(c, u, now)
|
||||||
|
waitPostLogging(c, u, err, now)
|
||||||
|
waitPostPrometheus(c, u, err, now)
|
||||||
}
|
}
|
||||||
|
|
||||||
// returns 0 if the command did not yet finish
|
// returns 0 if the command did not yet finish
|
||||||
|
|||||||
@@ -27,11 +27,12 @@ func waitPreLogging(c *Cmd, now time.Time) {
|
|||||||
c.log().Debug("start waiting")
|
c.log().Debug("start waiting")
|
||||||
}
|
}
|
||||||
|
|
||||||
func waitPostLogging(c *Cmd, err error, now time.Time) {
|
func waitPostLogging(c *Cmd, u usage, err error, now time.Time) {
|
||||||
|
|
||||||
log := c.log().
|
log := c.log().
|
||||||
WithField("total_time_s", c.Runtime().Seconds()).
|
WithField("total_time_s", u.total_secs).
|
||||||
WithField("systemtime_s", c.cmd.ProcessState.SystemTime().Seconds()).
|
WithField("systemtime_s", u.system_secs).
|
||||||
WithField("usertime_s", c.cmd.ProcessState.UserTime().Seconds())
|
WithField("usertime_s", u.user_secs)
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
log.Info("command exited without error")
|
log.Info("command exited without error")
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
package zfscmd
|
package zfscmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"io"
|
"io"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -58,3 +60,30 @@ func TestCmdStderrBehaviorStdoutPipe(t *testing.T) {
|
|||||||
require.True(t, ok)
|
require.True(t, ok)
|
||||||
require.Empty(t, ee.Stderr) // !!!!! probably not what one would expect if we only redirect stdout
|
require.Empty(t, ee.Stderr) // !!!!! probably not what one would expect if we only redirect stdout
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCmdProcessState(t *testing.T) {
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
cmd := exec.CommandContext(ctx, "bash", "-c", "echo running; sleep 3600")
|
||||||
|
stdout, err := cmd.StdoutPipe()
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = cmd.Start()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
r := bufio.NewReader(stdout)
|
||||||
|
line, err := r.ReadString('\n')
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, "running\n", line)
|
||||||
|
|
||||||
|
// we know it's running and sleeping
|
||||||
|
cancel()
|
||||||
|
err = cmd.Wait()
|
||||||
|
t.Logf("wait err %T\n%s", err, err)
|
||||||
|
require.Error(t, err)
|
||||||
|
ee, ok := err.(*exec.ExitError)
|
||||||
|
require.True(t, ok)
|
||||||
|
require.NotNil(t, ee.ProcessState)
|
||||||
|
require.Contains(t, ee.Error(), "killed")
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ func RegisterMetrics(r prometheus.Registerer) {
|
|||||||
r.MustRegister(metrics.usertime)
|
r.MustRegister(metrics.usertime)
|
||||||
}
|
}
|
||||||
|
|
||||||
func waitPostPrometheus(c *Cmd, err error, now time.Time) {
|
func waitPostPrometheus(c *Cmd, u usage, err error, now time.Time) {
|
||||||
|
|
||||||
if len(c.cmd.Args) < 2 {
|
if len(c.cmd.Args) < 2 {
|
||||||
getLogger(c.ctx).WithField("args", c.cmd.Args).
|
getLogger(c.ctx).WithField("args", c.cmd.Args).
|
||||||
@@ -64,10 +64,10 @@ func waitPostPrometheus(c *Cmd, err error, now time.Time) {
|
|||||||
|
|
||||||
metrics.totaltime.
|
metrics.totaltime.
|
||||||
WithLabelValues(labelValues...).
|
WithLabelValues(labelValues...).
|
||||||
Observe(c.Runtime().Seconds())
|
Observe(u.total_secs)
|
||||||
metrics.systemtime.WithLabelValues(labelValues...).
|
metrics.systemtime.WithLabelValues(labelValues...).
|
||||||
Observe(c.cmd.ProcessState.SystemTime().Seconds())
|
Observe(u.system_secs)
|
||||||
metrics.usertime.WithLabelValues(labelValues...).
|
metrics.usertime.WithLabelValues(labelValues...).
|
||||||
Observe(c.cmd.ProcessState.UserTime().Seconds())
|
Observe(u.user_secs)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ func startPostReport(c *Cmd, err error, now time.Time) {
|
|||||||
active.mtx.Unlock()
|
active.mtx.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
func waitPostReport(c *Cmd, now time.Time) {
|
func waitPostReport(c *Cmd, _ usage, now time.Time) {
|
||||||
active.mtx.Lock()
|
active.mtx.Lock()
|
||||||
defer active.mtx.Unlock()
|
defer active.mtx.Unlock()
|
||||||
prev := active.cmds[c]
|
prev := active.cmds[c]
|
||||||
|
|||||||
Reference in New Issue
Block a user