Compare commits

..

7 Commits

Author SHA1 Message Date
Christian Schwarz 30057d4e59 build: fix warning for cached builds with Go 1.10 2018-04-01 17:53:51 +02:00
Christian Schwarz 75fd21e454 make generate: stringer was updated and now uses strconv instead of fmt
https://github.com/golang/tools/commit/bd4635fd25596cdd56c1fb399c53b351d1a81f2d#diff-0415b5286e4cf3e373f349d917e5e039
2018-04-01 15:30:04 +02:00
Christian Schwarz 0d2f73d728 docs: tutorial: minor refinements 2018-04-01 14:58:12 +02:00
Christian Schwarz 9b803aad2d docs: tutorial: document known_hosts file setup
fixes #64
2018-04-01 14:58:04 +02:00
Christian Schwarz fb74addc1e bump go-rwccmd to support ssh error messages
this is a follow-up to ccd062e

fixes #65
2018-04-01 14:34:05 +02:00
Christian Schwarz 7f89372cfa docs: fix enumeration in ssh+stdinserver docs 2018-03-04 17:20:08 +01:00
Christian Schwarz 26b436463d ssh+stdinserver: connect: dial_timeout
This  is a follow-up to ccd062e
2018-03-04 17:19:41 +01:00
14 changed files with 111 additions and 124 deletions
Generated
+1 -1
View File
@@ -83,7 +83,7 @@
branch = "master"
name = "github.com/problame/go-netssh"
packages = ["."]
revision = "ffa145d2506e222977205e7666a9722d6b9959ac"
revision = "53a2e445f8ace7ec678f2d8cdd9c1428dfef4562"
[[projects]]
branch = "master"
+1
View File
@@ -11,6 +11,7 @@ RUN /tmp/lazy.sh devsetup
# prepare volume mount of git checkout to /zrepl
RUN mkdir -p /go/src/github.com/zrepl/zrepl
RUN chmod -R 0777 /go
RUN mkdir -p /.cache && chmod -R 0777 /.cache
WORKDIR /go/src/github.com/zrepl/zrepl
+16 -30
View File
@@ -5,7 +5,6 @@ import (
"fmt"
"github.com/zrepl/zrepl/zfs"
"sort"
"sync"
"time"
)
@@ -166,46 +165,33 @@ func (a *IntervalAutosnap) doSnapshots(didSnaps chan struct{}) {
// don't cache the result from previous run in case the user added
// a new dataset in the meantime
fss, stop := a.filterFilesystems()
ds, stop := a.filterFilesystems()
if stop {
return
}
a.task.Log().Info("beginning parallel snapshots")
// TODO channel programs -> allow a little jitter?
var wg sync.WaitGroup
for fsi := range fss {
wg.Add(1)
go func(fs *zfs.DatasetPath) {
defer wg.Done()
for _, d := range ds {
suffix := time.Now().In(time.UTC).Format("20060102_150405_000")
snapname := fmt.Sprintf("%s%s", a.Prefix, suffix)
suffix := time.Now().In(time.UTC).Format("20060102_150405_000")
snapname := fmt.Sprintf("%s%s", a.Prefix, suffix)
l := a.task.Log().WithField(logFSField, d.ToString()).
WithField("snapname", snapname)
l := a.task.Log().WithField(logFSField, fs.ToString()).
WithField("snapname", snapname)
l.Info("create snapshot")
err := zfs.ZFSSnapshot(d, snapname, false)
if err != nil {
a.task.Log().WithError(err).Error("cannot create snapshot")
}
l.Info("create snapshot")
err := zfs.ZFSSnapshot(fs, snapname, false)
if err != nil {
l.WithError(err).Error("cannot create snapshot")
return
}
l.Info("create corresponding bookmark")
err = zfs.ZFSBookmark(d, snapname, snapname)
if err != nil {
a.task.Log().WithError(err).Error("cannot create bookmark")
}
l.Info("create corresponding bookmark")
err = zfs.ZFSBookmark(fs, snapname, snapname)
if err != nil {
l.WithError(err).Error("cannot create bookmark")
}
}(fss[fsi])
}
a.task.Log().Info("waiting for parallel snapshots to finish")
wg.Wait()
a.task.Log().Info("snapshots finished")
select {
case didSnaps <- struct{}{}:
default:
+20 -2
View File
@@ -9,6 +9,7 @@ import (
"github.com/mitchellh/mapstructure"
"github.com/pkg/errors"
"github.com/problame/go-netssh"
"time"
)
type SSHStdinserverConnecter struct {
@@ -19,6 +20,8 @@ type SSHStdinserverConnecter struct {
TransportOpenCommand []string `mapstructure:"transport_open_command"`
SSHCommand string `mapstructure:"ssh_command"`
Options []string
DialTimeout string `mapstructure:"dial_timeout"`
dialTimeout time.Duration
}
func parseSSHStdinserverConnecter(i map[string]interface{}) (c *SSHStdinserverConnecter, err error) {
@@ -29,6 +32,15 @@ func parseSSHStdinserverConnecter(i map[string]interface{}) (c *SSHStdinserverCo
return nil, err
}
if c.DialTimeout != "" {
c.dialTimeout, err = time.ParseDuration(c.DialTimeout)
if err != nil {
return nil, errors.Wrap(err, "cannot parse dial_timeout")
}
} else {
c.dialTimeout = 10 * time.Second
}
// TODO assert fields are filled
return
@@ -38,9 +50,15 @@ func (c *SSHStdinserverConnecter) Connect() (rwc io.ReadWriteCloser, err error)
var endpoint netssh.Endpoint
if err = copier.Copy(&endpoint, c); err != nil {
return
return nil, errors.WithStack(err)
}
if rwc, err = netssh.Dial(context.TODO(), endpoint); err != nil {
var dialCtx context.Context
dialCtx, dialCancel := context.WithTimeout(context.TODO(), c.dialTimeout) // context.TODO tied to error handling below
defer dialCancel()
if rwc, err = netssh.Dial(dialCtx, endpoint); err != nil {
if err == context.DeadlineExceeded {
err = errors.Errorf("dial_timeout of %s exceeded", c.dialTimeout)
}
err = errors.WithStack(err)
return
}
+40 -59
View File
@@ -3,9 +3,7 @@ package cmd
import (
"context"
"fmt"
"github.com/zrepl/zrepl/util"
"github.com/zrepl/zrepl/zfs"
"sync"
"time"
)
@@ -25,8 +23,9 @@ type PruneResult struct {
Remove []zfs.FilesystemVersion
}
// FIXME must not call p.task.Enter because it runs in parallel
func (p *Pruner) filterFilesystems() (filesystems []*zfs.DatasetPath, stop bool) {
p.task.Enter("filter_fs")
defer p.task.Finish()
filesystems, err := zfs.ZFSListMapping(p.DatasetFilter)
if err != nil {
p.task.Log().WithError(err).Error("error applying filesystem filter")
@@ -39,8 +38,9 @@ func (p *Pruner) filterFilesystems() (filesystems []*zfs.DatasetPath, stop bool)
return filesystems, false
}
// FIXME must not call p.task.Enter because it runs in parallel
func (p *Pruner) filterVersions(fs *zfs.DatasetPath) (fsversions []zfs.FilesystemVersion, stop bool) {
p.task.Enter("filter_versions")
defer p.task.Finish()
log := p.task.Log().WithField(logFSField, fs.ToString())
filter := NewPrefixFilter(p.SnapshotPrefix)
@@ -56,8 +56,9 @@ func (p *Pruner) filterVersions(fs *zfs.DatasetPath) (fsversions []zfs.Filesyste
return fsversions, false
}
// FIXME must not call p.task.Enter because it runs in parallel
func (p *Pruner) pruneFilesystem(fs *zfs.DatasetPath, destroySemaphore util.Semaphore) (r PruneResult, valid bool) {
func (p *Pruner) pruneFilesystem(fs *zfs.DatasetPath) (r PruneResult, valid bool) {
p.task.Enter("prune_fs")
defer p.task.Finish()
log := p.task.Log().WithField(logFSField, fs.ToString())
fsversions, stop := p.filterVersions(fs)
@@ -65,7 +66,9 @@ func (p *Pruner) pruneFilesystem(fs *zfs.DatasetPath, destroySemaphore util.Sema
return
}
p.task.Enter("prune_policy")
keep, remove, err := p.PrunePolicy.Prune(fs, fsversions)
p.task.Finish()
if err != nil {
log.WithError(err).Error("error evaluating prune policy")
return
@@ -78,37 +81,33 @@ func (p *Pruner) pruneFilesystem(fs *zfs.DatasetPath, destroySemaphore util.Sema
r = PruneResult{fs, fsversions, keep, remove}
var wg sync.WaitGroup
for v := range remove {
wg.Add(1)
go func(v zfs.FilesystemVersion) {
defer wg.Done()
// log fields
fields := make(map[string]interface{})
fields["version"] = v.ToAbsPath(fs)
timeSince := v.Creation.Sub(p.Now)
fields["age_ns"] = timeSince
const day time.Duration = 24 * time.Hour
days := timeSince / day
remainder := timeSince % day
fields["age_str"] = fmt.Sprintf("%dd%s", days, remainder)
log.WithFields(fields).Info("destroying version")
// echo what we'll do and exec zfs destroy if not dry run
// TODO special handling for EBUSY (zfs hold)
// TODO error handling for clones? just echo to cli, skip over, and exit with non-zero status code (we're idempotent)
if !p.DryRun {
destroySemaphore.Down()
err := zfs.ZFSDestroyFilesystemVersion(fs, v)
destroySemaphore.Up()
if err != nil {
log.WithFields(fields).WithError(err).Error("error destroying version")
}
}
}(remove[v])
makeFields := func(v zfs.FilesystemVersion) (fields map[string]interface{}) {
fields = make(map[string]interface{})
fields["version"] = v.ToAbsPath(fs)
timeSince := v.Creation.Sub(p.Now)
fields["age_ns"] = timeSince
const day time.Duration = 24 * time.Hour
days := timeSince / day
remainder := timeSince % day
fields["age_str"] = fmt.Sprintf("%dd%s", days, remainder)
return
}
wg.Wait()
for _, v := range remove {
fields := makeFields(v)
log.WithFields(fields).Info("destroying version")
// echo what we'll do and exec zfs destroy if not dry run
// TODO special handling for EBUSY (zfs hold)
// TODO error handling for clones? just echo to cli, skip over, and exit with non-zero status code (we're idempotent)
if !p.DryRun {
p.task.Enter("destroy")
err := zfs.ZFSDestroyFilesystemVersion(fs, v)
p.task.Finish()
if err != nil {
log.WithFields(fields).WithError(err).Error("error destroying version")
}
}
}
return r, true
}
@@ -125,31 +124,13 @@ func (p *Pruner) Run(ctx context.Context) (r []PruneResult, err error) {
return
}
maxConcurrentDestroy := len(filesystems)
p.task.Log().WithField("max_concurrent_destroy", maxConcurrentDestroy).Info("begin concurrent destroy")
destroySem := util.NewSemaphore(maxConcurrentDestroy)
resChan := make(chan PruneResult, len(filesystems))
var wg sync.WaitGroup
for _, fs := range filesystems {
wg.Add(1)
go func(fs *zfs.DatasetPath) {
defer wg.Done()
res, ok := p.pruneFilesystem(fs, destroySem)
if ok {
resChan <- res
}
}(fs)
}
wg.Wait()
close(resChan)
p.task.Log().Info("destroys done")
r = make([]PruneResult, 0, len(filesystems))
for res := range resChan {
r = append(r, res)
for _, fs := range filesystems {
res, ok := p.pruneFilesystem(fs)
if ok {
r = append(r, res)
}
}
return
+9 -1
View File
@@ -1,6 +1,7 @@
.. |break_config| replace:: **[BREAK]**
.. |break| replace:: **[BREAK]**
.. |bugfix| replace:: [BUG]
.. |docs| replace:: [DOCS]
.. |feature| replace:: [FEATURE]
Changelog
@@ -19,7 +20,13 @@ Developers should consult the git commit log or GitHub issue tracker.
* Make sure to understand the meaning bookmarks have for :ref:`maximum replication downtime <replication-downtime>`.
* Example: :sampleconf:`pullbackup/productionhost.yml`
* |break| :commit:`ccd062e`: both sides of a replication setup must be updated and restarted. Otherwise the connecting side will hang and not time out.
* |break| :commit:`ccd062e`: ``ssh+stdinserver`` transport: changed protocol requires daemon restart on both sides
* The delicate procedure of talking to the serving-side zrepl daemon via the stdinserver proxy command now has better error handling.
* This includes handshakes between client+proxy and client + remote daemo, which is not implemented in previous versions of zrepl.
* The connecting side will therefore time out, with the message ``dial_timeout of 10s exceeded``.
* Both sides of a replication setup must be updated and restarted. Otherwise the connecting side will hang and not time out.
* |break_config| :commit:`2bfcfa5`: first outlet in ``global.logging`` is now used for logging meta-errors, for example problems encountered when writing to other outlets.
* |feature| :issue:`10`: ``zrepl control status`` subcommand
@@ -34,6 +41,7 @@ Developers should consult the git commit log or GitHub issue tracker.
* |bugfix| :issue:`8` and :issue:`56`: ``ssh+stdinserver`` transport properly reaps SSH child processes
* |bugfix| :commit:`cef63ac`: ``human`` format now prints non-string values correctly
* |bugfix| :issue:`26`: slow TCP outlets no longer block the daemon
* |docs| :issue:`64`: tutorial: document ``known_host`` file entry
0.0.2
-----
+6 -2
View File
@@ -85,8 +85,9 @@ Connect Mode
user: root
port: 22
identity_file: /etc/zrepl/ssh/identity
options: # optional
options: # optional, default [], `-o` arguments passed to ssh
- "Compression=on"
dial_timeout: # optional, default 10s, max time.Duration until initial handshake is completed
The connecting zrepl daemon
@@ -101,10 +102,13 @@ The connecting zrepl daemon
#. 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.
#. 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.
The ``known_hosts`` file used by the ssh command must contain an entry for the serving host, e.g., ``app-srv.example.com`` in the example above.
.. NOTE::
The environment variables of the underlying SSH process are cleared. ``$SSH_AUTH_SOCK`` will not be available.
+1 -1
View File
@@ -16,7 +16,7 @@ zrepl - ZFS replication
Getting started
~~~~~~~~~~~~~~~
The :ref:`5 minute tutorial setup <tutorial>` gives you a first impression.
The :ref:`10 minutes tutorial setup <tutorial>` gives you a first impression.
Main Features
~~~~~~~~~~~~~
+9 -2
View File
@@ -60,7 +60,7 @@ Follow the :ref:`OS-specific installation instructions <installation>` and come
Configure ``backup-srv``
------------------------
We define a **pull job** named ``pull_app-srv`` in the |mainconfig|: ::
We define a **pull job** named ``pull_app-srv`` in the |mainconfig| on host ``backup-srv``: ::
jobs:
- name: pull_app-srv
@@ -94,6 +94,13 @@ It uses the private key specified at ``connect.identity_file`` which we still ne
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.
zrepl uses ssh's default ``known_hosts`` file, which must contain a host identification entry for ``app-srv.example.com``.
If that entry does not already exist, we need to generate it.
Run the following command, compare the host fingerprints, and confirm with yes if they match.
You will not be able to get a shell with the identity file we just generated, which is fine. ::
ssh -i /etc/zrepl/ssh/identity root@app-srv.example.com
Learn more about :ref:`transport-ssh+stdinserver` transport and the :ref:`pull job <job-pull>` format.
.. _tutorial-configure-app-srv:
@@ -101,7 +108,7 @@ Learn more about :ref:`transport-ssh+stdinserver` transport and the :ref:`pull j
Configure ``app-srv``
---------------------
We define a corresponding **source job** named ``pull_backup`` in the |mainconfig|: ::
We define a corresponding **source job** named ``pull_backup`` in the |mainconfig| on host ``app-srv``: ::
jobs:
- name: pull_backup
+2 -2
View File
@@ -2,7 +2,7 @@
package rpc
import "fmt"
import "strconv"
const _DataType_name = "DataTypeNoneDataTypeControlDataTypeMarshaledJSONDataTypeOctets"
@@ -11,7 +11,7 @@ var _DataType_index = [...]uint8{0, 12, 27, 48, 62}
func (i DataType) String() string {
i -= 1
if i >= DataType(len(_DataType_index)-1) {
return fmt.Sprintf("DataType(%d)", i+1)
return "DataType(" + strconv.FormatInt(int64(i+1), 10) + ")"
}
return _DataType_name[_DataType_index[i]:_DataType_index[i+1]]
}
+2 -3
View File
@@ -2,7 +2,7 @@
package rpc
import "fmt"
import "strconv"
const (
_FrameType_name_0 = "FrameTypeHeaderFrameTypeDataFrameTypeTrailer"
@@ -11,7 +11,6 @@ const (
var (
_FrameType_index_0 = [...]uint8{0, 15, 28, 44}
_FrameType_index_1 = [...]uint8{0, 12}
)
func (i FrameType) String() string {
@@ -22,6 +21,6 @@ func (i FrameType) String() string {
case i == 255:
return _FrameType_name_1
default:
return fmt.Sprintf("FrameType(%d)", i)
return "FrameType(" + strconv.FormatInt(int64(i), 10) + ")"
}
}
+2 -2
View File
@@ -2,7 +2,7 @@
package rpc
import "fmt"
import "strconv"
const _Status_name = "StatusOKStatusRequestErrorStatusServerErrorStatusError"
@@ -11,7 +11,7 @@ var _Status_index = [...]uint8{0, 8, 26, 43, 54}
func (i Status) String() string {
i -= 1
if i >= Status(len(_Status_index)-1) {
return fmt.Sprintf("Status(%d)", i+1)
return "Status(" + strconv.FormatInt(int64(i+1), 10) + ")"
}
return _Status_name[_Status_index[i]:_Status_index[i+1]]
}
-17
View File
@@ -1,17 +0,0 @@
package util
type Semaphore struct {
c chan struct{}
}
func NewSemaphore(cap int) Semaphore {
return Semaphore{make(chan struct{}, cap)}
}
func (s Semaphore) Down() {
s.c <- struct{}{}
}
func (s Semaphore) Up() {
<-s.c
}
+2 -2
View File
@@ -2,7 +2,7 @@
package zfs
import "fmt"
import "strconv"
const _Conflict_name = "ConflictIncrementalConflictAllRightConflictNoCommonAncestorConflictDiverged"
@@ -10,7 +10,7 @@ var _Conflict_index = [...]uint8{0, 19, 35, 59, 75}
func (i Conflict) String() string {
if i < 0 || i >= Conflict(len(_Conflict_index)-1) {
return fmt.Sprintf("Conflict(%d)", i)
return "Conflict(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _Conflict_name[_Conflict_index[i]:_Conflict_index[i+1]]
}