Compare commits

..

60 Commits

Author SHA1 Message Date
Christian Schwarz fe40352f8e docs: link to github 2017-11-12 16:45:11 +01:00
Christian Schwarz fd123fc6c4 docs: add warning about lack of async TCP outlet
refs #26
2017-11-12 16:41:25 +01:00
Christian Schwarz 961500cc2c clone_and_build.sh: move set -e out of sheband to work on curl pipe bash 2017-11-12 16:27:10 +01:00
Christian Schwarz 47726ad877 improve install from source
* Idempotent clone_and_build.sh does everything
* Add documentation for how to build in Docker

Had to sacrificy go generate because stringer apparently can't handle
vendor directory used by go dep, fails with error
on go generate rpc/frame_layer.go

refs #37
2017-11-12 16:15:12 +01:00
Christian Schwarz 3b6cede108 go dep: run dep ensure, apparently cut off all unused transitive dependencies 2017-11-12 14:19:53 +01:00
Christian Schwarz 2cad13f27b docs: add changelog 2017-11-12 14:12:57 +01:00
Christian Schwarz b5475921a8 docs: fixup wrong fieldname in source-job
3e647c1 config: source job: rename field 'datasets' to 'filesystems'

BREAK CONFIG
2017-11-12 14:11:48 +01:00
Christian Schwarz 88c684e1d0 README.md: document procedure for breaking changes 2017-11-12 14:10:16 +01:00
Christian Schwarz a4d28701d9 docs: fix publish.sh script (was not pushing changes to master) 2017-11-12 13:33:34 +01:00
Christian Schwarz 000c8b4186 README.md: some documentation on how to build the docs 2017-11-11 23:33:09 +01:00
Christian Schwarz ae4b12c9ba docs: adjust README 2017-11-11 23:25:12 +01:00
Christian Schwarz 8cc31bd76a docs: publishing workflow as script 2017-11-11 23:25:12 +01:00
Christian Schwarz 43871a9211 docs: fix minor syntactical bugs 2017-11-11 23:25:12 +01:00
Christian Schwarz 77576164ae docs: add logo 2017-11-11 23:25:12 +01:00
Christian Schwarz 4c450a640c docs: logging: outlet type in comment field 2017-11-11 23:25:12 +01:00
Christian Schwarz 36d2cb115a docs: fixup index site 2017-11-11 23:25:12 +01:00
Christian Schwarz 7ba5c14679 docs: refine tutorial and installation pages 2017-11-11 23:25:12 +01:00
Christian Schwarz ab7eb47483 docs: adjust pr page to rst 2017-11-11 23:25:12 +01:00
Christian Schwarz 707a189144 docs: adjust implementation article to rst 2017-11-11 23:25:12 +01:00
Christian Schwarz 4f37dccb76 docs: adjust transports to rst 2017-11-11 23:25:12 +01:00
Christian Schwarz 69084fb08f docs: adjust prune to rst 2017-11-11 23:25:12 +01:00
Christian Schwarz 0a77be0ff2 docs: adjust misc to rst 2017-11-11 23:25:12 +01:00
Christian Schwarz 597302de3f docs: adjust map_filter_syntax to rst 2017-11-11 23:25:12 +01:00
Christian Schwarz 828c2982f3 docs: adjust logging to rst 2017-11-11 23:25:12 +01:00
Christian Schwarz 6f7b8ca1af docs: adjust jobs documentation to rst + use extlinks extension 2017-11-11 23:25:12 +01:00
Christian Schwarz e0f40de69f docs: adjust installation section to rst 2017-11-11 23:25:12 +01:00
Christian Schwarz 2fe7f29d31 docs: index + tutorial rst adjustments 2017-11-11 23:25:12 +01:00
Christian Schwarz df181108b4 docs: initial port of hugo to sphinx, including rtd theme 2017-11-11 23:25:12 +01:00
Christian Schwarz c3af267f48 docs: initial empty sphinx docs directory 2017-11-11 23:25:12 +01:00
Christian Schwarz a15a73e884 docs: move hugo docs to old directory 2017-11-11 23:25:12 +01:00
Christian Schwarz ff10a71f3a docs: add warning on replication lag & retention grid. 2017-11-04 13:04:32 +01:00
Christian Schwarz 1f266d02ce docs: tutorial, ssh+stdinserver: mention PermitRootLogin option
fixes #21
2017-10-16 21:58:02 +02:00
Christian Schwarz 4efff312ea docs: bump theme version 2017-10-16 21:55:38 +02:00
Christian Schwarz 0ed5c01473 docs: add talks & presentation page
refs #16
2017-10-05 22:28:43 +02:00
Christian Schwarz 63bc27e6e1 docs: fix new paragraph after zrepl-issue shortcode 2017-10-05 22:16:09 +02:00
Christian Schwarz e72d274e88 docs: add notice on missing property replication feature
fixes #23
2017-10-05 22:13:05 +02:00
Christian Schwarz 007da664ea README: document naming inconsitency with datasets & filesystems 2017-10-05 21:56:37 +02:00
Christian Schwarz f3433df617 cmd/sampleconf/zrep.yml: remove it, it's from the stone ages 2017-10-05 21:48:18 +02:00
Christian Schwarz 493a01c4fe logger: fix nil pointer deref in WithError
fixes #9
2017-10-05 21:23:39 +02:00
Christian Schwarz 161ce3b3c3 autosnap: fix log level when fs filter does not match any fs 2017-10-05 21:22:17 +02:00
Christian Schwarz 83bb97a845 control job: wrong error on context done 2017-10-05 21:20:01 +02:00
Christian Schwarz 40919d06c2 source job: fix errnous log message when accept() on closed listener 2017-10-05 21:19:42 +02:00
Christian Schwarz c48069ce88 retention grid: interva length monotonicity: exception for keep=all
fixes #6
2017-10-05 20:34:35 +02:00
Christian Schwarz 4b489ad2c7 config: connect: ssh_command parameter did not work 2017-10-05 20:11:04 +02:00
Christian Schwarz 72d288567e mappings: fix aliasing bug with '<' wildcards
In contrast to any 'something<' mapping, a '<' mapping cannot be unique
Thus, '<' mappings are thus just an append to target, which is exactly
what we get when trimming empty prefix ''.

