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" branch = "master"
name = "github.com/problame/go-netssh" name = "github.com/problame/go-netssh"
packages = ["."] packages = ["."]
revision = "ffa145d2506e222977205e7666a9722d6b9959ac" revision = "53a2e445f8ace7ec678f2d8cdd9c1428dfef4562"
[[projects]] [[projects]]
branch = "master" branch = "master"
+1
View File
@@ -11,6 +11,7 @@ RUN /tmp/lazy.sh devsetup
# prepare volume mount of git checkout to /zrepl # prepare volume mount of git checkout to /zrepl
RUN mkdir -p /go/src/github.com/zrepl/zrepl RUN mkdir -p /go/src/github.com/zrepl/zrepl
RUN chmod -R 0777 /go RUN chmod -R 0777 /go
RUN mkdir -p /.cache && chmod -R 0777 /.cache
WORKDIR /go/src/github.com/zrepl/zrepl WORKDIR /go/src/github.com/zrepl/zrepl
+16 -30
View File
@@ -5,7 +5,6 @@ import (
"fmt" "fmt"
"github.com/zrepl/zrepl/zfs" "github.com/zrepl/zrepl/zfs"
"sort" "sort"
"sync"
"time" "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 // don't cache the result from previous run in case the user added
// a new dataset in the meantime // a new dataset in the meantime
fss, stop := a.filterFilesystems() ds, stop := a.filterFilesystems()
if stop { if stop {
return return
} }
a.task.Log().Info("beginning parallel snapshots")
// TODO channel programs -> allow a little jitter? // TODO channel programs -> allow a little jitter?
var wg sync.WaitGroup for _, d := range ds {
for fsi := range fss { suffix := time.Now().In(time.UTC).Format("20060102_150405_000")
wg.Add(1) snapname := fmt.Sprintf("%s%s", a.Prefix, suffix)
go func(fs *zfs.DatasetPath) {
defer wg.Done()
suffix := time.Now().In(time.UTC).Format("20060102_150405_000") l := a.task.Log().WithField(logFSField, d.ToString()).
snapname := fmt.Sprintf("%s%s", a.Prefix, suffix) WithField("snapname", snapname)
l := a.task.Log().WithField(logFSField, fs.ToString()). l.Info("create snapshot")
WithField("snapname", snapname) err := zfs.ZFSSnapshot(d, snapname, false)
if err != nil {
a.task.Log().WithError(err).Error("cannot create snapshot")
}
l.Info("create snapshot") l.Info("create corresponding bookmark")
err := zfs.ZFSSnapshot(fs, snapname, false) err = zfs.ZFSBookmark(d, snapname, snapname)
if err != nil { if err != nil {
l.WithError(err).Error("cannot create snapshot") a.task.Log().WithError(err).Error("cannot create bookmark")
return }
}
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 { select {
case didSnaps <- struct{}{}: case didSnaps <- struct{}{}:
default: default:
+20 -2
View File
@@ -9,6 +9,7 @@ import (
"github.com/mitchellh/mapstructure" "github.com/mitchellh/mapstructure"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/problame/go-netssh" "github.com/problame/go-netssh"
"time"
) )
type SSHStdinserverConnecter struct { type SSHStdinserverConnecter struct {
@@ -19,6 +20,8 @@ type SSHStdinserverConnecter struct {
TransportOpenCommand []string `mapstructure:"transport_open_command"` TransportOpenCommand []string `mapstructure:"transport_open_command"`
SSHCommand string `mapstructure:"ssh_command"` SSHCommand string `mapstructure:"ssh_command"`
Options []string Options []string
DialTimeout string `mapstructure:"dial_timeout"`
dialTimeout time.Duration
} }
func parseSSHStdinserverConnecter(i map[string]interface{}) (c *SSHStdinserverConnecter, err error) { 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 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 // TODO assert fields are filled
return return
@@ -38,9 +50,15 @@ func (c *SSHStdinserverConnecter) Connect() (rwc io.ReadWriteCloser, err error)
var endpoint netssh.Endpoint var endpoint netssh.Endpoint
if err = copier.Copy(&endpoint, c); err != nil { 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) err = errors.WithStack(err)
return return
} }
+40 -59
View File
@@ -3,9 +3,7 @@ package cmd
import ( import (
"context" "context"
"fmt" "fmt"
"github.com/zrepl/zrepl/util"
"github.com/zrepl/zrepl/zfs" "github.com/zrepl/zrepl/zfs"
"sync"
"time" "time"
) )
@@ -25,8 +23,9 @@ type PruneResult struct {
Remove []zfs.FilesystemVersion Remove []zfs.FilesystemVersion
} }
// FIXME must not call p.task.Enter because it runs in parallel
func (p *Pruner) filterFilesystems() (filesystems []*zfs.DatasetPath, stop bool) { func (p *Pruner) filterFilesystems() (filesystems []*zfs.DatasetPath, stop bool) {
p.task.Enter("filter_fs")
defer p.task.Finish()
filesystems, err := zfs.ZFSListMapping(p.DatasetFilter) filesystems, err := zfs.ZFSListMapping(p.DatasetFilter)
if err != nil { if err != nil {
p.task.Log().WithError(err).Error("error applying filesystem filter") 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 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) { 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()) log := p.task.Log().WithField(logFSField, fs.ToString())
filter := NewPrefixFilter(p.SnapshotPrefix) filter := NewPrefixFilter(p.SnapshotPrefix)
@@ -56,8 +56,9 @@ func (p *Pruner) filterVersions(fs *zfs.DatasetPath) (fsversions []zfs.Filesyste
return fsversions, false return fsversions, false
} }
// FIXME must not call p.task.Enter because it runs in parallel func (p *Pruner) pruneFilesystem(fs *zfs.DatasetPath) (r PruneResult, valid bool) {
func (p *Pruner) pruneFilesystem(fs *zfs.DatasetPath, destroySemaphore util.Semaphore) (r PruneResult, valid bool) { p.task.Enter("prune_fs")
defer p.task.Finish()
log := p.task.Log().WithField(logFSField, fs.ToString()) log := p.task.Log().WithField(logFSField, fs.ToString())
fsversions, stop := p.filterVersions(fs) fsversions, stop := p.filterVersions(fs)
@@ -65,7 +66,9 @@ func (p *Pruner) pruneFilesystem(fs *zfs.DatasetPath, destroySemaphore util.Sema
return return
} }
p.task.Enter("prune_policy")
keep, remove, err := p.PrunePolicy.Prune(fs, fsversions) keep, remove, err := p.PrunePolicy.Prune(fs, fsversions)
p.task.Finish()
if err != nil { if err != nil {
log.WithError(err).Error("error evaluating prune policy") log.WithError(err).Error("error evaluating prune policy")
return return
@@ -78,37 +81,33 @@ func (p *Pruner) pruneFilesystem(fs *zfs.DatasetPath, destroySemaphore util.Sema
r = PruneResult{fs, fsversions, keep, remove} r = PruneResult{fs, fsversions, keep, remove}
var wg sync.WaitGroup makeFields := func(v zfs.FilesystemVersion) (fields map[string]interface{}) {
for v := range remove { fields = make(map[string]interface{})
wg.Add(1) fields["version"] = v.ToAbsPath(fs)
go func(v zfs.FilesystemVersion) { timeSince := v.Creation.Sub(p.Now)
defer wg.Done() fields["age_ns"] = timeSince
// log fields const day time.Duration = 24 * time.Hour
fields := make(map[string]interface{}) days := timeSince / day
fields["version"] = v.ToAbsPath(fs) remainder := timeSince % day
timeSince := v.Creation.Sub(p.Now) fields["age_str"] = fmt.Sprintf("%dd%s", days, remainder)
fields["age_ns"] = timeSince return
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])
} }
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 return r, true
} }
@@ -125,31 +124,13 @@ func (p *Pruner) Run(ctx context.Context) (r []PruneResult, err error) {
return 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)) 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 return
+9 -1
View File
@@ -1,6 +1,7 @@
.. |break_config| replace:: **[BREAK]** .. |break_config| replace:: **[BREAK]**
.. |break| replace:: **[BREAK]** .. |break| replace:: **[BREAK]**
.. |bugfix| replace:: [BUG] .. |bugfix| replace:: [BUG]
.. |docs| replace:: [DOCS]
.. |feature| replace:: [FEATURE] .. |feature| replace:: [FEATURE]
Changelog 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>`. * Make sure to understand the meaning bookmarks have for :ref:`maximum replication downtime <replication-downtime>`.
* Example: :sampleconf:`pullbackup/productionhost.yml` * 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. * |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 * |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| :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| :commit:`cef63ac`: ``human`` format now prints non-string values correctly
* |bugfix| :issue:`26`: slow TCP outlets no longer block the daemon * |bugfix| :issue:`26`: slow TCP outlets no longer block the daemon
* |docs| :issue:`64`: tutorial: document ``known_host`` file entry
0.0.2 0.0.2
----- -----
+6 -2
View File
@@ -85,8 +85,9 @@ Connect Mode
user: root user: root
port: 22 port: 22
identity_file: /etc/zrepl/ssh/identity identity_file: /etc/zrepl/ssh/identity
options: # optional options: # optional, default [], `-o` arguments passed to ssh
- "Compression=on" - "Compression=on"
dial_timeout: # optional, default 10s, max time.Duration until initial handshake is completed
The connecting zrepl daemon The connecting zrepl daemon
@@ -101,10 +102,13 @@ The connecting zrepl daemon
#. The remote user, host and port correspond to those configured. #. 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``. #. 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. 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:: .. NOTE::
The environment variables of the underlying SSH process are cleared. ``$SSH_AUTH_SOCK`` will not be available. 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 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 Main Features
~~~~~~~~~~~~~ ~~~~~~~~~~~~~
+9 -2
View File
@@ -60,7 +60,7 @@ Follow the :ref:`OS-specific installation instructions <installation>` and come
Configure ``backup-srv`` 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: jobs:
- name: pull_app-srv - 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. 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. 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. Learn more about :ref:`transport-ssh+stdinserver` transport and the :ref:`pull job <job-pull>` format.
.. _tutorial-configure-app-srv: .. _tutorial-configure-app-srv:
@@ -101,7 +108,7 @@ Learn more about :ref:`transport-ssh+stdinserver` transport and the :ref:`pull j
Configure ``app-srv`` 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: jobs:
- name: pull_backup - name: pull_backup
+2 -2
View File
@@ -2,7 +2,7 @@
package rpc package rpc
import "fmt" import "strconv"
const _DataType_name = "DataTypeNoneDataTypeControlDataTypeMarshaledJSONDataTypeOctets" const _DataType_name = "DataTypeNoneDataTypeControlDataTypeMarshaledJSONDataTypeOctets"
@@ -11,7 +11,7 @@ var _DataType_index = [...]uint8{0, 12, 27, 48, 62}
func (i DataType) String() string { func (i DataType) String() string {
i -= 1 i -= 1
if i >= DataType(len(_DataType_index)-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]] return _DataType_name[_DataType_index[i]:_DataType_index[i+1]]
} }
+2 -3
View File
@@ -2,7 +2,7 @@
package rpc package rpc
import "fmt" import "strconv"
const ( const (
_FrameType_name_0 = "FrameTypeHeaderFrameTypeDataFrameTypeTrailer" _FrameType_name_0 = "FrameTypeHeaderFrameTypeDataFrameTypeTrailer"
@@ -11,7 +11,6 @@ const (
var ( var (
_FrameType_index_0 = [...]uint8{0, 15, 28, 44} _FrameType_index_0 = [...]uint8{0, 15, 28, 44}
_FrameType_index_1 = [...]uint8{0, 12}
) )
func (i FrameType) String() string { func (i FrameType) String() string {
@@ -22,6 +21,6 @@ func (i FrameType) String() string {
case i == 255: case i == 255:
return _FrameType_name_1 return _FrameType_name_1
default: default:
return fmt.Sprintf("FrameType(%d)", i) return "FrameType(" + strconv.FormatInt(int64(i), 10) + ")"
} }
} }
+2 -2
View File
@@ -2,7 +2,7 @@
package rpc package rpc
import "fmt" import "strconv"
const _Status_name = "StatusOKStatusRequestErrorStatusServerErrorStatusError" const _Status_name = "StatusOKStatusRequestErrorStatusServerErrorStatusError"
@@ -11,7 +11,7 @@ var _Status_index = [...]uint8{0, 8, 26, 43, 54}
func (i Status) String() string { func (i Status) String() string {
i -= 1 i -= 1
if i >= Status(len(_Status_index)-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]] 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 package zfs
import "fmt" import "strconv"
const _Conflict_name = "ConflictIncrementalConflictAllRightConflictNoCommonAncestorConflictDiverged" const _Conflict_name = "ConflictIncrementalConflictAllRightConflictNoCommonAncestorConflictDiverged"
@@ -10,7 +10,7 @@ var _Conflict_index = [...]uint8{0, 19, 35, 59, 75}
func (i Conflict) String() string { func (i Conflict) String() string {
if i < 0 || i >= Conflict(len(_Conflict_index)-1) { 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]] return _Conflict_name[_Conflict_index[i]:_Conflict_index[i+1]]
} }