Compare commits
60 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fe40352f8e | |||
| fd123fc6c4 | |||
| 961500cc2c | |||
| 47726ad877 | |||
| 3b6cede108 | |||
| 2cad13f27b | |||
| b5475921a8 | |||
| 88c684e1d0 | |||
| a4d28701d9 | |||
| 000c8b4186 | |||
| ae4b12c9ba | |||
| 8cc31bd76a | |||
| 43871a9211 | |||
| 77576164ae | |||
| 4c450a640c | |||
| 36d2cb115a | |||
| 7ba5c14679 | |||
| ab7eb47483 | |||
| 707a189144 | |||
| 4f37dccb76 | |||
| 69084fb08f | |||
| 0a77be0ff2 | |||
| 597302de3f | |||
| 828c2982f3 | |||
| 6f7b8ca1af | |||
| e0f40de69f | |||
| 2fe7f29d31 | |||
| df181108b4 | |||
| c3af267f48 | |||
| a15a73e884 | |||
| ff10a71f3a | |||
| 1f266d02ce | |||
| 4efff312ea | |||
| 0ed5c01473 | |||
| 63bc27e6e1 | |||
| e72d274e88 | |||
| 007da664ea | |||
| f3433df617 | |||
| 493a01c4fe | |||
| 161ce3b3c3 | |||
| 83bb97a845 | |||
| 40919d06c2 | |||
| c48069ce88 | |||
| 4b489ad2c7 | |||
| 72d288567e | |||
| b5d46e2ec3 | |||
| 83d450b1f2 | |||
| 3e647c14c0 | |||
| b95260f4b5 | |||
| 2764c95952 | |||
| 678b4a6f4b | |||
| 79ab43ebca | |||
| a4963cecb7 | |||
| e6d08149ef | |||
| ea6f02368b | |||
| d891b2b119 | |||
| 5c6c9485a8 | |||
| b4d8c93fae | |||
| 3f394a8960 | |||
| 164f77d80c |
@@ -1,4 +0,0 @@
|
|||||||
[submodule "docs/themes/docdock"]
|
|
||||||
path = docs/themes/docdock
|
|
||||||
url = git@github.com:zrepl/hugo-theme-docdock.git
|
|
||||||
|
|
||||||
|
|||||||
Generated
+1
-79
@@ -7,12 +7,6 @@
|
|||||||
revision = "346938d642f2ec3594ed81d874461961cd0faa76"
|
revision = "346938d642f2ec3594ed81d874461961cd0faa76"
|
||||||
version = "v1.1.0"
|
version = "v1.1.0"
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
name = "github.com/fsnotify/fsnotify"
|
|
||||||
packages = ["."]
|
|
||||||
revision = "629574ca2a5df945712d3079857300b5e4da0236"
|
|
||||||
version = "v1.4.2"
|
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
branch = "master"
|
branch = "master"
|
||||||
name = "github.com/ftrvxmtrx/fd"
|
name = "github.com/ftrvxmtrx/fd"
|
||||||
@@ -31,12 +25,6 @@
|
|||||||
packages = ["."]
|
packages = ["."]
|
||||||
revision = "eb3733d160e74a9c7e442f435eb3bea458e1d19f"
|
revision = "eb3733d160e74a9c7e442f435eb3bea458e1d19f"
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
name = "github.com/hashicorp/hcl"
|
|
||||||
packages = [".","hcl/ast","hcl/parser","hcl/scanner","hcl/strconv","hcl/token","json/parser","json/scanner","json/token"]
|
|
||||||
revision = "68e816d1c783414e79bc65b3994d9ab6b0a722ab"
|
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
name = "github.com/inconshreveable/mousetrap"
|
name = "github.com/inconshreveable/mousetrap"
|
||||||
packages = ["."]
|
packages = ["."]
|
||||||
@@ -67,36 +55,12 @@
|
|||||||
packages = ["."]
|
packages = ["."]
|
||||||
revision = "7cafcd837844e784b526369c9bce262804aebc60"
|
revision = "7cafcd837844e784b526369c9bce262804aebc60"
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
name = "github.com/magiconair/properties"
|
|
||||||
packages = ["."]
|
|
||||||
revision = "be5ece7dd465ab0765a9682137865547526d1dfb"
|
|
||||||
version = "v1.7.3"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
name = "github.com/mitchellh/go-homedir"
|
|
||||||
packages = ["."]
|
|
||||||
revision = "b8bc1bf767474819792c23f32d8286a45736f1c6"
|
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
branch = "master"
|
branch = "master"
|
||||||
name = "github.com/mitchellh/mapstructure"
|
name = "github.com/mitchellh/mapstructure"
|
||||||
packages = ["."]
|
packages = ["."]
|
||||||
revision = "d0303fe809921458f417bcf828397a65db30a7e4"
|
revision = "d0303fe809921458f417bcf828397a65db30a7e4"
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
name = "github.com/pelletier/go-buffruneio"
|
|
||||||
packages = ["."]
|
|
||||||
revision = "c37440a7cf42ac63b919c752ca73a85067e05992"
|
|
||||||
version = "v0.2.0"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
name = "github.com/pelletier/go-toml"
|
|
||||||
packages = ["."]
|
|
||||||
revision = "5ccdfb18c776b740aecaf085c4d9a2779199c279"
|
|
||||||
version = "v1.0.0"
|
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
name = "github.com/pkg/errors"
|
name = "github.com/pkg/errors"
|
||||||
packages = ["."]
|
packages = ["."]
|
||||||
@@ -109,69 +73,27 @@
|
|||||||
revision = "792786c7400a136282c1664665ae0a8db921c6c2"
|
revision = "792786c7400a136282c1664665ae0a8db921c6c2"
|
||||||
version = "v1.0.0"
|
version = "v1.0.0"
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
name = "github.com/spf13/afero"
|
|
||||||
packages = [".","mem"]
|
|
||||||
revision = "ee1bd8ee15a1306d1f9201acc41ef39cd9f99a1b"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
name = "github.com/spf13/cast"
|
|
||||||
packages = ["."]
|
|
||||||
revision = "acbeb36b902d72a7a4c18e8f3241075e7ab763e4"
|
|
||||||
version = "v1.1.0"
|
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
branch = "master"
|
branch = "master"
|
||||||
name = "github.com/spf13/cobra"
|
name = "github.com/spf13/cobra"
|
||||||
packages = ["."]
|
packages = ["."]
|
||||||
revision = "b78744579491c1ceeaaa3b40205e56b0591b93a3"
|
revision = "b78744579491c1ceeaaa3b40205e56b0591b93a3"
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
name = "github.com/spf13/jwalterweatherman"
|
|
||||||
packages = ["."]
|
|
||||||
revision = "12bd96e66386c1960ab0f74ced1362f66f552f7b"
|
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
name = "github.com/spf13/pflag"
|
name = "github.com/spf13/pflag"
|
||||||
packages = ["."]
|
packages = ["."]
|
||||||
revision = "e57e3eeb33f795204c1ca35f56c44f83227c6e66"
|
revision = "e57e3eeb33f795204c1ca35f56c44f83227c6e66"
|
||||||
version = "v1.0.0"
|
version = "v1.0.0"
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
name = "github.com/spf13/viper"
|
|
||||||
packages = ["."]
|
|
||||||
revision = "25b30aa063fc18e48662b86996252eabdcf2f0c7"
|
|
||||||
version = "v1.0.0"
|
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
name = "github.com/stretchr/testify"
|
name = "github.com/stretchr/testify"
|
||||||
packages = ["assert"]
|
packages = ["assert"]
|
||||||
revision = "69483b4bd14f5845b5a1e55bca19e954e827f1d0"
|
revision = "69483b4bd14f5845b5a1e55bca19e954e827f1d0"
|
||||||
version = "v1.1.4"
|
version = "v1.1.4"
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
name = "golang.org/x/sys"
|
|
||||||
packages = ["unix"]
|
|
||||||
revision = "429f518978ab01db8bb6f44b66785088e7fba58b"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
name = "golang.org/x/text"
|
|
||||||
packages = ["internal/gen","internal/triegen","internal/ucd","transform","unicode/cldr","unicode/norm"]
|
|
||||||
revision = "1cbadb444a806fd9430d14ad08967ed91da4fa0a"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "v2"
|
|
||||||
name = "gopkg.in/yaml.v2"
|
|
||||||
packages = ["."]
|
|
||||||
revision = "eb3733d160e74a9c7e442f435eb3bea458e1d19f"
|
|
||||||
|
|
||||||
[solve-meta]
|
[solve-meta]
|
||||||
analyzer-name = "dep"
|
analyzer-name = "dep"
|
||||||
analyzer-version = 1
|
analyzer-version = 1
|
||||||
inputs-digest = "7ce2ead5225e4bb72a34132538171a649b26de574596e909730658ddbc904cd5"
|
inputs-digest = "d654c5e91ee04baf1a373bb241b94ab41d3838fb69a56e91fe54cc8b962eab56"
|
||||||
solver-name = "gps-cdcl"
|
solver-name = "gps-cdcl"
|
||||||
solver-version = 1
|
solver-version = 1
|
||||||
|
|||||||
@@ -7,14 +7,14 @@ _TESTPKGS := $(ROOT) $(foreach p,$(SUBPKGS),$(ROOT)/$(p))
|
|||||||
|
|
||||||
ARTIFACTDIR := artifacts
|
ARTIFACTDIR := artifacts
|
||||||
|
|
||||||
build: generate
|
generate: #not part of the build, must do that manually
|
||||||
go build -o $(ARTIFACTDIR)/zrepl
|
|
||||||
|
|
||||||
generate:
|
|
||||||
@for pkg in $(_TESTPKGS); do\
|
@for pkg in $(_TESTPKGS); do\
|
||||||
go generate "$$pkg" || exit 1; \
|
go generate "$$pkg" || exit 1; \
|
||||||
done;
|
done;
|
||||||
|
|
||||||
|
build:
|
||||||
|
go build -o $(ARTIFACTDIR)/zrepl
|
||||||
|
|
||||||
test:
|
test:
|
||||||
@for pkg in $(_TESTPKGS); do \
|
@for pkg in $(_TESTPKGS); do \
|
||||||
echo "Testing $$pkg"; \
|
echo "Testing $$pkg"; \
|
||||||
@@ -39,7 +39,7 @@ cover: artifacts
|
|||||||
artifacts:
|
artifacts:
|
||||||
mkdir artifacts
|
mkdir artifacts
|
||||||
|
|
||||||
release: generate artifacts vet test
|
release: artifacts vet test
|
||||||
GOOS=linux GOARCH=amd64 go build -o "$(ARTIFACTDIR)/zrepl-linux-amd64"
|
GOOS=linux GOARCH=amd64 go build -o "$(ARTIFACTDIR)/zrepl-linux-amd64"
|
||||||
GOOS=freebsd GOARCH=amd64 go build -o "$(ARTIFACTDIR)/zrepl-freebsd-amd64"
|
GOOS=freebsd GOARCH=amd64 go build -o "$(ARTIFACTDIR)/zrepl-freebsd-amd64"
|
||||||
|
|
||||||
|
|||||||
@@ -32,12 +32,11 @@ Make sure to develop an understanding how zrepl is typically used by studying th
|
|||||||
```
|
```
|
||||||
├── cmd
|
├── cmd
|
||||||
│ ├── sampleconf # example configuration
|
│ ├── sampleconf # example configuration
|
||||||
├── docs
|
├── docs # sphinx-based documentation
|
||||||
│ ├── content # hugo-based documentation -> sources for ./public_git
|
│ ├── **/*.rst # documentation in reStructuredText
|
||||||
│ ├── deploy.sh # shell script for automated rendering & deploy to zrepl.github.io repo
|
│ ├── conf.py # sphinx configuration
|
||||||
│ ├── public_git # used by above shell script
|
│ ├── publish.sh # shell script for automated rendering & deploy to zrepl.github.io repo
|
||||||
│ └── themes
|
│ ├── public_git # checkout of zrepl.github.io used by above shell script
|
||||||
│ └── docdock # submodule of our docdock theme fork
|
|
||||||
├── jobrun # OBSOLETE
|
├── jobrun # OBSOLETE
|
||||||
├── logger # logger package used by zrepl
|
├── logger # logger package used by zrepl
|
||||||
├── rpc # rpc protocol implementation
|
├── rpc # rpc protocol implementation
|
||||||
@@ -52,3 +51,36 @@ Make sure to develop an understanding how zrepl is typically used by studying th
|
|||||||
* Open an issue when starting to hack on a new feature
|
* Open an issue when starting to hack on a new feature
|
||||||
* Commits should reference the issue they are related to
|
* Commits should reference the issue they are related to
|
||||||
* Docs improvements not documenting new features do not require an issue.
|
* Docs improvements not documenting new features do not require an issue.
|
||||||
|
|
||||||
|
### Breaking Changes
|
||||||
|
|
||||||
|
Backward-incompatible changes must be documented in the git commit message and are listed in `docs/changelog.rst`.
|
||||||
|
|
||||||
|
* Config-breaking changes must contain a line `BREAK CONFIG` in the commit message
|
||||||
|
* Other breaking changes must contain a line `BREAK` in the commit message
|
||||||
|
|
||||||
|
### Glossary & Naming Inconsistencies
|
||||||
|
|
||||||
|
In ZFS, *dataset* refers to the objects *filesystem*, *ZVOL* and *snapshot*. <br />
|
||||||
|
However, we need a word for *filesystem* & *ZVOL* but not a snapshot, bookmark, etc.
|
||||||
|
|
||||||
|
Toward the user, the following terminology is used:
|
||||||
|
|
||||||
|
* **filesystem**: a ZFS filesystem or a ZVOL
|
||||||
|
* **filesystem version**: a ZFS snapshot or a bookmark
|
||||||
|
|
||||||
|
Sadly, the zrepl implementation is inconsistent in its use of these words:
|
||||||
|
variables and types are often named *dataset* when they in fact refer to a *filesystem*.
|
||||||
|
|
||||||
|
There will not be a big refactoring (an attempt was made, but it's destroying too much history without much gain).
|
||||||
|
|
||||||
|
However, new contributions & patches should fix naming without further notice in the commit message.
|
||||||
|
|
||||||
|
### Building `docs`
|
||||||
|
|
||||||
|
```
|
||||||
|
cd docs
|
||||||
|
pip install sphinx sphinx-rtd-theme
|
||||||
|
make clean html
|
||||||
|
xdg-open _build/html/index.html
|
||||||
|
```
|
||||||
|
|||||||
Executable
+46
@@ -0,0 +1,46 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
|
||||||
|
bold=$(tput bold)
|
||||||
|
normal=$(tput sgr0)
|
||||||
|
|
||||||
|
step() {
|
||||||
|
echo "${bold}$1${normal}"
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ -z "$GOPATH" ]; then
|
||||||
|
step "Make sure you have your GOPATH configured correctly" 1>&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
step "Checkout sources to \$GOPATH/github.com/zrepl/zrepl"
|
||||||
|
CHECKOUTPATH="${GOPATH}/src/github.com/zrepl/zrepl"
|
||||||
|
if [ -e "$CHECKOUTPATH" ]; then
|
||||||
|
echo "${CHECKOUTPATH} already exists"
|
||||||
|
if [ ! -d "$CHECKOUTPATH" ]; then
|
||||||
|
echo "${CHECKOUTPATH} is not a directory, aborting" 1>&2
|
||||||
|
else
|
||||||
|
cd "$CHECKOUTPATH"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
mkdir -p "$GOPATH/src/github.com/zrepl"
|
||||||
|
cd "$GOPATH/src/github.com/zrepl"
|
||||||
|
git clone https://github.com/zrepl/zrepl.git
|
||||||
|
cd zrepl
|
||||||
|
fi
|
||||||
|
|
||||||
|
step "Install build depdencies using 'go get' to \$GOPATH/bin"
|
||||||
|
go get -u golang.org/x/tools/cmd/stringer
|
||||||
|
go get -u github.com/golang/dep/cmd/dep
|
||||||
|
if ! type stringer || ! type dep; then
|
||||||
|
echo "Installed dependencies but can't find them in \$PATH, adjust it to contain \$GOPATH/bin" 1>&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
step "Fetching dependencies using 'dep ensure'"
|
||||||
|
dep ensure
|
||||||
|
|
||||||
|
step "Making release"
|
||||||
|
make release
|
||||||
|
|
||||||
|
step "Release artifacts are available in $(pwd)/artifacts"
|
||||||
+1
-1
@@ -34,7 +34,7 @@ func (a *IntervalAutosnap) Run(ctx context.Context, didSnaps chan struct{}) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if len(ds) == 0 {
|
if len(ds) == 0 {
|
||||||
a.log.WithError(err).Error("no datasets matching dataset filter")
|
a.log.Warn("no filesystem matching filesystem filter")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ outer:
|
|||||||
|
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
log.WithError(err).Info("contex done")
|
log.WithError(ctx.Err()).Info("context done")
|
||||||
server.Shutdown(context.Background())
|
server.Shutdown(context.Background())
|
||||||
break outer
|
break outer
|
||||||
case err = <-served:
|
case err = <-served:
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ func parseLocalJob(c JobParsingContext, name string, i map[string]interface{}) (
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if j.Interval, err = time.ParseDuration(asMap.Interval); err != nil {
|
if j.Interval, err = parsePostitiveDuration(asMap.Interval); err != nil {
|
||||||
err = errors.Wrap(err, "cannot parse interval")
|
err = errors.Wrap(err, "cannot parse interval")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ func parsePullJob(c JobParsingContext, name string, i map[string]interface{}) (j
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if j.Interval, err = time.ParseDuration(asMap.Interval); err != nil {
|
if j.Interval, err = parsePostitiveDuration(asMap.Interval); err != nil {
|
||||||
err = errors.Wrap(err, "cannot parse 'interval'")
|
err = errors.Wrap(err, "cannot parse 'interval'")
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
+22
-16
@@ -2,18 +2,19 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"io"
|
||||||
|
"time"
|
||||||
|
|
||||||
mapstructure "github.com/mitchellh/mapstructure"
|
mapstructure "github.com/mitchellh/mapstructure"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/zrepl/zrepl/rpc"
|
"github.com/zrepl/zrepl/rpc"
|
||||||
"github.com/zrepl/zrepl/util"
|
"github.com/zrepl/zrepl/util"
|
||||||
"io"
|
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type SourceJob struct {
|
type SourceJob struct {
|
||||||
Name string
|
Name string
|
||||||
Serve AuthenticatedChannelListenerFactory
|
Serve AuthenticatedChannelListenerFactory
|
||||||
Datasets *DatasetMapFilter
|
Filesystems *DatasetMapFilter
|
||||||
SnapshotPrefix string
|
SnapshotPrefix string
|
||||||
Interval time.Duration
|
Interval time.Duration
|
||||||
Prune PrunePolicy
|
Prune PrunePolicy
|
||||||
@@ -24,7 +25,7 @@ func parseSourceJob(c JobParsingContext, name string, i map[string]interface{})
|
|||||||
|
|
||||||
var asMap struct {
|
var asMap struct {
|
||||||
Serve map[string]interface{}
|
Serve map[string]interface{}
|
||||||
Datasets map[string]string
|
Filesystems map[string]string
|
||||||
SnapshotPrefix string `mapstructure:"snapshot_prefix"`
|
SnapshotPrefix string `mapstructure:"snapshot_prefix"`
|
||||||
Interval string
|
Interval string
|
||||||
Prune map[string]interface{}
|
Prune map[string]interface{}
|
||||||
@@ -42,7 +43,7 @@ func parseSourceJob(c JobParsingContext, name string, i map[string]interface{})
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if j.Datasets, err = parseDatasetMapFilter(asMap.Datasets, true); err != nil {
|
if j.Filesystems, err = parseDatasetMapFilter(asMap.Filesystems, true); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,7 +51,7 @@ func parseSourceJob(c JobParsingContext, name string, i map[string]interface{})
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if j.Interval, err = time.ParseDuration(asMap.Interval); err != nil {
|
if j.Interval, err = parsePostitiveDuration(asMap.Interval); err != nil {
|
||||||
err = errors.Wrap(err, "cannot parse 'interval'")
|
err = errors.Wrap(err, "cannot parse 'interval'")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -77,7 +78,7 @@ func (j *SourceJob) JobStart(ctx context.Context) {
|
|||||||
log := ctx.Value(contextKeyLog).(Logger)
|
log := ctx.Value(contextKeyLog).(Logger)
|
||||||
defer log.Info("exiting")
|
defer log.Info("exiting")
|
||||||
|
|
||||||
a := IntervalAutosnap{DatasetFilter: j.Datasets, Prefix: j.SnapshotPrefix, SnapshotInterval: j.Interval}
|
a := IntervalAutosnap{DatasetFilter: j.Filesystems, Prefix: j.SnapshotPrefix, SnapshotInterval: j.Interval}
|
||||||
p, err := j.Pruner(PrunePolicySideDefault, false)
|
p, err := j.Pruner(PrunePolicySideDefault, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithError(err).Error("error creating pruner")
|
log.WithError(err).Error("error creating pruner")
|
||||||
@@ -111,7 +112,7 @@ func (j *SourceJob) Pruner(side PrunePolicySide, dryRun bool) (p Pruner, err err
|
|||||||
p = Pruner{
|
p = Pruner{
|
||||||
time.Now(),
|
time.Now(),
|
||||||
dryRun,
|
dryRun,
|
||||||
j.Datasets,
|
j.Filesystems,
|
||||||
j.SnapshotPrefix,
|
j.SnapshotPrefix,
|
||||||
j.Prune,
|
j.Prune,
|
||||||
}
|
}
|
||||||
@@ -128,7 +129,11 @@ func (j *SourceJob) serve(ctx context.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
rwcChan := make(chan io.ReadWriteCloser)
|
type rwcChanMsg struct {
|
||||||
|
rwc io.ReadWriteCloser
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
rwcChan := make(chan rwcChanMsg)
|
||||||
|
|
||||||
// Serve connections until interrupted or error
|
// Serve connections until interrupted or error
|
||||||
outer:
|
outer:
|
||||||
@@ -137,28 +142,29 @@ outer:
|
|||||||
go func() {
|
go func() {
|
||||||
rwc, err := listener.Accept()
|
rwc, err := listener.Accept()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithError(err).Error("error accepting connection")
|
rwcChan <- rwcChanMsg{rwc, err}
|
||||||
close(rwcChan)
|
close(rwcChan)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
rwcChan <- rwc
|
rwcChan <- rwcChanMsg{rwc, err}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
select {
|
select {
|
||||||
|
|
||||||
case rwc, notClosed := <-rwcChan:
|
case rwcMsg := <-rwcChan:
|
||||||
|
|
||||||
if !notClosed {
|
if rwcMsg.err != nil {
|
||||||
break outer // closed because of accept error
|
log.WithError(err).Error("error accepting connection")
|
||||||
|
break outer
|
||||||
}
|
}
|
||||||
|
|
||||||
rwc, err := util.NewReadWriteCloserLogger(rwc, j.Debug.Conn.ReadDump, j.Debug.Conn.WriteDump)
|
rwc, err := util.NewReadWriteCloserLogger(rwcMsg.rwc, j.Debug.Conn.ReadDump, j.Debug.Conn.WriteDump)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// construct connection handler
|
// construct connection handler
|
||||||
handler := NewHandler(log, j.Datasets, &PrefixSnapshotFilter{j.SnapshotPrefix})
|
handler := NewHandler(log, j.Filesystems, &PrefixSnapshotFilter{j.SnapshotPrefix})
|
||||||
|
|
||||||
// handle connection
|
// handle connection
|
||||||
rpcServer := rpc.NewServer(rwc)
|
rpcServer := rpc.NewServer(rwc)
|
||||||
|
|||||||
+171
-129
@@ -19,149 +19,52 @@ type SetNoMetadataFormatter interface {
|
|||||||
SetNoMetadata(noMetadata bool)
|
SetNoMetadata(noMetadata bool)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type OutletCommon struct {
|
||||||
|
MinLevel logger.Level
|
||||||
|
Formatter EntryFormatter
|
||||||
|
}
|
||||||
|
|
||||||
func parseLogging(i interface{}) (c *LoggingConfig, err error) {
|
func parseLogging(i interface{}) (c *LoggingConfig, err error) {
|
||||||
|
|
||||||
c = &LoggingConfig{}
|
c = &LoggingConfig{}
|
||||||
if i == nil {
|
|
||||||
return c, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var asMap struct {
|
|
||||||
Stdout struct {
|
|
||||||
Level string
|
|
||||||
Format string
|
|
||||||
}
|
|
||||||
TCP struct {
|
|
||||||
Level string
|
|
||||||
Format string
|
|
||||||
Net string
|
|
||||||
Address string
|
|
||||||
RetryInterval string `mapstructure:"retry_interval"`
|
|
||||||
TLS *struct {
|
|
||||||
CA string
|
|
||||||
Cert string
|
|
||||||
Key string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Syslog struct {
|
|
||||||
Enable bool
|
|
||||||
Format string
|
|
||||||
RetryInterval string `mapstructure:"retry_interval"`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err = mapstructure.Decode(i, &asMap); err != nil {
|
|
||||||
return nil, errors.Wrap(err, "mapstructure error")
|
|
||||||
}
|
|
||||||
|
|
||||||
c.Outlets = logger.NewOutlets()
|
c.Outlets = logger.NewOutlets()
|
||||||
|
|
||||||
if asMap.Stdout.Level != "" {
|
var asList []interface{}
|
||||||
|
if err = mapstructure.Decode(i, &asList); err != nil {
|
||||||
|
return nil, errors.Wrap(err, "mapstructure error")
|
||||||
|
}
|
||||||
|
if len(asList) == 0 {
|
||||||
|
// Default config
|
||||||
|
out := WriterOutlet{&HumanFormatter{}, os.Stdout}
|
||||||
|
c.Outlets.Add(out, logger.Warn)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
out := WriterOutlet{
|
var syslogOutlets, stdoutOutlets int
|
||||||
&HumanFormatter{},
|
for lei, le := range asList {
|
||||||
os.Stdout,
|
|
||||||
}
|
|
||||||
|
|
||||||
level, err := logger.ParseLevel(asMap.Stdout.Level)
|
outlet, minLevel, err := parseOutlet(le)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "cannot parse 'level'")
|
return nil, errors.Wrapf(err, "cannot parse outlet #%d", lei)
|
||||||
|
}
|
||||||
|
var _ logger.Outlet = WriterOutlet{}
|
||||||
|
var _ logger.Outlet = &SyslogOutlet{}
|
||||||
|
switch outlet.(type) {
|
||||||
|
case *SyslogOutlet:
|
||||||
|
syslogOutlets++
|
||||||
|
case WriterOutlet:
|
||||||
|
stdoutOutlets++
|
||||||
}
|
}
|
||||||
|
|
||||||
if asMap.Stdout.Format != "" {
|
c.Outlets.Add(outlet, minLevel)
|
||||||
out.Formatter, err = parseLogFormat(asMap.Stdout.Format)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "cannot parse 'format'")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
c.Outlets.Add(out, level)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if asMap.TCP.Address != "" {
|
if syslogOutlets > 1 {
|
||||||
|
return nil, errors.Errorf("can only define one 'syslog' outlet")
|
||||||
out := &TCPOutlet{}
|
|
||||||
|
|
||||||
out.Formatter, err = parseLogFormat(asMap.TCP.Format)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "cannot parse 'format'")
|
|
||||||
}
|
|
||||||
|
|
||||||
lvl, err := logger.ParseLevel(asMap.TCP.Level)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "cannot parse 'level'")
|
|
||||||
}
|
|
||||||
|
|
||||||
out.RetryInterval, err = time.ParseDuration(asMap.TCP.RetryInterval)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "cannot parse 'retry_interval'")
|
|
||||||
}
|
|
||||||
|
|
||||||
out.Net, out.Address = asMap.TCP.Net, asMap.TCP.Address
|
|
||||||
|
|
||||||
if asMap.TCP.TLS != nil {
|
|
||||||
|
|
||||||
cert, err := tls.LoadX509KeyPair(asMap.TCP.TLS.Cert, asMap.TCP.TLS.Key)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "cannot load client cert")
|
|
||||||
}
|
|
||||||
|
|
||||||
var rootCAs *x509.CertPool
|
|
||||||
if asMap.TCP.TLS.CA == "" {
|
|
||||||
if rootCAs, err = x509.SystemCertPool(); err != nil {
|
|
||||||
return nil, errors.Wrap(err, "cannot open system cert pool")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
rootCAs = x509.NewCertPool()
|
|
||||||
rootCAPEM, err := ioutil.ReadFile(asMap.TCP.TLS.CA)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "cannot load CA cert")
|
|
||||||
}
|
|
||||||
if !rootCAs.AppendCertsFromPEM(rootCAPEM) {
|
|
||||||
return nil, errors.New("cannot parse CA cert")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err != nil && asMap.TCP.TLS.CA == "" {
|
|
||||||
return nil, errors.Wrap(err, "cannot load root ca pool")
|
|
||||||
}
|
|
||||||
|
|
||||||
out.TLS = &tls.Config{
|
|
||||||
Certificates: []tls.Certificate{cert},
|
|
||||||
RootCAs: rootCAs,
|
|
||||||
}
|
|
||||||
|
|
||||||
out.TLS.BuildNameToCertificate()
|
|
||||||
}
|
|
||||||
|
|
||||||
c.Outlets.Add(out, lvl)
|
|
||||||
}
|
}
|
||||||
|
if stdoutOutlets > 1 {
|
||||||
if asMap.Syslog.Enable {
|
return nil, errors.Errorf("can only define one 'stdout' outlet")
|
||||||
|
|
||||||
out := &SyslogOutlet{}
|
|
||||||
|
|
||||||
out.Formatter = &HumanFormatter{}
|
|
||||||
if asMap.Syslog.Format != "" {
|
|
||||||
out.Formatter, err = parseLogFormat(asMap.Syslog.Format)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "cannot parse 'format'")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if f, ok := out.Formatter.(SetNoMetadataFormatter); ok {
|
|
||||||
f.SetNoMetadata(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
out.RetryInterval = 0 // default to 0 as we assume local syslog will just work
|
|
||||||
if asMap.Syslog.RetryInterval != "" {
|
|
||||||
out.RetryInterval, err = time.ParseDuration(asMap.Syslog.RetryInterval)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "cannot parse 'retry_interval'")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
c.Outlets.Add(out, logger.Debug)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return c, nil
|
return c, nil
|
||||||
@@ -189,3 +92,142 @@ func parseLogFormat(i interface{}) (f EntryFormatter, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseOutlet(i interface{}) (o logger.Outlet, level logger.Level, err error) {
|
||||||
|
|
||||||
|
var in struct {
|
||||||
|
Outlet string
|
||||||
|
Level string
|
||||||
|
Format string
|
||||||
|
}
|
||||||
|
if err = mapstructure.Decode(i, &in); err != nil {
|
||||||
|
err = errors.Wrap(err, "mapstructure error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if in.Outlet == "" || in.Level == "" || in.Format == "" {
|
||||||
|
err = errors.Errorf("must specify 'outlet', 'level' and 'format' field")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
common := &OutletCommon{}
|
||||||
|
common.MinLevel, err = logger.ParseLevel(in.Level)
|
||||||
|
if err != nil {
|
||||||
|
err = errors.Wrap(err, "cannot parse 'level' field")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
common.Formatter, err = parseLogFormat(in.Format)
|
||||||
|
if err != nil {
|
||||||
|
err = errors.Wrap(err, "cannot parse")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch in.Outlet {
|
||||||
|
case "stdout":
|
||||||
|
o, err = parseStdoutOutlet(i, common)
|
||||||
|
case "tcp":
|
||||||
|
o, err = parseTCPOutlet(i, common)
|
||||||
|
case "syslog":
|
||||||
|
o, err = parseSyslogOutlet(i, common)
|
||||||
|
default:
|
||||||
|
err = errors.Errorf("unknown outlet type '%s'", in.Outlet)
|
||||||
|
}
|
||||||
|
return o, common.MinLevel, err
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseStdoutOutlet(i interface{}, common *OutletCommon) (WriterOutlet, error) {
|
||||||
|
return WriterOutlet{
|
||||||
|
common.Formatter,
|
||||||
|
os.Stdout,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseTCPOutlet(i interface{}, common *OutletCommon) (out *TCPOutlet, err error) {
|
||||||
|
|
||||||
|
out = &TCPOutlet{}
|
||||||
|
out.Formatter = common.Formatter
|
||||||
|
|
||||||
|
var in struct {
|
||||||
|
Net string
|
||||||
|
Address string
|
||||||
|
RetryInterval string `mapstructure:"retry_interval"`
|
||||||
|
TLS *struct {
|
||||||
|
CA string
|
||||||
|
Cert string
|
||||||
|
Key string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err = mapstructure.Decode(i, &in); err != nil {
|
||||||
|
return nil, errors.Wrap(err, "mapstructure error")
|
||||||
|
}
|
||||||
|
|
||||||
|
out.RetryInterval, err = time.ParseDuration(in.RetryInterval)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "cannot parse 'retry_interval'")
|
||||||
|
}
|
||||||
|
|
||||||
|
out.Net, out.Address = in.Net, in.Address
|
||||||
|
|
||||||
|
if in.TLS != nil {
|
||||||
|
|
||||||
|
cert, err := tls.LoadX509KeyPair(in.TLS.Cert, in.TLS.Key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "cannot load client cert")
|
||||||
|
}
|
||||||
|
|
||||||
|
var rootCAs *x509.CertPool
|
||||||
|
if in.TLS.CA == "" {
|
||||||
|
if rootCAs, err = x509.SystemCertPool(); err != nil {
|
||||||
|
return nil, errors.Wrap(err, "cannot open system cert pool")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
rootCAs = x509.NewCertPool()
|
||||||
|
rootCAPEM, err := ioutil.ReadFile(in.TLS.CA)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "cannot load CA cert")
|
||||||
|
}
|
||||||
|
if !rootCAs.AppendCertsFromPEM(rootCAPEM) {
|
||||||
|
return nil, errors.New("cannot parse CA cert")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != nil && in.TLS.CA == "" {
|
||||||
|
return nil, errors.Wrap(err, "cannot load root ca pool")
|
||||||
|
}
|
||||||
|
|
||||||
|
out.TLS = &tls.Config{
|
||||||
|
Certificates: []tls.Certificate{cert},
|
||||||
|
RootCAs: rootCAs,
|
||||||
|
}
|
||||||
|
|
||||||
|
out.TLS.BuildNameToCertificate()
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseSyslogOutlet(i interface{}, common *OutletCommon) (out *SyslogOutlet, err error) {
|
||||||
|
|
||||||
|
var in struct {
|
||||||
|
RetryInterval string `mapstructure:"retry_interval"`
|
||||||
|
}
|
||||||
|
if err = mapstructure.Decode(i, &in); err != nil {
|
||||||
|
return nil, errors.Wrap(err, "mapstructure error")
|
||||||
|
}
|
||||||
|
|
||||||
|
out = &SyslogOutlet{}
|
||||||
|
out.Formatter = common.Formatter
|
||||||
|
if f, ok := out.Formatter.(SetNoMetadataFormatter); ok {
|
||||||
|
f.SetNoMetadata(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
out.RetryInterval = 0 // default to 0 as we assume local syslog will just work
|
||||||
|
if in.RetryInterval != "" {
|
||||||
|
out.RetryInterval, err = time.ParseDuration(in.RetryInterval)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "cannot parse 'retry_interval'")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|||||||
@@ -124,15 +124,10 @@ func (m DatasetMapFilter) Map(source *zfs.DatasetPath) (target *zfs.DatasetPath,
|
|||||||
err = fmt.Errorf("mapping target is not a dataset path: %s", err)
|
err = fmt.Errorf("mapping target is not a dataset path: %s", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if m.entries[mi].subtreeMatch {
|
if me.subtreeMatch {
|
||||||
// strip common prefix
|
// strip common prefix ('<' wildcards are no special case here)
|
||||||
extendComps := source.Copy()
|
extendComps := source.Copy()
|
||||||
if me.path.Empty() {
|
extendComps.TrimPrefix(me.path)
|
||||||
// special case: trying to map the root => strip first component
|
|
||||||
extendComps.TrimNPrefixComps(1)
|
|
||||||
} else {
|
|
||||||
extendComps.TrimPrefix(me.path)
|
|
||||||
}
|
|
||||||
target.Extend(extendComps)
|
target.Extend(extendComps)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ import (
|
|||||||
"github.com/mitchellh/mapstructure"
|
"github.com/mitchellh/mapstructure"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"os"
|
"os"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
var ConfigFileDefaultLocations []string = []string{
|
var ConfigFileDefaultLocations []string = []string{
|
||||||
@@ -252,3 +255,41 @@ func parseAuthenticatedChannelListenerFactory(c JobParsingContext, v map[string]
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var durationStringRegex *regexp.Regexp = regexp.MustCompile(`^\s*(\d+)\s*(s|m|h|d|w)\s*$`)
|
||||||
|
|
||||||
|
func parsePostitiveDuration(e string) (d time.Duration, err error) {
|
||||||
|
comps := durationStringRegex.FindStringSubmatch(e)
|
||||||
|
if len(comps) != 3 {
|
||||||
|
err = fmt.Errorf("does not match regex: %s %#v", e, comps)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
durationFactor, err := strconv.ParseInt(comps[1], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if durationFactor <= 0 {
|
||||||
|
return 0, errors.New("duration must be positive integer")
|
||||||
|
}
|
||||||
|
|
||||||
|
var durationUnit time.Duration
|
||||||
|
switch comps[2] {
|
||||||
|
case "s":
|
||||||
|
durationUnit = time.Second
|
||||||
|
case "m":
|
||||||
|
durationUnit = time.Minute
|
||||||
|
case "h":
|
||||||
|
durationUnit = time.Hour
|
||||||
|
case "d":
|
||||||
|
durationUnit = 24 * time.Hour
|
||||||
|
case "w":
|
||||||
|
durationUnit = 24 * 7 * time.Hour
|
||||||
|
default:
|
||||||
|
err = fmt.Errorf("contains unknown time unit '%s'", comps[2])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
d = time.Duration(durationFactor) * durationUnit
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|||||||
+14
-41
@@ -80,55 +80,28 @@ func parseGridPrunePolicy(e map[string]interface{}) (p *GridPrunePolicy, err err
|
|||||||
// Assert intervals are of increasing length (not necessarily required, but indicates config mistake)
|
// Assert intervals are of increasing length (not necessarily required, but indicates config mistake)
|
||||||
lastDuration := time.Duration(0)
|
lastDuration := time.Duration(0)
|
||||||
for i := range intervals {
|
for i := range intervals {
|
||||||
|
|
||||||
if intervals[i].Length < lastDuration {
|
if intervals[i].Length < lastDuration {
|
||||||
err = fmt.Errorf("retention grid interval length must be monotonically increasing:"+
|
// If all intervals before were keep=all, this is ok
|
||||||
"interval %d is shorter than %d", i+1, i)
|
allPrevKeepCountAll := true
|
||||||
|
for j := i - 1; allPrevKeepCountAll && j >= 0; j-- {
|
||||||
|
allPrevKeepCountAll = intervals[j].KeepCount == util.RetentionGridKeepCountAll
|
||||||
|
}
|
||||||
|
if allPrevKeepCountAll {
|
||||||
|
goto isMonotonicIncrease
|
||||||
|
}
|
||||||
|
err = errors.New("retention grid interval length must be monotonically increasing")
|
||||||
return
|
return
|
||||||
} else {
|
|
||||||
lastDuration = intervals[i].Length
|
|
||||||
}
|
}
|
||||||
|
isMonotonicIncrease:
|
||||||
|
lastDuration = intervals[i].Length
|
||||||
|
|
||||||
}
|
}
|
||||||
p.RetentionGrid = util.NewRetentionGrid(intervals)
|
p.RetentionGrid = util.NewRetentionGrid(intervals)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var durationStringRegex *regexp.Regexp = regexp.MustCompile(`^\s*(\d+)\s*(s|m|h|d|w)\s*$`)
|
|
||||||
|
|
||||||
func parseDuration(e string) (d time.Duration, err error) {
|
|
||||||
comps := durationStringRegex.FindStringSubmatch(e)
|
|
||||||
if len(comps) != 3 {
|
|
||||||
err = fmt.Errorf("does not match regex: %s %#v", e, comps)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
durationFactor, err := strconv.ParseInt(comps[1], 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var durationUnit time.Duration
|
|
||||||
switch comps[2] {
|
|
||||||
case "s":
|
|
||||||
durationUnit = time.Second
|
|
||||||
case "m":
|
|
||||||
durationUnit = time.Minute
|
|
||||||
case "h":
|
|
||||||
durationUnit = time.Hour
|
|
||||||
case "d":
|
|
||||||
durationUnit = 24 * time.Hour
|
|
||||||
case "w":
|
|
||||||
durationUnit = 24 * 7 * time.Hour
|
|
||||||
default:
|
|
||||||
err = fmt.Errorf("contains unknown time unit '%s'", comps[2])
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
d = time.Duration(durationFactor) * durationUnit
|
|
||||||
return
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
var retentionStringIntervalRegex *regexp.Regexp = regexp.MustCompile(`^\s*(\d+)\s*x\s*([^\(]+)\s*(\((.*)\))?\s*$`)
|
var retentionStringIntervalRegex *regexp.Regexp = regexp.MustCompile(`^\s*(\d+)\s*x\s*([^\(]+)\s*(\((.*)\))?\s*$`)
|
||||||
|
|
||||||
func parseRetentionGridIntervalString(e string) (intervals []util.RetentionInterval, err error) {
|
func parseRetentionGridIntervalString(e string) (intervals []util.RetentionInterval, err error) {
|
||||||
@@ -146,7 +119,7 @@ func parseRetentionGridIntervalString(e string) (intervals []util.RetentionInter
|
|||||||
return nil, fmt.Errorf("contains factor <= 0")
|
return nil, fmt.Errorf("contains factor <= 0")
|
||||||
}
|
}
|
||||||
|
|
||||||
duration, err := parseDuration(comps[2])
|
duration, err := parsePostitiveDuration(comps[2])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -129,8 +129,8 @@ func TestDatasetMapFilter(t *testing.T) {
|
|||||||
expectMapping(map1, "a/b/c/d/e", "")
|
expectMapping(map1, "a/b/c/d/e", "")
|
||||||
expectMapping(map1, "a/b/e", "root2/e")
|
expectMapping(map1, "a/b/e", "root2/e")
|
||||||
expectMapping(map1, "a/b", "root2")
|
expectMapping(map1, "a/b", "root2")
|
||||||
expectMapping(map1, "x", "root3/b/c")
|
expectMapping(map1, "x", "root3/b/c/x")
|
||||||
expectMapping(map1, "x/y", "root3/b/c/y")
|
expectMapping(map1, "x/y", "root3/b/c/x/y")
|
||||||
expectMapping(map1, "q", "root4/1/2")
|
expectMapping(map1, "q", "root4/1/2")
|
||||||
expectMapping(map1, "b", "")
|
expectMapping(map1, "b", "")
|
||||||
expectMapping(map1, "q/r", "root4/1/2/r")
|
expectMapping(map1, "q/r", "root4/1/2/r")
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ jobs:
|
|||||||
mapping: {
|
mapping: {
|
||||||
"zroot/var/db<": "storage/backups/local/zroot/var/db",
|
"zroot/var/db<": "storage/backups/local/zroot/var/db",
|
||||||
"zroot/usr/home<": "storage/backups/local/zroot/usr/home",
|
"zroot/usr/home<": "storage/backups/local/zroot/usr/home",
|
||||||
"zroot/var/tmp": "!", #don't backup /tmp
|
"zroot/usr/home/paranoid": "!", #don't backup paranoid user
|
||||||
|
"zroot/poudriere/ports<": "!", #don't backup the ports trees
|
||||||
}
|
}
|
||||||
snapshot_prefix: zrepl_
|
snapshot_prefix: zrepl_
|
||||||
interval: 10m
|
interval: 10m
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ jobs:
|
|||||||
client_identity: fullbackup_prod1
|
client_identity: fullbackup_prod1
|
||||||
|
|
||||||
# snapshot these filesystems every 10m with zrepl_ as prefix
|
# snapshot these filesystems every 10m with zrepl_ as prefix
|
||||||
datasets: {
|
filesystems: {
|
||||||
"zroot/var/db<": "ok",
|
"zroot/var/db<": "ok",
|
||||||
"zroot/usr/home<": "ok",
|
"zroot/usr/home<": "ok",
|
||||||
"zroot/var/tmp": "!", #don't backup /tmp
|
"zroot/var/tmp": "!", #don't backup /tmp
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ jobs:
|
|||||||
identity_file: /root/.ssh/id_ed25519
|
identity_file: /root/.ssh/id_ed25519
|
||||||
|
|
||||||
# snapshot these datsets every 10m with zrepl_ as prefix
|
# snapshot these datsets every 10m with zrepl_ as prefix
|
||||||
datasets: {
|
filesystems: {
|
||||||
"zroot/var/db<": "ok",
|
"zroot/var/db<": "ok",
|
||||||
"zroot/usr/home<": "!",
|
"zroot/usr/home<": "!",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ jobs:
|
|||||||
serve:
|
serve:
|
||||||
type: stdinserver
|
type: stdinserver
|
||||||
client_identity: debian2
|
client_identity: debian2
|
||||||
datasets: {
|
filesystems: {
|
||||||
"pool1/db<": ok
|
"pool1/db<": ok
|
||||||
}
|
}
|
||||||
snapshot_prefix: zrepl_
|
snapshot_prefix: zrepl_
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
global:
|
global:
|
||||||
logging:
|
logging:
|
||||||
stdout:
|
|
||||||
|
- outlet: stdout
|
||||||
level: warn
|
level: warn
|
||||||
format: human
|
format: human
|
||||||
tcp:
|
|
||||||
|
- outlet: tcp
|
||||||
level: debug
|
level: debug
|
||||||
format: json
|
format: json
|
||||||
net: tcp
|
net: tcp
|
||||||
@@ -13,8 +15,10 @@ global:
|
|||||||
ca: sampleconf/random/logging/logserver.crt
|
ca: sampleconf/random/logging/logserver.crt
|
||||||
cert: sampleconf/random/logging/client.crt
|
cert: sampleconf/random/logging/client.crt
|
||||||
key: sampleconf/random/logging/client.key
|
key: sampleconf/random/logging/client.key
|
||||||
syslog:
|
|
||||||
enable: true
|
- outlet: syslog
|
||||||
|
level: debug
|
||||||
format: logfmt
|
format: logfmt
|
||||||
|
|
||||||
|
|
||||||
jobs: []
|
jobs: []
|
||||||
|
|||||||
@@ -1,111 +0,0 @@
|
|||||||
remotes:
|
|
||||||
offsite_backups:
|
|
||||||
transport:
|
|
||||||
ssh:
|
|
||||||
host: 192.168.122.6
|
|
||||||
user: root
|
|
||||||
port: 22
|
|
||||||
identity_file: /etc/zrepl/identities/offsite_backups
|
|
||||||
|
|
||||||
push:
|
|
||||||
|
|
||||||
offsite:
|
|
||||||
to: offsite_backups
|
|
||||||
filter: {
|
|
||||||
# like in pull_acls
|
|
||||||
"tank/var/db<": ok,
|
|
||||||
"tank/usr/home<": ok,
|
|
||||||
}
|
|
||||||
|
|
||||||
pull:
|
|
||||||
|
|
||||||
offsite:
|
|
||||||
from: offsite_backups
|
|
||||||
mapping: {
|
|
||||||
# like in sinks
|
|
||||||
}
|
|
||||||
|
|
||||||
# local replication, only allowed in pull mode
|
|
||||||
# the from name 'local' is reserved for this purpose
|
|
||||||
homemirror:
|
|
||||||
from: local
|
|
||||||
repeat:
|
|
||||||
interval: 15m
|
|
||||||
mapping: {
|
|
||||||
"tank/usr/home":"mirrorpool/foo/bar"
|
|
||||||
}
|
|
||||||
|
|
||||||
sink:
|
|
||||||
|
|
||||||
db1:
|
|
||||||
mapping: {
|
|
||||||
# direct mapping
|
|
||||||
"ssdpool/var/db/postgresql9.6":"zroot/backups/db1/pg_data"
|
|
||||||
}
|
|
||||||
|
|
||||||
mirror1:
|
|
||||||
mapping: {
|
|
||||||
# "<" subtree wildcard matches the dataset left of < and all its children
|
|
||||||
"tank/foo/bar<":"zroot/backups/mirror1"
|
|
||||||
}
|
|
||||||
|
|
||||||
mirror2:
|
|
||||||
# more specific path patterns win over less specific ones
|
|
||||||
# direct mappings win over subtree wildcards
|
|
||||||
# detailed rule precedence: check unit tests & docs for exact behavior
|
|
||||||
# TODO subcommand to test a mapping & filter
|
|
||||||
mapping: {
|
|
||||||
"tank<": "zroot/backups/mirror1/tank1",
|
|
||||||
"tank/cdn/root<": "storage/cdn/root",
|
|
||||||
"tank/legacydb": "legacypool/backups/legacydb",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
pull_acl:
|
|
||||||
|
|
||||||
office_backup:
|
|
||||||
filter: {
|
|
||||||
# valid filter results (right hand side): ok, omit
|
|
||||||
# default is to omit
|
|
||||||
# rule precedence is same as for mappings
|
|
||||||
"tank<": omit,
|
|
||||||
"tank/usr/home": ok,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
prune:
|
|
||||||
|
|
||||||
clean_backups:
|
|
||||||
policy: grid
|
|
||||||
grid: 6x10m | 24x1h | 7x1d | 5 x 1w | 4 x 5w
|
|
||||||
dataset_filter: {
|
|
||||||
"tank/backups/legacyscript<": omit,
|
|
||||||
"tank/backups<": ok,
|
|
||||||
}
|
|
||||||
snapshot_filter: {
|
|
||||||
prefix: zrepl_
|
|
||||||
}
|
|
||||||
|
|
||||||
hfbak_prune: # cleans up after hfbak autosnap job
|
|
||||||
policy: grid
|
|
||||||
grid: 1x1m(keep=all)
|
|
||||||
dataset_filter: {
|
|
||||||
"pool1<": ok
|
|
||||||
}
|
|
||||||
snapshot_filter: {
|
|
||||||
prefix: zrepl_hfbak_
|
|
||||||
}
|
|
||||||
repeat:
|
|
||||||
interval: 10s
|
|
||||||
|
|
||||||
autosnap:
|
|
||||||
|
|
||||||
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)
|
|
||||||
+1
-1
@@ -109,7 +109,7 @@ func doTestDatasetMapFilter(cmd *cobra.Command, args []string) {
|
|||||||
case *PullJob:
|
case *PullJob:
|
||||||
mf = j.Mapping
|
mf = j.Mapping
|
||||||
case *SourceJob:
|
case *SourceJob:
|
||||||
mf = j.Datasets
|
mf = j.Filesystems
|
||||||
case *LocalJob:
|
case *LocalJob:
|
||||||
mf = j.Mapping
|
mf = j.Mapping
|
||||||
default:
|
default:
|
||||||
|
|||||||
+2
-1
@@ -1 +1,2 @@
|
|||||||
public_git
|
_build/
|
||||||
|
public_git/
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# Minimal makefile for Sphinx documentation
|
||||||
|
#
|
||||||
|
|
||||||
|
# You can set these variables from the command line.
|
||||||
|
SPHINXOPTS =
|
||||||
|
SPHINXBUILD = sphinx-build
|
||||||
|
SPHINXPROJ = zrepl
|
||||||
|
SOURCEDIR = .
|
||||||
|
BUILDDIR = _build
|
||||||
|
|
||||||
|
# Put it first so that "make" without argument is like "make help".
|
||||||
|
help:
|
||||||
|
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||||
|
|
||||||
|
.PHONY: help Makefile
|
||||||
|
|
||||||
|
# Catch-all target: route all unknown targets to Sphinx using the new
|
||||||
|
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
||||||
|
%: Makefile
|
||||||
|
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||||
Vendored
+147
@@ -0,0 +1,147 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
id="svg8"
|
||||||
|
version="1.1"
|
||||||
|
viewBox="0 0 70.555558 24.694445"
|
||||||
|
height="70pt"
|
||||||
|
width="200pt"
|
||||||
|
sodipodi:docname="zrepl.svg"
|
||||||
|
inkscape:version="0.92.2 5c3e80d, 2017-08-06">
|
||||||
|
<sodipodi:namedview
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1"
|
||||||
|
objecttolerance="10"
|
||||||
|
gridtolerance="10"
|
||||||
|
guidetolerance="10"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:window-width="2560"
|
||||||
|
inkscape:window-height="1405"
|
||||||
|
id="namedview10"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:zoom="3.1350001"
|
||||||
|
inkscape:cx="6.2268563"
|
||||||
|
inkscape:cy="-2.6953765"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="35"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="layer1" />
|
||||||
|
<defs
|
||||||
|
id="defs2" />
|
||||||
|
<metadata
|
||||||
|
id="metadata5">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
<dc:title />
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<g
|
||||||
|
transform="translate(0,-272.30556)"
|
||||||
|
id="layer1">
|
||||||
|
<g
|
||||||
|
aria-label="zrepl"
|
||||||
|
transform="matrix(0.26458334,0,0,0.26458334,0.52916655,273.36389)"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:40px;line-height:25px;font-family:monospace;-inkscape-font-specification:'monospace, Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:center;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:middle;fill:#f8f8f8;fill-opacity:1;stroke:#f8f8f8;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||||
|
id="flowRoot829">
|
||||||
|
<path
|
||||||
|
d="m 74.504883,26.943359 11.523437,-13.125 H 74.973633 V 9.5410156 h 17.480469 v 4.4726564 l -11.503907,13.125 h 11.503907 v 4.277344 H 74.504883 Z"
|
||||||
|
style="stroke:#f8f8f8;stroke-opacity:1;fill:#f8f8f8;fill-opacity:1"
|
||||||
|
id="path873" />
|
||||||
|
<path
|
||||||
|
d="m 100.11035,9.5410156 h 5.15625 l 0.56641,3.3984374 q 0.83984,-1.875 2.57812,-2.910156 1.73828,-1.0156251 4.00391,-1.0156251 1.26953,0 2.36328,0.2734375 1.25,0.3515625 2.12891,0.8398436 v 5.449219 q -0.9375,-0.859375 -2.1875,-1.289063 -1.23047,-0.429687 -2.75391,-0.429687 -1.81641,0 -3.18359,0.625 -1.36719,0.605469 -2.10938,1.835937 -0.19531,0.3125 -0.37109,0.761719 -0.17578,0.449219 -0.27344,1.054688 -0.0977,0.566406 -0.15625,1.308593 -0.0391,0.742188 -0.0391,1.894532 v 10.078125 h -5.72266 z"
|
||||||
|
style="stroke:#f8f8f8;stroke-opacity:1;fill:#f8f8f8;fill-opacity:1"
|
||||||
|
id="path875" />
|
||||||
|
<path
|
||||||
|
d="m 132.4541,31.982422 q -2.85156,0 -4.9414,-0.722656 -2.07032,-0.742188 -3.53516,-2.226563 -2.91016,-2.96875 -2.91016,-8.4375 0,-5.371094 2.79297,-8.476562 2.79297,-3.1054691 7.65625,-3.1054691 3.10547,-0.097656 5.42969,1.2500001 2.34375,1.347656 3.59375,3.867187 1.26953,2.519532 1.17187,5.878907 v 2.324218 H 126.8877 q 0.0195,2.578125 1.52343,3.847657 1.50391,1.269531 4.47266,1.269531 1.93359,0 3.90625,-0.566406 0.89844,-0.273438 1.89453,-0.703125 0.99609,-0.429688 2.10938,-1.09375 v 5.253906 q -1.99219,0.820312 -4.0625,1.230469 -1.05469,0.214843 -2.14844,0.3125 -1.07422,0.09766 -2.12891,0.09766 z m 3.4961,-13.945313 q -0.0195,-2.285156 -1.15235,-3.4375 -1.11328,-1.152343 -3.28125,-1.152343 -2.01172,0 -3.18359,1.191406 -1.17188,1.210937 -1.38672,3.417969 z"
|
||||||
|
style="stroke:#f8f8f8;stroke-opacity:1;fill:#f8f8f8;fill-opacity:1"
|
||||||
|
id="path877" />
|
||||||
|
<path
|
||||||
|
d="m 146.45801,9.5410156 h 5.13672 l 0.5664,3.2812504 q 0.89844,-1.894532 2.36328,-2.8515629 1.42579,-0.9570312 3.45704,-0.9570312 3.71093,0 5.80078,2.9882811 1.05468,1.523438 1.5625,3.691406 0.50781,2.148438 0.50781,4.824219 0,2.851563 -0.54688,4.980469 -0.54687,2.109375 -1.54296,3.515625 -1.07422,1.523437 -2.59766,2.246094 -1.50391,0.722656 -3.28125,0.722656 -0.91797,0 -1.73828,-0.214844 -0.80078,-0.195312 -1.54297,-0.703125 -0.625,-0.410156 -1.28906,-1.09375 -0.64453,-0.703125 -1.15235,-1.640625 v 11.40625 h -5.70312 z m 9.70703,17.6757814 q 1.85547,0 2.91016,-1.796875 1.05468,-1.777344 1.05468,-4.960938 0,-3.164062 -1.05468,-4.960937 -1.05469,-1.796875 -2.91016,-1.796875 -1.83594,0 -2.92969,1.796875 -1.07422,1.777344 -1.07422,4.960937 0,3.183594 1.07422,4.960938 1.09375,1.796875 2.92969,1.796875 z"
|
||||||
|
style="stroke:#f8f8f8;stroke-opacity:1;fill:#f8f8f8;fill-opacity:1"
|
||||||
|
id="path879" />
|
||||||
|
<path
|
||||||
|
d="m 182.98145,31.572266 q -4.0625,0 -5.85938,-2.109375 -1.77734,-2.070313 -1.77734,-7.011719 V 5.4199219 h -5.80078 V 1.0253906 h 11.52343 V 22.451172 q 0,2.558594 0.80078,3.652344 0.78125,1.074218 2.71485,1.074218 h 4.57031 v 4.394532 z"
|
||||||
|
style="stroke:#f8f8f8;stroke-opacity:1;fill:#f8f8f8;fill-opacity:1"
|
||||||
|
id="path881" />
|
||||||
|
</g>
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path837"
|
||||||
|
d="M 26.170418,286.30982 H 43.35751"
|
||||||
|
style="font-style:normal;font-weight:normal;font-size:medium;line-height:25px;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#f8f8f8;fill-opacity:1;fill-rule:evenodd;stroke:#f8f8f8;stroke-width:0.26458335px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
|
||||||
|
<g
|
||||||
|
aria-label="ZFS replication"
|
||||||
|
style="font-style:normal;font-weight:normal;font-size:10.58333397px;line-height:6.61458349px;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#f8f8f8;fill-opacity:1;stroke:#f8f8f8;stroke-width:0.26458335px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||||
|
id="text820">
|
||||||
|
<path
|
||||||
|
d="m 14.758548,294.66467 h -3.493327 v -0.47887 l 2.652724,-3.99286 h -2.570041 v -0.565 h 3.338297 v 0.47887 l -2.652723,3.99287 h 2.72507 z"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.05555582px;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;writing-mode:lr-tb;text-anchor:start;fill:#f8f8f8;fill-opacity:1;stroke:#f8f8f8;stroke-width:0.26458335px;stroke-opacity:1"
|
||||||
|
id="path844" />
|
||||||
|
<path
|
||||||
|
d="M 16.353627,294.66467 H 15.71973 v -5.03673 h 2.814643 v 0.55811 h -2.180746 v 1.78456 h 2.046387 v 0.55466 h -2.046387 z"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.05555582px;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;writing-mode:lr-tb;text-anchor:start;fill:#f8f8f8;fill-opacity:1;stroke:#f8f8f8;stroke-width:0.26458335px;stroke-opacity:1"
|
||||||
|
id="path846" />
|
||||||
|
<path
|
||||||
|
d="m 22.241296,293.31764 q 0,0.65801 -0.475423,1.03697 -0.475424,0.37896 -1.322917,0.37896 -0.895725,0 -1.384929,-0.23771 v -0.60633 q 0.320394,0.1378 0.699355,0.21704 0.382406,0.0792 0.713135,0.0792 0.564995,0 0.850938,-0.21704 0.285943,-0.21704 0.285943,-0.596 0,-0.25149 -0.103353,-0.41341 -0.103352,-0.16192 -0.347954,-0.30317 -0.241157,-0.14125 -0.733806,-0.32039 -0.699354,-0.25494 -0.999077,-0.59945 -0.299724,-0.34795 -0.299724,-0.89572 0,-0.58911 0.440973,-0.93707 0.444417,-0.34795 1.167887,-0.34795 0.754476,0 1.388373,0.28249 l -0.19637,0.54433 q -0.647678,-0.26872 -1.205783,-0.26872 -0.447863,0 -0.7028,0.19293 -0.251492,0.19292 -0.251492,0.54088 0,0.24804 0.09646,0.41341 0.09991,0.16192 0.316949,0.29628 0.220486,0.13436 0.689019,0.30661 0.56155,0.20671 0.837158,0.40308 0.275608,0.19292 0.406521,0.44786 0.130914,0.25149 0.130914,0.60289 z"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.05555582px;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;writing-mode:lr-tb;text-anchor:start;fill:#f8f8f8;fill-opacity:1;stroke:#f8f8f8;stroke-width:0.26458335px;stroke-opacity:1"
|
||||||
|
id="path848" />
|
||||||
|
<path
|
||||||
|
d="m 26.768151,290.81306 q 0.251492,0 0.444418,0.0413 l -0.07579,0.57188 q -0.21015,-0.0482 -0.409966,-0.0482 -0.299723,0 -0.558105,0.16537 -0.254937,0.16536 -0.399631,0.45819 -0.144694,0.28939 -0.144694,0.64424 v 2.01882 h -0.620118 v -3.78271 h 0.509874 l 0.0689,0.69246 h 0.02756 q 0.217041,-0.37551 0.513319,-0.56844 0.296279,-0.19292 0.644233,-0.19292 z"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.05555582px;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;writing-mode:lr-tb;text-anchor:start;fill:#f8f8f8;fill-opacity:1;stroke:#f8f8f8;stroke-width:0.26458335px;stroke-opacity:1"
|
||||||
|
id="path850" />
|
||||||
|
<path
|
||||||
|
d="m 29.465661,294.73357 q -0.850939,0 -1.336697,-0.51331 -0.482313,-0.51677 -0.482313,-1.41938 0,-0.90951 0.451307,-1.44694 0.451308,-0.54088 1.219564,-0.54088 0.713135,0 1.136881,0.46164 0.423747,0.4582 0.423747,1.24368 v 0.37551 h -2.590712 q 0.01723,0.64424 0.327284,0.97841 0.310059,0.33418 0.8785,0.33418 0.303168,0 0.575331,-0.0517 0.272162,-0.0551 0.637342,-0.21015 v 0.54432 q -0.313503,0.13436 -0.589111,0.18948 -0.275608,0.0551 -0.651123,0.0551 z m -0.155029,-3.41408 q -0.444418,0 -0.7028,0.28594 -0.258382,0.28594 -0.306613,0.79582 h 1.925808 q -0.0069,-0.53055 -0.244602,-0.80616 -0.237711,-0.2756 -0.671793,-0.2756 z"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.05555582px;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;writing-mode:lr-tb;text-anchor:start;fill:#f8f8f8;fill-opacity:1;stroke:#f8f8f8;stroke-width:0.26458335px;stroke-opacity:1"
|
||||||
|
id="path852" />
|
||||||
|
<path
|
||||||
|
d="m 33.630782,294.73357 q -0.761366,0 -1.171333,-0.54777 h -0.04134 l 0.01378,0.14125 q 0.02756,0.26872 0.02756,0.47887 v 1.55374 h -0.620117 v -5.4777 h 0.509874 l 0.08268,0.51676 h 0.02756 q 0.220486,-0.31006 0.509874,-0.44786 0.289388,-0.1378 0.668349,-0.1378 0.73725,0 1.143772,0.51332 0.409966,0.50987 0.409966,1.44005 0,0.92673 -0.409966,1.44694 -0.409967,0.5202 -1.150662,0.5202 z m -0.103353,-3.4003 q -0.561551,0 -0.813043,0.31694 -0.248047,0.31695 -0.254937,0.99564 v 0.12058 q 0,0.76825 0.254937,1.11276 0.254937,0.34107 0.826823,0.34107 0.475423,0 0.740696,-0.38586 0.268717,-0.38585 0.268717,-1.07487 0,-0.69246 -0.268717,-1.05764 -0.265273,-0.36862 -0.754476,-0.36862 z"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.05555582px;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;writing-mode:lr-tb;text-anchor:start;fill:#f8f8f8;fill-opacity:1;stroke:#f8f8f8;stroke-width:0.26458335px;stroke-opacity:1"
|
||||||
|
id="path854" />
|
||||||
|
<path
|
||||||
|
d="m 36.800269,294.66467 h -0.620117 v -5.36057 h 0.620117 z"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.05555582px;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;writing-mode:lr-tb;text-anchor:start;fill:#f8f8f8;fill-opacity:1;stroke:#f8f8f8;stroke-width:0.26458335px;stroke-opacity:1"
|
||||||
|
id="path856" />
|
||||||
|
<path
|
||||||
|
d="m 38.619281,294.66467 h -0.620117 v -3.78271 h 0.620117 z m -0.668348,-4.80591 q 0,-0.2067 0.103352,-0.29972 0.106798,-0.093 0.261828,-0.093 0.144694,0 0.251492,0.093 0.110243,0.093 0.110243,0.29972 0,0.20327 -0.110243,0.29973 -0.106798,0.0965 -0.251492,0.0965 -0.15503,0 -0.261828,-0.0965 -0.103352,-0.0965 -0.103352,-0.29973 z"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.05555582px;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;writing-mode:lr-tb;text-anchor:start;fill:#f8f8f8;fill-opacity:1;stroke:#f8f8f8;stroke-width:0.26458335px;stroke-opacity:1"
|
||||||
|
id="path858" />
|
||||||
|
<path
|
||||||
|
d="m 41.334016,294.73357 q -0.816488,0 -1.27124,-0.49953 -0.454753,-0.50299 -0.454753,-1.44005 0,-0.95085 0.458198,-1.46417 0.461642,-0.51676 1.312581,-0.51676 0.275608,0 0.544325,0.0586 0.272163,0.0551 0.447863,0.1447 l -0.189481,0.51676 q -0.478868,-0.17914 -0.816487,-0.17914 -0.571886,0 -0.844049,0.36173 -0.272162,0.35829 -0.272162,1.07143 0,0.68557 0.272162,1.05075 0.272163,0.36173 0.806153,0.36173 0.499538,0 0.981852,-0.22048 v 0.55121 q -0.392741,0.20326 -0.974962,0.20326 z"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.05555582px;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;writing-mode:lr-tb;text-anchor:start;fill:#f8f8f8;fill-opacity:1;stroke:#f8f8f8;stroke-width:0.26458335px;stroke-opacity:1"
|
||||||
|
id="path860" />
|
||||||
|
<path
|
||||||
|
d="m 45.543923,294.66467 -0.120578,-0.53743 h -0.02756 q -0.282498,0.35484 -0.564996,0.48231 -0.279052,0.12402 -0.706244,0.12402 -0.558106,0 -0.875055,-0.29283 -0.316948,-0.29283 -0.316948,-0.82682 0,-1.15066 1.815565,-1.20578 l 0.640788,-0.0241 v -0.22393 q 0,-0.43064 -0.186035,-0.6339 -0.186035,-0.2067 -0.596002,-0.2067 -0.299723,0 -0.568441,0.0896 -0.265272,0.0896 -0.499538,0.19981 l -0.189481,-0.46508 q 0.285943,-0.15159 0.623563,-0.23772 0.337619,-0.0861 0.668348,-0.0861 0.685574,0 1.019748,0.30316 0.334175,0.30317 0.334175,0.96463 v 2.57693 z m -1.27813,-0.43063 q 0.520209,0 0.816487,-0.27906 0.299724,-0.28249 0.299724,-0.79926 v -0.34106 l -0.558106,0.0241 q -0.651123,0.0241 -0.950846,0.20671 -0.296278,0.18259 -0.296278,0.57533 0,0.29628 0.179145,0.45475 0.18259,0.15848 0.509874,0.15848 z"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.05555582px;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;writing-mode:lr-tb;text-anchor:start;fill:#f8f8f8;fill-opacity:1;stroke:#f8f8f8;stroke-width:0.26458335px;stroke-opacity:1"
|
||||||
|
id="path862" />
|
||||||
|
<path
|
||||||
|
d="m 48.427466,294.22715 q 0.120578,0 0.282498,-0.0241 0.161919,-0.0241 0.244602,-0.0551 v 0.47542 q -0.08613,0.0379 -0.268718,0.0723 -0.179145,0.0379 -0.36518,0.0379 -1.109321,0 -1.109321,-1.16788 v -2.20142 h -0.53399 v -0.29628 l 0.54088,-0.24804 0.248047,-0.80616 h 0.368625 v 0.86817 h 1.092096 v 0.48231 h -1.092096 v 2.18419 q 0,0.32729 0.15503,0.50299 0.158474,0.1757 0.437527,0.1757 z"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.05555582px;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;writing-mode:lr-tb;text-anchor:start;fill:#f8f8f8;fill-opacity:1;stroke:#f8f8f8;stroke-width:0.26458335px;stroke-opacity:1"
|
||||||
|
id="path864" />
|
||||||
|
<path
|
||||||
|
d="m 50.332607,294.66467 h -0.620118 v -3.78271 h 0.620118 z m -0.668349,-4.80591 q 0,-0.2067 0.103353,-0.29972 0.106798,-0.093 0.261827,-0.093 0.144694,0 0.251492,0.093 0.110243,0.093 0.110243,0.29972 0,0.20327 -0.110243,0.29973 -0.106798,0.0965 -0.251492,0.0965 -0.155029,0 -0.261827,-0.0965 -0.103353,-0.0965 -0.103353,-0.29973 z"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.05555582px;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;writing-mode:lr-tb;text-anchor:start;fill:#f8f8f8;fill-opacity:1;stroke:#f8f8f8;stroke-width:0.26458335px;stroke-opacity:1"
|
||||||
|
id="path866" />
|
||||||
|
<path
|
||||||
|
d="m 54.818119,292.76643 q 0,0.92673 -0.468533,1.44694 -0.468533,0.5202 -1.295356,0.5202 -0.51332,0 -0.909506,-0.23771 -0.396186,-0.24115 -0.609782,-0.68902 -0.213595,-0.44786 -0.213595,-1.04041 0,-0.92329 0.461642,-1.43661 0.461643,-0.51676 1.295356,-0.51676 0.799262,0 1.267795,0.5271 0.471979,0.52365 0.471979,1.42627 z m -2.855985,0 q 0,0.71313 0.279053,1.0852 0.282498,0.36863 0.830268,0.36863 0.54088,0 0.823378,-0.36863 0.282498,-0.37207 0.282498,-1.0852 0,-0.71314 -0.285943,-1.07143 -0.282498,-0.36173 -0.830268,-0.36173 -1.098986,0 -1.098986,1.43316 z"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.05555582px;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;writing-mode:lr-tb;text-anchor:start;fill:#f8f8f8;fill-opacity:1;stroke:#f8f8f8;stroke-width:0.26458335px;stroke-opacity:1"
|
||||||
|
id="path868" />
|
||||||
|
<path
|
||||||
|
d="m 58.380348,294.66467 v -2.41845 q 0,-0.46165 -0.206706,-0.68558 -0.20326,-0.22737 -0.640787,-0.22737 -0.582222,0 -0.847494,0.32383 -0.261827,0.3204 -0.261827,1.04731 v 1.96026 h -0.620117 v -3.78271 h 0.499538 l 0.09302,0.51676 h 0.03445 q 0.1757,-0.27905 0.492649,-0.43063 0.316948,-0.15503 0.699354,-0.15503 0.695909,0 1.033529,0.33762 0.337619,0.33761 0.337619,1.05075 v 2.46324 z"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.05555582px;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;writing-mode:lr-tb;text-anchor:start;fill:#f8f8f8;fill-opacity:1;stroke:#f8f8f8;stroke-width:0.26458335px;stroke-opacity:1"
|
||||||
|
id="path870" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 19 KiB |
@@ -1,6 +0,0 @@
|
|||||||
---
|
|
||||||
title: "{{ replace .TranslationBaseName "-" " " | title }}"
|
|
||||||
date: {{ .Date }}
|
|
||||||
draft: true
|
|
||||||
---
|
|
||||||
|
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
.. |break_config| replace:: **[BREAK]**
|
||||||
|
.. |bugfix| replace:: [BUG]
|
||||||
|
|
||||||
|
Changelog
|
||||||
|
=========
|
||||||
|
|
||||||
|
The changelog summarized bugfixes that are deemed relevant for users.
|
||||||
|
Developers should consult the git commit log or GitHub issue tracker.
|
||||||
|
|
||||||
|
0.0.2
|
||||||
|
-----
|
||||||
|
|
||||||
|
Breaking
|
||||||
|
~~~~~~~~
|
||||||
|
|
||||||
|
* |break_config| :commit:`b95260f`: ``global.logging`` is no longer a dictionary but a list
|
||||||
|
|
||||||
|
* |break_config| :commit:`3e647c1`: ``source`` job field ``datasets`` renamed to ``filesystems``
|
||||||
|
|
||||||
|
* **NOTE**: zrepl will parse missing ``filesystems`` field as an empty filter,
|
||||||
|
i.e. no filesystems are presented to the other side.
|
||||||
|
|
||||||
|
* |bugfix| :commit:`72d2885` fix aliasing bug with root `<` subtree wildcard
|
||||||
|
|
||||||
|
* Filesystems paths with final match at blank `s` subtree wildcard are now appended to the target path
|
||||||
|
* Non-root subtree wildcards, e.g. `zroot/foo/bar<` still map directrly onto the target path
|
||||||
|
|
||||||
|
* Support days (``d``) and weeks (``w``) in durations
|
||||||
|
|
||||||
|
* Docs
|
||||||
|
|
||||||
|
* Ditch Hugo, move to Python Sphinx
|
||||||
|
* Improve & simplify tutorial (single SSH key per installation)
|
||||||
|
* Document pruning policies
|
||||||
|
* Document job types
|
||||||
|
* Document logging
|
||||||
|
* Start updating implementation overview
|
||||||
|
|
||||||
|
|
||||||
|
0.0.1
|
||||||
|
-----
|
||||||
|
|
||||||
|
* Initial release
|
||||||
+169
@@ -0,0 +1,169 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# zrepl documentation build configuration file, created by
|
||||||
|
# sphinx-quickstart on Wed Nov 8 22:28:10 2017.
|
||||||
|
#
|
||||||
|
# This file is execfile()d with the current directory set to its
|
||||||
|
# containing dir.
|
||||||
|
#
|
||||||
|
# Note that not all possible configuration values are present in this
|
||||||
|
# autogenerated file.
|
||||||
|
#
|
||||||
|
# All configuration values have a default; values that are commented out
|
||||||
|
# serve to show the default.
|
||||||
|
|
||||||
|
# If extensions (or modules to document with autodoc) are in another directory,
|
||||||
|
# add these directories to sys.path here. If the directory is relative to the
|
||||||
|
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||||
|
#
|
||||||
|
# import os
|
||||||
|
# import sys
|
||||||
|
# sys.path.insert(0, os.path.abspath('.'))
|
||||||
|
|
||||||
|
|
||||||
|
# -- General configuration ------------------------------------------------
|
||||||
|
|
||||||
|
# If your documentation needs a minimal Sphinx version, state it here.
|
||||||
|
#
|
||||||
|
# needs_sphinx = '1.0'
|
||||||
|
|
||||||
|
# Add any Sphinx extension module names here, as strings. They can be
|
||||||
|
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||||
|
# ones.
|
||||||
|
extensions = ['sphinx.ext.todo',
|
||||||
|
'sphinx.ext.githubpages',
|
||||||
|
'sphinx.ext.extlinks']
|
||||||
|
|
||||||
|
# Add any paths that contain templates here, relative to this directory.
|
||||||
|
templates_path = ['_templates']
|
||||||
|
|
||||||
|
# The suffix(es) of source filenames.
|
||||||
|
# You can specify multiple suffix as a list of string:
|
||||||
|
#
|
||||||
|
# source_suffix = ['.rst', '.md']
|
||||||
|
source_suffix = '.rst'
|
||||||
|
|
||||||
|
# The master toctree document.
|
||||||
|
master_doc = 'index'
|
||||||
|
|
||||||
|
# General information about the project.
|
||||||
|
project = 'zrepl'
|
||||||
|
copyright = '2017, Christian Schwarz'
|
||||||
|
author = 'Christian Schwarz'
|
||||||
|
|
||||||
|
# The version info for the project you're documenting, acts as replacement for
|
||||||
|
# |version| and |release|, also used in various other places throughout the
|
||||||
|
# built documents.
|
||||||
|
#
|
||||||
|
# The short X.Y version.
|
||||||
|
import subprocess
|
||||||
|
version = subprocess.check_output(["git", "describe"]).decode("utf-8")
|
||||||
|
# The full version, including alpha/beta/rc tags.
|
||||||
|
release = version
|
||||||
|
|
||||||
|
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||||
|
# for a list of supported languages.
|
||||||
|
#
|
||||||
|
# This is also used if you do content translation via gettext catalogs.
|
||||||
|
# Usually you set "language" from the command line for these cases.
|
||||||
|
language = None
|
||||||
|
|
||||||
|
# List of patterns, relative to source directory, that match files and
|
||||||
|
# directories to ignore when looking for source files.
|
||||||
|
# This patterns also effect to html_static_path and html_extra_path
|
||||||
|
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
|
||||||
|
|
||||||
|
# The name of the Pygments (syntax highlighting) style to use.
|
||||||
|
pygments_style = 'sphinx'
|
||||||
|
|
||||||
|
# If true, `todo` and `todoList` produce output, else they produce nothing.
|
||||||
|
todo_include_todos = True
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for HTML output ----------------------------------------------
|
||||||
|
|
||||||
|
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||||
|
# a list of builtin themes.
|
||||||
|
#
|
||||||
|
html_theme = 'sphinx_rtd_theme'
|
||||||
|
|
||||||
|
# Theme options are theme-specific and customize the look and feel of a theme
|
||||||
|
# further. For a list of options available for each theme, see the
|
||||||
|
# documentation.
|
||||||
|
#
|
||||||
|
# html_theme_options = {}
|
||||||
|
|
||||||
|
# Add any paths that contain custom static files (such as style sheets) here,
|
||||||
|
# relative to this directory. They are copied after the builtin static files,
|
||||||
|
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||||
|
html_static_path = ['_static']
|
||||||
|
|
||||||
|
|
||||||
|
html_logo = '_static/zrepl.svg'
|
||||||
|
|
||||||
|
# -- Options for HTMLHelp output ------------------------------------------
|
||||||
|
|
||||||
|
# Output file base name for HTML help builder.
|
||||||
|
htmlhelp_basename = 'zrepldoc'
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for LaTeX output ---------------------------------------------
|
||||||
|
|
||||||
|
latex_elements = {
|
||||||
|
# The paper size ('letterpaper' or 'a4paper').
|
||||||
|
#
|
||||||
|
# 'papersize': 'letterpaper',
|
||||||
|
|
||||||
|
# The font size ('10pt', '11pt' or '12pt').
|
||||||
|
#
|
||||||
|
# 'pointsize': '10pt',
|
||||||
|
|
||||||
|
# Additional stuff for the LaTeX preamble.
|
||||||
|
#
|
||||||
|
# 'preamble': '',
|
||||||
|
|
||||||
|
# Latex figure (float) alignment
|
||||||
|
#
|
||||||
|
# 'figure_align': 'htbp',
|
||||||
|
}
|
||||||
|
|
||||||
|
# Grouping the document tree into LaTeX files. List of tuples
|
||||||
|
# (source start file, target name, title,
|
||||||
|
# author, documentclass [howto, manual, or own class]).
|
||||||
|
latex_documents = [
|
||||||
|
(master_doc, 'zrepl.tex', 'zrepl Documentation',
|
||||||
|
'Christian Schwarz', 'manual'),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for manual page output ---------------------------------------
|
||||||
|
|
||||||
|
# One entry per manual page. List of tuples
|
||||||
|
# (source start file, name, description, authors, manual section).
|
||||||
|
man_pages = [
|
||||||
|
(master_doc, 'zrepl', 'zrepl Documentation',
|
||||||
|
[author], 1)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for Texinfo output -------------------------------------------
|
||||||
|
|
||||||
|
# Grouping the document tree into Texinfo files. List of tuples
|
||||||
|
# (source start file, target name, title, author,
|
||||||
|
# dir menu entry, description, category)
|
||||||
|
texinfo_documents = [
|
||||||
|
(master_doc, 'zrepl', 'zrepl Documentation',
|
||||||
|
author, 'zrepl', 'One line description of project.',
|
||||||
|
'Miscellaneous'),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for the extlinks extension -----------------------------------
|
||||||
|
# http://www.sphinx-doc.org/en/stable/ext/extlinks.html
|
||||||
|
extlinks = {
|
||||||
|
'issue':('https://github.com/zrepl/zrepl/issues/%s', 'issue #'),
|
||||||
|
'sampleconf':('https://github.com/zrepl/zrepl/blob/master/cmd/sampleconf/%s', 'cmd/sampleconf'),
|
||||||
|
'commit':('https://github.com/zrepl/zrepl/commit/%s', 'commit '),
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
baseURL = "https://zrepl.github.io"
|
|
||||||
languageCode = "en-us"
|
|
||||||
title = "zrepl - ZFS replication"
|
|
||||||
theme = "docdock"
|
|
||||||
|
|
||||||
[[menu.shortcuts]]
|
|
||||||
pre = "<h3>More</h3>"
|
|
||||||
name = "<i class='fa fa-github'></i> GitHub repo"
|
|
||||||
identifier = "ds"
|
|
||||||
url = "https://github.com/zrepl/zrepl"
|
|
||||||
weight = 10
|
|
||||||
|
|
||||||
[[menu.shortcuts]]
|
|
||||||
name = "<i class='fa fa-twitter'></i> Primary Author on Twitter"
|
|
||||||
url = "https://twitter.com/problame"
|
|
||||||
weight = 11
|
|
||||||
|
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
|
||||||
|
.. _configuration_toc:
|
||||||
|
|
||||||
|
*************
|
||||||
|
Configuration
|
||||||
|
*************
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
|
||||||
|
configuration/jobs
|
||||||
|
configuration/transports
|
||||||
|
configuration/map_filter_syntax
|
||||||
|
configuration/prune
|
||||||
|
configuration/logging
|
||||||
|
configuration/misc
|
||||||
@@ -0,0 +1,184 @@
|
|||||||
|
.. include:: ../global.rst.inc
|
||||||
|
|
||||||
|
.. |patient| replace:: :ref:`patient <job-term-patient>`
|
||||||
|
.. |serve-transport| replace:: :ref:`serve transport<transport-ssh+stdinserver-serve>`
|
||||||
|
.. |connect-transport| replace:: :ref:`connect transport<transport-ssh+stdinserver-connect>`
|
||||||
|
.. |mapping| replace:: :ref:`mapping <pattern-mapping>`
|
||||||
|
.. |filter| replace:: :ref:`filter <pattern-filter>`
|
||||||
|
.. |prune| replace:: :ref:`prune <prune>`
|
||||||
|
|
||||||
|
.. _job:
|
||||||
|
|
||||||
|
Job Types
|
||||||
|
=========
|
||||||
|
|
||||||
|
A *job* is the unit of activity tracked by the zrepl daemon and configured in the |mainconfig|.
|
||||||
|
Every job has a unique ``name``, a ``type`` and type-dependent fields which are documented on this page.
|
||||||
|
Check out the :ref:`tutorial` and :sampleconf:`/` for examples on how job types are actually used.
|
||||||
|
|
||||||
|
.. ATTENTION::
|
||||||
|
|
||||||
|
Currently, zrepl does not replicate filesystem properties.
|
||||||
|
Whe receiving a filesystem, it is never mounted (`-u` flag) and `mountpoint=none` is set.
|
||||||
|
This is temporary and being worked on :issue:`24`.
|
||||||
|
|
||||||
|
.. _job-source:
|
||||||
|
|
||||||
|
Source Job
|
||||||
|
----------
|
||||||
|
|
||||||
|
Example: :sampleconf:`pullbackup/productionhost.yml`.
|
||||||
|
|
||||||
|
.. list-table::
|
||||||
|
:widths: 20 80
|
||||||
|
:header-rows: 1
|
||||||
|
|
||||||
|
* - Parameter
|
||||||
|
- Comment
|
||||||
|
* - ``type``
|
||||||
|
- = ``source``
|
||||||
|
* - ``name``
|
||||||
|
- unique name of the job
|
||||||
|
* - ``serve``
|
||||||
|
- |serve-transport|
|
||||||
|
* - ``filesystems``
|
||||||
|
- |filter| for filesystems to expose to client
|
||||||
|
* - ``snapshot_prefix``
|
||||||
|
- prefix for ZFS snapshots taken by this job
|
||||||
|
* - ``interval``
|
||||||
|
- snapshotting interval
|
||||||
|
* - ``prune``
|
||||||
|
- |prune| policy for filesytems in ``filesystems`` with prefix ``snapshot_prefix``
|
||||||
|
|
||||||
|
|
||||||
|
- Snapshotting Task (every ``interval``, |patient|)
|
||||||
|
|
||||||
|
- A snapshot of filesystems matched by ``filesystems`` is taken every ``interval`` with prefix ``snapshot_prefix``.
|
||||||
|
- The ``prune`` policy is triggered on filesystems matched by ``filesystems`` with snapshots matched by ``snapshot_prefix``.
|
||||||
|
|
||||||
|
- Serve Task
|
||||||
|
|
||||||
|
- Wait for connections from pull job using ``serve``.
|
||||||
|
|
||||||
|
A source job is the counterpart to a :ref:`job-pull`.
|
||||||
|
|
||||||
|
Note that the prune policy determines the maximum replication lag:
|
||||||
|
a pull job may stop replication due to link failure, misconfiguration or administrative action.
|
||||||
|
The source prune policy will eventually destroy the last common snapshot between source and pull job, requiring full replication.
|
||||||
|
Make sure you read the |prune| policy documentation.
|
||||||
|
|
||||||
|
|
||||||
|
.. _job-pull:
|
||||||
|
|
||||||
|
Pull Job
|
||||||
|
--------
|
||||||
|
|
||||||
|
Example: :sampleconf:`pullbackup/backuphost.yml`
|
||||||
|
|
||||||
|
.. list-table::
|
||||||
|
:widths: 20 80
|
||||||
|
:header-rows: 1
|
||||||
|
|
||||||
|
* - Parameter
|
||||||
|
- Comment
|
||||||
|
* - ``type``
|
||||||
|
- = ``pull``
|
||||||
|
* - ``name``
|
||||||
|
- unqiue name of the job
|
||||||
|
* - ``connect``
|
||||||
|
- |connect-transport|
|
||||||
|
* - ``interval``
|
||||||
|
- Interval between pull attempts
|
||||||
|
* - ``mapping``
|
||||||
|
- |mapping| for remote to local filesystems
|
||||||
|
* - ``initial_repl_policy``
|
||||||
|
- default = ``most_recent``, initial replication policy
|
||||||
|
* - ``snapshot_prefix``
|
||||||
|
- prefix snapshots must match to be considered for replication & pruning
|
||||||
|
* - ``prune``
|
||||||
|
- |prune| policy for local filesystems reachable by ``mapping``
|
||||||
|
|
||||||
|
* Main Task (every ``interval``, |patient|)
|
||||||
|
|
||||||
|
#. A connection to the remote source job is established using the strategy in ``connect``
|
||||||
|
#. ``mapping`` maps filesystems presented by the remote side to local *target filesystems*
|
||||||
|
#. Those remote filesystems with a local *target filesystem* are replicated
|
||||||
|
|
||||||
|
#. Only snapshots with prefix ``snapshot_prefix`` are replicated.
|
||||||
|
#. If possible, incremental replication takes place.
|
||||||
|
#. If the local target filesystem does not exist, ``initial_repl_policy`` is used.
|
||||||
|
#. On conflicts, an error is logged but replication of other filesystems with mapping continues.
|
||||||
|
|
||||||
|
#. The ``prune`` policy is triggered for all *target filesystems*
|
||||||
|
|
||||||
|
A pull job is the counterpart to a :ref:`job-source`.
|
||||||
|
|
||||||
|
|
||||||
|
.. _job-local:
|
||||||
|
|
||||||
|
Local Job
|
||||||
|
---------
|
||||||
|
|
||||||
|
Example: :sampleconf:`localbackup/host1.yml`
|
||||||
|
|
||||||
|
.. list-table::
|
||||||
|
:widths: 20 80
|
||||||
|
:header-rows: 1
|
||||||
|
|
||||||
|
* - Parameter
|
||||||
|
- Comment
|
||||||
|
* - ``type``
|
||||||
|
- = ``local``
|
||||||
|
* - ``name``
|
||||||
|
- unqiue name of the job
|
||||||
|
* - ``mapping``
|
||||||
|
- |mapping| from source to target filesystem (both local)
|
||||||
|
* - ``snapshot_prefix``
|
||||||
|
- prefix for ZFS snapshots taken by this job
|
||||||
|
* - ``interval``
|
||||||
|
- snapshotting & replication interval
|
||||||
|
* - ``initial_repl_policy``
|
||||||
|
- default = ``most_recent``, initial replication policy
|
||||||
|
* - ``prune_lhs``
|
||||||
|
- pruning policy on left-hand-side (source)
|
||||||
|
* - ``prune_rhs``
|
||||||
|
- pruning policy on right-hand-side (target)
|
||||||
|
|
||||||
|
* Main Task (every ``interval``, |patient|)
|
||||||
|
|
||||||
|
#. Evaluate ``mapping`` for local filesystems, those with a *target filesystem* are called *mapped filesystems*.
|
||||||
|
#. Snapshot *mapped filesystems* with ``snapshot_prefix``.
|
||||||
|
#. Replicate *mapped filesystems* to their respective *target filesystems*:
|
||||||
|
|
||||||
|
#. Only snapshots with prefix ``snapshot_prefix`` are replicated.
|
||||||
|
#. If possible, incremental replication takes place.
|
||||||
|
#. If the *target filesystem* does not exist, ``initial_repl_policy`` is used.
|
||||||
|
#. On conflicts, an error is logged but replication of other *mapped filesystems* continues.
|
||||||
|
|
||||||
|
#. The ``prune_lhs`` policy is triggered for all *mapped filesystems*
|
||||||
|
#. The ``prune_rhs`` policy is triggered for all *target filesystems*
|
||||||
|
|
||||||
|
A local job is combination of source & pull job executed on the same machine.
|
||||||
|
|
||||||
|
Terminology
|
||||||
|
-----------
|
||||||
|
|
||||||
|
task
|
||||||
|
|
||||||
|
A job consists of one or more tasks and a task consists of one or more steps.
|
||||||
|
Some tasks may be periodic while others wait for an event to occur.
|
||||||
|
|
||||||
|
|
||||||
|
patient task
|
||||||
|
|
||||||
|
.. _job-term-patient:
|
||||||
|
|
||||||
|
A patient task is supposed to execute some task every `interval`.
|
||||||
|
We call the start of the task an *invocation*.
|
||||||
|
|
||||||
|
* If the task completes in less than `interval`, the task is restarted at `last_invocation + interval`.
|
||||||
|
* Otherwise, a patient job
|
||||||
|
* logs a warning as soon as a task exceeds its configured `interval`
|
||||||
|
* waits for the last invocation to finish
|
||||||
|
* logs a warning with the effective task duration
|
||||||
|
* immediately starts a new invocation of the task
|
||||||
@@ -0,0 +1,198 @@
|
|||||||
|
.. include:: ../global.rst.inc
|
||||||
|
|
||||||
|
.. _logging:
|
||||||
|
|
||||||
|
Logging
|
||||||
|
=======
|
||||||
|
|
||||||
|
zrepl uses structured logging to provide users with easily processable log messages.
|
||||||
|
|
||||||
|
Logging outlets are configured in the ``global`` section of the |mainconfig|.
|
||||||
|
Check out :sampleconf:`random/logging.yml` for an example on how to configure multiple outlets:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
global:
|
||||||
|
logging:
|
||||||
|
|
||||||
|
- outlet: OUTLET_TYPE
|
||||||
|
level: MINIMUM_LEVEL
|
||||||
|
format: FORMAT
|
||||||
|
|
||||||
|
- outlet: OUTLET_TYPE
|
||||||
|
level: MINIMUM_LEVEL
|
||||||
|
format: FORMAT
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
jobs: ...
|
||||||
|
|
||||||
|
Default Configuration
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
By default, the following logging configuration is used
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
global:
|
||||||
|
logging:
|
||||||
|
|
||||||
|
- outlet: "stdout"
|
||||||
|
level: "warn"
|
||||||
|
format: "human"
|
||||||
|
|
||||||
|
.. ATTENTION::
|
||||||
|
Output to **stderr** should always be considered a **critical error**.
|
||||||
|
Only errors in the logging infrastructure itself, e.g. IO errors when writing to an outlet, are sent to stderr.
|
||||||
|
|
||||||
|
Building Blocks
|
||||||
|
---------------
|
||||||
|
|
||||||
|
The following sections document the semantics of the different log levels, formats and outlet types.
|
||||||
|
|
||||||
|
.. _logging-levels:
|
||||||
|
|
||||||
|
Levels
|
||||||
|
~~~~~~
|
||||||
|
|
||||||
|
.. list-table::
|
||||||
|
:widths: 10 10 80
|
||||||
|
:header-rows: 1
|
||||||
|
|
||||||
|
* - Level
|
||||||
|
- SHORT
|
||||||
|
- Description
|
||||||
|
* - ``error``
|
||||||
|
- ``ERRO``
|
||||||
|
- immediate action required
|
||||||
|
* - ``warn``
|
||||||
|
- ``WARN``
|
||||||
|
- symptoms for misconfiguration, soon expected failure, etc.
|
||||||
|
* - ``info``
|
||||||
|
- ``INFO``
|
||||||
|
- explains what happens without too much detail
|
||||||
|
* - ``debug``
|
||||||
|
- ``DEBG``
|
||||||
|
- tracing information, state dumps, etc. useful for debugging.
|
||||||
|
|
||||||
|
Incorrectly classified messages are considered a bug and should be reported.
|
||||||
|
|
||||||
|
.. _logging-formats:
|
||||||
|
|
||||||
|
Formats
|
||||||
|
~~~~~~~
|
||||||
|
|
||||||
|
.. list-table::
|
||||||
|
:widths: 10 90
|
||||||
|
:header-rows: 1
|
||||||
|
|
||||||
|
* - Format
|
||||||
|
- Description
|
||||||
|
* - ``human``
|
||||||
|
- emphasizes context by putting job, task, step and other context variables into brackets
|
||||||
|
before the actual message, followed by remaining fields in logfmt style|
|
||||||
|
* - ``logfmt``
|
||||||
|
- `logfmt <https://brandur.org/logfmt>`_ output. zrepl uses `this Go package <https://github.com/go-logfmt/logfmt>`_.
|
||||||
|
* - ``json``
|
||||||
|
- JSON formatted output. Each line is a valid JSON document. Fields are marshaled by
|
||||||
|
``encoding/json.Marshal()``, which is particularly useful for processing in
|
||||||
|
log aggregation or when processing state dumps.
|
||||||
|
|
||||||
|
.. _logging-outlet-stdout:
|
||||||
|
|
||||||
|
``stdout`` Outlet
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
.. list-table::
|
||||||
|
:widths: 10 90
|
||||||
|
:header-rows: 1
|
||||||
|
|
||||||
|
* - Parameter
|
||||||
|
- Comment
|
||||||
|
* - ``outlet``
|
||||||
|
- ``stdout``
|
||||||
|
* - ``level``
|
||||||
|
- minimum :ref:`log level <logging-levels>`
|
||||||
|
* - ``format``
|
||||||
|
- output :ref:`format <logging-formats>`
|
||||||
|
|
||||||
|
Writes all log entries with minimum level ``level`` formatted by ``format`` to stdout.
|
||||||
|
|
||||||
|
Can only be specified once.
|
||||||
|
|
||||||
|
``syslog`` Outlet
|
||||||
|
-----------------
|
||||||
|
.. list-table::
|
||||||
|
:widths: 10 90
|
||||||
|
:header-rows: 1
|
||||||
|
|
||||||
|
* - Parameter
|
||||||
|
- Comment
|
||||||
|
* - ``outlet``
|
||||||
|
- ``syslog``
|
||||||
|
* - ``level``
|
||||||
|
- minimum :ref:`log level <logging-levels>`
|
||||||
|
* - ``format``
|
||||||
|
- output :ref:`format <logging-formats>`
|
||||||
|
* - ``retry_interval``
|
||||||
|
- Interval between reconnection attempts to syslog (default = 0)
|
||||||
|
|
||||||
|
Writes all log entries formatted by ``format`` to syslog.
|
||||||
|
On normal setups, you should not need to change the ``retry_interval``.
|
||||||
|
|
||||||
|
Can only be specified once.
|
||||||
|
|
||||||
|
``tcp`` Outlet
|
||||||
|
--------------
|
||||||
|
|
||||||
|
.. WARNING::
|
||||||
|
|
||||||
|
The TCP outlet is not fully asynchronous and blocks the calling goroutine when it cannot connect.
|
||||||
|
Currently it should only be used for local connections that are guaranteed to not fail / be slow.
|
||||||
|
This issue is tracked in :issue:`26`
|
||||||
|
|
||||||
|
.. list-table::
|
||||||
|
:widths: 10 90
|
||||||
|
:header-rows: 1
|
||||||
|
|
||||||
|
* - Parameter
|
||||||
|
- Comment
|
||||||
|
* - ``outlet``
|
||||||
|
- ``tcp``
|
||||||
|
* - ``level``
|
||||||
|
- minimum :ref:`log level <logging-levels>`
|
||||||
|
* - ``format``
|
||||||
|
- output :ref:`format <logging-formats>`
|
||||||
|
* - ``net``
|
||||||
|
- ``tcp`` in most cases
|
||||||
|
* - ``address``
|
||||||
|
- remote network, e.g. ``logs.example.com:10202``
|
||||||
|
* - ``retry_interval``
|
||||||
|
- Interval between reconnection attempts to ``address``
|
||||||
|
* - ``tls``
|
||||||
|
- TLS config (see below)
|
||||||
|
|
||||||
|
Establishes a TCP connection to ``address`` and sends log messages with minimum level ``level`` formatted by ``format``.
|
||||||
|
|
||||||
|
If ``tls`` is not specified, an unencrypted connection is established.
|
||||||
|
|
||||||
|
If ``tls`` is specified, the TCP connection is secured with TLS + Client Authentication.
|
||||||
|
This is particularly useful in combination with log aggregation services that run on an other machine.
|
||||||
|
|
||||||
|
.. list-table::
|
||||||
|
:widths: 10 90
|
||||||
|
:header-rows: 1
|
||||||
|
|
||||||
|
* - Parameter
|
||||||
|
- Description
|
||||||
|
* - ``ca``
|
||||||
|
- PEM-encoded certificate authority that signed the remote server's TLS certificate
|
||||||
|
* - ``cert``
|
||||||
|
- PEM-encoded client certificate identifying this zrepl daemon toward the remote server
|
||||||
|
* - ``key``
|
||||||
|
- PEM-encoded, unencrypted client private key identifying this zrepl daemon toward the remote server
|
||||||
|
|
||||||
|
.. NOTE::
|
||||||
|
|
||||||
|
zrepl uses Go's ``crypto/tls`` and ``crypto/x509`` packages and leaves all but the required fields in ``tls.Config`` at their default values.
|
||||||
|
In case of a security defect in these packages, zrepl has to be rebuilt because Go binaries are statically linked.
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
.. include:: ../global.rst.inc
|
||||||
|
|
||||||
|
Mapping & Filter Syntax
|
||||||
|
=======================
|
||||||
|
|
||||||
|
For various job types, a filesystem ``mapping`` or ``filter`` needs to be
|
||||||
|
specified.
|
||||||
|
|
||||||
|
Both have in common that they take a filesystem path (in the ZFS filesystem hierarchy)as parameters and return something.
|
||||||
|
Mappings return a *target filesystem* and filters return a *filter result*.
|
||||||
|
|
||||||
|
The pattern syntax is the same for mappings and filters and is documented in the following section.
|
||||||
|
|
||||||
|
Common Pattern Syntax
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
A mapping / filter is specified as a **YAML dictionary** with patterns as keys and
|
||||||
|
results as values.
|
||||||
|
The following rules determine which result is chosen for a given filesystem path:
|
||||||
|
|
||||||
|
* More specific path patterns win over less specific ones
|
||||||
|
* Non-wildcard patterns (full path patterns) win over *subtree wildcards* (`<` at end of pattern)
|
||||||
|
|
||||||
|
The **subtree wildcard** ``<`` means "the dataset left of ``<`` and all its children".
|
||||||
|
|
||||||
|
Example
|
||||||
|
~~~~~~~
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
# Rule number and its pattern
|
||||||
|
1: tank< # tank and all its children
|
||||||
|
2: tank/foo/bar # full path pattern (no wildcard)
|
||||||
|
3: tank/foo< # tank/foo and all its children
|
||||||
|
|
||||||
|
# Which rule applies to given path?
|
||||||
|
tank/foo/bar/loo => 3
|
||||||
|
tank/bar => 1
|
||||||
|
tank/foo/bar => 2
|
||||||
|
zroot => NO MATCH
|
||||||
|
tank/var/log => 1
|
||||||
|
|
||||||
|
.. _pattern-mapping:
|
||||||
|
|
||||||
|
Mappings
|
||||||
|
--------
|
||||||
|
|
||||||
|
Mappings map a *source filesystem path* to a *target filesystem path*.
|
||||||
|
Per pattern, either a target filesystem path or ``"!"`` is specified as a result.
|
||||||
|
|
||||||
|
* If no pattern matches, there exists no target filesystem (``NO MATCH``).
|
||||||
|
* If the result is a ``"!"``, there exists no target filesystem (``NO MATCH``).
|
||||||
|
* If the pattern is a non-wildcard pattern, the source path is mapped to the target path on the right.
|
||||||
|
* If the pattern ends with a *subtree wildcard* (``<``), the source path is **prefix-trimmed** with the path specified left of ``<``.
|
||||||
|
|
||||||
|
* Note: this means that only for *wildcard-only* patterns (pattern= ``<`` ) is the source path simply appended to the target path.
|
||||||
|
|
||||||
|
The example is from the :sampleconf:`localbackup/host1.yml` example config.
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
- name: mirror_local
|
||||||
|
type: local
|
||||||
|
mapping: {
|
||||||
|
"zroot/var/db<": "storage/backups/local/zroot/var/db",
|
||||||
|
"zroot/usr/home<": "storage/backups/local/zroot/usr/home",
|
||||||
|
"zroot/usr/home/paranoid": "!", #don't backup paranoid user
|
||||||
|
"zroot/poudriere/ports<": "!", #don't backup the ports trees
|
||||||
|
}
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
zroot/var/db => storage/backups/local/zroot/var/db
|
||||||
|
zroot/var/db/a/child => storage/backups/local/zroot/var/db/a/child
|
||||||
|
zroot/usr/home => storage/backups/local/zroot/usr/home
|
||||||
|
zroot/usr/home/paranoid => NOT MAPPED
|
||||||
|
zroot/usr/home/bob => storage/backups/local/zroot/usr/home/bob
|
||||||
|
zroot/usr/src => NOT MAPPED
|
||||||
|
zroot/poudriere/ports/2017Q3 => NOT MAPPED
|
||||||
|
zroot/poudriere/ports/HEAD => NOT MAPPED
|
||||||
|
|
||||||
|
.. _pattern-filter:
|
||||||
|
|
||||||
|
Filters
|
||||||
|
-------
|
||||||
|
|
||||||
|
Valid filter results: ``ok`` or ``!``.
|
||||||
|
|
||||||
|
The example below show the source job from the :ref:`tutorial <tutorial-configure-app-srv>`:
|
||||||
|
The corresponding pull job is allowed access to ``zroot/var/db``, ``zroot/usr/home`` + children except ``zroot/usr/home/paranoid``::
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
- name: pull_backup
|
||||||
|
type: source
|
||||||
|
...
|
||||||
|
filesystems: {
|
||||||
|
"zroot/var/db": "ok",
|
||||||
|
"zroot/usr/home<": "ok",
|
||||||
|
"zroot/usr/home/paranoid": "!",
|
||||||
|
}
|
||||||
|
...
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
Miscellaneous
|
||||||
|
=============
|
||||||
|
|
||||||
|
.. _conf-runtime-directories:
|
||||||
|
|
||||||
|
Runtime Directories & UNIX Sockets
|
||||||
|
----------------------------------
|
||||||
|
|
||||||
|
zrepl daemon creates various UNIX sockets to allow communicating with it:
|
||||||
|
|
||||||
|
* the :ref:`transport-ssh+stdinserver` transport connects to a socket named after ``client_identity`` parameter
|
||||||
|
* the ``control`` CLI subcommand connects to a defined control socket
|
||||||
|
|
||||||
|
There is no further authentication on these sockets.
|
||||||
|
Therefore we have to make sure they can only be created and accessed by ``zrepl daemon``.
|
||||||
|
|
||||||
|
In fact, ``zrepl daemon`` will not bind a socket to a path in a directory that is world-accessible.
|
||||||
|
|
||||||
|
The directories can be configured in the main configuration file:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
global:
|
||||||
|
control:
|
||||||
|
sockpath: /var/run/zrepl/control
|
||||||
|
serve:
|
||||||
|
stdinserver:
|
||||||
|
sockdir: /var/run/zrepl/stdinserver
|
||||||
|
|
||||||
|
|
||||||
|
Durations & Intervals
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
Interval & duration fields in job definitions, pruning configurations, etc. must match the following regex:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
var durationStringRegex *regexp.Regexp = regexp.MustCompile(`^\s*(\d+)\s*(s|m|h|d|w)\s*$`)
|
||||||
|
// s = second, m = minute, h = hour, d = day, w = week (7 days)
|
||||||
|
|
||||||
|
Super-Verbose Job Debugging
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
You have probably landed here because you opened an issue on GitHub and some developer told you to do this...
|
||||||
|
So just read the annotated comments ;)
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
job:
|
||||||
|
- name: ...
|
||||||
|
...
|
||||||
|
# JOB DEBUGGING OPTIONS
|
||||||
|
# should be equal for all job types, but each job implements the debugging itself
|
||||||
|
debug:
|
||||||
|
conn: # debug the io.ReadWriteCloser connection
|
||||||
|
read_dump: /tmp/connlog_read # dump results of Read() invocations to this file
|
||||||
|
write_dump: /tmp/connlog_write # dump results of Write() invocations to this file
|
||||||
|
rpc: # debug the RPC protocol implementation
|
||||||
|
log: true # log output from rpc layer to the job log
|
||||||
|
|
||||||
|
.. ATTENTION::
|
||||||
|
|
||||||
|
Connection dumps will almost certainly contain your or other's private data. Do not share it in a bug report.
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
.. _prune:
|
||||||
|
|
||||||
|
Pruning Policies
|
||||||
|
================
|
||||||
|
|
||||||
|
In zrepl, *pruning* means *destroying snapshots by some policy*.
|
||||||
|
|
||||||
|
A *pruning policy* takes a list of snapshots and -- for each snapshot -- decides whether it should be kept or destroyed.
|
||||||
|
|
||||||
|
The job context defines which snapshots are even considered for pruning, for example through the ``snapshot_prefix`` variable.
|
||||||
|
Check the respective :ref:`job definition <job>` for details.
|
||||||
|
|
||||||
|
Currently, the :ref:`prune-retention-grid` is the only supported pruning policy.
|
||||||
|
|
||||||
|
.. _prune-retention-grid:
|
||||||
|
|
||||||
|
Retention Grid
|
||||||
|
--------------
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
- name: pull_app-srv
|
||||||
|
...
|
||||||
|
prune:
|
||||||
|
policy: grid
|
||||||
|
grid: 1x1h(keep=all) | 24x1h | 35x1d | 6x30d
|
||||||
|
│ │
|
||||||
|
└─ one hour interval
|
||||||
|
│
|
||||||
|
└─ 24 adjacent one-hour intervals
|
||||||
|
|
||||||
|
The retention grid can be thought of as a time-based sieve:
|
||||||
|
The ``grid`` field specifies a list of adjacent time intervals:
|
||||||
|
the left edge of the leftmost (first) interval is the ``creation`` date of the youngest snapshot.
|
||||||
|
All intervals to its right describe time intervals further in the past.
|
||||||
|
|
||||||
|
Each interval carries a maximum number of snapshots to keep.
|
||||||
|
It is secified via ``(keep=N)``, where ``N`` is either ``all`` (all snapshots are kept) or a positive integer.
|
||||||
|
The default value is **1**.
|
||||||
|
|
||||||
|
The following procedure happens during pruning:
|
||||||
|
|
||||||
|
#. The list of snapshots eligible for pruning is sorted by ``creation``
|
||||||
|
#. The left edge of the first interval is aligned to the ``creation`` date of the youngest snapshot
|
||||||
|
#. A list of buckets is created, one for each interval
|
||||||
|
#. The list of snapshots is split up into the buckets.
|
||||||
|
#. For each bucket
|
||||||
|
|
||||||
|
#. the contained snapshot list is sorted by creation.
|
||||||
|
#. snapshots from the list, oldest first, are destroyed until the specified ``keep`` count is reached.
|
||||||
|
#. all remaining snapshots on the list are kept.
|
||||||
|
|
||||||
|
.. ATTENTION::
|
||||||
|
|
||||||
|
.. TODO this is obsolete as soon as bookmarks are created during snapshotting
|
||||||
|
|
||||||
|
The configuration of the first interval (``1x1h(keep=all)`` in the example) determines the **maximum allowable replication lag** between source and destination.
|
||||||
|
After the first interval, source and destination likely have different retention settings.
|
||||||
|
This means source and destination may prune different snapshots, prohibiting incremental replication froms snapshots that are not in the first interval.
|
||||||
|
|
||||||
|
**Always** configure the first interval to ``1x?(keep=all)``, substituting ``?`` with the maximum time replication may fail due to downtimes, maintenance, connectivity issues, etc.
|
||||||
|
After outages longer than ``?`` you may be required to perform **full replication** again.
|
||||||
|
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
.. highlight:: bash
|
||||||
|
|
||||||
|
.. _transport:
|
||||||
|
|
||||||
|
Transports
|
||||||
|
==========
|
||||||
|
|
||||||
|
A transport provides an authenticated `io.ReadWriteCloser <https://golang.org/pkg/io/#ReadWriteCloser>`_ to the RPC layer.
|
||||||
|
(An ``io.ReadWriteCloser`` is essentially a bidirectional reliable communication channel.)
|
||||||
|
|
||||||
|
Currently, only the ``ssh+stdinserver`` transport is supported.
|
||||||
|
|
||||||
|
.. _transport-ssh+stdinserver:
|
||||||
|
|
||||||
|
``ssh+stdinserver`` Transport
|
||||||
|
-----------------------------
|
||||||
|
|
||||||
|
The way the ``ssh+stdinserver`` transport works is inspired by `git shell <https://git-scm.com/docs/git-shell>`_ and `Borg Backup <https://borgbackup.readthedocs.io/en/stable/deployment.html>`_.
|
||||||
|
It is implemented in the Go package ``github.com/zrepl/zrepl/sshbytestream``.
|
||||||
|
The config excerpts are taken from the :ref:`tutorial` which you should complete before reading further.
|
||||||
|
|
||||||
|
.. _transport-ssh+stdinserver-serve:
|
||||||
|
|
||||||
|
Serve Mode
|
||||||
|
~~~~~~~~~~
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
- name: pull_backup
|
||||||
|
type: source
|
||||||
|
serve:
|
||||||
|
type: stdinserver
|
||||||
|
client_identity: backup-srv.example.com
|
||||||
|
...
|
||||||
|
|
||||||
|
The serving job opens a UNIX socket named after ``client_identity`` in the runtime directory, e.g. ``/var/run/zrepl/stdinserver/backup-srv.example.com``.
|
||||||
|
|
||||||
|
On the same machine, the ``zrepl stdinserver $client_identity`` command connects to that socket.
|
||||||
|
For example, ``zrepl stdinserver backup-srv.example.com`` connects to the UNIX socket ``/var/run/zrepl/stdinserver/backup-srv.example.com``.
|
||||||
|
|
||||||
|
It then passes its stdin and stdout file descriptors to the zrepl daemon via *cmsg(3)*.
|
||||||
|
zrepl daemon in turn combines them into an ``io.ReadWriteCloser``:
|
||||||
|
a ``Write()`` turns into a write to stdout, a ``Read()`` turns into a read from stdin.
|
||||||
|
|
||||||
|
Interactive use of the ``stdinserver`` subcommand does not make much sense.
|
||||||
|
However, we can force its execution when a user with a particular SSH pubkey connects via SSH.
|
||||||
|
This can be achieved with an entry in the ``authorized_keys`` file of the serving zrepl daemon.
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
# for OpenSSH >= 7.2
|
||||||
|
command="zrepl stdinserver CLIENT_IDENTITY",restrict CLIENT_SSH_KEY
|
||||||
|
# for older OpenSSH versions
|
||||||
|
command="zrepl stdinserver CLIENT_IDENTITY",no-port-forwarding,no-X11-forwarding,no-pty,no-agent-forwarding,no-user-rc CLIENT_SSH_KEY
|
||||||
|
|
||||||
|
* CLIENT_IDENTITY is substituted with ``backup-srv.example.com`` in our example
|
||||||
|
* CLIENT_SSH_KEY is substituted with the public part of the SSH keypair specified in the ``connect`` directive on the connecting host.
|
||||||
|
|
||||||
|
.. NOTE::
|
||||||
|
|
||||||
|
You may need to adjust the ``PermitRootLogin`` option in ``/etc/ssh/sshd_config`` to ``forced-commands-only`` or higher for this to work.
|
||||||
|
Refer to sshd_config(5) for details.
|
||||||
|
|
||||||
|
To recap, this is of how client authentication works with the ``ssh+stdinserver`` transport:
|
||||||
|
|
||||||
|
* Connections to the ``client_identity`` UNIX socket are blindly trusted by zrepl daemon.
|
||||||
|
* Thus, the runtime directory must be private to the zrepl user (checked by zrepl daemon)
|
||||||
|
* The admin of the host with the serving zrepl daemon controls the ``authorized_keys`` file.
|
||||||
|
* Thus, the administrator controls the mapping ``PUBKEY -> CLIENT_IDENTITY``.
|
||||||
|
|
||||||
|
.. _transport-ssh+stdinserver-connect:
|
||||||
|
|
||||||
|
Connect Mode
|
||||||
|
~~~~~~~~~~~~
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
- name: pull_app-srv
|
||||||
|
type: pull
|
||||||
|
connect:
|
||||||
|
type: ssh+stdinserver
|
||||||
|
host: app-srv.example.com
|
||||||
|
user: root
|
||||||
|
port: 22
|
||||||
|
identity_file: /etc/zrepl/ssh/identity
|
||||||
|
options: # optional
|
||||||
|
- "Compression=on"
|
||||||
|
|
||||||
|
The connecting zrepl daemon
|
||||||
|
|
||||||
|
#. Creates a pipe
|
||||||
|
#. Forks
|
||||||
|
#. In the forked process
|
||||||
|
|
||||||
|
#. Replaces forked stdin and stdout with the corresponding pipe ends
|
||||||
|
#. Executes the ``ssh`` binary found in ``$PATH``.
|
||||||
|
|
||||||
|
#. The identity file (``-i``) is set to ``$identity_file``.
|
||||||
|
#. The remote user, host and port correspond to those configured.
|
||||||
|
#. Further options can be specified using the ``options`` field, which appends each entry in the list to the command line using ``-o $entry``.
|
||||||
|
|
||||||
|
1. Wraps the pipe ends in an ``io.ReadWriteCloser`` and uses it for RPC.
|
||||||
|
|
||||||
|
As discussed in the section above, the connecting zrepl daemon expects that ``zrepl stdinserver $client_identity`` is executed automatically via an ``authorized_keys`` file entry.
|
||||||
|
|
||||||
|
.. NOTE::
|
||||||
|
|
||||||
|
The environment variables of the underlying SSH process are cleared. ``$SSH_AUTH_SOCK`` will not be available.
|
||||||
|
It is suggested to create a separate, unencrypted SSH key solely for that purpose.
|
||||||
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
zrepl Documentation
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
+++
|
|
||||||
title = "zrepl - ZFS replication"
|
|
||||||
+++
|
|
||||||
|
|
||||||
# zrepl - ZFS replication
|
|
||||||
|
|
||||||
{{% notice info %}}
|
|
||||||
zrepl as well as this documentation is still under active development.
|
|
||||||
Use & test at your own risk ;)
|
|
||||||
{{% /notice %}}
|
|
||||||
|
|
||||||
## Getting started
|
|
||||||
|
|
||||||
The [5 minute tutorial setup](/tutorial/) gives you a first impression.
|
|
||||||
|
|
||||||
## Main Features
|
|
||||||
|
|
||||||
* Filesystem Replication
|
|
||||||
* [x] Local & Remote
|
|
||||||
* [x] Pull mode
|
|
||||||
* [ ] Push mode
|
|
||||||
* [x] Access control checks when pulling datasets
|
|
||||||
* [x] [Flexible mapping]({{< ref "configuration/map_filter_syntax.md" >}}) rules
|
|
||||||
* [x] Bookmarks support
|
|
||||||
* [ ] Feature-negotiation for
|
|
||||||
* Resumable `send & receive`
|
|
||||||
* Compressed `send & receive`
|
|
||||||
* Raw encrypted `send & receive` (as soon as it is available)
|
|
||||||
* Automatic snapshot creation
|
|
||||||
* [x] Ensure fixed time interval between snapshots
|
|
||||||
* Automatic snapshot pruning
|
|
||||||
* [x] <i class="fa fa-arrow-right" aria-hidden="true"></i> [Retention Grid]({{< ref "configuration/prune.md#retention-grid" >}})
|
|
||||||
* Maintainable implementation in Go
|
|
||||||
* [x] Cross platform
|
|
||||||
* [x] Type safe & testable code
|
|
||||||
|
|
||||||
## Contributing
|
|
||||||
|
|
||||||
zrepl is usable but nowhere near a stable release right now - we are happy about contributors!
|
|
||||||
|
|
||||||
* Explore the codebase
|
|
||||||
* These docs live in the `docs/` subdirectory
|
|
||||||
* Document any non-obvious / confusing / plain broken behavior you encounter when setting up zrepl for the first time
|
|
||||||
* Check the *Issues* and *Projects* sections for things to do
|
|
||||||
|
|
||||||
{{% panel header="<i class='fa fa-github'></i> Getting your code merged"%}}
|
|
||||||
[The <i class='fa fa-github'></i> GitHub repository](https://github.com/zrepl/zrepl) is where all development happens.
|
|
||||||
Open your issue / PR there.
|
|
||||||
{{% /panel %}}
|
|
||||||
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
+++
|
|
||||||
title = "Configuration"
|
|
||||||
alwaysopen = true
|
|
||||||
+++
|
|
||||||
|
|
||||||
{{% alert theme="warning" %}}Under Construction{{% /alert %}}
|
|
||||||
|
|
||||||
{{% children description="true" %}}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
+++
|
|
||||||
title = "Job Types"
|
|
||||||
weight = 10
|
|
||||||
+++
|
|
||||||
|
|
||||||
{{% alert theme="warning" %}}Under Construction{{% /alert %}}
|
|
||||||
|
|
||||||
See `sampleconf/*/*.yml` for examples.
|
|
||||||
|
|
||||||
## Source
|
|
||||||
|
|
||||||
## Pull
|
|
||||||
|
|
||||||
## Local
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
+++
|
|
||||||
title = "Logging"
|
|
||||||
weight = 90
|
|
||||||
+++
|
|
||||||
|
|
||||||
{{% alert theme="warning" %}}Under Construction{{% /alert %}}
|
|
||||||
|
|
||||||
See `cmd/sampleconf/random/logging.yml`.
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
+++
|
|
||||||
title = "Mapping & Filter Syntax"
|
|
||||||
weight = 20
|
|
||||||
description = "How to specify mappings & filters"
|
|
||||||
+++
|
|
||||||
|
|
||||||
{{% alert theme="warning" %}}Under Construction{{% /alert %}}
|
|
||||||
|
|
||||||
## Mapping & Filter Syntax
|
|
||||||
|
|
||||||
For various job types, a filesystem `mapping` or `filter` needs to be
|
|
||||||
specified.
|
|
||||||
|
|
||||||
Both have in common that they return a result for a given filesystem path (in
|
|
||||||
the ZFS filesystem hierarchy): mappings return a *target filesystem*, filters
|
|
||||||
return a *filter result* (`omit`, `ok`).
|
|
||||||
|
|
||||||
The pattern syntax is the same for mappings and filters and is documented in
|
|
||||||
the following section.
|
|
||||||
|
|
||||||
#### Pattern Syntax
|
|
||||||
|
|
||||||
A mapping / filter is specified as a **YAML dictionary** with patterns as keys and
|
|
||||||
results as values.<br />
|
|
||||||
The following rules determine which result is chosen for a given filesystem path:
|
|
||||||
|
|
||||||
* More specific path patterns win over less specific ones
|
|
||||||
* Non-wildcard patterns (full path patterns) win over *subtree wildcards* (`<` at end of pattern)
|
|
||||||
|
|
||||||
{{% panel %}}
|
|
||||||
The **subtree wildcard** `<` means "*the dataset left of `<` and all its children*".
|
|
||||||
{{% / panel %}}
|
|
||||||
|
|
||||||
##### Example
|
|
||||||
|
|
||||||
```
|
|
||||||
# Rule number and its pattern
|
|
||||||
1: tank< # tank and all its children
|
|
||||||
2: tank/foo/bar # full path pattern (no wildcard)
|
|
||||||
3: tank/foo< # tank/foo and all its children
|
|
||||||
|
|
||||||
# Which rule applies to given path?
|
|
||||||
tank/foo/bar/loo => 3
|
|
||||||
tank/bar => 1
|
|
||||||
tank/foo/bar => 2
|
|
||||||
zroot => NO MATCH
|
|
||||||
tank/var/log => 1
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Mappings
|
|
||||||
|
|
||||||
The example below shows a pull job that would pull remote datasets to the given local paths.<br />
|
|
||||||
If there exists no mapping (`NO MATCH`), the filesystem will not be pulled.
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
pull:
|
|
||||||
app01.example.com: # client identity
|
|
||||||
from: app01.example.com
|
|
||||||
mapping: {
|
|
||||||
"tank/var/db<": "zroot/backups/app01/tank/var/db",
|
|
||||||
"tank/var/www<": "zroot/backups/app01/tank/var/www",
|
|
||||||
"tank/var/log": "zroot/logbackup/app01",
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
```
|
|
||||||
tank/var/db` => zroot/backups/app01/tank/var/db
|
|
||||||
tank/var/db/a/child => zroot/backups/app01/tank/var/db/a/child
|
|
||||||
...
|
|
||||||
tank/var/www => zroot/backups/app01/tank/var/www
|
|
||||||
tank/var/www/a/child => zroot/backups/app01/tank/var/www/a/child
|
|
||||||
...
|
|
||||||
tank/var/log => zroot/logbackup/app01
|
|
||||||
tank/var/log/foo => NOT MAPPED
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Filters
|
|
||||||
|
|
||||||
Valid filter results: `ok` or `omit`.
|
|
||||||
|
|
||||||
The example below shows a pull ACL that allows access to the user homes but not
|
|
||||||
to the rest of the system's datasets.
|
|
||||||
|
|
||||||
```
|
|
||||||
# Example for filter syntax
|
|
||||||
pull_acls:
|
|
||||||
backups.example.com: # client identity
|
|
||||||
filter: {
|
|
||||||
"<": omit,
|
|
||||||
"tank/usr/home<": ok,
|
|
||||||
}
|
|
||||||
```
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
+++
|
|
||||||
title = "Miscellaneous"
|
|
||||||
weight = 100
|
|
||||||
+++
|
|
||||||
|
|
||||||
## Runtime Directories & UNIX Sockets
|
|
||||||
|
|
||||||
zrepl daemon creates various UNIX sockets to allow communicating with it:
|
|
||||||
|
|
||||||
* the `stdinserver` transport connects to a socket named after `client_identity` parameter
|
|
||||||
* the `control` subcommand connects to a defined control socket
|
|
||||||
|
|
||||||
There is no further authentication on these sockets.
|
|
||||||
Therefore we have to make sure they can only be created and accessed by `zrepl daemon`.
|
|
||||||
|
|
||||||
In fact, `zrepl daemon` will not bind a socket to a path in a directory that is world-accessible.
|
|
||||||
|
|
||||||
The directories can be configured in the main configuration file:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
global:
|
|
||||||
control:
|
|
||||||
sockpath: /var/run/zrepl/control
|
|
||||||
serve:
|
|
||||||
stdinserver:
|
|
||||||
sockdir: /var/run/zrepl/stdinserver
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
## Super-Verbose Job Debugging
|
|
||||||
|
|
||||||
You have probably landed here because you opened an issue on GitHub and some developer told you to do this...
|
|
||||||
So just read the annotated comments ;)
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
|
|
||||||
job:
|
|
||||||
- name: ...
|
|
||||||
...
|
|
||||||
# JOB DEBUGGING OPTIONS
|
|
||||||
# should be equal for all job types, but each job implements the debugging itself
|
|
||||||
debug:
|
|
||||||
conn: # debug the io.ReadWriteCloser connection
|
|
||||||
read_dump: /tmp/connlog_read # dump results of Read() invocations to this file
|
|
||||||
write_dump: /tmp/connlog_write # dump results of Write() invocations to this file
|
|
||||||
rpc: # debug the RPC protocol implementation
|
|
||||||
log: true # log output from rpc layer to the job log
|
|
||||||
```
|
|
||||||
|
|
||||||
{{% notice info %}}
|
|
||||||
Connection dumps will almost certainly contain your or other's private data. Do not share it in a bug report.
|
|
||||||
{{% /notice %}}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
+++
|
|
||||||
title = "Pruning"
|
|
||||||
description = "Automated pruning of snapshots"
|
|
||||||
weight = 40
|
|
||||||
+++
|
|
||||||
|
|
||||||
{{% alert theme="warning" %}}Under Construction{{% /alert %}}
|
|
||||||
|
|
||||||
## Retention Grid
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
+++
|
|
||||||
title = "Transports"
|
|
||||||
weight = 30
|
|
||||||
+++
|
|
||||||
|
|
||||||
{{% alert theme="warning" %}}Under Construction{{% /alert %}}
|
|
||||||
|
|
||||||
## Stdinserver
|
|
||||||
|
|
||||||
|
|
||||||
The SSH transport connects to the remote server using the SSH binary in
|
|
||||||
`$PATH` and the parameters specified in the `zrepl` config file.
|
|
||||||
|
|
||||||
However, instead of a traditional interactive SSH session, `zrepl` expects
|
|
||||||
another instance of `zrepl` on the other side of the connection; You may be
|
|
||||||
familiar with this concept from [git shell](https://git-scm.com/docs/git-shell)
|
|
||||||
or [Borg Backup](https://borgbackup.readthedocs.io/en/stable/deployment.html).
|
|
||||||
|
|
||||||
Check the examples for instructions on how to set this up on your machines!
|
|
||||||
|
|
||||||
{{% panel %}}
|
|
||||||
The environment variables of the underlying SSH process are cleared. `$SSH_AUTH_SOCK` will not be available. We suggest creating a separate, unencrypted SSH key.
|
|
||||||
{{% / panel %}}
|
|
||||||
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
+++
|
|
||||||
title = "Implementation Overview"
|
|
||||||
+++
|
|
||||||
|
|
||||||
{{% alert theme="warning" %}}Under Construction{{% /alert %}}
|
|
||||||
|
|
||||||
The following design aspects may convince you that `zrepl` is superior to a hacked-together shell script solution.
|
|
||||||
|
|
||||||
## Language
|
|
||||||
|
|
||||||
`zrepl` is written in Go, a real programming language with type safety,
|
|
||||||
reasonable performance, testing infrastructure and an (opinionated) idea of
|
|
||||||
software engineering.
|
|
||||||
|
|
||||||
* key parts & algorithms of `zrepl` are covered by unit tests
|
|
||||||
* zrepl is noticably faster than comparable shell scripts
|
|
||||||
|
|
||||||
|
|
||||||
## RPC protocol
|
|
||||||
|
|
||||||
While it is tempting to just issue a few `ssh remote 'zfs send ...' | zfs recv`, this has a number of drawbacks:
|
|
||||||
|
|
||||||
* The snapshot streams need to be compatible.
|
|
||||||
* Communication is still unidirectional. Thus, you will most likely
|
|
||||||
* either not take advantage of features such as *compressed send & recv*
|
|
||||||
* or issue additional `ssh` commands in advance to figure out what features are supported on the other side.
|
|
||||||
* Advanced logic in shell scripts is ugly to read, poorly testable and a pain to maintain.
|
|
||||||
|
|
||||||
`zrepl` takes a different approach:
|
|
||||||
|
|
||||||
* Define an RPC protocol.
|
|
||||||
* Establish an encrypted, authenticated, bidirectional communication channel...
|
|
||||||
* ... with `zrepl` running at both ends of it.
|
|
||||||
|
|
||||||
This has several obvious benefits:
|
|
||||||
|
|
||||||
* No blank root shell access is given to the other side.
|
|
||||||
* Instead, access control lists (ACLs) are used to grant permissions to *authenticated* peers.
|
|
||||||
* The transport mechanism is decoupled from the remaining logic, keeping it extensible (e.g. TCP+TLS)
|
|
||||||
|
|
||||||
{{% panel %}}
|
|
||||||
Currently, the bidirectional communication channel is multiplexed on top of a single SSH connection.
|
|
||||||
Local replication is of course handled efficiently via simple method calls
|
|
||||||
See TODO for details.
|
|
||||||
{{% / panel %}}
|
|
||||||
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
+++
|
|
||||||
title = "Installation"
|
|
||||||
weight = 20
|
|
||||||
+++
|
|
||||||
|
|
||||||
{{% notice note %}}
|
|
||||||
Note: check out the [tutorial]({{< relref "tutorial/_index.md" >}}) if you want a first impression of zrepl.
|
|
||||||
{{% /notice %}}
|
|
||||||
|
|
||||||
## 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.
|
|
||||||
|
|
||||||
However, until we get around documenting those setups, you will have to run zrepl as root or experiment yourself :)
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
zrepl is currently not packaged on any operating system. Signed & versioned releases are planned but not available yet.
|
|
||||||
|
|
||||||
Check out the sources yourself, fetch dependencies using dep, compile and install to the zrepl user's `$PATH`.<br />
|
|
||||||
**Note**: if the zrepl binary is not in `$PATH`, you will have to adjust the examples in the [tutorial]({{< relref "tutorial/_index.md" >}}).
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# NOTE: you may want to checkout & build as an unprivileged user
|
|
||||||
cd /root
|
|
||||||
git clone https://github.com/zrepl/zrepl.git
|
|
||||||
cd zrepl
|
|
||||||
dep ensure
|
|
||||||
go build -o zrepl
|
|
||||||
cp zrepl /usr/local/bin/zrepl
|
|
||||||
rehash
|
|
||||||
# see if it worked
|
|
||||||
zrepl help
|
|
||||||
```
|
|
||||||
|
|
||||||
## Configuration Files
|
|
||||||
|
|
||||||
zrepl searches for its main configuration file in the following locations (in that order):
|
|
||||||
|
|
||||||
* `/etc/zrepl/zrepl.yml`
|
|
||||||
* `/usr/local/etc/zrepl/zrepl.yml`
|
|
||||||
|
|
||||||
Alternatively, use CLI flags to specify a config location.
|
|
||||||
|
|
||||||
Copy a config from the [tutorial](/tutorial) or the `cmd/sampleconf` directory to one of these locations and customize it to your setup.
|
|
||||||
|
|
||||||
## Runtime Directories
|
|
||||||
|
|
||||||
Check the the [configuration documentation]({{< relref "configuration/misc.md#runtime-directories-unix-sockets" >}}) for more information.
|
|
||||||
For default settings, the following should to the trick.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mkdir -p /var/run/zrepl/stdinserver
|
|
||||||
chmod -R 0700 /var/run/zrepl
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
## Running the Daemon
|
|
||||||
|
|
||||||
All actual work zrepl does is performed by a daemon process.
|
|
||||||
|
|
||||||
Logging is configurable via the config file. Please refer to the [logging documentation]({{< relref "configuration/logging.md" >}}).
|
|
||||||
|
|
||||||
```bash
|
|
||||||
zrepl daemon
|
|
||||||
```
|
|
||||||
|
|
||||||
There are no *rc(8)* or *systemd.service(5)* service definitions yet. Note the *daemon(8)* utility on FreeBSD.
|
|
||||||
|
|
||||||
{{% notice info %}}
|
|
||||||
Make sure to actually monitor the error level output of zrepl: some configuration errors will not make the daemon exit.<br />
|
|
||||||
Example: if the daemon cannot create the [stdinserver]({{< relref "configuration/transports.md#stdinserver" >}}) sockets
|
|
||||||
in the runtime directory, it will emit an error message but not exit because other tasks such as periodic snapshots & pruning are of equal importance.
|
|
||||||
{{% / notice %}}
|
|
||||||
|
|
||||||
### Restarting
|
|
||||||
|
|
||||||
The daemon handles SIGINT and SIGTERM for graceful shutdown.
|
|
||||||
|
|
||||||
Graceful shutdown means at worst that a job will not be rescheduled for the next interval.
|
|
||||||
|
|
||||||
The daemon exits as soon as all jobs have reported shut down.
|
|
||||||
@@ -1,173 +0,0 @@
|
|||||||
---
|
|
||||||
title: "Tutorial"
|
|
||||||
weight: 1
|
|
||||||
---
|
|
||||||
|
|
||||||
This tutorial shows how zrepl can be used to implement a ZFS-based pull backup.
|
|
||||||
|
|
||||||
We assume the following scenario
|
|
||||||
|
|
||||||
* Production server `prod1` with filesystems to back up
|
|
||||||
* `zroot/var/db`
|
|
||||||
* `zroot/usr/home` and all its child filesystems
|
|
||||||
* **except** `zroot/usr/home/paranoid` belonging to a user doing backups themselves
|
|
||||||
* Backup server `backups` with
|
|
||||||
* Filesystem `storage/zrepl/pull/prod1` + children dedicated to backups of `prod1`
|
|
||||||
|
|
||||||
Our backup solution should fulfill the following requirements:
|
|
||||||
|
|
||||||
* Periodically snapshot the filesystems on `prod1` *every 10 minutes*
|
|
||||||
* Incrementally replicate these snapshots to `storage/zrepl/pull/prod1/*` on `backups`
|
|
||||||
* Keep only very few snapshots on `prod1` to save disk space
|
|
||||||
* Keep a fading history (24 hourly, 30 daily, 6 monthly) of snapshots on `backups`
|
|
||||||
|
|
||||||
## Analysis
|
|
||||||
|
|
||||||
We can model this situation as two jobs:
|
|
||||||
|
|
||||||
* A **source job** on `prod1`
|
|
||||||
* Creates the snapshots
|
|
||||||
* Keeps a few snapshots that are also on `prod1` to enable incremental replication
|
|
||||||
* A **pull job** on `prod1`
|
|
||||||
* Pulls the snapshots
|
|
||||||
* Fades out snapshots as they age
|
|
||||||
|
|
||||||
{{%expand "Side note: why doesn't `backups` take the snapshots right before replication?" %}}
|
|
||||||
After all, a little `ssh prod1 'zfs snapshot...'` wouldn't be so bad, right?
|
|
||||||
|
|
||||||
As is the case with all distributed systems, the link between `prod1` and `backups` might be down for an hour or two.
|
|
||||||
We do not want to sacrifice our required backup resolution of 10 minute intervals for a temporary connection outage.
|
|
||||||
|
|
||||||
When the link comes up again, `backups` will happily catch up the 12 snapshots taken by `prod1` in the meantime, without
|
|
||||||
a gap in our backup history.
|
|
||||||
{{%/expand%}}
|
|
||||||
|
|
||||||
## Install zrepl
|
|
||||||
|
|
||||||
Follow the [OS-specific installation instructions](/install/) and come back here.
|
|
||||||
|
|
||||||
## Configure `backups`
|
|
||||||
|
|
||||||
We define a **pull job** named `pull_prod1` in the [main configuration file](/install/#main-configuration-file):
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
jobs:
|
|
||||||
- name: pull_prod1
|
|
||||||
type: pull
|
|
||||||
connect:
|
|
||||||
type: ssh+stdinserver
|
|
||||||
host: prod1.example.com
|
|
||||||
user: root
|
|
||||||
port: 22
|
|
||||||
identity_file: /etc/zrepl/ssh/prod1
|
|
||||||
interval: 10m
|
|
||||||
mapping: {
|
|
||||||
"<":"storage/zrepl/pull/prod1"
|
|
||||||
}
|
|
||||||
initial_repl_policy: most_recent
|
|
||||||
snapshot_prefix: zrepl_pull_backup_
|
|
||||||
prune:
|
|
||||||
policy: grid
|
|
||||||
grid: 1x1h(keep=all) | 24x1h | 35x1d | 6x30d
|
|
||||||
```
|
|
||||||
|
|
||||||
The `connect` section instructs zrepl to use the `stdinserver` transport: instead of directly exposing zrepl on `prod1`
|
|
||||||
to the internet, `backups` starts the `zrepl stdinserver` on `prod1` via SSH.
|
|
||||||
(You can learn more about what happens [here]({{< relref "configuration/transports.md#stdinserver" >}}), or just continue following this tutorial.)
|
|
||||||
|
|
||||||
Thus, we need to create the SSH key pair `/etc/zrepl/ssh/prod1{,.pub}` and later pass the public part to `prod1`
|
|
||||||
which will use it to authenticate `backups`. Execute the following commands on `backups` as the root user:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /etc/zrepl
|
|
||||||
mkdir -p ssh
|
|
||||||
chmod 0700 ssh
|
|
||||||
ssh-keygen -t ed25519 -N '' -f /etc/zrepl/ssh/prod1
|
|
||||||
```
|
|
||||||
You can learn more about the [**pull job** format here]({{< relref "configuration/jobs.md#pull" >}}) but for now we are good to go.
|
|
||||||
|
|
||||||
## Configure `prod1`
|
|
||||||
|
|
||||||
We define a corresponding **source job** named `pull_backup` in the [main configuration file](/install/#main-configuration-file)
|
|
||||||
`zrepl.yml`:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
jobs:
|
|
||||||
|
|
||||||
- name: pull_backup
|
|
||||||
type: source
|
|
||||||
serve:
|
|
||||||
type: stdinserver
|
|
||||||
client_identity: backups.example.com
|
|
||||||
datasets: {
|
|
||||||
"zroot/var/db": "ok",
|
|
||||||
"zroot/usr/home<": "ok",
|
|
||||||
"zroot/usr/home/paranoid": "!",
|
|
||||||
}
|
|
||||||
snapshot_prefix: zrepl_pull_backup_
|
|
||||||
interval: 10m
|
|
||||||
prune:
|
|
||||||
policy: grid
|
|
||||||
grid: 1x1d(keep=all)
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
The `serve` section corresponds to the `connect` section in the configuration of `backups`.
|
|
||||||
|
|
||||||
We need to allow the SSH key on `backups` to execute `zrepl stdinserver backups.example.com` on
|
|
||||||
`prod1`. For good measure, we will in fact enforce that only this command can be executed.
|
|
||||||
|
|
||||||
Open `/root/.ssh/authorized_keys` and add either of the the following lines, replacing BACKUPS_SSH_PUBKEY at the end
|
|
||||||
of the line with the contents of `/etc/zrepl/ssh/prod1.pub` (note the **.pub** !) from `backups`.
|
|
||||||
|
|
||||||
```
|
|
||||||
# for OpenSSH >= 7.2
|
|
||||||
command="zrepl stdinserver backups.example.com",restrict BACKUPS_SSH_PUBKEY
|
|
||||||
# for older OpenSSH versions
|
|
||||||
command="zrepl stdinserver backups.example.com",no-port-forwarding,no-X11-forwarding,no-pty,no-agent-forwarding,no-user-rc BACKUPS_SSH_PUBKEY
|
|
||||||
```
|
|
||||||
|
|
||||||
{{% alert theme="info" %}}The entries **must** be on a single line, including the replaced BACKUPS_SSH_PUBKEY{{% /alert %}}
|
|
||||||
|
|
||||||
Again, you can learn more about the [**source job** format here]({{< ref "configuration/jobs.md#source" >}}).
|
|
||||||
|
|
||||||
## Apply Configuration Changes
|
|
||||||
|
|
||||||
We need to restart the zrepl daemon on **both** `prod1` and `backups`.
|
|
||||||
|
|
||||||
This is [OS-specific](/install/#restarting).
|
|
||||||
|
|
||||||
## Watch it Work
|
|
||||||
|
|
||||||
A common setup is to `watch` the log output and `zfs list` of snapshots on both machines.
|
|
||||||
|
|
||||||
If you like tmux, here is a handy script that works on FreeBSD:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pkg install gnu-watch tmux
|
|
||||||
tmux new-window
|
|
||||||
tmux split-window "tail -f /var/log/zrepl.log"
|
|
||||||
tmux split-window "gnu-watch 'zfs list -t snapshot -o name,creation -s creation | grep zrepl_pull_backup_'"
|
|
||||||
tmux select-layout tiled
|
|
||||||
```
|
|
||||||
|
|
||||||
The Linux equivalent might look like this
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# make sure tmux is installed & let's assume you use systemd + journald
|
|
||||||
tmux new-window
|
|
||||||
tmux split-window "journalctl -f -u zrepl.service"
|
|
||||||
tmux split-window "watch 'zfs list -t snapshot -o name,creation -s creation | grep zrepl_pull_backup_'"
|
|
||||||
tmux select-layout tiled
|
|
||||||
```
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
|
|
||||||
Congratulations, you have a working pull backup. Where to go next?
|
|
||||||
|
|
||||||
* Read more about [configuration format, options & job types](/configuration/)
|
|
||||||
* Learn about [implementation details](/impl/) of zrepl.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
.. global list of substitutions
|
||||||
|
|
||||||
|
.. |mainconfig| replace:: :ref:`main configuration file <mainconfigfile>`
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
.. _implementation_toc:
|
||||||
|
|
||||||
|
Implementation Overview
|
||||||
|
=======================
|
||||||
|
|
||||||
|
.. WARNING::
|
||||||
|
|
||||||
|
Incomplete / under construction
|
||||||
|
|
||||||
|
The following design aspects may convince you that ``zrepl`` is superior to a hacked-together shell script solution.
|
||||||
|
|
||||||
|
Testability & Performance
|
||||||
|
-------------------------
|
||||||
|
|
||||||
|
zrepl is written in Go, a real programming language with type safety,
|
||||||
|
reasonable performance, testing infrastructure and an (opinionated) idea of
|
||||||
|
software engineering.
|
||||||
|
|
||||||
|
* key parts & algorithms of zrepl are covered by unit tests (work in progress)
|
||||||
|
* zrepl is noticably faster than comparable shell scripts
|
||||||
|
|
||||||
|
|
||||||
|
RPC protocol
|
||||||
|
------------
|
||||||
|
|
||||||
|
While it is tempting to just issue a few ``ssh remote 'zfs send ...' | zfs recv``, this has a number of drawbacks:
|
||||||
|
|
||||||
|
* The snapshot streams need to be compatible.
|
||||||
|
* Communication is still unidirectional. Thus, you will most likely
|
||||||
|
|
||||||
|
* either not take advantage of features such as *compressed send & recv*
|
||||||
|
* or issue additional ``ssh`` commands in advance to figure out what features are supported on the other side.
|
||||||
|
|
||||||
|
* Advanced logic in shell scripts is ugly to read, poorly testable and a pain to maintain.
|
||||||
|
|
||||||
|
zrepl takes a different approach:
|
||||||
|
|
||||||
|
* Define an RPC protocol.
|
||||||
|
* Establish an encrypted, authenticated, bidirectional communication channel...
|
||||||
|
* ... with zrepl running at both ends of it.
|
||||||
|
|
||||||
|
This has several obvious benefits:
|
||||||
|
|
||||||
|
* No blank root shell access is given to the other side.
|
||||||
|
* Instead, an *authenticated* peer can *request* filesystem lists, snapshot streams, etc.
|
||||||
|
* Requests are then checked against job-specific ACLs, limiting a client to the filesystems it is actually allowed to replicate.
|
||||||
|
* The :ref:`transport mechanism <transport>` is decoupled from the remaining logic, keeping it extensible.
|
||||||
|
|
||||||
|
Protocol Implementation
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
zrepl implements its own RPC protocol.
|
||||||
|
This is mostly due to the fact that existing solutions do not provide efficient means to transport large amounts of data.
|
||||||
|
|
||||||
|
Package `github.com/zrepl/zrepl/rpc <https://github.com/zrepl/zrepl/tree/master/rpc>`_ builds a special-case handling around returning an ``io.Reader`` as part of a unary RPC call.
|
||||||
|
|
||||||
|
Measurements show only a single memory-to-memory copy of a snapshot stream is made using ``github.com/zrepl/zrepl/rpc``, and there is still potential for further optimizations.
|
||||||
|
|
||||||
|
Logging & Transparency
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
zrepl comes with :ref:`rich, structured and configurable logging <logging>`, allowing administators to understand what the software is actually doing.
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
.. zrepl documentation master file, created by
|
||||||
|
sphinx-quickstart on Wed Nov 8 22:28:10 2017.
|
||||||
|
You can adapt this file completely to your liking, but it should at least
|
||||||
|
contain the root `toctree` directive.
|
||||||
|
|
||||||
|
.. include:: global.rst.inc
|
||||||
|
|
||||||
|
zrepl - ZFS replication
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
.. ATTENTION::
|
||||||
|
zrepl as well as this documentation is still under active development.
|
||||||
|
It is neither feature complete nor is there a stability guarantee on the configuration format.
|
||||||
|
Use & test at your own risk ;)
|
||||||
|
|
||||||
|
Getting started
|
||||||
|
~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
The :ref:`5 minute tutorial setup <tutorial>` gives you a first impression.
|
||||||
|
|
||||||
|
Main Features
|
||||||
|
~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
* Filesystem Replication
|
||||||
|
|
||||||
|
* [x] Local & Remote
|
||||||
|
* [x] Pull mode
|
||||||
|
* [ ] Push mode
|
||||||
|
* [x] Access control checks when pulling datasets
|
||||||
|
* [x] :ref:`Flexible mapping <pattern-mapping>` rules
|
||||||
|
* [x] Bookmarks support
|
||||||
|
* [ ] Feature-negotiation for
|
||||||
|
|
||||||
|
* Resumable `send & receive`
|
||||||
|
* Compressed `send & receive`
|
||||||
|
* Raw encrypted `send & receive` (as soon as it is available)
|
||||||
|
|
||||||
|
* Automatic snapshot creation
|
||||||
|
|
||||||
|
* [x] Ensure fixed time interval between snapshots
|
||||||
|
|
||||||
|
* Automatic snapshot :ref:`pruning <prune>`
|
||||||
|
|
||||||
|
* [x] Age-based fading (grandfathering scheme)
|
||||||
|
|
||||||
|
* Flexible, detailed & structured :ref:`logging <logging>`
|
||||||
|
|
||||||
|
* [x] ``human``, ``logfmt`` and ``json`` formatting
|
||||||
|
* [x] stdout, syslog and TCP (+TLS client auth) outlets
|
||||||
|
|
||||||
|
* Maintainable implementation in Go
|
||||||
|
|
||||||
|
* [x] Cross platform
|
||||||
|
* [x] Type safe & testable code
|
||||||
|
|
||||||
|
Contributing
|
||||||
|
~~~~~~~~~~~~
|
||||||
|
|
||||||
|
We are happy about any help we can get!
|
||||||
|
|
||||||
|
* Explore the codebase
|
||||||
|
|
||||||
|
* These docs live in the ``docs/`` subdirectory
|
||||||
|
|
||||||
|
* Document any non-obvious / confusing / plain broken behavior you encounter when setting up zrepl for the first time
|
||||||
|
* Check the *Issues* and *Projects* sections for things to do
|
||||||
|
|
||||||
|
.. admonition:: Development Workflow
|
||||||
|
:class: note
|
||||||
|
|
||||||
|
The `GitHub repository <https://github.com/zrepl/zrepl>`_ is where all development happens.
|
||||||
|
Make sure to read the `Developer Documentation section <https://github.com/zrepl/zrepl>`_ and open new issues or pull requests there.
|
||||||
|
|
||||||
|
|
||||||
|
Table of Contents
|
||||||
|
~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 2
|
||||||
|
:caption: Contents:
|
||||||
|
|
||||||
|
tutorial
|
||||||
|
installation
|
||||||
|
configuration
|
||||||
|
implementation
|
||||||
|
changelog
|
||||||
|
GitHub Repository & Issue Tracker <https://github.com/zrepl/zrepl>
|
||||||
|
pr
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
.. _installation:
|
||||||
|
|
||||||
|
Installation
|
||||||
|
============
|
||||||
|
|
||||||
|
.. TIP::
|
||||||
|
|
||||||
|
Note: check out the :ref:`tutorial` if you want a first impression of zrepl.
|
||||||
|
|
||||||
|
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.
|
||||||
|
However, until we get around documenting those setups, you will have to run zrepl as root or experiment yourself :)
|
||||||
|
|
||||||
|
Packages
|
||||||
|
--------
|
||||||
|
|
||||||
|
zrepl releases are signed & tagged by the author in the git repository.
|
||||||
|
Your OS vendor may provide binary packages of zrepl through the package manager.
|
||||||
|
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/>`_
|
||||||
|
* - Others
|
||||||
|
-
|
||||||
|
- Install from source, see below
|
||||||
|
|
||||||
|
Compile From Source
|
||||||
|
~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Go 1.9 or newer and a configured ``$GOPATH`` environment variable and a few build dependencies are required to build zrepl.
|
||||||
|
A tutorial is available over at `golang.org <https://golang.org/doc/install>`_.
|
||||||
|
If Go 1.9 is not available on your distro consider build in Docker (see below).
|
||||||
|
|
||||||
|
The following shell script checks out the zrepl project into your ``$GOPATH``,
|
||||||
|
installs the build dependencies, installs dependencies using ``dep ensure`` and does a ``make release``.
|
||||||
|
Build artifacts are placed into ``$GOPATH/github.com/zrepl/zrepl/artifacts/``.
|
||||||
|
|
||||||
|
When doing builds afterwards, it should be sufficient to checkout the new revision, run ``dep ensure`` and ``make release``.
|
||||||
|
You may want to switch to a tagged commit (we use `semver <http://semver.org>`_) but master should generally be considered stable.
|
||||||
|
|
||||||
|
**Note**: it is your job to install the apropriate 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.
|
||||||
|
|
||||||
|
**You are encouraged to understand what happens by auditing the script.**
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
curl 'https://raw.githubusercontent.com/zrepl/zrepl/master/clone_and_build.sh' | sh
|
||||||
|
|
||||||
|
You can also build in a Docker container if you want an isolated build environment or don't have a compatible Go version.
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
git clone https://github.com/zrepl/zrepl.git
|
||||||
|
cd zrepl
|
||||||
|
sudo docker run -it --rm \
|
||||||
|
-v "${PWD}:/zrepl" \
|
||||||
|
--user "$(id -u):$(id -g)" \
|
||||||
|
golang:latest bash -c 'export CLONEPATH=/go/src/github.com/zrepl; mkdir -p "$CLONEPATH" && ln -s /zrepl $CLONEPATH/zrepl && ${CLONEPATH}/zrepl/clone_and_build.sh'
|
||||||
|
|
||||||
|
.. literalinclude:: ../clone_and_build.sh
|
||||||
|
:language: sh
|
||||||
|
|
||||||
|
|
||||||
|
.. _mainconfigfile:
|
||||||
|
|
||||||
|
Configuration Files
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
zrepl searches for its main configuration file in the following locations (in that order):
|
||||||
|
|
||||||
|
* ``/etc/zrepl/zrepl.yml``
|
||||||
|
* ``/usr/local/etc/zrepl/zrepl.yml``
|
||||||
|
|
||||||
|
Alternatively, use CLI flags to specify a config location.
|
||||||
|
Copy a config from the :ref:`tutorial` or the ``cmd/sampleconf`` directory to one of these locations and customize it to your setup.
|
||||||
|
|
||||||
|
Runtime Directories
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
zrepl requires ephemeral runtime directories where control sockets, etc are placed.
|
||||||
|
Refer to the :ref:`configuration documentation <conf-runtime-directories>` for more information.
|
||||||
|
|
||||||
|
When installing from a package, the package maintainer should have taken care of setting them up through the init system.
|
||||||
|
Alternatively, for default settings, the following should to the trick.
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
mkdir -p /var/run/zrepl/stdinserver
|
||||||
|
chmod -R 0700 /var/run/zrepl
|
||||||
|
|
||||||
|
|
||||||
|
Running the Daemon
|
||||||
|
------------------
|
||||||
|
|
||||||
|
All actual work zrepl does is performed by a daemon process.
|
||||||
|
Logging is configurable via the config file. Please refer to the :ref:`logging documention <logging>`.
|
||||||
|
|
||||||
|
When installating from a package, the package maintainer should have provided an init script / systemd.service file.
|
||||||
|
You should thus be able to start zrepl daemon using your init system.
|
||||||
|
|
||||||
|
Alternatively, or for running zrepl in the foreground, simply execute ``zrepl daemon``.
|
||||||
|
Note that you won't see any output unless you configure :ref:`stdout logging outlet <logging-outlet-stdout>`.
|
||||||
|
|
||||||
|
.. ATTENTION::
|
||||||
|
|
||||||
|
Make sure to actually monitor the error level output of zrepl: some configuration errors will not make the daemon exit.
|
||||||
|
|
||||||
|
Example: if the daemon cannot create the :ref:`transport-ssh+stdinserver` sockets in the runtime directory,
|
||||||
|
it will emit an error message but not exit because other tasks such as periodic snapshots & pruning are of equal importance.
|
||||||
|
|
||||||
|
.. _install-restarting:
|
||||||
|
|
||||||
|
Restarting
|
||||||
|
~~~~~~~~~~
|
||||||
|
|
||||||
|
The daemon handles SIGINT and SIGTERM for graceful shutdown.
|
||||||
|
Graceful shutdown means at worst that a job will not be rescheduled for the next interval.
|
||||||
|
The daemon exits as soon as all jobs have reported shut down.
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
Talks & Presentations
|
||||||
|
=====================
|
||||||
|
|
||||||
|
* Talk at EuroBSDCon2017 FreeBSD DevSummit (
|
||||||
|
`Slides <https://docs.google.com/presentation/d/1EmmeEvOXAWJHCVnOS9-TTsxswbcGKmeLWdY_6BH4w0Q/edit?usp=sharing>`_,
|
||||||
|
`Event <https://wiki.freebsd.org/DevSummit/201709>`_
|
||||||
|
)
|
||||||
|
|
||||||
+17
-4
@@ -3,11 +3,12 @@ set -eo pipefail
|
|||||||
|
|
||||||
GHPAGESREPO="git@github.com:zrepl/zrepl.github.io.git"
|
GHPAGESREPO="git@github.com:zrepl/zrepl.github.io.git"
|
||||||
SCRIPTDIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
|
SCRIPTDIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
|
||||||
PUBLICDIR="${SCRIPTDIR}/public_git" #use different dir than default dir 'public' (hugo server -D)
|
PUBLICDIR="${SCRIPTDIR}/public_git"
|
||||||
|
|
||||||
checkout_repo_msg() {
|
checkout_repo_msg() {
|
||||||
echo "checkout ${GHPAGESREPO} to ${PUBLICDIR}:"
|
echo "clone ${GHPAGESREPO} to ${PUBLICDIR}:"
|
||||||
echo " git clone ${GHPAGESREPO} ${PUBLICDIR}"
|
echo " git clone ${GHPAGESREPO} ${PUBLICDIR}"
|
||||||
|
git clone "${GHPAGESREPO}" "${PUBLICDIR}"
|
||||||
}
|
}
|
||||||
|
|
||||||
exit_msg() {
|
exit_msg() {
|
||||||
@@ -31,6 +32,7 @@ echo "verify we're in the GitHub pages repo..."
|
|||||||
git remote get-url origin | grep -E "^${GHPAGESREPO}\$"
|
git remote get-url origin | grep -E "^${GHPAGESREPO}\$"
|
||||||
if [ "$?" -ne "0" ] ;then
|
if [ "$?" -ne "0" ] ;then
|
||||||
checkout_repo_msg
|
checkout_repo_msg
|
||||||
|
echo "finished checkout, please run again"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -44,12 +46,23 @@ git rm -rf .
|
|||||||
popd
|
popd
|
||||||
|
|
||||||
echo "building site"
|
echo "building site"
|
||||||
hugo -d "$PUBLICDIR"
|
set -e
|
||||||
|
make clean
|
||||||
|
make html
|
||||||
|
rsync -a _build/html/ public_git/
|
||||||
|
set +e
|
||||||
|
|
||||||
|
CURRENT_COMMIT=$(git rev-parse HEAD)
|
||||||
|
git status --porcelain
|
||||||
|
if [[ "$(git status --porcelain)" != "" ]]; then
|
||||||
|
CURRENT_COMMIT="${CURRENT_COMMIT}(dirty)"
|
||||||
|
fi
|
||||||
|
COMMIT_MSG="sphinx render from publish.sh - `date -u` - ${CURRENT_COMMIT}"
|
||||||
|
|
||||||
pushd "$PUBLICDIR"
|
pushd "$PUBLICDIR"
|
||||||
|
|
||||||
echo "adding and commiting all changes in GitHub pages repo"
|
echo "adding and commiting all changes in GitHub pages repo"
|
||||||
git add -A
|
git add -A
|
||||||
git commit -m "hugo render from publish.sh - `date -u`" #TODO include parent commit id
|
git commit -m "$COMMIT_MSG"
|
||||||
git push origin master
|
git push origin master
|
||||||
|
|
||||||
|
|||||||
Vendored
-1
Submodule docs/themes/docdock deleted from 9e429b2c20
@@ -0,0 +1,187 @@
|
|||||||
|
.. include:: global.rst.inc
|
||||||
|
|
||||||
|
.. _tutorial:
|
||||||
|
|
||||||
|
Tutorial
|
||||||
|
========
|
||||||
|
|
||||||
|
|
||||||
|
This tutorial shows how zrepl can be used to implement a ZFS-based pull backup.
|
||||||
|
We assume the following scenario:
|
||||||
|
|
||||||
|
* Production server ``app-srv`` with filesystems to back up:
|
||||||
|
|
||||||
|
* ``zroot/var/db``
|
||||||
|
* ``zroot/usr/home`` and all its child filesystems
|
||||||
|
* **except** ``zroot/usr/home/paranoid`` belonging to a user doing backups themselves
|
||||||
|
|
||||||
|
* Backup server ``backup-srv`` with
|
||||||
|
|
||||||
|
* Filesystem ``storage/zrepl/pull/app-srv`` + children dedicated to backups of ``app-srv``
|
||||||
|
|
||||||
|
Our backup solution should fulfill the following requirements:
|
||||||
|
|
||||||
|
* Periodically snapshot the filesystems on ``app-srv`` *every 10 minutes*
|
||||||
|
* Incrementally replicate these snapshots to ``storage/zrepl/pull/app-srv/*`` on ``backup-srv``
|
||||||
|
* Keep only very few snapshots on ``app-srv`` to save disk space
|
||||||
|
* Keep a fading history (24 hourly, 30 daily, 6 monthly) of snapshots on ``backup-srv``
|
||||||
|
|
||||||
|
Analysis
|
||||||
|
--------
|
||||||
|
|
||||||
|
We can model this situation as two jobs:
|
||||||
|
|
||||||
|
* A **source job** on ``app-srv``
|
||||||
|
|
||||||
|
* Creates the snapshots
|
||||||
|
* Keeps a short history of snapshots to enable incremental replication to ``backup-srv``
|
||||||
|
* Accepts connections from ``backup-srv``
|
||||||
|
|
||||||
|
* A **pull job** on ``backup-srv``
|
||||||
|
|
||||||
|
* Connects to the ``zrepl daemon`` process on ``app-srv``
|
||||||
|
* Pulls the snapshots to ``storage/zrepl/pull/app-srv/*``
|
||||||
|
* Fades out snapshots in ``storage/zrepl/pull/app-srv/*`` as they age
|
||||||
|
|
||||||
|
|
||||||
|
Why doesn't the **pull job** create the snapshots before pulling?
|
||||||
|
|
||||||
|
As is the case with all distributed systems, the link between ``app-srv`` and ``backup-srv`` might be down for an hour or two.
|
||||||
|
We do not want to sacrifice our required backup resolution of 10 minute intervals for a temporary connection outage.
|
||||||
|
|
||||||
|
When the link comes up again, ``backup-srv`` will happily catch up the 12 snapshots taken by ``app-srv`` in the meantime, without
|
||||||
|
a gap in our backup history.
|
||||||
|
|
||||||
|
Install zrepl
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Follow the :ref:`OS-specific installation instructions <installation>` and come back here.
|
||||||
|
|
||||||
|
Configure ``backup-srv``
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
We define a **pull job** named ``pull_app-srv`` in the |mainconfig|: ::
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
- name: pull_app-srv
|
||||||
|
type: pull
|
||||||
|
connect:
|
||||||
|
type: ssh+stdinserver
|
||||||
|
host: app-srv.example.com
|
||||||
|
user: root
|
||||||
|
port: 22
|
||||||
|
identity_file: /etc/zrepl/ssh/identity
|
||||||
|
interval: 10m
|
||||||
|
mapping: {
|
||||||
|
"<":"storage/zrepl/pull/app-srv"
|
||||||
|
}
|
||||||
|
initial_repl_policy: most_recent
|
||||||
|
snapshot_prefix: zrepl_pull_backup_
|
||||||
|
prune:
|
||||||
|
policy: grid
|
||||||
|
grid: 1x1h(keep=all) | 24x1h | 35x1d | 6x30d
|
||||||
|
|
||||||
|
The ``connect`` section instructs the zrepl daemon to use the ``stdinserver`` transport:
|
||||||
|
``backup-srv`` will connect to the specified SSH server and expect ``zrepl stdinserver CLIENT_IDENTITY`` instead of the shell on the other side.
|
||||||
|
|
||||||
|
It uses the private key specified at ``connect.identity_file`` which we still need to create: ::
|
||||||
|
|
||||||
|
cd /etc/zrepl
|
||||||
|
mkdir -p ssh
|
||||||
|
chmod 0700 ssh
|
||||||
|
ssh-keygen -t ed25519 -N '' -f /etc/zrepl/ssh/identity
|
||||||
|
|
||||||
|
Note that most use cases do not benefit from separate keypairs per remote endpoint.
|
||||||
|
Thus, it is sufficient to create one keypair and use it for all ``connect`` directives on one host.
|
||||||
|
|
||||||
|
Learn more about :ref:`transport-ssh+stdinserver` transport and the :ref:`pull job <job-pull>` format.
|
||||||
|
|
||||||
|
.. _tutorial-configure-app-srv:
|
||||||
|
|
||||||
|
Configure ``app-srv``
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
We define a corresponding **source job** named ``pull_backup`` in the |mainconfig|: ::
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
- name: pull_backup
|
||||||
|
type: source
|
||||||
|
serve:
|
||||||
|
type: stdinserver
|
||||||
|
client_identity: backup-srv.example.com
|
||||||
|
filesystems: {
|
||||||
|
"zroot/var/db": "ok",
|
||||||
|
"zroot/usr/home<": "ok",
|
||||||
|
"zroot/usr/home/paranoid": "!",
|
||||||
|
}
|
||||||
|
snapshot_prefix: zrepl_pull_backup_
|
||||||
|
interval: 10m
|
||||||
|
prune:
|
||||||
|
policy: grid
|
||||||
|
grid: 1x1d(keep=all)
|
||||||
|
|
||||||
|
|
||||||
|
The ``serve`` section corresponds to the ``connect`` section in the configuration of ``backup-srv``.
|
||||||
|
|
||||||
|
We now want to authenticate ``backup-srv`` before allowing it to pull data.
|
||||||
|
This is done by limiting SSH connections from ``backup-srv`` to execute the ``stdinserver`` subcommand.
|
||||||
|
|
||||||
|
Open ``/root/.ssh/authorized_keys`` and add either of the the following lines.::
|
||||||
|
|
||||||
|
# for OpenSSH >= 7.2
|
||||||
|
command="zrepl stdinserver backup-srv.example.com",restrict CLIENT_SSH_KEY
|
||||||
|
# for older OpenSSH versions
|
||||||
|
command="zrepl stdinserver backup-srv.example.com",no-port-forwarding,no-X11-forwarding,no-pty,no-agent-forwarding,no-user-rc CLIENT_SSH_KEY
|
||||||
|
|
||||||
|
.. ATTENTION::
|
||||||
|
|
||||||
|
Replace CLIENT_SSH_KEY with the contents of ``/etc/zrepl/ssh/identity.pub`` from ``app-srv``.
|
||||||
|
Mind the trailing ``.pub`` in the filename.
|
||||||
|
The entries **must** be on a single line, including the replaced CLIENT_SSH_KEY.
|
||||||
|
|
||||||
|
|
||||||
|
.. HINT::
|
||||||
|
|
||||||
|
You may need to adjust the ``PermitRootLogin`` option in ``/etc/ssh/sshd_config`` to ``forced-commands-only`` or higher for this to work.
|
||||||
|
Refer to sshd_config(5) for details.
|
||||||
|
|
||||||
|
The argument ``backup-srv.example.com`` is the client identity of ``backup-srv`` as defined in ``jobs.serve.client_identity``.
|
||||||
|
|
||||||
|
Again, both :ref:`transport-ssh+stdinserver` transport and the :ref:`job-source` format are documented.
|
||||||
|
|
||||||
|
Apply Configuration Changes
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
We need to restart the zrepl daemon on **both** ``app-srv`` and ``backup-srv``.
|
||||||
|
This is :ref:`OS-specific <install-restarting>`.
|
||||||
|
|
||||||
|
Watch it Work
|
||||||
|
-------------
|
||||||
|
|
||||||
|
A common setup is to ``watch`` the log output and ``zfs list`` of snapshots on both machines.
|
||||||
|
|
||||||
|
If you like tmux, here is a handy script that works on FreeBSD: ::
|
||||||
|
|
||||||
|
pkg install gnu-watch tmux
|
||||||
|
tmux new-window
|
||||||
|
tmux split-window "tail -f /var/log/zrepl.log"
|
||||||
|
tmux split-window "gnu-watch 'zfs list -t snapshot -o name,creation -s creation | grep zrepl_pull_backup_'"
|
||||||
|
tmux select-layout tiled
|
||||||
|
|
||||||
|
The Linux equivalent might look like this: ::
|
||||||
|
|
||||||
|
# make sure tmux is installed & let's assume you use systemd + journald
|
||||||
|
tmux new-window
|
||||||
|
tmux split-window "journalctl -f -u zrepl.service"
|
||||||
|
tmux split-window "watch 'zfs list -t snapshot -o name,creation -s creation | grep zrepl_pull_backup_'"
|
||||||
|
tmux select-layout tiled
|
||||||
|
|
||||||
|
Summary
|
||||||
|
-------
|
||||||
|
|
||||||
|
Congratulations, you have a working pull backup. Where to go next?
|
||||||
|
|
||||||
|
* Read more about :ref:`configuration format, options & job types <configuration_toc>`
|
||||||
|
* Learn about :ref:`implementation details <implementation_toc>` of zrepl.
|
||||||
|
|
||||||
|
|
||||||
+5
-1
@@ -102,7 +102,11 @@ func (l *Logger) WithFields(fields Fields) (ret *Logger) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (l *Logger) WithError(err error) *Logger {
|
func (l *Logger) WithError(err error) *Logger {
|
||||||
return l.WithField(FieldError, err.Error())
|
val := interface{}(nil)
|
||||||
|
if err != nil {
|
||||||
|
val = err.Error()
|
||||||
|
}
|
||||||
|
return l.WithField(FieldError, val)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *Logger) Debug(msg string) {
|
func (l *Logger) Debug(msg string) {
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ func Outgoing(remote SSHTransport) (s OutgoingSSHByteStream, err error) {
|
|||||||
|
|
||||||
var sshCommand = SSHCommand
|
var sshCommand = SSHCommand
|
||||||
if len(remote.SSHCommand) > 0 {
|
if len(remote.SSHCommand) > 0 {
|
||||||
sshCommand = SSHCommand
|
sshCommand = remote.SSHCommand
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.c, err = util.NewIOCommand(sshCommand, sshArgs, util.IOCommandStderrBufSize); err != nil {
|
if s.c, err = util.NewIOCommand(sshCommand, sshArgs, util.IOCommandStderrBufSize); err != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user