Otherwise, given mapping

{ "<": "storage/backups/app-srv" }

Before (clearly a conflict)
zroot     => storage/backups/app-srv
storage   => storage/backups/app-srv
After:
zroot     => storage/backups/app-srv/zroot
storage   => storage/backups/app-srv/storage

However, mapping directly with subtree wildcard is still possible, just
not with the root wildcard

{
    "<"              "storage/backups/app-srv"
    "zroot/var/db<": "storage/db_replication/app-srv"
}

fixes #22
2017-10-05 20:10:05 +02:00
Christian Schwarz b5d46e2ec3 impl: don't reference m.entries again 2017-10-05 18:55:02 +02:00
Christian Schwarz 83d450b1f2 config: support days (d) and weeks (w) in durations
fixes #18
2017-10-05 15:17:37 +02:00
Christian Schwarz 3e647c14c0 config: source job: rename field 'datasets' to 'filesystems'
While filesystems is also not the right term (since it excludes ZVOLs),
we want to stay consistent with comments & terminology used in docs.

BREAK CONFIG

fixes #17
2017-10-05 13:39:05 +02:00
Christian Schwarz b95260f4b5 config: logging: defaults + definition as list
* Stdout logger as default logger
* Clearer keyword / value separation
* Allows multiple outlet definitions

BREAK CONFIG

fixes #20
fixes #19
2017-10-05 13:31:16 +02:00
Christian Schwarz 2764c95952 docs: update front page with new features & refs 2017-10-03 16:07:21 +02:00
Christian Schwarz 678b4a6f4b docs: update implementation overview 2017-10-03 16:06:58 +02:00
Christian Schwarz 79ab43ebca docs: add docs for logging 2017-10-03 15:41:44 +02:00
Christian Schwarz a4963cecb7 docs: document job types
The documentation describes intended behavior.

Apparently, there are some bugs regarding *patient* tasks.

refs #8
refs #13
2017-10-03 14:21:10 +02:00
Christian Schwarz e6d08149ef docs: update 'mappping & filter syntax' + more elaborate sampleconf 2017-10-02 18:29:58 +02:00
Christian Schwarz ea6f02368b docs: document pruning policies
refs #13
2017-10-02 17:51:28 +02:00
Christian Schwarz d891b2b119 docs: shortcode for links to the cmd/sampleconf directory
fixes #11
2017-10-02 15:12:35 +02:00
Christian Schwarz 5c6c9485a8 docs: tutorial: clarify identity semantics
* use only one identity file for all connect instructions
* Explain where the argument to stdinserver comes from.

fixes #14
2017-10-02 14:50:28 +02:00
Christian Schwarz b4d8c93fae docs: transport: document ssh+stdinserver 2017-10-02 14:21:22 +02:00
Christian Schwarz 3f394a8960 docs: tutorial: unambiguous hostnames
s/backups/backup-srv
s/prod1/app-srv

Also fixes wrong hostname in Analysis section.

fixes #15
2017-10-02 13:51:05 +02:00
Christian Schwarz 164f77d80c docs: switch back to patched up relref for links to section pages 2017-10-02 12:42:09 +02:00
58 changed files with 1983 additions and 1007 deletions
-4
View File
@@ -1,4 +0,0 @@
[submodule "docs/themes/docdock"]
path = docs/themes/docdock
url = git@github.com:zrepl/hugo-theme-docdock.git
Generated
+1 -79
View File
@@ -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
+5 -5
View File
@@ -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"
+38 -6
View File
@@ -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
```
+46
View File
@@ -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
View File
@@ -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
} }
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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
} }
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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 {
out := WriterOutlet{ return nil, errors.Wrap(err, "mapstructure error")
&HumanFormatter{}, }
os.Stdout, if len(asList) == 0 {
// Default config
out := WriterOutlet{&HumanFormatter{}, os.Stdout}
c.Outlets.Add(out, logger.Warn)
return
} }
level, err := logger.ParseLevel(asMap.Stdout.Level) var syslogOutlets, stdoutOutlets int
for lei, le := range asList {
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'")
} }
if stdoutOutlets > 1 {
lvl, err := logger.ParseLevel(asMap.TCP.Level) return nil, errors.Errorf("can only define one 'stdout' outlet")
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 asMap.Syslog.Enable {
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
}
+2 -7
View File
@@ -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() {
// special case: trying to map the root => strip first component
extendComps.TrimNPrefixComps(1)
} else {
extendComps.TrimPrefix(me.path) extendComps.TrimPrefix(me.path)
}
target.Extend(extendComps) target.Extend(extendComps)
} }
return return
+41
View File
@@ -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
}
+15 -42
View File
@@ -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
return for j := i - 1; allPrevKeepCountAll && j >= 0; j-- {
} else { allPrevKeepCountAll = intervals[j].KeepCount == util.RetentionGridKeepCountAll
lastDuration = intervals[i].Length
} }
if allPrevKeepCountAll {
goto isMonotonicIncrease
}
err = errors.New("retention grid interval length must be monotonically increasing")
return
}
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
View File
@@ -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")
+2 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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<": "!",
} }
+1 -1
View File
@@ -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_
+8 -4
View File
@@ -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: []
-111
View File
@@ -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
View File
@@ -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
View File
@@ -1 +1,2 @@
public_git _build/
public_git/
+20
View File
@@ -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)
+147
View File
@@ -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

-6
View File
@@ -1,6 +0,0 @@
---
title: "{{ replace .TranslationBaseName "-" " " | title }}"
date: {{ .Date }}
draft: true
---
+43
View File
@@ -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
View File
@@ -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 '),
}
-17
View File
@@ -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
+15
View File
@@ -0,0 +1,15 @@
.. _configuration_toc:
*************
Configuration
*************
.. toctree::
configuration/jobs
configuration/transports
configuration/map_filter_syntax
configuration/prune
configuration/logging
configuration/misc
+184
View File
@@ -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
+198
View File
@@ -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.
+104
View File
@@ -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": "!",
}
...
+63
View File
@@ -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.
+64
View File
@@ -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.
+112
View File
@@ -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
View File
@@ -1 +0,0 @@
zrepl Documentation
-50
View File
@@ -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 %}}
-8
View File
@@ -1,8 +0,0 @@
+++
title = "Configuration"
alwaysopen = true
+++
{{% alert theme="warning" %}}Under Construction{{% /alert %}}
{{% children description="true" %}}
-14
View File
@@ -1,14 +0,0 @@
+++
title = "Job Types"
weight = 10
+++
{{% alert theme="warning" %}}Under Construction{{% /alert %}}
See `sampleconf/*/*.yml` for examples.
## Source
## Pull
## Local
-8
View File
@@ -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,
}
```
-52
View File
@@ -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 %}}
-9
View File
@@ -1,9 +0,0 @@
+++
title = "Pruning"
description = "Automated pruning of snapshots"
weight = 40
+++
{{% alert theme="warning" %}}Under Construction{{% /alert %}}
## Retention Grid
-24
View File
@@ -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 %}}
-46
View File
@@ -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 %}}
-85
View File
@@ -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.
-173
View File
@@ -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.
+3
View File
@@ -0,0 +1,3 @@
.. global list of substitutions
.. |mainconfig| replace:: :ref:`main configuration file <mainconfigfile>`
+62
View File
@@ -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.
+88
View File
@@ -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
+130
View File
@@ -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.
+8
View File
@@ -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
View File
@@ -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
Submodule docs/themes/docdock deleted from 9e429b2c20
+187
View File
@@ -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
View File
@@ -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) {
+1 -1
View File
@@ -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 {