Compare commits

...

262 Commits

Author SHA1 Message Date
Christian Schwarz 42ffca09db endpoint: Receiver.Receive: save a peek of recv stream to a temporary dataset if placeholder + encryption recv -F error is detected 2020-04-25 10:43:59 +02:00
Christian Schwarz 05a39eaddf endpont: Receiver.Receive: error message explaining problem with placeholders and encryption 2020-04-25 10:40:53 +02:00
Christian Schwarz 347d0f1aa2 endpoint: Receiver.Receive: better logging + placeholder state error early exit 2020-04-25 10:40:42 +02:00
Christian Schwarz 5aaac49382 rpc + zfs: drop zfs.StreamCopier, use io.ReadCloser instead 2020-04-25 10:40:05 +02:00
Christian Schwarz c1c9d99a6f fixup "replication/driver: enforce ordering during initial replication in order to support encrypted send": correctly propagate non-inital parent failures 2020-04-25 10:39:50 +02:00
Christian Schwarz d59b64df86 replication/driver: enforce ordering during initial replication in order to support encrypted send
fixes #277
2020-04-25 10:06:18 +02:00
Christian Schwarz 1540a478b0 replication/driver: fs.debug() helper that automatically prefixes with fs name 2020-04-25 10:06:18 +02:00
Christian Schwarz 15715b1e2a replication/driver: rename receiver variable (fs *fs) to (f *fs) 2020-04-25 10:06:18 +02:00
Christian Schwarz afab031b7d replication/driver: envconst for experimental parallel replication
refs #140
refs #302
2020-04-25 10:06:18 +02:00
Christian Schwarz d52acfe10b endpoint: log %#v recv options 2020-04-25 10:06:18 +02:00
Christian Schwarz 07ae6f2aad build: Makefile: GO_EXTRA_BUILDFLAGS 2020-04-25 10:06:18 +02:00
Christian Schwarz 7b34d6cba5 Merge pull request #311 from zrepl/problame/zfscmd-fixes-backported-from-307-tracing-wip
zfscmd & zfs fixes created during WIP on #307
2020-04-21 14:24:42 +02:00
Christian Schwarz 70f9c6482f zfs: context propagation to ZFSListFilesystemVersions
fixup of 9568e46f05
2020-04-21 14:10:53 +02:00
Christian Schwarz aed6149c8c zfscmd: fix crash in zfscmd_prometheus.go due to incorrectly extracted ProcessState
fixup of 96e188d7c4
refs #196
refs #301

panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x10 pc=0x9a472a]

goroutine 15826 [running]:
os.(*ProcessState).systemTime(...)
        /home/cs/go1.13/src/os/exec_unix.go:98
os.(*ProcessState).SystemTime(...)
        /home/cs/go1.13/src/os/exec.go:141
github.com/zrepl/zrepl/zfs/zfscmd.waitPostPrometheus(0xc000c04800, 0xe21ce0, 0xc000068270, 0xbf9f80d88107e861, 0x19bae710e6, 0x13a8b60)
        /home/cs/zrepl/zrepl/zfs/zfscmd/zfscmd_prometheus.go:69 +0x22a
github.com/zrepl/zrepl/zfs/zfscmd.(*Cmd).waitPost(0xc000c04800, 0xe21ce0, 0xc000068270)
        /home/cs/zrepl/zrepl/zfs/zfscmd/zfscmd.go:155 +0x18a
github.com/zrepl/zrepl/zfs/zfscmd.(*Cmd).CombinedOutput(0xc000c04800, 0xc0004b8270, 0xd02eea, 0x3, 0xc0001f6c40, 0x3)
        /home/cs/zrepl/zrepl/zfs/zfscmd/zfscmd.go:40 +0xb3
github.com/zrepl/zrepl/zfs.ZFSRelease(0xe36aa0, 0xc0004b8270, 0xc0009a3a40, 0x13, 0xc0004a5d00, 0x1, 0x1, 0xed62eb221, 0x13a8b60)
        /home/cs/zrepl/zrepl/zfs/holds.go:102 +0x2a7
github.com/zrepl/zrepl/endpoint.ReleaseStep(0xe36aa0, 0xc0004b8270, 0xc0004befc0, 0xe, 0xd08482, 0x8, 0xc0001cb02f, 0x2, 0x1eeea3bff89dc90b, 0x134d6, ...)
        /home/cs/zrepl/zrepl/endpoint/endpoint_zfs_abstraction_step.go:130 +0x367
github.com/zrepl/zrepl/endpoint.(*Sender).SendCompleted.func2(0xc000459190, 0xc000390e30, 0xc00041fd80, 0xc0004befc0, 0xe, 0xd08482, 0x8, 0xc0001cb02f, 0x2, 0x1eeea3bff89dc90b, ...)
        /home/cs/zrepl/zrepl/endpoint/endpoint.go:419 +0x1c3
created by github.com/zrepl/zrepl/endpoint.(*Sender).SendCompleted
        /home/cs/zrepl/zrepl/endpoint/endpoint.go:413 +0x776
2020-04-21 14:10:25 +02:00
Christian Schwarz 0834a184b8 zfscmd: do not do duplicate waitPre callbacks
it just makes sense that if we only dispatch one waitPost, we should
also only dispatch one waitPre
2020-04-21 14:10:18 +02:00
Christian Schwarz f61295f76c build: Makefile: zsh completions (fixup 0920a40751)
refs #308
fixes #309
2020-04-18 21:36:48 +02:00
Christian Schwarz 0920a40751 reorganize shell completion generator command + support zsh
fixes #308
2020-04-18 19:23:04 +02:00
Christian Schwarz e0b5bd75f8 endpoint: refactor, fix stale holds on initial replication failure, zfs-abstractions subcmd, more efficient ZFS queries
The motivation for this recatoring are based on two independent issues:

- @JMoVS found that the changes merged as part of #259 slowed his OS X
  based installation down significantly.
  Analysis of the zfs command logging introduced in #296 showed that
  `zfs holds` took most of the execution time, and they pointed out
  that not all of those `zfs holds` invocations were actually necessary.
  I.e.: zrepl was inefficient about retrieving information from ZFS.

- @InsanePrawn found that failures on initial replication would lead
  to step holds accumulating on the sending side, i.e. they would never
  be cleaned up in the HintMostRecentCommonAncestor RPC handler.
  That was because we only sent that RPC if there was a most recent
  common ancestor detected during replication planning.
  @InsanePrawn prototyped an implementation of a `zrepl zfs-abstractions release`
  command to mitigate the situation.
  As part of that development work and back-and-forth with @problame,
  it became evident that the abstractions that #259 built on top of
  zfs in package endpoint (step holds, replication cursor,
  last-received-hold), were not well-represented for re-use in the
  `zrepl zfs-abstractions release` subocommand prototype.

This commit refactors package endpoint to address both of these issues:

- endpoint abstractions now share an interface `Abstraction` that, among
  other things, provides a uniform `Destroy()` method.
  However, that method should not be destroyed directly but instead
  the package-level `BatchDestroy` function should be used in order
  to allow for a migration to zfs channel programs in the future.

- endpoint now has a query facitilty (`ListAbstractions`) which is
  used to find on-disk
    - step holds and bookmarks
    - replication cursors (v1, v2)
    - last-received-holds
  By describing the query in a struct, we can centralized the retrieval
  of information via the ZFS CLI and only have to be clever once.
  We are "clever" in the following ways:
  - When asking for hold-based abstractions, we only run `zfs holds` on
    snapshot that have `userrefs` > 0
    - To support this functionality, add field `UserRefs` to zfs.FilesystemVersion
      and retrieve it anywhere we retrieve zfs.FilesystemVersion from ZFS.
  - When asking only for bookmark-based abstractions, we only run
    `zfs list -t bookmark`, not with snapshots.
  - Currently unused (except for CLI) per-filesystem concurrent lookup
  - Option to only include abstractions with CreateTXG in a specified range

- refactor `endpoint`'s various ZFS info  retrieval methods to use
  `ListAbstractions`

- rename the `zrepl holds list` command to `zrepl zfs-abstractions list`
- make `zrepl zfs-abstractions list` consume endpoint.ListAbstractions

- Add a `ListStale` method which, given a query template,
  lists stale holds and bookmarks.
  - it uses replication cursor has different modes
- the new `zrepl zfs-abstractions release-{all,stale}` commands can be used
  to remove abstractions of package endpoint

- Adjust HintMostRecentCommonAncestor RPC for stale-holds cleanup:
    - send it also if no most recent common ancestor exists between sender and receiver
    - have the sender clean up its abstractions when it receives the RPC
      with no most recent common ancestor, using `ListStale`
    - Due to changed semantics, bump the protocol version.

- Adjust HintMostRecentCommonAncestor RPC for performance problems
  encountered by @JMoVS
    - by default, per (job,fs)-combination, only consider cleaning
      step holds in the createtxg range
      `[last replication cursor,conservatively-estimated-receive-side-version)`
    - this behavior ensures resumability at cost proportional to the
      time that replication was donw
    - however, as explained in a comment, we might leak holds if
      the zrepl daemon stops running
    - that  trade-off is acceptable because in the presumably rare
      this might happen the user has two tools at their hand:
    - Tool 1: run `zrepl zfs-abstractions release-stale`
    - Tool 2: use env var `ZREPL_ENDPOINT_SENDER_HINT_MOST_RECENT_STEP_HOLD_CLEANUP_MODE`
      to adjust the lower bound of the createtxg range (search for it in the code).
      The env var can also be used to disable hold-cleanup on the
      send-side entirely.

supersedes closes #293
supersedes closes #282
fixes #280
fixes #278

Additionaly, we fixed a couple of bugs:

- zfs: fix half-nil error reporting of dataset-does-not-exist for ZFSListChan and ZFSBookmark

- endpoint: Sender's `HintMostRecentCommonAncestor` handler would not
  check whether access to the specified filesystem was allowed.
2020-04-18 12:26:03 +02:00
Christian Schwarz 96e188d7c4 zfscmd: fix nil deref in waitPostLogging when command was killed
fixes #301
2020-04-08 00:26:56 +02:00
Christian Schwarz 1336c91865 zfs: introduce pkg zfs/zfscmd for command logging, status, prometheus metrics
refs #196
2020-04-05 20:47:25 +02:00
InsanePrawn 9568e46f05 zfs: use exec.CommandContext everywhere
Co-authored-by: InsanePrawn <insane.prawny@gmail.com>
2020-03-27 13:08:43 +01:00
Christian Schwarz 3187129672 build: go1.14 + address tlsconf deprecation notice
fixes #286
2020-03-27 12:40:57 +01:00
InsanePrawn 44bd354eae Spellcheck all files
Signed-off-by: InsanePrawn <insane.prawny@gmail.com>
2020-02-24 16:06:09 +01:00
InsanePrawn 94caf8b8db endpoint: fix typos in jobid.go
Signed-off-by: InsanePrawn <insane.prawny@gmail.com>
2020-02-23 11:17:45 +01:00
Christian Schwarz 3ff1966cab docs/installation: use && for early exit if build-in-docker step fails 2020-02-17 18:02:04 +01:00
Christian Schwarz a3842155c5 zrepl test filesystems: support snap job type 2020-02-17 18:02:04 +01:00
Christian Schwarz 02b3b4f80c fix some typos 2020-02-17 18:02:04 +01:00
Christian Schwarz 0882290595 README update
- donation links
- package overview
2020-02-17 18:02:04 +01:00
Christian Schwarz b8d9f4ba92 docs: supporters: update 2020-02-14 22:00:13 +01:00
Christian Schwarz 1462c5caa5 zfs: fix batch destroy panic if all snaps are undestroyable
See https://github.com/zrepl/zrepl/pull/259#issuecomment-585334023

panic: runtime error: index out of range [0] with length 0
goroutine 14 [running]:
github.com/zrepl/zrepl/zfs.tryBatch(0xd6aa20, 0xc0000b8018, 0xc00025e0c0, 0x0, 0x6, 0xd61d80, 0x1280df8, 0xd58920, 0xc000132000)
        zrepl/zfs/versions_destroy.go:129 +0x302
github.com/zrepl/zrepl/zfs.doDestroyBatchedRec(0xd6aa20, 0xc0000b8018, 0xc000578a80, 0x6, 0x6, 0xd61d80, 0x1280df8)
        zrepl/zfs/versions_destroy.go:184 +0x4a5
github.com/zrepl/zrepl/zfs.doDestroyBatched(0xd6aa20, 0xc0000b8018, 0xc000222780, 0x6, 0x8, 0xd61d80, 0x1280df8)
        zrepl/zfs/versions_destroy.go:95 +0xc7
github.com/zrepl/zrepl/zfs.doDestroy(0xd6aa20, 0xc0000b8018, 0xc0005788d0, 0x6, 0x6, 0xd61d80, 0x1280df8)
        zrepl/zfs/versions_destroy.go:82 +0x362
github.com/zrepl/zrepl/zfs.ZFSDestroyFilesystemVersions(...)
        zrepl/zfs/versions_destroy.go:41
github.com/zrepl/zrepl/endpoint.doDestroySnapshots(0xd6aaa0, 0xc0004412c0, 0xc00057ca00, 0xc0005785a0, 0x6, 0x6, 0xb68940, 0xc5df01, 0xc000150a80)
        zrepl/endpoint/endpoint.go:785 +0x388
github.com/zrepl/zrepl/endpoint.(*Receiver).DestroySnapshots(0xc000127500, 0xd6aaa0, 0xc0004412c0, 0xc0002ca280, 0xc000150880, 0xd73ca0, 0xc00057c960)
        zrepl/endpoint/endpoint.go:751 +0xdb
github.com/zrepl/zrepl/daemon/pruner.doOneAttemptExec(0xc000429980, 0xc000429958, 0xc0001cb180)
        zrepl/daemon/pruner/pruner.go:531 +0x51f
github.com/zrepl/zrepl/daemon/pruner.doOneAttempt(0xc000429980, 0xc000429958)
        zrepl/daemon/pruner/pruner.go:486 +0x1064
github.com/zrepl/zrepl/daemon/pruner.(*Pruner).prune(0xc00011e280, 0xd6aaa0, 0xc0004412c0, 0x7f4906fff7e8, 0xc000127500, 0x7f4906fff738, 0xc0001324e0, 0xc000064420, 0x1, 0x1, ...)
        zrepl/daemon/pruner/pruner.go:214 +0x53
github.com/zrepl/zrepl/daemon/pruner.(*Pruner).Prune(...)
        zrepl/daemon/pruner/pruner.go:200
github.com/zrepl/zrepl/daemon/job.(*ActiveSide).do(0xc000268000, 0xd6a9e0, 0xc0002223c0)
        zrepl/daemon/job/active.go:482 +0x906
github.com/zrepl/zrepl/daemon/job.(*ActiveSide).Run(0xc000268000, 0xd6aaa0, 0xc000127080)
        zrepl/daemon/job/active.go:404 +0x289
github.com/zrepl/zrepl/daemon.(*jobs).start.func1(0xc000032200, 0xd73ca0, 0xc00000f2e0, 0xd6efa0, 0xc000268000, 0xd6aaa0, 0xc000126c90)
        zrepl/daemon/daemon.go:220 +0x121
created by github.com/zrepl/zrepl/daemon.(*jobs).start
        zrepl/daemon/daemon.go:216 +0x52e
2020-02-14 22:00:13 +01:00
Christian Schwarz 58c08c855f new features: {resumable,encrypted,hold-protected} send-recv, last-received-hold
- **Resumable Send & Recv Support**
  No knobs required, automatically used where supported.
- **Hold-Protected Send & Recv**
  Automatic ZFS holds to ensure that we can always resume a replication step.
- **Encrypted Send & Recv Support** for OpenZFS native encryption.
  Configurable at the job level, i.e., for all filesystems a job is responsible for.
- **Receive-side hold on last received dataset**
  The counterpart to the replication cursor bookmark on the send-side.
  Ensures that incremental replication will always be possible between a sender and receiver.

Design Doc
----------

`replication/design.md` doc describes how we use ZFS holds and bookmarks to ensure that a single replication step is always resumable.

The replication algorithm described in the design doc introduces the notion of job IDs (please read the details on this design doc).
We reuse the job names for job IDs and use `JobID` type to ensure that a job name can be embedded into hold tags, bookmark names, etc.
This might BREAK CONFIG on upgrade.

Protocol Version Bump
---------------------

This commit makes backwards-incompatible changes to the replication/pdu protobufs.
Thus, bump the version number used in the protocol handshake.

Replication Cursor Format Change
--------------------------------

The new replication cursor bookmark format is: `#zrepl_CURSOR_G_${this.GUID}_J_${jobid}`
Including the GUID enables transaction-safe moving-forward of the cursor.
Including the job id enables that multiple sending jobs can send the same filesystem without interfering.
The `zrepl migrate replication-cursor:v1-v2` subcommand can be used to safely destroy old-format cursors once zrepl has created new-format cursors.

Changes in This Commit
----------------------

- package zfs
  - infrastructure for holds
  - infrastructure for resume token decoding
  - implement a variant of OpenZFS's `entity_namecheck` and use it for validation in new code
  - ZFSSendArgs to specify a ZFS send operation
    - validation code protects against malicious resume tokens by checking that the token encodes the same send parameters that the send-side would use if no resume token were available (i.e. same filesystem, `fromguid`, `toguid`)
  - RecvOptions support for `recv -s` flag
  - convert a bunch of ZFS operations to be idempotent
    - achieved through more differentiated error message scraping / additional pre-/post-checks

- package replication/pdu
  - add field for encryption to send request messages
  - add fields for resume handling to send & recv request messages
  - receive requests now contain `FilesystemVersion To` in addition to the filesystem into which the stream should be `recv`d into
    - can use `zfs recv $root_fs/$client_id/path/to/dataset@${To.Name}`, which enables additional validation after recv (i.e. whether `To.Guid` matched what we received in the stream)
    - used to set `last-received-hold`
- package replication/logic
  - introduce `PlannerPolicy` struct, currently only used to configure whether encrypted sends should be requested from the sender
  - integrate encryption and resume token support into `Step` struct

- package endpoint
  - move the concepts that endpoint builds on top of ZFS to a single file `endpoint/endpoint_zfs.go`
    - step-holds + step-bookmarks
    - last-received-hold
    - new replication cursor + old replication cursor compat code
  - adjust `endpoint/endpoint.go` handlers for
    - encryption
    - resumability
    - new replication cursor
    - last-received-hold

- client subcommand `zrepl holds list`: list all holds and hold-like bookmarks that zrepl thinks belong to it
- client subcommand `zrepl migrate replication-cursor:v1-v2`
2020-02-14 22:00:13 +01:00
Christian Schwarz 9a4763ceee client/status: notify user if size estimation is imprecise
There's plenty of room for improvement here.
For example, detect if we're past the last step without size estimation
and compute the remaining sum of bytes to be replicated from there on.
2020-02-14 21:42:03 +01:00
Christian Schwarz f8200a6386 replication/driver: regulate per-filesystem step planning through stepQueue
before this patch, we'd have unbounded parallelism for ListFilesystemVersions RPCs
2020-02-14 21:42:03 +01:00
Christian Schwarz e35320f8ee zfs: make StreamCopier wrapper for io.ReadCloser public 2020-02-14 21:42:03 +01:00
Christian Schwarz 5b52e5e331 rpc: fix missing logger context vars in control connection handlers
use ctxInterceptor in gRPC interceptors
also panic if the unimplemented stream interceptor is used
2020-02-14 21:42:03 +01:00
Christian Schwarz 6ebd9f1037 zfs: recv: fix deadlock if streamCopier returns io.EOF
Close the write end of the pipe
* before we start waiting on the error channels
* and after everything from streamCopier has been written to the pipe
2020-02-14 21:42:03 +01:00
Christian Schwarz 99ab16d7be zfs: send: improve error reporting by capturing stderr 2020-02-14 21:42:03 +01:00
Christian Schwarz e675b35cda platformtest: harness: run filter + summary 2020-02-14 21:42:01 +01:00
Christian Schwarz ddd7acec49 platformtest: harness: refactor + support SkipNow 2020-02-14 21:40:48 +01:00
Christian Schwarz e7aa08564b platformtest: harness: fix FailNow (harness wouldn't detect FailNow)
(harness checks for recover() != nil)
2020-02-14 21:40:48 +01:00
Christian Schwarz 93ccdb8024 docs: prune: fix typo 2020-02-14 21:40:48 +01:00
Christian Schwarz 501645f918 docs: filter syntax: reference 'snap' job type 2020-02-14 21:40:48 +01:00
Matthias Freund 4fd9d30c98 build: add linux/armhf target 2020-02-10 12:09:11 +01:00
Ben Woods e2b9c16959 build: fix freebsd/aarch64
by updating golang:sys dependency

This was original reported here:
https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=242456

And originally fixed upstream here:
https://go.googlesource.com/sys/+/33540a1f6037
2020-02-01 12:53:59 +01:00
Matthias Freund 4ba85c40cc daemon/job/build_jobs: fix validateReceivingSidesDoNotOverlap 2020-01-30 10:46:32 +01:00
Matthias Freund cca95f613b client/status: add ETA calculation 2020-01-26 14:45:01 +01:00
Christian Schwarz 5b50a66c6c daemon/snapper: refactor sync-up algorithm + warn about FSes awaiting first sync point
refs https://github.com/zrepl/zrepl/issues/256
2020-01-15 19:20:37 +01:00
Christian Schwarz dd508280f0 docs: tutorial: minor typo + language fixes 2020-01-15 19:12:09 +01:00
Christian Schwarz 2927d0ca15 rpc: use grpchelper package, add grpc.KeepaliveEnforcementPolicy, fix 'transport is closing' error
Symptom: zrepl log message:

    rpc error: code = Unavailable desc = transport is closing

Underlying Problem:

* rpc.NewServer was not using grpchelper.NewServer and not setting Server KeepaliveParams by itself
* and even grpchelper.NewServer didn't set a KeepaliveEnforcementPolicy
* However, KeepaliveEnforcementPolicy is necessary if the client keepalive is configured with non-default values
* .. which grpchelper.ClientConn does, and that method is used by rpc.NewClient

* rpc.Client was sending pings
* lacking server-side KeepaliveEnforcementPolicy caused grpc-hard-coded `pingStrikes` counter to go past limit of 2:
  https://github.com/grpc/grpc-go/blob/021bd5734e43b1b11073ea326de29af4de3dfa9b/internal/transport/http2_server.go#L726

How was this debugged:
* GRPC_GO_LOG_VERBOSITY_LEVEL=99 GRPC_GO_LOG_SEVERITY_LEVEL=info PATH=/root/mockpath:$PATH zrepl daemon
* with a patch on grpc package to get more log messages on pingStrikes increases:

    diff --git a/internal/transport/http2_server.go b/internal/transport/http2_server.go
    index 8b04b039..f68f55ea 100644
    --- a/internal/transport/http2_server.go
    +++ b/internal/transport/http2_server.go
    @@ -214,6 +214,7 @@ func newHTTP2Server(conn net.Conn, config *ServerConfig) (_ ServerTransport, err
            if kep.MinTime == 0 {
                    kep.MinTime = defaultKeepalivePolicyMinTime
            }
    +       errorf("effective keepalive enforcement policy: %#v", kep)
            done := make(chan struct{})
            t := &http2Server{
                    ctx:               context.Background(),
    @@ -696,6 +697,7 @@ func (t *http2Server) handlePing(f *http2.PingFrame) {
            t.controlBuf.put(pingAck)

            now := time.Now()
    +       errorf("transport:ping handlePing, last ping %s ago", now.Sub(t.lastPingAt))
            defer func() {
                    t.lastPingAt = now
            }()
    @@ -713,11 +715,13 @@ func (t *http2Server) handlePing(f *http2.PingFrame) {
                    // Keepalive shouldn't be active thus, this new ping should
                    // have come after at least defaultPingTimeout.
                    if t.lastPingAt.Add(defaultPingTimeout).After(now) {
    +                       errorf("transport:ping strike ns < 1 && !t.kep.PermitWithoutStream")
                            t.pingStrikes++
                    }
            } else {
                    // Check if keepalive policy is respected.
                    if t.lastPingAt.Add(t.kep.MinTime).After(now) {
    +                       errorf("transport:ping strike !(ns < 1 && !t.kep.PermitWithoutStream) kep.MinTime=%s ns=%d", t.kep.MinTime, ns)
                            t.pingStrikes++
                    }
            }

fixes #181
2020-01-04 21:10:41 +01:00
Juergen Hoetzel d35e2400b2 transport/{TCP,TLS}: optional IP_FREEBIND / IP_BINDANY bind socketops
Allows to bind to an address even if it is not actually (yet or ever)
configured. Fixes #238

Rationale:
https://www.freedesktop.org/wiki/Software/systemd/NetworkTarget/#whatdoesthismeanformeadeveloper
2020-01-04 17:21:48 +01:00
Frans Bergman 47ed599db7 docs: add Void Linux to installation instructions 2019-12-28 12:43:53 +01:00
Christian Schwarz f899f4cbe4 build: go.mod: bump go-netssh and drop go-critic replaces
(go-netssh vendored util/circlog, so the circular dep is gone)

fixes build failure reported by @poetterl-ric

```
  make ZREPL_VERSION=0.2.1 zrepl-bin
    GO111MODULE=on go build -mod=readonly -ldflags "-X github.com/zrepl/zrepl/version.zreplVersion=0.2.1" -o "artifacts/zrepl-linux-amd64"
    go: github.com/problame/go-netssh@v0.0.0-20191026123024-f34099f4f6b1 requires
            github.com/zrepl/zrepl@v0.2.0 requires
            github.com/golangci/lint-1@v0.0.0-20181222135242-d2cdd8c08219: invalid version: git fetch --unshallow -f origin in /builddir/go/pkg/mod/cache/vcs/ca789ff49d608cda239a48837cfeea6e9dcdb2bce20051383910eef46b623a33: exit status 128:
            fatal: git fetch-pack: expected shallow list
```
2019-12-28 12:42:33 +01:00
Juergen Hoetzel b3231d2bed daemon: fix typos in error messages
closes #255
2019-12-11 21:30:48 +01:00
Christian Schwarz c24c327151 build: fix build.Dockerfile + integrate into CircleCI
fixup of 080f2c0616
fixup of 4994b7a9ea
2019-11-28 15:19:46 +01:00
Christian Schwarz 5e17d7ba80 docs: add recent supporters 2019-11-26 00:45:13 +01:00
Christian Schwarz 0261dbfe3d docs: 0.2.1 changelog 2019-11-20 20:16:41 +01:00
Christian Schwarz 4301f741db dist/systemd: remove @privileged from SystemCallFilter + cleanup comments
fixes #237
2019-11-20 18:44:14 +01:00
Christian Schwarz 7e743c74dc docs + samples: adjust ssh 'Compression' arg in examples 2019-11-20 18:19:16 +01:00
Christian Schwarz ad0b055245 daemon/prometheus: fix crash if listener cannot be created
refs #238

  zrepl version=v0.2.0-11-gdc39c81 GOOS=linux GOARCH=amd64 Compiler=gc
 starting daemon
 [pull_source]: starting job
 [_prometheus]: starting job
 [connection_loss_tidyup]: starting job
 [connection_loss_tidyup]: wait for wakeups
 [_control]: starting job
 [_prometheus]: cannot listen err="listen tcp 10.0.0.200:9091: bind: cannot assign requested add
 [_prometheus]: job exited
 panic: runtime error: invalid memory address or nil pointer dereference
         panic: runtime error: invalid memory address or nil pointer dereference
 [signal SIGSEGV: segmentation violation code=0x1 addr=0x28 pc=0x81ea4d]
 goroutine 25 [running]:
 net/http.(*onceCloseListener).close(...)
         /usr/local/go/src/net/http/server.go:3330
 sync.(*Once).doSlow(0xc00018b060, 0xc0000c7bc0)
         /usr/local/go/src/sync/once.go:66 +0xe3
 sync.(*Once).Do(...)
         /usr/local/go/src/sync/once.go:57
 net/http.(*onceCloseListener).Close(0xc00018b050, 0xc0003a6000, 0xe0)
         /usr/local/go/src/net/http/server.go:3326 +0x77
 panic(0xb1d1c0, 0x11d7d90)
         /usr/local/go/src/runtime/panic.go:679 +0x1b2
 net/http.(*onceCloseListener).Accept(0xc00018b050, 0xc000120020, 0xb0fd20, 0x11d7ce0, 0xbee6e0)
         <autogenerated>:1 +0x32
 net/http.(*Server).Serve(0xc0003a6000, 0x0, 0x0, 0x0, 0x0)
         /usr/local/go/src/net/http/server.go:2896 +0x286
 net/http.Serve(...)
         /usr/local/go/src/net/http/server.go:2468
 github.com/zrepl/zrepl/daemon.(*prometheusJob).Run(0xc000109940, 0xd19ec0, 0xc00018a930)
         /go/src/github.com/zrepl/zrepl/daemon/prometheus.go:75 +0x23e
 github.com/zrepl/zrepl/daemon.(*jobs).start.func1(0xc0000f68c0, 0xd22c40, 0xc000116ee0, 0xd1ba4
         /go/src/github.com/zrepl/zrepl/daemon/daemon.go:220 +0x121
 created by github.com/zrepl/zrepl/daemon.(*jobs).start
         /go/src/github.com/zrepl/zrepl/daemon/daemon.go:216 +0x52e
 zrepl.service: Main process exited, code=exited, status=2/INVALIDARGUMENT
 zrepl.service: Failed with result 'exit-code'.
2019-11-16 22:11:13 +01:00
Christian Schwarz 27db3e6f70 docs: supporters: update & add viz for different kinds of support 2019-11-16 22:11:07 +01:00
Christian Schwarz 4994b7a9ea rpc/dataconn + build: support GOOS={solaris,illumos} 2019-11-16 22:07:47 +01:00
Christian Schwarz 080f2c0616 build: Makefile: refactor cross-builds + release, add i386 targets 2019-11-16 22:07:47 +01:00
Christian Schwarz d469cc04b6 transports/ssh: bump go-netssh to improve dial errors
from go-netssh changelog:

    dial: better error handling if ssh command exits with non-zero exit status

    SSHError.Error() relied on go-rwccmd behavior of returning io.EOF if the
    ssh binary exited with status code 0.

    We no longe ruse go-rwccmd => capture Stderr ourselves using zrepl's
    circlog (depending on zrepl is not pretty, but since this package is supposedly
    only used by zrepl ATM, this is fine)

    refs https://github.com/zrepl/zrepl/issues/237
2019-11-16 22:07:47 +01:00
Christian Schwarz e0a25d04ac build: Makefile: set GO111MODULE=on for all go commands 2019-11-16 22:07:47 +01:00
Christian Schwarz b0f2c79944 build: go mods: split build deps into subgomod, bump prometheus to 1.2.1, tweaked go mod tidy
tweaked go mod tidy: see comment in go.mod
2019-11-16 22:07:47 +01:00
Christian Schwarz d2bc40f78d docs: transports: ssh: better copy-pastable connect section 2019-11-16 22:07:47 +01:00
Christian Schwarz 9e54f11960 dist/systemd: fix ssh-transport: create stdinserver runtime directory
tested to work on Debian Stretch

refs #237
2019-11-16 22:07:38 +01:00
Andy Fiddaman 6eda1f743f Fix typo in tutorial.rst 2019-11-05 09:57:30 -08:00
Andy Fiddaman 6787decef1 Add OmniOS (illumos distribution) to list of OSs 2019-11-05 09:49:57 -08:00
Christian Schwarz d56d45a2ab docs: install: apt: fix snippet display & link to packaging repo 2019-10-21 16:35:23 +02:00
Christian Schwarz fcf16a163a docs: install: apt snippet: idempotent, bash compat, multiarch compat
Co-authored-by: Janis Streib <me@janis-streib.de>
Co-authored-by: Christian Schwarz <me@cschwarz.com>
2019-10-21 16:21:51 +02:00
Christian Schwarz dc39c819a3 docs: add debian + ubuntu installation 2019-10-18 20:18:42 +02:00
Christian Schwarz 1048b09487 build: include config examples and dist in noarch tarball 2019-10-18 20:18:42 +02:00
Richard Poettler 3806e97404 docs: add copr repo for Fedora/CentOS
closes #229
2019-10-16 10:46:02 +02:00
chenhao c396f9508a zfs: replace hard coded zfs command in ZFSDestroy
fixes #231
2019-10-16 10:22:53 +02:00
Christian Schwarz b9933f6cb2 platformtest: add zfsGet bookmark handling & replicationCursor tests
This encodes the observation made in issue #230 :
In the ZFS version shipped in Ubuntu 16.04 where
`zfs get someprop a#bookmark` does not work.
2019-10-14 17:54:14 +02:00
Christian Schwarz 0ba4b5eda6 zfs: helper for ZFSGet guid and createtxg 2019-10-14 17:54:14 +02:00
Christian Schwarz 18d2c350de platformtest: harness: -failure.stop-and-keep-pool mode, prettier logging 2019-10-14 17:54:14 +02:00
Christian Schwarz f8f9fd11cd platformtest: logging-related refactorings 2019-10-14 17:32:58 +02:00
John Ramsden b422e6f12e docs: installation: add Arch Linux 'from source' package 2019-10-13 12:33:27 +02:00
Christian Schwarz f8d5082bdd docs: remove outdated implementation references + remove 0.2-rc* from published docs 2019-10-13 12:26:39 +02:00
Christian Schwarz ffe677e55a docs: snapshotting: command hook type: not the only hook type anymore 2019-10-13 12:16:31 +02:00
Christian Schwarz 84eefa57bc rpc/grpcclientidentity: remove hard-coded deadline in listener adatper causing crash
Verified once again that grpc.DialContext is indeed non-blocking.
However, it checks in a defer stmt that the passed dial is not ctx.Done().
That is highly unusual if the dial is non-blocking.
But it might still happen, maybe because of machine suspend during the function call and before the defer stmt is executed.

panic:
context deadline exceeded
goroutine 49 [running]:
github.com/zrepl/zrepl/rpc/grpcclientidentity/grpchelper.ClientConn(0x1906ea0, 0xc0003ea1e0, 0x1921620, 0xc0002da660, 0x0)
        /gopath/src/github.com/zrepl/zrepl/rpc/grpcclientidentity/grpchelper/authlistener_grpc_adaptor_wrapper.go:49 +0x38c
github.com/zrepl/zrepl/rpc.NewClient(0x1906f00, 0xc0002d60f0, 0x1921620, 0xc0002da640, 0x1921620, 0xc0002da660, 0x1921620, 0xc0002da6a0, 0x1921620)
        /gopath/src/github.com/zrepl/zrepl/rpc/rpc_client.go:53 +0x199
github.com/zrepl/zrepl/daemon/job.(*modePush).ConnectEndpoints(0xc0000d1e90, 0x1921620, 0xc0002da640, 0x1921620, 0xc0002da660, 0x1921620, 0xc0002da6a0, 0x1906f00, 0xc0002d60f0)
        /gopath/src/github.com/zrepl/zrepl/daemon/job/active.go:105 +0x15d
github.com/zrepl/zrepl/daemon/job.(*ActiveSide).do(0xc0000d6120, 0x1918720, 0xc00020f170)
        /gopath/src/github.com/zrepl/zrepl/daemon/job/active.go:356 +0x236
github.com/zrepl/zrepl/daemon/job.(*ActiveSide).Run(0xc0000d6120, 0x1918720, 0xc00009c660)
        /gopath/src/github.com/zrepl/zrepl/daemon/job/active.go:347 +0x289
github.com/zrepl/zrepl/daemon.(*jobs).start.func1(0xc0000fc880, 0x1921620, 0xc0002da120, 0x191a320, 0xc0000d6120, 0x1918720, 0xc0002d6a80)
2019-10-10 14:02:12 +02:00
Juergen Hoetzel ad77371e38 docs: include Arch Linux installation 2019-10-06 20:38:00 +02:00
Juergen Hoetzel c524acb2df Fix invalid comment syntax 2019-10-06 16:23:20 +02:00
Christian Schwarz 3edfe535c6 docs: fix typo on index page 2019-10-05 14:59:51 +02:00
Juergen Hoetzel d3b99e8e39 Fix typo 2019-10-05 14:58:49 +02:00
Christian Schwarz 3c03f21419 docs: SEPA hint, supporters, fix publish script 2019-10-03 11:57:19 +02:00
Christian Schwarz 5c95c21727 transport/local: configurable dial_timeout for connect, default 2s 2019-09-29 19:05:54 +02:00
Christian Schwarz a6b578b648 rpc/dataconn/stream: Conn: handle concurrent Close calls + goroutine leak fix
* Add Close() in closeState to identify the first closer
* Non-first closers get an error
* Reads and Writes from the Conn get an error if the conn was closed
  during the Read / Write was running
* The first closer starts _separate_ goroutine draining the c.frameReads channel
* The first closer then waits for the goroutine that fills c.frameReads
  to exit

refs 3bfe0c16d0
fixes #174

readFrames would block on `reads <-`
   but only after that would stream.Conn.readFrames close c.waitReadFramesDone
   which was too late because stream.Conn.Close would wait for c.waitReadFramesDone to be closed before draining the channel
                              ^^^^^^ (not frameconn.Conn, that closed successfully)

   195 @ 0x1032ae0 0x1006cab 0x1006c81 0x1006a65 0x15505be 0x155163e 0x1060bc1
           0x15505bd       github.com/zrepl/zrepl/rpc/dataconn/stream.readFrames+0x16d             github.com/zrepl/zrepl/rpc/dataconn/stream/stream.go:220
           0x155163d       github.com/zrepl/zrepl/rpc/dataconn/stream.(*Conn).readFrames+0xbd      github.com/zrepl/zrepl/rpc/dataconn/stream/stream_conn.go:71

   195 @ 0x1032ae0 0x10078c8 0x100789e 0x100758b 0x1552678 0x1557a4b 0x1556aec 0x1060bc1
           0x1552677       github.com/zrepl/zrepl/rpc/dataconn/stream.(*Conn).Close+0x77           github.com/zrepl/zrepl/rpc/dataconn/stream/stream_conn.go:191
           0x1557a4a       github.com/zrepl/zrepl/rpc/dataconn.(*Server).serveConn.func1+0x5a      github.com/zrepl/zrepl/rpc/dataconn/dataconn_server.go:93
           0x1556aeb       github.com/zrepl/zrepl/rpc/dataconn.(*Server).serveConn+0x87b           github.com/zrepl/zrepl/rpc/dataconn/dataconn_server.go:176
2019-09-29 19:05:54 +02:00
Christian Schwarz 8af824df41 docs: promote monetary support in changelog 2019-09-29 19:04:53 +02:00
Christian Schwarz 58ab25919e platformtest: dedicated pool per test, Makefile target, maintainer notice
fixes #216
fixes #211
2019-09-29 18:48:44 +02:00
Christian Schwarz 215848f476 docs: 0.2 changelog 2019-09-28 17:50:07 +02:00
Christian Schwarz f9c7766073 replication/logic: fix race when reading byte counter pointer for report
fixes #214
2019-09-28 16:16:19 +02:00
Christian Schwarz f976212ec9 config: validate presence of port in addresses
fixes #213
2019-09-28 14:25:14 +02:00
Christian Schwarz 8c88e168c1 rpc/dataconn/client: ReqRecv to log level Debug
reported by @avg-l
2019-09-28 11:49:20 +02:00
Christian Schwarz a78c854404 rpc/dataconn/frameconn: mask ECONNRESET error on Close()
fixes #190
2019-09-28 11:49:20 +02:00
Christian Schwarz 8a5af2f80e build/circleci: apt update before installing
hope this fixes the spurious apt install failures
2019-09-27 21:31:05 +02:00
Christian Schwarz f7aa26d418 zrepl status: follow up c4be60c: import screen terminfo
This is $TERM on FreeBSD and FreeNAS.

fixes #204
ref https://github.com/gdamore/tcell/issues/252
2019-09-27 21:31:05 +02:00
Christian Schwarz b5ff1a9926 snapper + client/status: snapshotting reports 2019-09-27 21:31:00 +02:00
Ross Williams 729c83ee72 pre- and post-snapshot hooks
* stack-based execution model, documented in documentation
* circbuf for capturing hook output
* built-in hooks for postgres and mysql
* refactor docs, too much info on the jobs page, too difficult
  to discover snapshotting & hooks

Co-authored-by: Ross Williams <ross@ross-williams.net>
Co-authored-by: Christian Schwarz <me@cschwarz.com>

fixes #74
2019-09-27 21:25:59 +02:00
Ross Williams 00434f4ac9 daemon/logging: format human: treat 'subsystem' field prefixed
logging.Subsystem != string
=> typecast failed

(Also, nobody guarantees that e.Fields contains `field`)
2019-09-27 20:39:43 +02:00
Christian Schwarz 2cd9173bfb transport/local: hard fail, more aggressive connect timeout 2019-09-14 13:43:46 +02:00
Christian Schwarz 7ba3ae077f client/status: job filter flag 2019-09-14 13:43:46 +02:00
Christian Schwarz a6497b2c6e add platformtest: infrastructure for ZFS compatiblity testing 2019-09-14 13:43:46 +02:00
Christian Schwarz 07956c2299 zfs,endpoint: use zfs destroy batch syntax if available
refs #72
2019-09-14 13:43:46 +02:00
Christian Schwarz 77d3a1ad4d build: drop go Dep, switch to modules, support Go 1.13
bump enumer to v1.1.1
bump golangci-lint to v1.17.1

no `go mod tidy` because 1.13 and 1.12 seem to alter each other's output

fixes #112
2019-09-14 13:36:44 +02:00
Christian Schwarz 2e0ff9a582 github: Patreon account in FUNDING.yml 2019-09-08 00:49:05 +02:00
Christian Schwarz a65d8f1f4c docs: fix requirements.txt & pin sphinxcontrib-versioning version 2019-09-08 00:47:39 +02:00
Christian Schwarz 424234c2d1 docs: Patreon button + Supporters page 2019-09-07 23:16:57 +02:00
Christian Schwarz 3bfe0c16d0 rpc/dataconn/stream: fix goroutine leaks & transitive buffer leaks
fixes #174
2019-09-07 22:22:05 +02:00
Christian Schwarz e5f944c2f8 zfs: zfsGet: return *ZFSError on exec failure
refs #178
2019-09-07 20:12:46 +02:00
Christian Schwarz d81a1818d6 endpoint: Receiver: only create placeholders below root_fs
fixes #195
2019-09-07 20:01:15 +02:00
Christian Schwarz 921b34235e daemon: env var for autostarting pprof endpoint 2019-09-07 19:50:57 +02:00
JMoVS a0cf9cff2a docs: include homebrew installation 2019-08-21 12:47:24 +02:00
Christian Schwarz 3105609d39 Merge branch 'problame/184-i386-build-failure-timeoutconn-iovec-len' 2019-08-20 09:13:29 +02:00
Christian Schwarz 254a292362 rpc/timeoutconn: platform-independent sizes for computing syscall.Iovec.Len
refs #184
2019-08-19 18:11:25 +02:00
Christian Schwarz 95e16f01c1 rpc/dataconn: audit and comment use of unsafe for readv usage
Go 1.13 will add a more precise escape analysis:
https://tip.golang.org/doc/go1.13#compiler

Reviewed usage of unsafe to ensure we adhere to the unsafe.Pointer safety rules:
https://tip.golang.org/pkg/unsafe/#Pointer
2019-08-19 18:11:25 +02:00
Christian Schwarz df21b5d1b4 build/circleci: handle forked PR builds
disables artifact upload to external minio repo
2019-08-19 18:10:04 +02:00
Christian Schwarz c4be60c29f zrepl status: bump tcell version to fix "terminal entry not found" error
tcell 1.2 seems to fix a similar error to #204 :
https://github.com/gdamore/tcell/issues/252

fixes #204
2019-08-08 09:41:42 +02:00
Christian Schwarz ff18181064 Revert "build/circleci: disable minio client interactive prompt"
This reverts commit 9dd187e29d.
2019-07-14 18:28:06 +02:00
Christian Schwarz b167de3096 filters: add some basic test cases for DatasetMapFilter 2019-07-14 18:28:06 +02:00
Christian Schwarz 9dd187e29d build/circleci: disable minio client interactive prompt
TODO use stable binary
2019-07-11 09:28:25 +02:00
Christian Schwarz 413bd0f43b github: add FUNDING.yml 2019-07-11 09:28:25 +02:00
Christian Schwarz 3c3606d516 docs: show donations/week for liberaypay 2019-06-29 12:08:15 +02:00
Christian Schwarz b5dc04e62c transport/local: fix "client-provided callback did block on send" crash
fixes #173

(amendment of 0e419c11df2bc45feb6edb7d82a309b0fe174875)
2019-06-29 11:49:16 +02:00
Christian Schwarz 234a327a03 build: Linux arm64 support
* protoc zip fetching
* Makefile:
    * GOOS and GOARCH
    * run vet on all targets

Note: freebsd/arm64 is apparently not supported

fixes #180
refs #181
2019-06-23 15:25:26 +02:00
Christian Schwarz 90b32c7377 Merge pull request #183 from johnramsden/patch-1
status: fix typo 'follwing'
2019-06-08 11:42:57 +02:00
John 0fc6a63564 Fix typo 'follwing' 2019-06-06 22:04:57 -07:00
Christian Schwarz 5138681c13 docs: 0.1.1 changelog 2019-04-06 12:36:06 +02:00
Christian Schwarz d6304f4f1d rpc/dataconn: fix I/O timeout on variable receive rate
refs #162
2019-03-31 14:20:06 +02:00
Christian Schwarz 082335df5d docs: fix publish.sh branch whitelisting 2019-03-30 19:03:18 +01:00
Christian Schwarz be506661a5 docs: bump copyright 2019-03-30 19:01:34 +01:00
Christian Schwarz 38385adf22 docs: update front-page zrepl status 2019-03-30 18:53:28 +01:00
Christian Schwarz bbcfb47d28 docs: changelog: more maintainer notices 2019-03-30 18:53:28 +01:00
Christian Schwarz 32dd456ac6 Merge pull request #167 from zrepl/problame/161-limit-concurrency-io-intensive-zfs-ops
hotfix #161 for 0.1: limit concurrency of zfs send & recv commands
2019-03-30 18:53:11 +01:00
Christian Schwarz 4d2765e5f2 rpc: actually return an error if ReqSend fails 2019-03-28 22:17:12 +01:00
Christian Schwarz 000d8bba66 hotfix: limit concurrency of zfs send & recv commands
ATM, the replication logic sends all dry-run requests in parallel,
which might overwhelm the ZFS pool on the sending side.
Since we use rpc/dataconn for dry sends, this also opens one TCP
connection per dry-run request.

Use a sempahore to limit the degree of concurrency where we know it is a
problem ATM.
As indicated by the comments, the cleaner solution would involve some
kind of 'resource exhaustion' error code.

refs #161
refs #164
2019-03-28 22:17:12 +01:00
Christian Schwarz 5f909dab76 build: CircleCI build artifact upload 2019-03-28 14:41:20 +01:00
Christian Schwarz 56e63ff551 Merge pull request #159 from zrepl/problame/cleanup-project
cleanup project: formatting & basic lints
2019-03-27 16:25:10 +01:00
Christian Schwarz f31b54582f daemon/job: fix receiving-job root-fs overlap detection
fixup for 7756c9a5
fixes #160
2019-03-27 13:12:26 +01:00
Christian Schwarz 5b256a92b3 include linting in build process 2019-03-27 13:12:26 +01:00
Christian Schwarz cd829bd79a pin formatter and linter as deps 2019-03-27 13:12:26 +01:00
Christian Schwarz 5b97953bfb run golangci-lint and apply suggested fixes 2019-03-27 13:12:26 +01:00
Christian Schwarz afed762774 format source tree using goimports 2019-03-22 19:41:12 +01:00
Christian Schwarz 5324f29693 docs: publish.sh: allow v0.1.0-rc4 2019-03-22 12:02:07 +01:00
Christian Schwarz b96a287a2f docs: adjust publish.sh script 2019-03-22 11:54:49 +01:00
Christian Schwarz 0ab62f197a Merge branch 'problame/overlapping-dataset-hierarchy-improvements' into 'master'
closes #153
2019-03-22 11:02:57 +01:00
Christian Schwarz 9a2d1b3074 Merge branch 'problame/138-snapshotter-fails-silently' into 'master'
closes #152
2019-03-22 10:59:58 +01:00
Christian Schwarz e809dbf45e Merge branch 'problame/social_and_donation_shields' 2019-03-21 21:44:29 +01:00
Christian Schwarz 644d6f8ffb Merge branch 'problame/simplify-placeholder-property' into master
As became clear in #126 (comment) the current zrepl:placeholder user
property makes it hard to rename datasets because the placeholder status
is determined by computing a hash of the dataset path and comparing it
with the property value.

This PR

- changes zrepl:placeholder to a simple on|off property
- derives property status from the property source (local),avoiding the
  original problem of property inheritance in the original implementation
- provides a migration command that, based on the current zrepl
  configuration, migrates property values to the new on|off schema

refs #150
2019-03-21 21:37:40 +01:00
Christian Schwarz e37bb73b4e Merge branch 'problame/fix-spaces-in-dataset-names-131' 2019-03-21 21:09:08 +01:00
Christian Schwarz 26ec29d8b2 snapper: retry on errors during syncUp and log them
fixes #138
2019-03-21 17:17:10 +01:00
Christian Schwarz c0028c1c44 daemon/logging: add replication logic logger 2019-03-21 17:03:34 +01:00
Christian Schwarz ab38f24198 zfs: bookmark / replication cursor: handle spaces in ds names correctly
fixes #131
2019-03-21 17:03:26 +01:00
Christian Schwarz 7d9a1b7eae zfs: dry send: handle spaces in dataset names correctly
fixes #131
2019-03-21 17:03:19 +01:00
Christian Schwarz 2107483588 docs: jobs: correct root_fs field explanation 2019-03-21 12:51:39 +01:00
Christian Schwarz c2b05954e9 docs: jobs: explain multi-job & machine considerations
refs #136
refs #140
2019-03-21 12:51:07 +01:00
Christian Schwarz 7756c9a55c config + job: forbid non-verlapping receiver root_fs
refs #136
refs #140
2019-03-21 12:07:55 +01:00
Christian Schwarz 3e71542c78 endpoint: serialize dataset hierarchy modification within a receiver job
refs #136
refs #140
2019-03-20 23:02:10 +01:00
Christian Schwarz 2f2e6e6a00 receiving side: placeholder as simple on|off property 2019-03-20 20:26:30 +01:00
Christian Schwarz 6f7467e8d8 Merge branch 'InsanePrawn-master' into 'master' 2019-03-20 19:45:00 +01:00
Christian Schwarz f2a6193735 Merge branch 'problame/replication_refactor' 2019-03-20 19:44:15 +01:00
Christian Schwarz 84eca86e14 Merge branch 'problame/0.1-docs-fixes' 2019-03-20 19:31:32 +01:00
Christian Schwarz 86fdcfc437 replication: stepqueue: make TestPqNotconcurrent less flaky 2019-03-19 18:50:12 +01:00
Christian Schwarz 25eeedbce8 docs: link to good_first_issue and docs category 2019-03-19 18:22:15 +01:00
Christian Schwarz 2ba1ff15b7 docs: mention automatic retries 2019-03-19 18:18:41 +01:00
Christian Schwarz 5aefc47f71 daemon: remove last traces of watchdog mechanism 2019-03-19 18:15:34 +01:00
Christian Schwarz 7633c1cdf1 docs: tutorial: s/pull/push 2019-03-18 14:56:32 +01:00
Christian Schwarz 283c2821cd docs: add SnapJob to 0.1 changelog 2019-03-18 14:53:12 +01:00
Christian Schwarz fb999c8617 docs: drop references to 'main config file'
were just confusing in the remaining places where they were used

fixes #127
2019-03-18 14:50:32 +01:00
Christian Schwarz 53dc0c3c5e docs: document existence of zrepl:placeholder property
fixes #129
2019-03-18 14:50:32 +01:00
Christian Schwarz 84019238df docs: changelog: remove whitespace & churn 2019-03-18 14:50:32 +01:00
Christian Schwarz cef865f5ce docs: changelog 0.1: document forgotten bugfixes & features 2019-03-18 14:50:32 +01:00
Christian Schwarz bdf99f6bb4 docs: document material in dist/ 2019-03-18 12:45:27 +01:00
Christian Schwarz 7424423d9a Merge pull request #146 from zrepl/problame/add-some-dist-stuff
Grafana Dashboard & Systemd Unit
2019-03-18 12:41:39 +01:00
Christian Schwarz c9b812570d docs: changelog: document new rpc & retry behavior changes 2019-03-18 12:39:53 +01:00
Christian Schwarz e62d157aac docs: remove typo at end of changelog 2019-03-18 12:23:39 +01:00
Christian Schwarz 99b3337b1c docs: add shields for license, language, donations + tweeting 2019-03-18 11:13:50 +01:00
Christian Schwarz dd673bf923 docs: condense snap job overview table row 2019-03-17 21:29:09 +01:00
Christian Schwarz 158d1175e3 rename SinglePruner to LocalPruner 2019-03-17 21:18:25 +01:00
Christian Schwarz b25da7b9b0 job: snap: comment fix 2019-03-17 21:07:42 +01:00
Christian Schwarz 5cd2593f52 job: snap: workaround for replication cursor requirement 2019-03-17 21:07:01 +01:00
Christian Schwarz d8d9e34914 pruner: single: remove unused member considerSnapAtCursorReplicated 2019-03-17 20:57:34 +01:00
Christian Schwarz e8c0d206ea docs: fix nitpicks 2019-03-17 20:54:47 +01:00
Christian Schwarz 17818439a0 Merge branch 'problame/replication_refactor' into InsanePrawn-master 2019-03-17 17:33:51 +01:00
Christian Schwarz 056be1185d dist: add grafana dashboard
fixes #116
2019-03-16 16:12:34 +01:00
Christian Schwarz b0898ec8bc dist: systemd service definition template
fixes #117
refs #145
2019-03-16 16:12:34 +01:00
Christian Schwarz 133b7013a0 Merge remote-tracking branch 'origin/master' into problame/replication_refactor 2019-03-16 16:09:40 +01:00
Christian Schwarz dabf7e3ec9 build: travis: Go1.12 and some refactorings 2019-03-16 16:02:45 +01:00
Christian Schwarz da3ba50a2c Merge remote-tracking branch 'origin/master' into problame/replication_refactor 2019-03-16 14:48:01 +01:00
Christian Schwarz 5eababe0b0 endpoing: receiver: return visitErr on traversal error
fixes #137
2019-03-16 14:47:34 +01:00
Christian Schwarz b2c5ffcaea rpc: dataconn: handle incorrect handler return values
refs #137
2019-03-16 14:47:29 +01:00
Christian Schwarz 4ee00091d6 pull job: support manual-only invocation 2019-03-16 14:24:05 +01:00
Christian Schwarz c655622bf7 build: travis: allow_failures of go:master 2019-03-15 22:43:18 +01:00
Christian Schwarz 71d331af16 Add CircleCI config
Doesn't cover all of Travis, but CircleCI archives artifacts.
2019-03-15 22:15:15 +01:00
Christian Schwarz 34052d98d6 docs: move snap job below all replication-related job types 2019-03-15 22:00:29 +01:00
Christian Schwarz 3543fbbb65 docs: clarify language & example of source-side pruning workaround 2019-03-15 22:00:29 +01:00
Christian Schwarz aff639e87a Merge remote-tracking branch 'origin/master' into InsanePrawn-master 2019-03-15 21:05:20 +01:00
Christian Schwarz 5dfe24eeee Merge 'joshsouza/fix_peer_cert_chains' into 'master' 2019-03-15 18:40:05 +01:00
Christian Schwarz 78ec5aa716 Merge remote-tracking branch 'origin/master' into joshsouza-fix_peer_cert_chains 2019-03-15 18:37:11 +01:00
Christian Schwarz 457d5bad9a Merge 'ximalias/Ximalas-syslog-facility' into 'master' 2019-03-15 18:33:47 +01:00
Christian Schwarz a0f301d700 syslog logging: fix priority parsing + add test for default facility 2019-03-15 18:18:16 +01:00
Ximalas fc311a9fd6 syslog logging: support setting facility in config 2019-03-15 17:55:11 +01:00
Christian Schwarz a7993d18c6 transport/tls: clarify docs & error message language 2019-03-15 17:17:25 +01:00
Christian Schwarz 5595cff6a6 Merge branch 'master' into fix_peer_cert_chains 2019-03-15 16:34:21 +01:00
Christian Schwarz 2c3b3c093d rpc: do not leak grpc state change logger goroutine 2019-03-15 16:18:01 +01:00
Christian Schwarz ab3e783168 rpc: treat protocol handshake errors as permanent
treat handshake errors as permanent on the client

The issue was observed by 100% CPU usage due to lack ofrate-limiting in
dataconn.ReqPing retries=> safeguard that
2019-03-15 16:18:01 +01:00
Christian Schwarz 7584c66bdb pruner: remove retry handling + fix early give-up
Retry handling is broken since the gRPC changes (wrong error classification).
Will come back at some point, hopefully by merging the replication
driver retry infrastructure.

However, the simpler architecture allows an easy fix for the problem
that the pruner practically gave up on the first error it encountered.

fixes #123
2019-03-13 21:04:39 +01:00
Christian Schwarz d78d20e2d0 pruner: skip placeholders + FSes without correspondents on source
fixes #126
2019-03-13 20:42:37 +01:00
Christian Schwarz b85ec52387 rpc/ctrl: nicer perr info debug log messages 2019-03-13 19:20:04 +01:00
Christian Schwarz edcd258cc9 replication: more elaborate messages for Conflict errors 2019-03-13 18:46:04 +01:00
Christian Schwarz d5250bbf51 client/status: fix wrap for multiline strings with leading space 2019-03-13 18:46:04 +01:00
Christian Schwarz d50e553ebb handle changes to placeholder state correctly
We assumed that `zfs recv -F FS` would basically replace FS inplace, leaving its children untouched.
That is in fact not the case, it only works if `zfs send -R` is set, which we don't do.

Thus, implement the required functionality manually.

This solves a `zfs recv` error that would occur when a filesystem previously created as placeholder on the receiving side becomes a non-placeholder filesystem (likely due to config change on the sending side):

  zfs send pool1/foo@1 | zfs recv -F pool1/bar
  cannot receive new filesystem stream:
  destination has snapshots (eg. pool1/bar)
  must destroy them to overwrite it
2019-03-13 18:46:04 +01:00
Christian Schwarz 1eb0f12a61 replication: add diff test case 2019-03-13 18:45:40 +01:00
Christian Schwarz 8129ed91f1 zfs + replication: migrate dead zfs/diff_test.go to replication/logic/diff
(and remove the dead code from package zfs)
2019-03-13 16:39:10 +01:00
Christian Schwarz c87759affe replication/driver: automatic retries on connectivity-related errors 2019-03-13 15:00:40 +01:00
Christian Schwarz 07b43bffa4 replication: refactor driving logic (no more explicit state machine) 2019-03-13 15:00:40 +01:00
Christian Schwarz 0230c6321f rpc/dataconn: microbenchmark 2019-03-13 13:57:21 +01:00
Christian Schwarz 796c5ad42d rpc rewrite: control RPCs using gRPC + separate RPC for data transfer
transport/ssh: update go-netssh to new version
    => supports CloseWrite and Deadlines
    => build: require Go 1.11 (netssh requires it)
2019-03-13 13:53:48 +01:00
Christian Schwarz d281fb00e3 socketpair: directly export *net.UnixConn (and add test for that behavior) 2019-03-13 11:36:34 +01:00
Christian Schwarz 76a6c623f3 tlsconf and transport/tls: support NSS-formatted keylog file for debugging
... via env variable
2019-03-13 00:28:38 +01:00
Christian Schwarz 25c974f0b5 envconst: support for int64 2019-03-13 00:07:33 +01:00
Christian Schwarz ea719f5b5a build: use 'git describe --always' to determine ZREPL_VERSION 2019-03-13 00:07:33 +01:00
Christian Schwarz 3105fa4ff8 build: use dep's required feature for dev tools 2019-03-12 23:43:39 +01:00
Josh Souza f724480c7b Add documentation regarding using a certificate chain 2019-01-22 10:09:24 -08:00
Jakob Berger 5c5e8c0baf Documentation changes mostly as requested 2019-01-22 16:46:34 +01:00
Josh Souza bb5278fe9b Permit peers to provide a cert chain (multiple certs). fixes #103 2019-01-09 10:10:37 -08:00
Christian Schwarz 38b0bd76f5 build: just use go {test,vet} ./... for targets vet, test and generate 2018-12-11 22:00:03 +01:00
Christian Schwarz c1aab0bee9 config: update yaml-config and use zeropositive constraint for timeouts 2018-12-11 21:54:36 +01:00
Christian Schwarz ef3283638a logger: add stderrlogger (sometimes useful) 2018-12-11 21:24:54 +01:00
Christian Schwarz 68b62a5c00 tlsconf: clear handshake deadline after completed handshake 2018-12-11 21:24:26 +01:00
Christian Schwarz 7a75a4d384 util/iocommand: timeout kill on close + other hardening 2018-12-11 21:06:54 +01:00
Christian Schwarz 1aae7b222f docs: fix confusing description of the role of client identity for sink jobs 2018-12-01 15:19:59 +01:00
Christian Schwarz 3535b251ab freeze Go build dependencies in Gopkg.lock
* use pseudo-depdencies in build/build.go to convince dep
* update Travis, Dockerfile and Docs
* build.Dockerfile image now contains the Go build dependencies
* => faster builds
* bump pdu file after protoc update

fixes #106
2018-12-01 14:36:40 +01:00
Christian Schwarz 707f070a3c build: fix dirty detection at the end of release build
was using Bashisms
2018-12-01 14:36:40 +01:00
InsanePrawn 160a3b6d32 more gofmt, drop snapjob.go_prefmt after it was accidentally added 2018-11-21 22:14:43 +01:00
InsanePrawn d977796f18 Add SnapJob docs 2018-11-21 16:59:46 +01:00
InsanePrawn 3cef76d463 Refactor snapJob() to snapJobFromConfig() 2018-11-21 14:37:03 +01:00
InsanePrawn e9564a7e5c Inlined a couple legacy leftover functions from the mode copypasta 2018-11-21 14:35:40 +01:00
InsanePrawn b79ad3ddc3 Honour PruneKeepNotReplicated.KeepSnashotAtCursor in SinglePrunerFactory 2018-11-21 14:17:38 +01:00
InsanePrawn d0f898751f Gofmt snapjob.go 2018-11-21 14:02:21 +01:00
InsanePrawn 22d9830baa Fix prometheus with multiple jobs 2018-11-21 04:26:03 +01:00
InsanePrawn c4e23862cd Added status view for SnapJob. 2018-11-21 04:06:13 +01:00
InsanePrawn e10dc129de Make getPruner() private 2018-11-21 03:39:03 +01:00
InsanePrawn dd11fc96db Touchups in job.go 2018-11-21 03:27:39 +01:00
InsanePrawn 7de3c0a09a Removed the references to a pruning 'side' in the singlepruner logging code and the snapjob prometheus thing. 2018-11-21 02:52:33 +01:00
InsanePrawn 141e49727c Missed a last reference to tasks 2018-11-21 02:51:23 +01:00
InsanePrawn 442d61918b remove most of the watchdog machinery 2018-11-21 02:42:13 +01:00
InsanePrawn 58dcc07430 Added SnapJobStatus 2018-11-21 02:08:39 +01:00
InsanePrawn 19d0916e34 remove snapMode, rename snap_ActiveSide to SnapJob 2018-11-21 01:54:56 +01:00
InsanePrawn 1265cc7934 pruned unused lines and comments ;) 2018-11-21 01:34:50 +01:00
InsanePrawn 3d2688e959 Ugly but working inital snapjob implementation 2018-11-20 19:30:15 +01:00
Christian Schwarz 7ab51fad0d zfs: add 'received' property source, handle 'any' source correctly and use 'any' for placeholder FS detection
we want was first noticed in zfs 0.8rc1

Upstream doc PR: https://github.com/zfsonlinux/zfs/pull/8134
2018-11-16 13:07:13 +01:00
Christian Schwarz 3472145df6 pruner + proto change: better handling of missing replication cursor
- don't treat missing replication cursor as an error in protocol
- treat it as a per-fs planning error instead
2018-11-16 12:21:54 +01:00
Christian Schwarz 5e1ea21f85 pruning: add 'Negate' option to KeepRegex and expose it in config 2018-11-16 12:21:54 +01:00
Christian Schwarz 2db3977408 cli: add 'test placeholder' subcommand for placeholder debugging 2018-11-16 12:21:54 +01:00
Christian Schwarz ca6d5d3bb5 build: Travis CI configuration 2018-11-16 12:10:58 +01:00
Christian Schwarz 163c2bc533 docs: update requirements.txt 2018-11-16 12:10:58 +01:00
Christian Schwarz dd286aa12e client: fix status bytes per second measurement
still far from perfect, but better than incorrect values
2018-11-05 01:37:51 +01:00
Christian Schwarz 80babe3ab4 docs/README: update package hierarchy overview 2018-10-26 22:05:57 +02:00
Christian Schwarz ca0cab0a15 docs/tutorial: fix headlines 2018-10-26 21:52:49 +02:00
329 changed files with 28342 additions and 7150 deletions
+145
View File
@@ -0,0 +1,145 @@
version: 2.0
workflows:
version: 2
build:
jobs:
- build-1.11
- build-1.12
- build-1.13
- build-1.14
- build-latest
- test-build-in-docker
jobs:
# build-latest serves as the template
# we use YAML anchors & aliases to exchange the docker image (and hence Go version used for the build)
build-latest: &build-latest
description: Builds zrepl
parameters:
image:
description: "the docker image that the job should use"
type: string
docker:
- image: circleci/golang:latest
environment:
# required by lazy.sh
TERM: xterm
working_directory: /go/src/github.com/zrepl/zrepl
steps:
- run:
name: Setup environment variables
command: |
# used by pip (for docs)
echo 'export PATH="$HOME/.local/bin:$PATH"' >> $BASH_ENV
# we use modules
echo 'export GO111MODULE=on' >> $BASH_ENV
- restore_cache:
keys:
- source
- protobuf
- checkout
- save_cache:
key: source
paths:
- ".git"
# install deps
- run: wget https://github.com/protocolbuffers/protobuf/releases/download/v3.6.1/protoc-3.6.1-linux-x86_64.zip
- run: echo "6003de742ea3fcf703cfec1cd4a3380fd143081a2eb0e559065563496af27807 protoc-3.6.1-linux-x86_64.zip" | sha256sum -c
- run: sudo unzip -d /usr protoc-3.6.1-linux-x86_64.zip
- save_cache:
key: protobuf
paths:
- "/usr/include/google/protobuf"
- run: sudo apt update && sudo apt install python3 python3-pip libgirepository1.0-dev gawk
- run: ./lazy.sh devsetup
- run: make zrepl-bin
- run: make vet
- run: make test
- run: make lint
- run: make release
- store_artifacts:
path: ./artifacts/release
when: always
- run:
shell: /bin/bash -eo pipefail
when: always
command: |
if [ -n "$CIRCLE_PR_NUMBER" ]; then # CIRCLE_PR_NUMBER is guaranteed to be only present in forked PRs (external)
echo "Forked PR detected. Sry, can't trust you with credentials to external artifact store, use CircleCI's instead."
exit 0
fi
set -u # from now on
# Download and install minio
curl -sSL https://dl.minio.io/client/mc/release/linux-amd64/mc -o ${GOPATH}/bin/mc
chmod +x ${GOPATH}/bin/mc
mc config host add --api s3v4 zrepl-minio https://minio.cschwarz.com ${MINIO_ACCESS_KEY} ${MINIO_SECRET_KEY}
# Upload artifacts
echo "$CIRCLE_BUILD_URL" > ./artifacts/release/cirlceci_build_url
mc cp -r artifacts/release "zrepl-minio/zrepl-ci-artifacts/${CIRCLE_SHA1}/${CIRCLE_JOB}/"
# Push Artifact Link to GitHub
REPO="zrepl/zrepl"
COMMIT="${CIRCLE_SHA1}"
JOB_NAME="${CIRCLE_JOB}"
curl "https://api.github.com/repos/$REPO/statuses/$COMMIT?access_token=$GITHUB_COMMIT_STATUS_TOKEN" \
-H "Content-Type: application/json" \
-X POST \
-d '{"context":"zrepl/publish-ci-artifacts", "state": "success", "description":"CI Build Artifacts for '"$JOB_NAME"'", "target_url":"https://minio.cschwarz.com/minio/zrepl-ci-artifacts/'"$COMMIT"'/"}'
build-1.11:
<<: *build-latest
docker:
- image: circleci/golang:1.11
build-1.12:
<<: *build-latest
docker:
- image: circleci/golang:1.12
build-1.13:
<<: *build-latest
docker:
- image: circleci/golang:1.13
build-1.14:
<<: *build-latest
docker:
- image: circleci/golang:1.14
# this job tries to mimic the build-in-docker instructions
# given in docs/installation.rst
#
# However, CircleCI doesn't support volume mounts, so we have to copy
# the source into the build-container by modifying the Dockerfile
test-build-in-docker:
description: Check that build-in-docker works
docker:
- image: circleci/golang:latest
environment:
working_directory: /go/src/github.com/zrepl/zrepl
steps:
- checkout
- setup_remote_docker
- run:
name: (hacky) circleci doesn't allow volume mounts, so copy src to container
command: echo "ADD . /src" >> build.Dockerfile
- run:
name: (hacky) commit modified Dockerfile to avoid failing git clean check in Makefile
command: git -c user.name='circleci' -c user.email='circleci@localhost' commit -m 'CIRCLECI modified Dockerfile with zrepl src' --author 'autoauthor <circleci@localhost>' -- build.Dockerfile
- run:
name: build the build image (build deps)
command: docker build -t zrepl_build -f build.Dockerfile .
- run:
name: try compiling
command: docker run -it zrepl_build make release
+4
View File
@@ -0,0 +1,4 @@
patreon: zrepl
liberapay: zrepl
custom: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=R5QSXJVYHGX96
ko_fi: problame
+10
View File
@@ -0,0 +1,10 @@
linters:
enable:
- goimports
issues:
exclude-rules:
- path: _test\.go
linters:
- errcheck
+70
View File
@@ -0,0 +1,70 @@
dist: xenial
services:
- docker
env: # for allow_failures: https://docs.travis-ci.com/user/customizing-the-build/
matrix:
include:
- language: go
name: "Build in Docker (docs/installation.rst)"
script:
- sudo docker build -t zrepl_build -f build.Dockerfile .
- |
sudo docker run -it --rm \
-v "${PWD}:/go/src/github.com/zrepl/zrepl" \
--user "$(id -u):$(id -g)" \
zrepl_build make vendordeps release
- &zrepl_build_template
language: go
go_import_path: github.com/zrepl/zrepl
before_install:
- wget https://github.com/protocolbuffers/protobuf/releases/download/v3.6.1/protoc-3.6.1-linux-x86_64.zip
- echo "6003de742ea3fcf703cfec1cd4a3380fd143081a2eb0e559065563496af27807 protoc-3.6.1-linux-x86_64.zip" | sha256sum -c
- sudo unzip -d /usr protoc-3.6.1-linux-x86_64.zip
- ./lazy.sh godep
- make vendordeps
script:
- make
- make vet
- make test
- make lint
- make artifacts/zrepl-freebsd-amd64
- make artifacts/zrepl-linux-amd64
- make artifacts/zrepl-darwin-amd64
go:
- "1.11"
- <<: *zrepl_build_template
go:
- "1.12"
- <<: *zrepl_build_template
go:
- "master"
- &zrepl_docs_template
language: python
python:
- "3.4"
install:
- sudo apt-get install libgirepository1.0-dev
- pip install -r docs/requirements.txt
script:
- make docs
- <<: *zrepl_docs_template
python:
- "3.5"
- <<: *zrepl_docs_template
python:
- "3.6"
- <<: *zrepl_docs_template
python:
- "3.7"
allow_failures:
- <<: *zrepl_build_template
go:
- "master"
Generated
-313
View File
@@ -1,313 +0,0 @@
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
branch = "master"
digest = "1:c0bec5f9b98d0bc872ff5e834fac186b807b656683bd29cb82fb207a1513fabb"
name = "github.com/beorn7/perks"
packages = ["quantile"]
pruneopts = ""
revision = "3a771d992973f24aa725d07868b467d1ddfceafb"
[[projects]]
digest = "1:56c130d885a4aacae1dd9c7b71cfe39912c7ebc1ff7d2b46083c8812996dc43b"
name = "github.com/davecgh/go-spew"
packages = ["spew"]
pruneopts = ""
revision = "346938d642f2ec3594ed81d874461961cd0faa76"
version = "v1.1.0"
[[projects]]
digest = "1:e988ed0ca0d81f4d28772760c02ee95084961311291bdfefc1b04617c178b722"
name = "github.com/fatih/color"
packages = ["."]
pruneopts = ""
revision = "5b77d2a35fb0ede96d138fc9a99f5c9b6aef11b4"
version = "v1.7.0"
[[projects]]
branch = "master"
digest = "1:5d0a2385edf4ba44f3b7b76bc0436ceb8f62bf55aa5d540a9eb9ec6c58d86809"
name = "github.com/ftrvxmtrx/fd"
packages = ["."]
pruneopts = ""
revision = "c6d800382fff6dc1412f34269f71b7f83bd059ad"
[[projects]]
branch = "master"
digest = "1:4416ad04caf950d004921268f777e418f8c328080ccf6a91e799f977dc128808"
name = "github.com/gdamore/encoding"
packages = ["."]
pruneopts = ""
revision = "b23993cbb6353f0e6aa98d0ee318a34728f628b9"
[[projects]]
digest = "1:d8088c614a5bffaf38a40c116226e7d230750bafb77942dca25eab779ceeaf1f"
name = "github.com/gdamore/tcell"
packages = [
".",
"termbox",
"terminfo",
]
pruneopts = ""
revision = "de7e78efa4a71b3f36c7154989c529dbdf9ae623"
version = "v1.1.0"
[[projects]]
digest = "1:6a4a01d58b227c4b6b11111b9f172ec5c17682b82724e58e6daf3f19f4faccd8"
name = "github.com/go-logfmt/logfmt"
packages = ["."]
pruneopts = ""
revision = "390ab7935ee28ec6b286364bba9b4dd6410cb3d5"
version = "v0.3.0"
[[projects]]
digest = "1:3dd078fda7500c341bc26cfbc6c6a34614f295a2457149fc1045cab767cbcf18"
name = "github.com/golang/protobuf"
packages = ["proto"]
pruneopts = ""
revision = "aa810b61a9c79d51363740d207bb46cf8e620ed5"
version = "v1.2.0"
[[projects]]
branch = "master"
digest = "1:cb09475f771b9167fb9333629f5d6a7161572602ea040f1094602b0dc8709878"
name = "github.com/jinzhu/copier"
packages = ["."]
pruneopts = ""
revision = "db4671f3a9b8df855e993f7c94ec5ef1ffb0a23b"
[[projects]]
branch = "master"
digest = "1:1ed9eeebdf24aadfbca57eb50e6455bd1d2474525e0f0d4454de8c8e9bc7ee9a"
name = "github.com/kr/logfmt"
packages = ["."]
pruneopts = ""
revision = "b84e30acd515aadc4b783ad4ff83aff3299bdfe0"
[[projects]]
branch = "master"
digest = "1:591a2778aa6e896980757ea87e659b3aa13d8c0e790310614028463a31c0998b"
name = "github.com/kr/pretty"
packages = ["."]
pruneopts = ""
revision = "cfb55aafdaf3ec08f0db22699ab822c50091b1c4"
[[projects]]
branch = "master"
digest = "1:109e0a9b9e74c5c8adf94a2dc4dd4a9ca2a183d4d87ffecd4e62db69a5ede55a"
name = "github.com/kr/text"
packages = ["."]
pruneopts = ""
revision = "7cafcd837844e784b526369c9bce262804aebc60"
[[projects]]
digest = "1:863ba095ae0954100963826333bd1503c169424bb798d9ce800d9f6651f4c234"
name = "github.com/lucasb-eyer/go-colorful"
packages = ["."]
pruneopts = ""
revision = "345fbb3dbcdb252d9985ee899a84963c0fa24c82"
version = "v1.0"
[[projects]]
digest = "1:9ea83adf8e96d6304f394d40436f2eb44c1dc3250d223b74088cc253a6cd0a1c"
name = "github.com/mattn/go-colorable"
packages = ["."]
pruneopts = ""
revision = "167de6bfdfba052fa6b2d3664c8f5272e23c9072"
version = "v0.0.9"
[[projects]]
digest = "1:78229b46ddb7434f881390029bd1af7661294af31f6802e0e1bedaad4ab0af3c"
name = "github.com/mattn/go-isatty"
packages = ["."]
pruneopts = ""
revision = "0360b2af4f38e8d38c7fce2a9f4e702702d73a39"
version = "v0.0.3"
[[projects]]
digest = "1:82b912465c1da0668582a7d1117339c278e786c2536b3c3623029a0c7141c2d0"
name = "github.com/mattn/go-runewidth"
packages = ["."]
pruneopts = ""
revision = "ce7b0b5c7b45a81508558cd1dba6bb1e4ddb51bb"
version = "v0.0.3"
[[projects]]
digest = "1:4c23ced97a470b17d9ffd788310502a077b9c1f60221a85563e49696276b4147"
name = "github.com/matttproud/golang_protobuf_extensions"
packages = ["pbutil"]
pruneopts = ""
revision = "3247c84500bff8d9fb6d579d800f20b3e091582c"
version = "v1.0.0"
[[projects]]
digest = "1:7365acd48986e205ccb8652cc746f09c8b7876030d53710ea6ef7d0bd0dcd7ca"
name = "github.com/pkg/errors"
packages = ["."]
pruneopts = ""
revision = "645ef00459ed84a119197bfb8d8205042c6df63d"
version = "v0.8.0"
[[projects]]
digest = "1:256484dbbcd271f9ecebc6795b2df8cad4c458dd0f5fd82a8c2fa0c29f233411"
name = "github.com/pmezard/go-difflib"
packages = ["difflib"]
pruneopts = ""
revision = "792786c7400a136282c1664665ae0a8db921c6c2"
version = "v1.0.0"
[[projects]]
branch = "master"
digest = "1:1392748e290ca66ac8447ef24961f8ae9e1d846a53af0f58a5a0256982ce0577"
name = "github.com/problame/go-netssh"
packages = ["."]
pruneopts = ""
revision = "c56ad38d2c91397ad3c8dd9443d7448e328a9e9e"
[[projects]]
branch = "master"
digest = "1:8c63c44f018bd52b03ebad65c9df26aabbc6793138e421df1c8c84c285a45bc6"
name = "github.com/problame/go-rwccmd"
packages = ["."]
pruneopts = ""
revision = "391d2c78c8404a9683d79f75dd24ab53040f89f7"
[[projects]]
digest = "1:1bcbb0a7ad8d3392d446eb583ae5415ff987838a8f7331a36877789be20667e6"
name = "github.com/problame/go-streamrpc"
packages = [
".",
"internal/pdu",
]
pruneopts = ""
revision = "d5d111e014342fe1c37f0b71cc37ec5f2afdfd13"
version = "v0.5"
[[projects]]
branch = "master"
digest = "1:ebf8ffdde9bdbf9c83e22121875c68c01d821776523546554b2a3ff6f72773ab"
name = "github.com/prometheus/client_golang"
packages = [
"prometheus",
"prometheus/promhttp",
]
pruneopts = ""
revision = "e11c6ff8170beca9d5fd8b938e71165eeec53ac6"
[[projects]]
branch = "master"
digest = "1:60aca47f4eeeb972f1b9da7e7db51dee15ff6c59f7b401c1588b8e6771ba15ef"
name = "github.com/prometheus/client_model"
packages = ["go"]
pruneopts = ""
revision = "99fa1f4be8e564e8a6b613da7fa6f46c9edafc6c"
[[projects]]
branch = "master"
digest = "1:af21ee3e0a8212f17bb317cd7237f9920bcb2641a291ac111f30f63b3cab817f"
name = "github.com/prometheus/common"
packages = [
"expfmt",
"internal/bitbucket.org/ww/goautoneg",
"model",
]
pruneopts = ""
revision = "d0f7cd64bda49e08b22ae8a730aa57aa0db125d6"
[[projects]]
branch = "master"
digest = "1:61df0898746840afc7be5dc2c3eeec83022fab70df11ecee5b16c85e912cf5ed"
name = "github.com/prometheus/procfs"
packages = [
".",
"internal/util",
"nfs",
"xfs",
]
pruneopts = ""
revision = "8b1c2da0d56deffdbb9e48d4414b4e674bd8083e"
[[projects]]
branch = "master"
digest = "1:146327ce93be37e68bd3ff8541090d96da8cb3adc9e35d57570e9170a29f6bf6"
name = "github.com/spf13/cobra"
packages = ["."]
pruneopts = ""
revision = "b78744579491c1ceeaaa3b40205e56b0591b93a3"
[[projects]]
digest = "1:261bc565833ef4f02121450d74eb88d5ae4bd74bfe5d0e862cddb8550ec35000"
name = "github.com/spf13/pflag"
packages = ["."]
pruneopts = ""
revision = "e57e3eeb33f795204c1ca35f56c44f83227c6e66"
version = "v1.0.0"
[[projects]]
digest = "1:3926a4ec9a4ff1a072458451aa2d9b98acd059a45b38f7335d31e06c3d6a0159"
name = "github.com/stretchr/testify"
packages = [
"assert",
"require",
]
pruneopts = ""
revision = "69483b4bd14f5845b5a1e55bca19e954e827f1d0"
version = "v1.1.4"
[[projects]]
branch = "v2"
digest = "1:9d92186f609a73744232323416ddafd56fae67cb552162cc190ab903e36900dd"
name = "github.com/zrepl/yaml-config"
packages = ["."]
pruneopts = ""
revision = "af27d27978ad95808723a62d87557d63c3ff0605"
[[projects]]
branch = "master"
digest = "1:9c286cf11d0ca56368185bada5dd6d97b6be4648fc26c354fcba8df7293718f7"
name = "golang.org/x/sys"
packages = ["unix"]
pruneopts = ""
revision = "bf42f188b9bc6f2cf5b8ee5a912ef1aedd0eba4c"
[[projects]]
digest = "1:5acd3512b047305d49e8763eef7ba423901e85d5dd2fd1e71778a0ea8de10bd4"
name = "golang.org/x/text"
packages = [
"encoding",
"encoding/internal/identifier",
"internal/gen",
"transform",
"unicode/cldr",
]
pruneopts = ""
revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0"
version = "v0.3.0"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
input-imports = [
"github.com/fatih/color",
"github.com/gdamore/tcell/termbox",
"github.com/go-logfmt/logfmt",
"github.com/golang/protobuf/proto",
"github.com/jinzhu/copier",
"github.com/kr/pretty",
"github.com/mattn/go-isatty",
"github.com/pkg/errors",
"github.com/problame/go-netssh",
"github.com/problame/go-rwccmd",
"github.com/problame/go-streamrpc",
"github.com/prometheus/client_golang/prometheus",
"github.com/prometheus/client_golang/prometheus/promhttp",
"github.com/spf13/cobra",
"github.com/spf13/pflag",
"github.com/stretchr/testify/assert",
"github.com/stretchr/testify/require",
"github.com/zrepl/yaml-config",
"golang.org/x/sys/unix",
]
solver-name = "gps-cdcl"
solver-version = 1
-75
View File
@@ -1,75 +0,0 @@
ignored = [ "github.com/inconshreveable/mousetrap" ]
[[constraint]]
branch = "master"
name = "github.com/ftrvxmtrx/fd"
[[constraint]]
branch = "master"
name = "github.com/jinzhu/copier"
[[constraint]]
branch = "master"
name = "github.com/kr/pretty"
[[constraint]]
branch = "master"
name = "github.com/mitchellh/go-homedir"
[[constraint]]
branch = "master"
name = "github.com/mitchellh/mapstructure"
[[constraint]]
name = "github.com/pkg/errors"
version = "0.8.0"
[[constraint]]
branch = "master"
name = "github.com/spf13/cobra"
[[constraint]]
name = "github.com/spf13/viper"
version = "1.0.0"
[[constraint]]
name = "github.com/stretchr/testify"
version = "1.1.4"
[[constraint]]
branch = "v2"
name = "github.com/zrepl/yaml-config"
[[constraint]]
name = "github.com/go-logfmt/logfmt"
version = "*"
[[constraint]]
name = "github.com/problame/go-rwccmd"
branch = "master"
[[constraint]]
name = "github.com/problame/go-netssh"
branch = "master"
[[constraint]]
name = "github.com/prometheus/client_golang"
branch = "master"
[[constraint]]
name = "github.com/golang/protobuf"
version = "1.2.0"
[[constraint]]
name = "github.com/fatih/color"
version = "1.7.0"
[[constraint]]
name = "github.com/problame/go-streamrpc"
version = "0.5.0"
[[constraint]]
name = "github.com/gdamore/tcell"
version = "1.0.0"
+158 -107
View File
@@ -1,37 +1,6 @@
.PHONY: generate build test vet cover release docs docs-clean clean vendordeps .PHONY: generate build test vet cover release docs docs-clean clean format lint platformtest
.DEFAULT_GOAL := build .PHONY: release bins-all release-noarch
.DEFAULT_GOAL := zrepl-bin
ROOT := github.com/zrepl/zrepl
SUBPKGS += client
SUBPKGS += config
SUBPKGS += daemon
SUBPKGS += daemon/filters
SUBPKGS += daemon/job
SUBPKGS += daemon/logging
SUBPKGS += daemon/nethelpers
SUBPKGS += daemon/pruner
SUBPKGS += daemon/snapper
SUBPKGS += daemon/streamrpcconfig
SUBPKGS += daemon/transport
SUBPKGS += daemon/transport/connecter
SUBPKGS += daemon/transport/serve
SUBPKGS += endpoint
SUBPKGS += logger
SUBPKGS += pruning
SUBPKGS += pruning/retentiongrid
SUBPKGS += replication
SUBPKGS += replication/fsrep
SUBPKGS += replication/pdu
SUBPKGS += replication/internal/diff
SUBPKGS += tlsconf
SUBPKGS += util
SUBPKGS += util/socketpair
SUBPKGS += util/watchdog
SUBPKGS += util/envconst
SUBPKGS += version
SUBPKGS += zfs
_TESTPKGS := $(ROOT) $(foreach p,$(SUBPKGS),$(ROOT)/$(p))
ARTIFACTDIR := artifacts ARTIFACTDIR := artifacts
@@ -39,67 +8,180 @@ ifdef ZREPL_VERSION
_ZREPL_VERSION := $(ZREPL_VERSION) _ZREPL_VERSION := $(ZREPL_VERSION)
endif endif
ifndef _ZREPL_VERSION ifndef _ZREPL_VERSION
_ZREPL_VERSION := $(shell git describe --dirty 2>/dev/null || echo "ZREPL_BUILD_INVALID_VERSION" ) _ZREPL_VERSION := $(shell git describe --always --dirty 2>/dev/null || echo "ZREPL_BUILD_INVALID_VERSION" )
ifeq ($(_ZREPL_VERSION),ZREPL_BUILD_INVALID_VERSION) # can't use .SHELLSTATUS because Debian Stretch is still on gmake 4.1 ifeq ($(_ZREPL_VERSION),ZREPL_BUILD_INVALID_VERSION) # can't use .SHELLSTATUS because Debian Stretch is still on gmake 4.1
$(error cannot infer variable ZREPL_VERSION using git and variable is not overriden by make invocation) $(error cannot infer variable ZREPL_VERSION using git and variable is not overriden by make invocation)
endif endif
endif endif
GO := go
GOOS ?= $(shell bash -c 'source <($(GO) env) && echo "$$GOOS"')
GOARCH ?= $(shell bash -c 'source <($(GO) env) && echo "$$GOARCH"')
GOARM ?= $(shell bash -c 'source <($(GO) env) && echo "$$GOARM"')
GOHOSTOS ?= $(shell bash -c 'source <($(GO) env) && echo "$$GOHOSTOS"')
GOHOSTARCH ?= $(shell bash -c 'source <($(GO) env) && echo "$$GOHOSTARCH"')
GO_ENV_VARS := GO111MODULE=on
GO_LDFLAGS := "-X github.com/zrepl/zrepl/version.zreplVersion=$(_ZREPL_VERSION)" GO_LDFLAGS := "-X github.com/zrepl/zrepl/version.zreplVersion=$(_ZREPL_VERSION)"
GO_MOD_READONLY := -mod=readonly
GO_EXTRA_BUILDFLAGS :=
GO_BUILDFLAGS := $(GO_MOD_READONLY) $(GO_EXTRA_BUILDFLAGS)
GO_BUILD := $(GO_ENV_VARS) $(GO) build $(GO_BUILDFLAGS) -ldflags $(GO_LDFLAGS)
GOLANGCI_LINT := golangci-lint
ifneq ($(GOARM),)
ZREPL_TARGET_TUPLE := $(GOOS)-$(GOARCH)v$(GOARM)
else
ZREPL_TARGET_TUPLE := $(GOOS)-$(GOARCH)
endif
GO_BUILD := go build -ldflags $(GO_LDFLAGS) .PHONY: printvars
printvars:
@echo GOOS=$(GOOS)
@echo GOARCH=$(GOARCH)
@echo GOARM=$(GOARM)
RELEASE_BINS := $(ARTIFACTDIR)/zrepl-freebsd-amd64
RELEASE_BINS += $(ARTIFACTDIR)/zrepl-linux-amd64
RELEASE_BINS += $(ARTIFACTDIR)/zrepl-darwin-amd64
RELEASE_NOARCH := $(ARTIFACTDIR)/zrepl-noarch.tar ##################### PRODUCING A RELEASE #############
THIS_PLATFORM_RELEASE_BIN := $(shell bash -c 'source <(go env) && echo "zrepl-$${GOOS}-$${GOARCH}"' ) .PHONY: release wrapup-and-checksum check-git-clean sign clean
vendordeps: release: clean
dep ensure -v -vendor-only # no cross-platform support for target test
$(MAKE) test
$(MAKE) bins-all
$(MAKE) noarch
$(MAKE) wrapup-and-checksum
$(MAKE) check-git-clean
ifeq (SIGN, 1)
$(make) sign
endif
@echo "ZREPL RELEASE ARTIFACTS AVAILABLE IN artifacts/release"
generate: #not part of the build, must do that manually # expects `release` target to have run before
protoc -I=replication/pdu --go_out=replication/pdu replication/pdu/pdu.proto NOARCH_TARBALL := $(ARTIFACTDIR)/zrepl-noarch.tar
@for pkg in $(_TESTPKGS); do\ wrapup-and-checksum:
go generate "$$pkg" || exit 1; \ rm -f $(NOARCH_TARBALL)
done; tar --mtime='1970-01-01' --sort=name \
--transform 's/$(ARTIFACTDIR)/zrepl-$(_ZREPL_VERSION)-noarch/' \
--transform 's#dist#zrepl-$(_ZREPL_VERSION)-noarch/dist#' \
--transform 's#config/samples#zrepl-$(_ZREPL_VERSION)-noarch/config#' \
-acf $(NOARCH_TARBALL) \
$(ARTIFACTDIR)/docs/html \
$(ARTIFACTDIR)/bash_completion \
$(ARTIFACTDIR)/_zrepl.zsh_completion \
$(ARTIFACTDIR)/go_env.txt \
dist \
config/samples
rm -rf "$(ARTIFACTDIR)/release"
mkdir -p "$(ARTIFACTDIR)/release"
cp -l $(ARTIFACTDIR)/zrepl-* \
$(ARTIFACTDIR)/platformtest-* \
"$(ARTIFACTDIR)/release"
cd "$(ARTIFACTDIR)/release" && sha512sum $$(ls | sort) > sha512sum.txt
build: check-git-clean:
@echo "INFO: In case of missing dependencies, run 'make vendordeps'" @# note that we use ZREPL_VERSION and not _ZREPL_VERSION because we want to detect the override
$(GO_BUILD) -o "$(ARTIFACTDIR)/zrepl" @if git describe --always --dirty 2>/dev/null | grep dirty >/dev/null; then \
echo '[INFO] either git reports checkout is dirty or git is not installed or this is not a git checkout'; \
test: if [ "$(ZREPL_VERSION)" = "" ]; then \
@for pkg in $(_TESTPKGS); do \ echo '[WARN] git checkout is dirty and make variable ZREPL_VERSION was not used to override'; \
echo "Testing $$pkg"; \ git status; \
go test "$$pkg" || exit 1; \ echo "git diff:"; \
done; git diff | cat; \
exit 1; \
vet:
@for pkg in $(_TESTPKGS); do \
echo "Vetting $$pkg"; \
go vet "$$pkg" || exit 1; \
done;
cover: artifacts
@for pkg in $(_TESTPKGS); do \
profile="$(ARTIFACTDIR)/cover-$$(basename $$pkg).out"; \
go test -coverprofile "$$profile" $$pkg || exit 1; \
if [ -f "$$profile" ]; then \
go tool cover -html="$$profile" -o "$${profile}.html" || exit 2; \
fi; \ fi; \
done; fi;
sign:
gpg -u "89BC 5D89 C845 568B F578 B306 CDBD 8EC8 E27C A5FC" \
--armor \
--detach-sign $(ARTIFACTDIR)/release/sha512sum.txt
clean: docs-clean
rm -rf "$(ARTIFACTDIR)"
##################### BINARIES #####################
.PHONY: bins-all lint test vet zrepl-bin platformtest-bin
BINS_ALL_TARGETS := zrepl-bin platformtest-bin vet lint
GO_SUPPORTS_ILLUMOS := $(shell $(GO) version | gawk -F '.' '/^go version /{split($$0, comps, " "); split(comps[3], v, "."); if (v[1] == "go1" && v[2] >= 13) { print "illumos"; } else { print "noillumos"; }}')
bins-all:
$(MAKE) $(BINS_ALL_TARGETS) GOOS=freebsd GOARCH=amd64
$(MAKE) $(BINS_ALL_TARGETS) GOOS=freebsd GOARCH=386
$(MAKE) $(BINS_ALL_TARGETS) GOOS=linux GOARCH=amd64
$(MAKE) $(BINS_ALL_TARGETS) GOOS=linux GOARCH=arm64
$(MAKE) $(BINS_ALL_TARGETS) GOOS=linux GOARCH=arm GOARM=7
$(MAKE) $(BINS_ALL_TARGETS) GOOS=linux GOARCH=386
$(MAKE) $(BINS_ALL_TARGETS) GOOS=darwin GOARCH=amd64
$(MAKE) $(BINS_ALL_TARGETS) GOOS=solaris GOARCH=amd64
ifeq ($(GO_SUPPORTS_ILLUMOS), illumos)
$(MAKE) $(BINS_ALL_TARGETS) GOOS=illumos GOARCH=amd64
else ifeq ($(GO_SUPPORTS_ILLUMOS), noillumos)
@echo "SKIPPING ILLUMOS BUILD BECAUSE GO VERSION DOESN'T SUPPORT IT"
else
@echo "CANNOT DETERMINE WHETHER GO VERSION SUPPORTS GOOS=illumos"; exit 1
endif
lint:
$(GO_ENV_VARS) $(GOLANGCI_LINT) run ./...
test:
$(GO_ENV_VARS) $(GO) test $(GO_BUILDFLAGS) ./...
vet:
$(GO_ENV_VARS) $(GO) vet $(GO_BUILDFLAGS) ./...
zrepl-bin:
$(GO_BUILD) -o "$(ARTIFACTDIR)/zrepl-$(ZREPL_TARGET_TUPLE)"
platformtest-bin:
$(GO_BUILD) -o "$(ARTIFACTDIR)/platformtest-$(ZREPL_TARGET_TUPLE)" ./platformtest/harness
##################### DEV TARGETS #####################
# not part of the build, must do that manually
.PHONY: generate format platformtest
generate:
protoc -I=replication/logic/pdu --go_out=plugins=grpc:replication/logic/pdu replication/logic/pdu/pdu.proto
$(GO_ENV_VARS) $(GO) generate $(GO_BUILDFLAGS) -x ./...
format:
goimports -srcdir . -local 'github.com/zrepl/zrepl' -w $(shell find . -type f -name '*.go' -not -path "./vendor/*" -not -name '*.pb.go' -not -name '*_enumer.go')
ZREPL_PLATFORMTEST_POOLNAME := zreplplatformtest
ZREPL_PLATFORMTEST_IMAGEPATH := /tmp/zreplplatformtest.pool.img
ZREPL_PLATFORMTEST_MOUNTPOINT := /tmp/zreplplatformtest.pool
ZREPL_PLATFORMTEST_ZFS_LOG := /tmp/zreplplatformtest.zfs.log
# ZREPL_PLATFORMTEST_STOP_AND_KEEP := -failure.stop-and-keep-pool
ZREPL_PLATFORMTEST_ARGS :=
platformtest: # do not track dependency on platformtest-bin to allow build of platformtest outside of test VM
rm -f "$(ZREPL_PLATFORMTEST_ZFS_LOG)"
platformtest/logmockzfs/logzfsenv "$(ZREPL_PLATFORMTEST_ZFS_LOG)" `which zfs` \
"$(ARTIFACTDIR)/platformtest-$(ZREPL_TARGET_TUPLE)" \
-poolname "$(ZREPL_PLATFORMTEST_POOLNAME)" \
-imagepath "$(ZREPL_PLATFORMTEST_IMAGEPATH)" \
-mountpoint "$(ZREPL_PLATFORMTEST_MOUNTPOINT)" \
$(ZREPL_PLATFORMTEST_STOP_AND_KEEP) \
$(ZREPL_PLATFORMTEST_ARGS)
##################### NOARCH #####################
.PHONY: noarch $(ARTIFACTDIR)/bash_completion $(ARTIFACTDIR)/_zrepl.zsh_completion $(ARTIFACTDIR)/go_env.txt docs docs-clean
$(ARTIFACTDIR): $(ARTIFACTDIR):
mkdir -p "$@" mkdir -p "$@"
$(ARTIFACTDIR)/docs: $(ARTIFACTDIR) $(ARTIFACTDIR)/docs: $(ARTIFACTDIR)
mkdir -p "$@" mkdir -p "$@"
$(ARTIFACTDIR)/bash_completion: $(RELEASE_BINS) noarch: $(ARTIFACTDIR)/bash_completion $(ARTIFACTDIR)/_zrepl.zsh_completion $(ARTIFACTDIR)/go_env.txt docs
artifacts/$(THIS_PLATFORM_RELEASE_BIN) bashcomp "$@" # pass
$(ARTIFACTDIR)/go_version.txt: $(ARTIFACTDIR)/bash_completion:
go version > $@ $(MAKE) zrepl-bin GOOS=$(GOHOSTOS) GOARCH=$(GOHOSTARCH)
artifacts/zrepl-$(GOHOSTOS)-$(GOHOSTARCH) gencompletion bash "$@"
$(ARTIFACTDIR)/_zrepl.zsh_completion:
$(MAKE) zrepl-bin GOOS=$(GOHOSTOS) GOARCH=$(GOHOSTARCH)
artifacts/zrepl-$(GOHOSTOS)-$(GOHOSTARCH) gencompletion zsh "$@"
$(ARTIFACTDIR)/go_env.txt:
$(GO_ENV_VARS) $(GO) env > $@
docs: $(ARTIFACTDIR)/docs docs: $(ARTIFACTDIR)/docs
make -C docs \ make -C docs \
@@ -110,34 +192,3 @@ docs-clean:
make -C docs \ make -C docs \
clean \ clean \
BUILDDIR=../artifacts/docs BUILDDIR=../artifacts/docs
.PHONY: $(RELEASE_BINS)
# TODO: two wildcards possible
$(RELEASE_BINS): $(ARTIFACTDIR)/zrepl-%-amd64: generate $(ARTIFACTDIR) vet test
@echo "INFO: In case of missing dependencies, run 'make vendordeps'"
GOOS=$* GOARCH=amd64 $(GO_BUILD) -o "$(ARTIFACTDIR)/zrepl-$*-amd64"
$(RELEASE_NOARCH): docs $(ARTIFACTDIR)/bash_completion $(ARTIFACTDIR)/go_version.txt
tar --mtime='1970-01-01' --sort=name \
--transform 's/$(ARTIFACTDIR)/zrepl-$(_ZREPL_VERSION)-noarch/' \
-acf $@ \
$(ARTIFACTDIR)/docs/html \
$(ARTIFACTDIR)/bash_completion \
$(ARTIFACTDIR)/go_version.txt
release: $(RELEASE_BINS) $(RELEASE_NOARCH)
rm -rf "$(ARTIFACTDIR)/release"
mkdir -p "$(ARTIFACTDIR)/release"
cp $^ "$(ARTIFACTDIR)/release"
cd "$(ARTIFACTDIR)/release" && sha512sum $$(ls | sort) > sha512sum.txt
@# note that we use ZREPL_VERSION and not _ZREPL_VERSION because we want to detect the override
@if git describe --dirty 2>/dev/null | grep dirty >/dev/null; then \
if [ "$(ZREPL_VERSION)" == "" ]; then \
echo "[WARN] git checkout is dirty and make variable ZREPL_VERSION was not used to override"; \
exit 1; \
fi; \
fi;
clean: docs-clean
rm -rf "$(ARTIFACTDIR)"
+68 -14
View File
@@ -1,3 +1,11 @@
[![GitHub license](https://img.shields.io/github/license/zrepl/zrepl.svg)](https://github.com/zrepl/zrepl/blob/master/LICENSE)
[![Language: Go](https://img.shields.io/badge/language-Go-6ad7e5.svg)](https://golang.org/)
[![User Docs](https://img.shields.io/badge/docs-web-blue.svg)](https://zrepl.github.io)
[![Donate via Patreon](https://img.shields.io/endpoint.svg?url=https%3A%2F%2Fshieldsio-patreon.herokuapp.com%2Fzrepl%2Fpledges&style=flat&color=yellow)](https://www.patreon.com/zrepl)
[![Donate via Liberapay](https://img.shields.io/liberapay/receives/zrepl.svg?logo=liberapay)](https://liberapay.com/zrepl/donate)
[![Donate via PayPal](https://img.shields.io/badge/donate-paypal-yellow.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=R5QSXJVYHGX96)
[![Twitter](https://img.shields.io/twitter/url/https/github.com/zrepl/zrepl.svg?style=social)](https://twitter.com/intent/tweet?text=Wow:&url=https%3A%2F%2Fgithub.com%2Fzrepl%2Fzrepl)
# zrepl # zrepl
zrepl is a one-stop ZFS backup & replication solution. zrepl is a one-stop ZFS backup & replication solution.
@@ -16,6 +24,7 @@ zrepl is a one-stop ZFS backup & replication solution.
If so, think of an expressive configuration example. If so, think of an expressive configuration example.
2. Think of at least one use case that generalizes from your concrete application. 2. Think of at least one use case that generalizes from your concrete application.
3. Open an issue on GitHub with example conf & use case attached. 3. Open an issue on GitHub with example conf & use case attached.
4. **Optional**: [Post a bounty](https://www.bountysource.com/teams/zrepl) on the issue, or [contact Christian Schwarz](https://cschwarz.com) for contract work.
The above does not apply if you already implemented everything. The above does not apply if you already implemented everything.
Check out the *Coding Workflow* section below for details. Check out the *Coding Workflow* section below for details.
@@ -26,40 +35,73 @@ Check out the *Coding Workflow* section below for details.
* Make sure your distro is compatible with the paths in `docs/installation.rst`. * Make sure your distro is compatible with the paths in `docs/installation.rst`.
* Ship a default config that adheres to your distro's `hier` and logging system. * Ship a default config that adheres to your distro's `hier` and logging system.
* Ship a service manager file and _please_ try to upstream it to this repository. * Ship a service manager file and _please_ try to upstream it to this repository.
* `dist/systemd` contains a Systemd unit template.
* Ship other material provided in `./dist`, e.g. in `/usr/share/zrepl/`.
* Use `make release ZREPL_VERSION='mydistro-1.2.3_1'` * Use `make release ZREPL_VERSION='mydistro-1.2.3_1'`
* Your distro's name and any versioning supplemental to zrepl's (e.g. package revision) should be in this string * Your distro's name and any versioning supplemental to zrepl's (e.g. package revision) should be in this string
* Use `make platformtest` **on a test system** to validate that zrepl's abstractions on top of ZFS work with the system ZFS.
* Make sure you are informed about new zrepl versions, e.g. by subscribing to GitHub's release RSS feed. * Make sure you are informed about new zrepl versions, e.g. by subscribing to GitHub's release RSS feed.
## Developer Documentation ## Developer Documentation
zrepl is written in [Go](https://golang.org) and uses [`dep`](https://github.com/golang/dep) to manage dependencies. zrepl is written in [Go](https://golang.org) and uses [Go modules](https://github.com/golang/go/wiki/Modules) to manage dependencies.
The documentation is written in [ReStructured Text](http://docutils.sourceforge.net/rst.html) using the [Sphinx](https://www.sphinx-doc.org) framework. The documentation is written in [ReStructured Text](http://docutils.sourceforge.net/rst.html) using the [Sphinx](https://www.sphinx-doc.org) framework.
To get started, run `./lazy.sh devsetup` to easily install build dependencies and read `docs/installation.rst -> Compiling from Source`. To get started, run `./lazy.sh devsetup` to easily install build dependencies and read `docs/installation.rst -> Compiling from Source`.
`lazy.sh` uses `python3-pip` to fetch the build dependencies for the docs - you might want to use a [venv](https://docs.python.org/3/library/venv.html).
### Overall Architecture If you just want to install the Go dependencies, run `./lazy.sh godep`.
The application architecture is documented as part of the user docs in the *Implementation* section (`docs/content/impl`).
Make sure to develop an understanding how zrepl is typically used by studying the user docs first.
### Project Structure ### Project Structure
``` ```
├── cmd ├── artifacts # build artifcats generate by make
│   ├── endpoint # implementations of endpoints for package replication ├── cli # wrapper around CLI package cobra
│   ├── sampleconf # example configuration ├── client # all subcommands that are not `daemon`
├── config # config data types (=> package yaml-config)
│   └── samples
├── daemon # the implementation of `zrepl daemon` subcommand
│   ├── filters
│   ├── hooks # snapshot hooks
│   ├── job # job implementations
│   ├── logging # logging outlets + formatters
│   ├── nethelpers
│   ├── prometheus
│   ├── pruner # pruner implementation
│   ├── snapper # snapshotter implementation
├── dist # supplemental material for users & package maintainers
├── docs # sphinx-based documentation ├── docs # sphinx-based documentation
│   ├── **/*.rst # documentation in reStructuredText │   ├── **/*.rst # documentation in reStructuredText
│   ├── sphinxconf │   ├── sphinxconf
│   │   └── conf.py # sphinx config (see commit 445a280 why its not in docs/) │   │   └── conf.py # sphinx config (see commit 445a280 why its not in docs/)
│   ├── requirements.txt # pip3 requirements to build documentation │   ├── requirements.txt # pip3 requirements to build documentation
│   ├── publish.sh # shell script for automated rendering & deploy to zrepl.github.io repo │   ├── publish.sh # shell script for automated rendering & deploy to zrepl.github.io repo
│   ── public_git # checkout of zrepl.github.io managed by above shell script │   ── public_git # checkout of zrepl.github.io managed by above shell script
├── logger # logger package used by zrepl ├── endpoint # implementation of replication endpoints (=> package replication)
├── replication # replication functionality ├── logger # our own logger package
├── rpc # rpc protocol implementation ├── platformtest # test suite for our zfs abstractions (error classification, etc)
├── pruning # pruning rules (the logic, not the actual execution)
│   └── retentiongrid
├── replication
│ ├── driver # the driver of the replication logic (status reporting, error handling)
│ ├── logic # planning & executing replication steps via rpc
| |   └── pdu # the generated gRPC & protobuf code used in replication (and endpoints)
│ └── report # the JSON-serializable report datastructures exposed to the client
├── rpc # the hybrid gRPC + ./dataconn RPC client: connects to a remote replication.Endpoint
│ ├── dataconn # Bulk data-transfer RPC protocol
│ ├── grpcclientidentity # adaptor to inject package transport's 'client identity' concept into gRPC contexts
│ ├── netadaptor # adaptor to convert a package transport's Connecter and Listener into net.* primitives
│ ├── transportmux # TCP connecter and listener used to split control & data traffic
│ └── versionhandshake # replication protocol version handshake perfomed on newly established connections
├── tlsconf # abstraction for Go TLS server + client config
├── transport # transport implementations
│ ├── fromconfig
│ ├── local
│ ├── ssh
│ ├── tcp
│ └── tls
├── util ├── util
── zfs # ZFS wrappers, filesystemm diffing ── version # abstraction for versions (filled during build by Makefile)
└── zfs # zfs(8) wrappers
``` ```
### Coding Workflow ### Coding Workflow
@@ -92,3 +134,15 @@ There will not be a big refactoring (an attempt was made, but it's destroying to
However, new contributions & patches should fix naming without further notice in the commit message. However, new contributions & patches should fix naming without further notice in the commit message.
### RPC debugging
Optionally, there are various RPC-related environment variables, that if set to something != `""` will produce additional debug output on stderr:
https://github.com/zrepl/zrepl/blob/master/rpc/rpc_debug.go#L11
https://github.com/zrepl/zrepl/blob/master/rpc/dataconn/dataconn_debug.go#L11
https://github.com/zrepl/zrepl/blob/master/rpc/dataconn/stream/stream_debug.go#L11
https://github.com/zrepl/zrepl/blob/master/rpc/dataconn/heartbeatconn/heartbeatconn_debug.go#L11
+16 -8
View File
@@ -2,21 +2,29 @@ FROM golang:latest
RUN apt-get update && apt-get install -y \ RUN apt-get update && apt-get install -y \
python3-pip \ python3-pip \
unzip unzip \
gawk
RUN wget https://github.com/protocolbuffers/protobuf/releases/download/v3.6.1/protoc-3.6.1-linux-x86_64.zip ADD build.installprotoc.bash ./
RUN echo "6003de742ea3fcf703cfec1cd4a3380fd143081a2eb0e559065563496af27807 protoc-3.6.1-linux-x86_64.zip" | sha256sum -c RUN bash build.installprotoc.bash
RUN unzip -d /usr protoc-3.6.1-linux-x86_64.zip
ADD lazy.sh /tmp/lazy.sh ADD lazy.sh /tmp/lazy.sh
ADD docs/requirements.txt /tmp/requirements.txt ADD docs/requirements.txt /tmp/requirements.txt
ENV ZREPL_LAZY_DOCS_REQPATH=/tmp/requirements.txt ENV ZREPL_LAZY_DOCS_REQPATH=/tmp/requirements.txt
RUN /tmp/lazy.sh devsetup RUN /tmp/lazy.sh docdep
# 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 /src/github.com/zrepl/zrepl
RUN chmod -R 0777 /go
RUN mkdir -p /.cache && chmod -R 0777 /.cache RUN mkdir -p /.cache && chmod -R 0777 /.cache
WORKDIR /go/src/github.com/zrepl/zrepl
# $GOPATH is /go
# Go 1.12 doesn't use modules within GOPATH, but 1.13 and later do
# => store source outside of GOPATH
WORKDIR /src
# Install build tools (e.g. protobuf generator, stringer) into $GOPATH/bin
ADD build/ /tmp/build
RUN /tmp/lazy.sh godep
RUN chmod -R 0777 /go
+25
View File
@@ -0,0 +1,25 @@
#!/usr/bin/env bash
set -euo pipefail
set -x
MACH=$(uname -m)
MACH="${MACH/aarch64/aarch_64}"
VERSION=3.6.1
FILENAME=protoc-"$VERSION"-linux-"$MACH".zip
if [ -e "$FILENAME" ]; then
echo "$FILENAME" already exists 1>&2
exit 1
fi
wget https://github.com/protocolbuffers/protobuf/releases/download/v"$VERSION"/"$FILENAME"
stat "$FILENAME"
sha256sum -c --ignore-missing <<EOF
6003de742ea3fcf703cfec1cd4a3380fd143081a2eb0e559065563496af27807 protoc-3.6.1-linux-x86_64.zip
af8e5aaaf39ddec62ec8dd2be1b8d9602c6da66564883a16393ade5f71170922 protoc-3.6.1-linux-aarch_64.zip
EOF
unzip -d /usr "$FILENAME"
+51
View File
@@ -0,0 +1,51 @@
module github.com/zrepl/zrepl/build
go 1.12
require (
github.com/OpenPeeDeeP/depguard v0.0.0-20181229194401-1f388ab2d810 // indirect
github.com/alvaroloes/enumer v1.1.1
github.com/fatih/color v1.7.0 // indirect
github.com/go-critic/go-critic v0.3.4 // indirect
github.com/gogo/protobuf v1.2.1 // indirect
github.com/golang/mock v1.2.0 // indirect
github.com/golang/protobuf v1.2.0
github.com/golangci/errcheck v0.0.0-20181223084120-ef45e06d44b6 // indirect
github.com/golangci/go-tools v0.0.0-20190124090046-35a9f45a5db0 // indirect
github.com/golangci/gocyclo v0.0.0-20180528144436-0a533e8fa43d // indirect
github.com/golangci/gofmt v0.0.0-20181222123516-0b8337e80d98 // indirect
github.com/golangci/golangci-lint v1.17.1
github.com/golangci/gosec v0.0.0-20180901114220-8afd9cbb6cfb // indirect
github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219 // indirect
github.com/golangci/misspell v0.3.4 // indirect
github.com/golangci/revgrep v0.0.0-20180812185044-276a5c0a1039 // indirect
github.com/google/go-cmp v0.3.0 // indirect
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/pkg/errors v0.8.1 // indirect
github.com/sirupsen/logrus v1.4.2 // indirect
github.com/spf13/afero v1.2.2 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/viper v1.3.2 // indirect
github.com/stretchr/testify v1.4.0 // indirect
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980 // indirect
golang.org/x/sys v0.0.0-20191010194322-b09406accb47 // indirect
golang.org/x/tools v0.0.0-20190524210228-3d17549cdc6b
mvdan.cc/unparam v0.0.0-20190310220240-1b9ccfa71afe // indirect
sourcegraph.com/sqs/pbtypes v1.0.0 // indirect
)
// invalid dates in transitive dependencies (first validated in Go 1.13, didn't fail in earlier Go versions)
replace (
github.com/go-critic/go-critic v0.0.0-20181204210945-1df300866540 => github.com/go-critic/go-critic v0.3.5-0.20190526074819-1df300866540
github.com/go-critic/go-critic v0.0.0-20181204210945-ee9bf5809ead => github.com/go-critic/go-critic v0.3.5-0.20190210220443-ee9bf5809ead
github.com/golangci/errcheck v0.0.0-20181003203344-ef45e06d44b6 => github.com/golangci/errcheck v0.0.0-20181223084120-ef45e06d44b6
github.com/golangci/go-tools v0.0.0-20180109140146-35a9f45a5db0 => github.com/golangci/go-tools v0.0.0-20190124090046-35a9f45a5db0
github.com/golangci/go-tools v0.0.0-20180109140146-af6baa5dc196 => github.com/golangci/go-tools v0.0.0-20190318060251-af6baa5dc196
github.com/golangci/gofmt v0.0.0-20181105071733-0b8337e80d98 => github.com/golangci/gofmt v0.0.0-20181222123516-0b8337e80d98
github.com/golangci/gosec v0.0.0-20180901114220-66fb7fc33547 => github.com/golangci/gosec v0.0.0-20190211064107-66fb7fc33547
github.com/golangci/ineffassign v0.0.0-20180808204949-42439a7714cc => github.com/golangci/ineffassign v0.0.0-20190609212857-42439a7714cc
github.com/golangci/lint-1 v0.0.0-20180610141402-ee948d087217 => github.com/golangci/lint-1 v0.0.0-20190420132249-ee948d087217
golang.org/x/tools v0.0.0-20190125232054-379209517ffe => golang.org/x/tools v0.0.0-20190205201329-379209517ffe
mvdan.cc/unparam v0.0.0-20190124213536-fbb59629db34 => mvdan.cc/unparam v0.0.0-20190209190245-fbb59629db34
)
+280
View File
@@ -0,0 +1,280 @@
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/OpenPeeDeeP/depguard v0.0.0-20180806142446-a69c782687b2/go.mod h1:7/4sitnI9YlQgTLLk734QlzXT8DuHVnAyztLplQjk+o=
github.com/OpenPeeDeeP/depguard v0.0.0-20181229194401-1f388ab2d810 h1:pFks+oaqVWDFq0KsLQZFB2pQGB5Us0HfMSBENtieXOs=
github.com/OpenPeeDeeP/depguard v0.0.0-20181229194401-1f388ab2d810/go.mod h1:7/4sitnI9YlQgTLLk734QlzXT8DuHVnAyztLplQjk+o=
github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
github.com/alvaroloes/enumer v1.1.1 h1:v+1scNqEhSFAPb9tLwblQegeTXJhnw8hf9O0amTbOvQ=
github.com/alvaroloes/enumer v1.1.1/go.mod h1:FxrjvuXoDAx9isTJrv4c+T410zFi0DtXIT0m65DJ+Wo=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fatih/color v1.6.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/go-critic/go-critic v0.3.4 h1:FYaiaLjX0Nqei80KPhm4CyFQUBbmJwSrHxQ73taaGBc=
github.com/go-critic/go-critic v0.3.4/go.mod h1:AHR42Lk/E/aOznsrYdMYeIQS5RH10HZHSqP+rD6AJrc=
github.com/go-critic/go-critic v0.3.5-0.20190526074819-1df300866540/go.mod h1:+sE8vrLDS2M0pZkBk0wy6+nLdKexVDrl/jBqQOTDThA=
github.com/go-lintpack/lintpack v0.5.1/go.mod h1:NwZuYi2nUHho8XEIZ6SIxihrnPoqBTDqfpXvXAN0sXM=
github.com/go-lintpack/lintpack v0.5.2 h1:DI5mA3+eKdWeJ40nU4d6Wc26qmdG8RCi/btYq0TuRN0=
github.com/go-lintpack/lintpack v0.5.2/go.mod h1:NwZuYi2nUHho8XEIZ6SIxihrnPoqBTDqfpXvXAN0sXM=
github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8=
github.com/go-toolsmith/astcast v0.0.0-20181028201508-b7a89ed70af1/go.mod h1:TEo3Ghaj7PsZawQHxT/oBvo4HK/sl1RcuUHDKTTju+o=
github.com/go-toolsmith/astcast v1.0.0 h1:JojxlmI6STnFVG9yOImLeGREv8W2ocNUM+iOhR6jE7g=
github.com/go-toolsmith/astcast v1.0.0/go.mod h1:mt2OdQTeAQcY4DQgPSArJjHCcOwlX+Wl/kwN+LbLGQ4=
github.com/go-toolsmith/astcopy v0.0.0-20180903214859-79b422d080c4/go.mod h1:c9CPdq2AzM8oPomdlPniEfPAC6g1s7NqZzODt8y6ib8=
github.com/go-toolsmith/astcopy v1.0.0 h1:OMgl1b1MEpjFQ1m5ztEO06rz5CUd3oBv9RF7+DyvdG8=
github.com/go-toolsmith/astcopy v1.0.0/go.mod h1:vrgyG+5Bxrnz4MZWPF+pI4R8h3qKRjjyvV/DSez4WVQ=
github.com/go-toolsmith/astequal v0.0.0-20180903214952-dcb477bfacd6/go.mod h1:H+xSiq0+LtiDC11+h1G32h7Of5O3CYFJ99GVbS5lDKY=
github.com/go-toolsmith/astequal v1.0.0 h1:4zxD8j3JRFNyLN46lodQuqz3xdKSrur7U/sr0SDS/gQ=
github.com/go-toolsmith/astequal v1.0.0/go.mod h1:H+xSiq0+LtiDC11+h1G32h7Of5O3CYFJ99GVbS5lDKY=
github.com/go-toolsmith/astfmt v0.0.0-20180903215011-8f8ee99c3086/go.mod h1:mP93XdblcopXwlyN4X4uodxXQhldPGZbcEJIimQHrkg=
github.com/go-toolsmith/astfmt v1.0.0 h1:A0vDDXt+vsvLEdbMFJAUBI/uTbRw1ffOPnxsILnFL6k=
github.com/go-toolsmith/astfmt v1.0.0/go.mod h1:cnWmsOAuq4jJY6Ct5YWlVLmcmLMn1JUPuQIHCY7CJDw=
github.com/go-toolsmith/astinfo v0.0.0-20180906194353-9809ff7efb21/go.mod h1:dDStQCHtmZpYOmjRP/8gHHnCCch3Zz3oEgCdZVdtweU=
github.com/go-toolsmith/astp v0.0.0-20180903215135-0af7e3c24f30/go.mod h1:SV2ur98SGypH1UjcPpCatrV5hPazG6+IfNHbkDXBRrk=
github.com/go-toolsmith/astp v1.0.0 h1:alXE75TXgcmupDsMK1fRAy0YUzLzqPVvBKoyWV+KPXg=
github.com/go-toolsmith/astp v1.0.0/go.mod h1:RSyrtpVlfTFGDYRbrjyWP1pYu//tSFcvdYrA8meBmLI=
github.com/go-toolsmith/pkgload v0.0.0-20181119091011-e9e65178eee8 h1:vVouagbdmqTVlCIAxpyYsNNTbkKZ3V66VpKOLU/s6W4=
github.com/go-toolsmith/pkgload v0.0.0-20181119091011-e9e65178eee8/go.mod h1:WoMrjiy4zvdS+Bg6z9jZH82QXwkcgCBX6nOfnmdaHks=
github.com/go-toolsmith/pkgload v1.0.0 h1:4DFWWMXVfbcN5So1sBNW9+yeiMqLFGl1wFLTL5R0Tgg=
github.com/go-toolsmith/pkgload v1.0.0/go.mod h1:5eFArkbO80v7Z0kdngIxsRXRMTaX4Ilcwuh3clNrQJc=
github.com/go-toolsmith/strparse v0.0.0-20180903215201-830b6daa1241/go.mod h1:YI2nUKP9YGZnL/L1/DLFBfixrcjslWct4wyljWhSRy8=
github.com/go-toolsmith/strparse v1.0.0 h1:Vcw78DnpCAKlM20kSbAyO4mPfJn/lyYA4BJUDxe2Jb4=
github.com/go-toolsmith/strparse v1.0.0/go.mod h1:YI2nUKP9YGZnL/L1/DLFBfixrcjslWct4wyljWhSRy8=
github.com/go-toolsmith/typep v0.0.0-20181030061450-d63dc7650676/go.mod h1:JSQCQMUPdRlMZFswiq3TGpNp1GMktqkR2Ns5AIQkATU=
github.com/go-toolsmith/typep v1.0.0 h1:zKymWyA1TRYvqYrYDrfEMZULyrhcnGY3x7LDKU2XQaA=
github.com/go-toolsmith/typep v1.0.0/go.mod h1:JSQCQMUPdRlMZFswiq3TGpNp1GMktqkR2Ns5AIQkATU=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/golang/mock v1.0.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0 h1:28o5sBqPkBsMGnC6b4MvE2TzSr5/AT4c/1fLqVGIwlk=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2 h1:23T5iq8rbUYlhpt5DB4XJkc6BU31uODLD1o1gKvZmD0=
github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2/go.mod h1:k9Qvh+8juN+UKMCS/3jFtGICgW8O96FVaZsaxdzDkR4=
github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a h1:w8hkcTqaFpzKqonE9uMCefW1WDie15eSP/4MssdenaM=
github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a/go.mod h1:ryS0uhF+x9jgbj/N71xsEqODy9BN81/GonCZiOzirOk=
github.com/golangci/errcheck v0.0.0-20181223084120-ef45e06d44b6 h1:YYWNAGTKWhKpcLLt7aSj/odlKrSrelQwlovBpDuf19w=
github.com/golangci/errcheck v0.0.0-20181223084120-ef45e06d44b6/go.mod h1:DbHgvLiFKX1Sh2T1w8Q/h4NAI8MHIpzCdnBUDTXU3I0=
github.com/golangci/go-misc v0.0.0-20180628070357-927a3d87b613 h1:9kfjN3AdxcbsZBf8NjltjWihK2QfBBBZuv91cMFfDHw=
github.com/golangci/go-misc v0.0.0-20180628070357-927a3d87b613/go.mod h1:SyvUF2NxV+sN8upjjeVYr5W7tyxaT1JVtvhKhOn2ii8=
github.com/golangci/go-tools v0.0.0-20190124090046-35a9f45a5db0 h1:MRhC9XbUjE6XDOInSJ8pwHuPagqsyO89QDU9IdVhe3o=
github.com/golangci/go-tools v0.0.0-20190124090046-35a9f45a5db0/go.mod h1:unzUULGw35sjyOYjUt0jMTXqHlZPpPc6e+xfO4cd6mM=
github.com/golangci/go-tools v0.0.0-20190318060251-af6baa5dc196/go.mod h1:unzUULGw35sjyOYjUt0jMTXqHlZPpPc6e+xfO4cd6mM=
github.com/golangci/goconst v0.0.0-20180610141641-041c5f2b40f3 h1:pe9JHs3cHHDQgOFXJJdYkK6fLz2PWyYtP4hthoCMvs8=
github.com/golangci/goconst v0.0.0-20180610141641-041c5f2b40f3/go.mod h1:JXrF4TWy4tXYn62/9x8Wm/K/dm06p8tCKwFRDPZG/1o=
github.com/golangci/gocyclo v0.0.0-20180528134321-2becd97e67ee/go.mod h1:ozx7R9SIwqmqf5pRP90DhR2Oay2UIjGuKheCBCNwAYU=
github.com/golangci/gocyclo v0.0.0-20180528144436-0a533e8fa43d h1:pXTK/gkVNs7Zyy7WKgLXmpQ5bHTrq5GDsp8R9Qs67g0=
github.com/golangci/gocyclo v0.0.0-20180528144436-0a533e8fa43d/go.mod h1:ozx7R9SIwqmqf5pRP90DhR2Oay2UIjGuKheCBCNwAYU=
github.com/golangci/gofmt v0.0.0-20181222123516-0b8337e80d98 h1:0OkFarm1Zy2CjCiDKfK9XHgmc2wbDlRMD2hD8anAJHU=
github.com/golangci/gofmt v0.0.0-20181222123516-0b8337e80d98/go.mod h1:9qCChq59u/eW8im404Q2WWTrnBUQKjpNYKMbU4M7EFU=
github.com/golangci/golangci-lint v1.17.1 h1:lc8Hf9GPCjIr0hg3S/xhvFT1+Hydass8F1xchr8jkME=
github.com/golangci/golangci-lint v1.17.1/go.mod h1:+5sJSl2h3aly+fpmL2meSP8CaSKua2E4Twi9LPy7b1g=
github.com/golangci/gosec v0.0.0-20180901114220-8afd9cbb6cfb h1:Bi7BYmZVg4C+mKGi8LeohcP2GGUl2XJD4xCkJoZSaYc=
github.com/golangci/gosec v0.0.0-20180901114220-8afd9cbb6cfb/go.mod h1:ON/c2UR0VAAv6ZEAFKhjCLplESSmRFfZcDLASbI1GWo=
github.com/golangci/gosec v0.0.0-20190211064107-66fb7fc33547/go.mod h1:0qUabqiIQgfmlAmulqxyiGkkyF6/tOGSnY2cnPVwrzU=
github.com/golangci/ineffassign v0.0.0-20190609212857-42439a7714cc h1:gLLhTLMk2/SutryVJ6D4VZCU3CUqr8YloG7FPIBWFpI=
github.com/golangci/ineffassign v0.0.0-20190609212857-42439a7714cc/go.mod h1:e5tpTHCfVze+7EpLEozzMB3eafxo2KT5veNg1k6byQU=
github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219 h1:utua3L2IbQJmauC5IXdEA547bcoU5dozgQAfc8Onsg4=
github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219/go.mod h1:/X8TswGSh1pIozq4ZwCfxS0WA5JGXguxk94ar/4c87Y=
github.com/golangci/lint-1 v0.0.0-20190420132249-ee948d087217/go.mod h1:66R6K6P6VWk9I95jvqGxkqJxVWGFy9XlDwLwVz1RCFg=
github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca h1:kNY3/svz5T29MYHubXix4aDDuE3RWHkPvopM/EDv/MA=
github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca/go.mod h1:tvlJhZqDe4LMs4ZHD0oMUlt9G2LWuDGoisJTBzLMV9o=
github.com/golangci/misspell v0.0.0-20180809174111-950f5d19e770/go.mod h1:dEbvlSfYbMQDtrpRMQU675gSDLDNa8sCPPChZ7PhiVA=
github.com/golangci/misspell v0.3.4 h1:kAD5J0611Zo2VirUn9KFP15RjEqkmfEfnFxyIVxZCYg=
github.com/golangci/misspell v0.3.4/go.mod h1:dEbvlSfYbMQDtrpRMQU675gSDLDNa8sCPPChZ7PhiVA=
github.com/golangci/prealloc v0.0.0-20180630174525-215b22d4de21 h1:leSNB7iYzLYSSx3J/s5sVf4Drkc68W2wm4Ixh/mr0us=
github.com/golangci/prealloc v0.0.0-20180630174525-215b22d4de21/go.mod h1:tf5+bzsHdTM0bsB7+8mt0GUMvjCgwLpTapNZHU8AajI=
github.com/golangci/revgrep v0.0.0-20180526074752-d9c87f5ffaf0/go.mod h1:qOQCunEYvmd/TLamH+7LlVccLvUH5kZNhbCgTHoBbp4=
github.com/golangci/revgrep v0.0.0-20180812185044-276a5c0a1039 h1:XQKc8IYQOeRwVs36tDrEmTgDgP88d5iEURwpmtiAlOM=
github.com/golangci/revgrep v0.0.0-20180812185044-276a5c0a1039/go.mod h1:qOQCunEYvmd/TLamH+7LlVccLvUH5kZNhbCgTHoBbp4=
github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4 h1:zwtduBRr5SSWhqsYNgcuWO2kFlpdOZbP0+yRjmvPGys=
github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4/go.mod h1:Izgrg8RkN3rCIMLGE9CyYmU9pY2Jer6DgANEnZ/L/cQ=
github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/gostaticanalysis/analysisutil v0.0.0-20190318220348-4088753ea4d3 h1:JVnpOZS+qxli+rgVl98ILOXVNbW+kb5wcxeGx8ShUIw=
github.com/gostaticanalysis/analysisutil v0.0.0-20190318220348-4088753ea4d3/go.mod h1:eEOZF4jCKGi+aprrirO9e7WKB3beBRtWgqGunKl6pKE=
github.com/hashicorp/hcl v0.0.0-20180404174102-ef8a98b0bbce/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/gotool v0.0.0-20161130080628-0de1eaf82fa3/go.mod h1:jxZFDH7ILpTPQTk+E2s+z4CUas9lVNjIuKR4c5/zKgM=
github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.4.0/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/cpuid v0.0.0-20180405133222-e7e905edc00e/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
github.com/magiconair/properties v1.7.6/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-isatty v0.0.3 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-ps v0.0.0-20170309133038-4fdf99ab2936/go.mod h1:r1VsdOzOPt1ZSrGZWFoNhsAedKnEd6r9Np1+5blZCWk=
github.com/mitchellh/mapstructure v0.0.0-20180220230111-00c29f56e238/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mozilla/tls-observatory v0.0.0-20180409132520-8791a200eb40/go.mod h1:SrKMQvPiws7F7iqYp8/TX+IhxCYhzr6N/1yb8cwHsGk=
github.com/nbutton23/zxcvbn-go v0.0.0-20160627004424-a22cb81b2ecd/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU=
github.com/nbutton23/zxcvbn-go v0.0.0-20171102151520-eafdab6b0663 h1:Ri1EhipkbhWsffPJ3IPlrb4SkTOPa2PfRXp3jchBczw=
github.com/nbutton23/zxcvbn-go v0.0.0-20171102151520-eafdab6b0663/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU=
github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.6.0 h1:Ix8l273rp3QzYgXSR+c8d1fTG7UPgYkOSELPhiY/YGw=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
github.com/onsi/gomega v1.4.2 h1:3mYCb7aPxS/RU7TI1y4rkEn1oKmPRjNJLNEXgw7MH2I=
github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/pascaldekloe/name v0.0.0-20180628100202-0fd16699aae1 h1:/I3lTljEEDNYLho3/FUB7iD/oc2cEFgVmbHzV+O0PtU=
github.com/pascaldekloe/name v0.0.0-20180628100202-0fd16699aae1/go.mod h1:eD5JxqMiuNYyFNmyY9rkJ/slN8y59oEu4Ei7F8OoKWQ=
github.com/pelletier/go-toml v1.1.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quasilyte/go-consistent v0.0.0-20190521200055-c6f3937de18c/go.mod h1:5STLWrekHfjyYwxBRVRXNOSewLJ3PWfDJd1VyTS21fI=
github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.2.1/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/ryanuber/go-glob v0.0.0-20170128012129-256dc444b735/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
github.com/shirou/gopsutil v0.0.0-20180427012116-c95755e4bcd7/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q1u/4XEfrquwF8Lw7D7y5cD8CuHnfIc=
github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e h1:MZM7FHLqUHYI0Y/mQAt3d2aYa0SiNms/hFqC9qJYolM=
github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041 h1:llrF3Fs4018ePo4+G/HV/uQUqEI1HMDjCeOf2V6puPc=
github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ=
github.com/sirupsen/logrus v1.0.5/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sourcegraph/go-diff v0.5.1 h1:gO6i5zugwzo1RVTvgvfwCOSVegNuvnNi6bAD1QCmkHs=
github.com/sourcegraph/go-diff v0.5.1/go.mod h1:j2dHj3m8aZgQO8lMTcTnBcXkRRRqi34cd2MNlA9u1mE=
github.com/spf13/afero v1.1.0/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc=
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
github.com/spf13/cast v1.2.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg=
github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v0.0.2 h1:NfkwRbgViGoyjBKsLI0QMDcuMnhM+SBg3T0cGfpvKDE=
github.com/spf13/cobra v0.0.2/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/jwalterweatherman v0.0.0-20180109140146-7c0cea34c8ec/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/viper v1.0.2/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM=
github.com/spf13/viper v1.3.2 h1:VUFqw5KcqRf7i70GOzW7N+Q7+gxVBkSSqiXB12+JQ4M=
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/timakin/bodyclose v0.0.0-20190407043127-4a873e97b2bb h1:lI9ufgFfvuqRctP9Ny8lDDLbSWCMxBPletcSqrnyFYM=
github.com/timakin/bodyclose v0.0.0-20190407043127-4a873e97b2bb/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.2.0/go.mod h1:4vX61m6KN+xDduDNwXrhIAVZaZaZiQ1luJk8LWSxF3s=
github.com/valyala/quicktemplate v1.1.1/go.mod h1:EH+4AkTd43SvgIbQHYu59/cJyxDoOVRUAfrukLPuGJ4=
github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/net v0.0.0-20170915142106-8351a756f30f/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180911220305-26e67e76b6c3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190313220215-9f648a60d977 h1:actzWV6iWn3GLqN8dZjzsB+CLt+gaV2+wsxroxiQI8I=
golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980 h1:dfGZHvZk057jK2MCeWus/TowKpJ8y4AmooUzdBSR9GU=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20171026204733-164713f0dfce/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191010194322-b09406accb47 h1:/XfQ9z7ib8eEJX2hdgFTZJ/ntt0swNk5oYBziWeTCvY=
golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.0.0-20170915090833-1cbadb444a80/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20170915040203-e531a2a1c15f/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181117154741-2ddaf7f79a09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181205014116-22934f0fdb62/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190110163146-51295c7ec13a/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190121143147-24cd39ecf745/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190213192042-740235f6c0d8 h1:b0PLhFjEMqrIqsD4rS5sp0YxBYgdqKzX0KkkGGKLV8I=
golang.org/x/tools v0.0.0-20190213192042-740235f6c0d8/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190311215038-5c2858a9cfe5/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190322203728-c1a832b0ad89/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190521203540-521d6ed310dd/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524210228-3d17549cdc6b h1:iEAPfYPbYbxG/2lNN4cMOHkmgKNsCuUwkxlDCK46UlU=
golang.org/x/tools v0.0.0-20190524210228-3d17549cdc6b/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed h1:WX1yoOaKQfddO/mLzdV4wptyWgoH/6hwLs7QHTixo0I=
mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed/go.mod h1:Xkxe497xwlCKkIaQYRfC7CSLworTXY9RMqwhhCm+8Nc=
mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b h1:DxJ5nJdkhDlLok9K6qO+5290kphDJbHOQO1DFFFTeBo=
mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b/go.mod h1:2odslEg/xrtNQqCYg2/jCoyKnw3vv5biOc3JnIcYfL4=
mvdan.cc/unparam v0.0.0-20190209190245-fbb59629db34/go.mod h1:H6SUd1XjIs+qQCyskXg5OFSrilMRUkD8ePJpHKDPaeY=
mvdan.cc/unparam v0.0.0-20190310220240-1b9ccfa71afe h1:Ekmnp+NcP2joadI9pbK4Bva87QKZSeY7le//oiMrc9g=
mvdan.cc/unparam v0.0.0-20190310220240-1b9ccfa71afe/go.mod h1:BnhuWBAqxH3+J5bDybdxgw5ZfS+DsVd4iylsKQePN8o=
sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0=
sourcegraph.com/sqs/pbtypes v1.0.0 h1:f7lAwqviDEGvON4kRv0o5V7FT/IQK+tbkF664XMbP3o=
sourcegraph.com/sqs/pbtypes v1.0.0/go.mod h1:3AciMUv4qUuRHRHhOG4TZOB+72GdPVz5k+c648qsFS4=
+11
View File
@@ -0,0 +1,11 @@
// +build tools
package main
import (
_ "github.com/alvaroloes/enumer"
_ "github.com/golang/protobuf/protoc-gen-go"
_ "github.com/golangci/golangci-lint/cmd/golangci-lint"
_ "golang.org/x/tools/cmd/goimports"
_ "golang.org/x/tools/cmd/stringer"
)
+60 -28
View File
@@ -2,10 +2,12 @@ package cli
import ( import (
"fmt" "fmt"
"os"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag" "github.com/spf13/pflag"
"github.com/zrepl/zrepl/config" "github.com/zrepl/zrepl/config"
"os"
) )
var rootArgs struct { var rootArgs struct {
@@ -17,37 +19,67 @@ var rootCmd = &cobra.Command{
Short: "One-stop ZFS replication solution", Short: "One-stop ZFS replication solution",
} }
var bashcompCmd = &cobra.Command{ func init() {
Use: "bashcomp path/to/out/file", rootCmd.PersistentFlags().StringVar(&rootArgs.configPath, "config", "", "config file path")
Short: "generate bash completions", }
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 1 { var genCompletionCmd = &cobra.Command{
fmt.Fprintf(os.Stderr, "specify exactly one positional agument\n") Use: "gencompletion",
cmd.Usage() Short: "generate shell auto-completions",
os.Exit(1) }
}
if err := rootCmd.GenBashCompletionFile(args[0]); err != nil { type completionCmdInfo struct {
fmt.Fprintf(os.Stderr, "error generating bash completion: %s", err) genFunc func(outpath string) error
os.Exit(1) help string
} }
var completionCmdMap = map[string]completionCmdInfo{
"zsh": {
rootCmd.GenZshCompletionFile,
" save to file `_zrepl` in your zsh's $fpath",
},
"bash": {
rootCmd.GenBashCompletionFile,
" save to a path and source that path in your .bashrc",
}, },
Hidden: true,
} }
func init() { func init() {
rootCmd.PersistentFlags().StringVar(&rootArgs.configPath, "config", "", "config file path") for sh, info := range completionCmdMap {
rootCmd.AddCommand(bashcompCmd) sh, info := sh, info
genCompletionCmd.AddCommand(&cobra.Command{
Use: fmt.Sprintf("%s path/to/out/file", sh),
Short: fmt.Sprintf("generate %s completions", sh),
Example: info.help,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 1 {
fmt.Fprintf(os.Stderr, "specify exactly one positional agument\n")
err := cmd.Usage()
if err != nil {
panic(err)
}
os.Exit(1)
}
if err := info.genFunc(args[0]); err != nil {
fmt.Fprintf(os.Stderr, "error generating %s completion: %s", sh, err)
os.Exit(1)
}
},
})
}
rootCmd.AddCommand(genCompletionCmd)
} }
type Subcommand struct { type Subcommand struct {
Use string Use string
Short string Short string
NoRequireConfig bool Example string
Run func(subcommand *Subcommand, args []string) error NoRequireConfig bool
SetupFlags func(f *pflag.FlagSet) Run func(subcommand *Subcommand, args []string) error
SetupSubcommands func() []*Subcommand SetupFlags func(f *pflag.FlagSet)
SetupSubcommands func() []*Subcommand
config *config.Config config *config.Config
configErr error configErr error
} }
@@ -92,8 +124,9 @@ func AddSubcommand(s *Subcommand) {
func addSubcommandToCobraCmd(c *cobra.Command, s *Subcommand) { func addSubcommandToCobraCmd(c *cobra.Command, s *Subcommand) {
cmd := cobra.Command{ cmd := cobra.Command{
Use: s.Use, Use: s.Use,
Short: s.Short, Short: s.Short,
Example: s.Example,
} }
if s.SetupSubcommands == nil { if s.SetupSubcommands == nil {
cmd.Run = s.run cmd.Run = s.run
@@ -108,9 +141,8 @@ func addSubcommandToCobraCmd(c *cobra.Command, s *Subcommand) {
c.AddCommand(&cmd) c.AddCommand(&cmd)
} }
func Run() { func Run() {
if err := rootCmd.Execute(); err != nil { if err := rootCmd.Execute(); err != nil {
os.Exit(1) os.Exit(1)
} }
} }
+20 -12
View File
@@ -3,39 +3,49 @@ package client
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"os"
"github.com/kr/pretty" "github.com/kr/pretty"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/spf13/pflag" "github.com/spf13/pflag"
"github.com/zrepl/yaml-config" "github.com/zrepl/yaml-config"
"github.com/zrepl/zrepl/cli" "github.com/zrepl/zrepl/cli"
"github.com/zrepl/zrepl/config" "github.com/zrepl/zrepl/config"
"github.com/zrepl/zrepl/daemon/job" "github.com/zrepl/zrepl/daemon/job"
"github.com/zrepl/zrepl/daemon/logging" "github.com/zrepl/zrepl/daemon/logging"
"github.com/zrepl/zrepl/logger" "github.com/zrepl/zrepl/logger"
"os"
) )
var configcheckArgs struct { var configcheckArgs struct {
format string format string
what string what string
} }
var ConfigcheckCmd = &cli.Subcommand{ var ConfigcheckCmd = &cli.Subcommand{
Use: "configcheck", Use: "configcheck",
Short: "check if config can be parsed without errors", Short: "check if config can be parsed without errors",
SetupFlags: func(f *pflag.FlagSet) { SetupFlags: func(f *pflag.FlagSet) {
f.StringVar(&configcheckArgs.format, "format", "", "dump parsed config object [pretty|yaml|json]") f.StringVar(&configcheckArgs.format, "format", "", "dump parsed config object [pretty|yaml|json]")
f.StringVar(&configcheckArgs.what, "what", "all", "what to print [all|config|jobs|logging]") f.StringVar(&configcheckArgs.what, "what", "all", "what to print [all|config|jobs|logging]")
}, },
Run: func(subcommand *cli.Subcommand, args []string) error { Run: func(subcommand *cli.Subcommand, args []string) error {
formatMap := map[string]func(interface{}) { formatMap := map[string]func(interface{}){
"": func(i interface{}) {}, "": func(i interface{}) {},
"pretty": func(i interface{}) { pretty.Println(i) }, "pretty": func(i interface{}) {
if _, err := pretty.Println(i); err != nil {
panic(err)
}
},
"json": func(i interface{}) { "json": func(i interface{}) {
json.NewEncoder(os.Stdout).Encode(subcommand.Config()) if err := json.NewEncoder(os.Stdout).Encode(subcommand.Config()); err != nil {
panic(err)
}
}, },
"yaml": func(i interface{}) { "yaml": func(i interface{}) {
yaml.NewEncoder(os.Stdout).Encode(subcommand.Config()) if err := yaml.NewEncoder(os.Stdout).Encode(subcommand.Config()); err != nil {
panic(err)
}
}, },
} }
@@ -71,12 +81,11 @@ var ConfigcheckCmd = &cli.Subcommand{
} }
} }
whatMap := map[string]func(){
whatMap := map[string]func() {
"all": func() { "all": func() {
o := struct { o := struct {
config *config.Config config *config.Config
jobs []job.Job jobs []job.Job
logging *logger.Outlets logging *logger.Outlets
}{ }{
subcommand.Config(), subcommand.Config(),
@@ -109,4 +118,3 @@ var ConfigcheckCmd = &cli.Subcommand{
} }
}, },
} }
+3 -2
View File
@@ -4,10 +4,11 @@ import (
"bytes" "bytes"
"context" "context"
"encoding/json" "encoding/json"
"github.com/pkg/errors"
"io" "io"
"net" "net"
"net/http" "net/http"
"github.com/pkg/errors"
) )
func controlHttpClient(sockpath string) (client http.Client, err error) { func controlHttpClient(sockpath string) (client http.Client, err error) {
@@ -35,7 +36,7 @@ func jsonRequestResponse(c http.Client, endpoint string, req interface{}, res in
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
var msg bytes.Buffer var msg bytes.Buffer
io.CopyN(&msg, resp.Body, 4096) _, _ = io.CopyN(&msg, resp.Body, 4096) // ignore error, just display what we got
return errors.Errorf("%s", msg.String()) return errors.Errorf("%s", msg.String())
} }
+271
View File
@@ -0,0 +1,271 @@
package client
import (
"context"
"fmt"
"github.com/fatih/color"
"github.com/kr/pretty"
"github.com/pkg/errors"
"github.com/spf13/pflag"
"github.com/zrepl/zrepl/daemon/job"
"github.com/zrepl/zrepl/endpoint"
"github.com/zrepl/zrepl/zfs"
"github.com/zrepl/zrepl/cli"
"github.com/zrepl/zrepl/config"
)
var (
MigrateCmd = &cli.Subcommand{
Use: "migrate",
Short: "perform migration of the on-disk / zfs properties",
SetupSubcommands: func() []*cli.Subcommand {
return migrations
},
}
)
var migrations = []*cli.Subcommand{
&cli.Subcommand{
Use: "0.0.X:0.1:placeholder",
Run: doMigratePlaceholder0_1,
SetupFlags: func(f *pflag.FlagSet) {
f.BoolVar(&migratePlaceholder0_1Args.dryRun, "dry-run", false, "dry run")
},
},
&cli.Subcommand{
Use: "replication-cursor:v1-v2",
Run: doMigrateReplicationCursor,
SetupFlags: func(f *pflag.FlagSet) {
f.BoolVar(&migrateReplicationCursorArgs.dryRun, "dry-run", false, "dry run")
},
},
}
var migratePlaceholder0_1Args struct {
dryRun bool
}
func doMigratePlaceholder0_1(sc *cli.Subcommand, args []string) error {
if len(args) != 0 {
return fmt.Errorf("migration does not take arguments, got %v", args)
}
cfg := sc.Config()
ctx := context.Background()
allFSS, err := zfs.ZFSListMapping(ctx, zfs.NoFilter())
if err != nil {
return errors.Wrap(err, "cannot list filesystems")
}
type workItem struct {
jobName string
rootFS *zfs.DatasetPath
fss []*zfs.DatasetPath
}
var wis []workItem
for i, j := range cfg.Jobs {
var rfsS string
switch job := j.Ret.(type) {
case *config.SinkJob:
rfsS = job.RootFS
case *config.PullJob:
rfsS = job.RootFS
default:
fmt.Printf("ignoring job %q (%d/%d, type %T)\n", j.Name(), i, len(cfg.Jobs), j.Ret)
continue
}
rfs, err := zfs.NewDatasetPath(rfsS)
if err != nil {
return errors.Wrapf(err, "root fs for job %q is not a valid dataset path", j.Name())
}
var fss []*zfs.DatasetPath
for _, fs := range allFSS {
if fs.HasPrefix(rfs) {
fss = append(fss, fs)
}
}
wis = append(wis, workItem{j.Name(), rfs, fss})
}
for _, wi := range wis {
fmt.Printf("job %q => migrate filesystems below root_fs %q\n", wi.jobName, wi.rootFS.ToString())
if len(wi.fss) == 0 {
fmt.Printf("\tno filesystems\n")
continue
}
for _, fs := range wi.fss {
fmt.Printf("\t%q ... ", fs.ToString())
r, err := zfs.ZFSMigrateHashBasedPlaceholderToCurrent(ctx, fs, migratePlaceholder0_1Args.dryRun)
if err != nil {
fmt.Printf("error: %s\n", err)
} else if !r.NeedsModification {
fmt.Printf("unchanged (placeholder=%v)\n", r.OriginalState.IsPlaceholder)
} else {
fmt.Printf("migrate (placeholder=%v) (old value = %q)\n",
r.OriginalState.IsPlaceholder, r.OriginalState.RawLocalPropertyValue)
}
}
}
return nil
}
var migrateReplicationCursorArgs struct {
dryRun bool
}
var bold = color.New(color.Bold)
var succ = color.New(color.FgGreen)
var fail = color.New(color.FgRed)
var migrateReplicationCursorSkipSentinel = fmt.Errorf("skipping this filesystem")
func doMigrateReplicationCursor(sc *cli.Subcommand, args []string) error {
if len(args) != 0 {
return fmt.Errorf("migration does not take arguments, got %v", args)
}
cfg := sc.Config()
jobs, err := job.JobsFromConfig(cfg)
if err != nil {
fmt.Printf("cannot parse config:\n%s\n\n", err)
fmt.Printf("NOTE: this migration was released together with a change in job name requirements.\n")
return fmt.Errorf("exiting migration after error")
}
ctx := context.Background()
v1cursorJobs := make([]job.Job, 0, len(cfg.Jobs))
for i, j := range cfg.Jobs {
if jobs[i].Name() != j.Name() {
panic("implementation error")
}
switch j.Ret.(type) {
case *config.PushJob:
v1cursorJobs = append(v1cursorJobs, jobs[i])
case *config.SourceJob:
v1cursorJobs = append(v1cursorJobs, jobs[i])
default:
fmt.Printf("ignoring job %q (%d/%d, type %T), not supposed to create v1 replication cursors\n", j.Name(), i, len(cfg.Jobs), j.Ret)
continue
}
}
// scan all filesystems for v1 replication cursors
fss, err := zfs.ZFSListMapping(ctx, zfs.NoFilter())
if err != nil {
return errors.Wrap(err, "list filesystems")
}
var hadError bool
for _, fs := range fss {
bold.Printf("INSPECT FILESYSTEM %q\n", fs.ToString())
err := doMigrateReplicationCursorFS(ctx, v1cursorJobs, fs)
if err == migrateReplicationCursorSkipSentinel {
bold.Printf("FILESYSTEM SKIPPED\n")
} else if err != nil {
hadError = true
fail.Printf("MIGRATION FAILED: %s\n", err)
} else {
succ.Printf("FILESYSTEM %q COMPLETE\n", fs.ToString())
}
}
if hadError {
fail.Printf("\n\none or more filesystems could not be migrated, please inspect output and or re-run migration")
return errors.Errorf("")
}
return nil
}
func doMigrateReplicationCursorFS(ctx context.Context, v1CursorJobs []job.Job, fs *zfs.DatasetPath) error {
var owningJob job.Job = nil
for _, job := range v1CursorJobs {
conf := job.SenderConfig()
if conf == nil {
continue
}
pass, err := conf.FSF.Filter(fs)
if err != nil {
return errors.Wrapf(err, "filesystem filter error in job %q for fs %q", job.Name(), fs.ToString())
}
if !pass {
continue
}
if owningJob != nil {
return errors.Errorf("jobs %q and %q both match %q\ncannot attribute replication cursor to either one", owningJob.Name(), job.Name(), fs)
}
owningJob = job
}
if owningJob == nil {
fmt.Printf("no job's Filesystems filter matches\n")
return migrateReplicationCursorSkipSentinel
}
fmt.Printf("identified owning job %q\n", owningJob.Name())
bookmarks, err := zfs.ZFSListFilesystemVersions(ctx, fs, zfs.ListFilesystemVersionsOptions{
Types: zfs.Bookmarks,
})
if err != nil {
return errors.Wrapf(err, "list filesystem versions of %q", fs.ToString())
}
var oldCursor *zfs.FilesystemVersion
for i, fsv := range bookmarks {
_, _, err := endpoint.ParseReplicationCursorBookmarkName(fsv.ToAbsPath(fs))
if err != endpoint.ErrV1ReplicationCursor {
continue
}
if oldCursor != nil {
fmt.Printf("unexpected v1 replication cursor candidate: %q", fsv.ToAbsPath(fs))
return errors.Wrap(err, "multiple filesystem versions identified as v1 replication cursors")
}
oldCursor = &bookmarks[i]
}
if oldCursor == nil {
bold.Printf("no v1 replication cursor found for filesystem %q\n", fs.ToString())
return migrateReplicationCursorSkipSentinel
}
fmt.Printf("found v1 replication cursor:\n%s\n", pretty.Sprint(oldCursor))
mostRecentNew, err := endpoint.GetMostRecentReplicationCursorOfJob(ctx, fs.ToString(), owningJob.SenderConfig().JobID)
if err != nil {
return errors.Wrapf(err, "get most recent v2 replication cursor")
}
if mostRecentNew == nil {
return errors.Errorf("no v2 replication cursor found for job %q on filesystem %q", owningJob.SenderConfig().JobID, fs.ToString())
}
fmt.Printf("most recent v2 replication cursor:\n%#v", oldCursor)
if !(mostRecentNew.CreateTXG >= oldCursor.CreateTXG) {
return errors.Errorf("v1 replication cursor createtxg is higher than v2 cursor's, skipping this filesystem")
}
fmt.Printf("determined that v2 cursor is bookmark of same or newer version than v1 cursor\n")
fmt.Printf("destroying v1 cursor %q\n", oldCursor.ToAbsPath(fs))
if migrateReplicationCursorArgs.dryRun {
succ.Printf("DRY RUN\n")
} else {
if err := zfs.ZFSDestroyFilesystemVersion(ctx, fs, oldCursor); err != nil {
return err
}
}
return nil
}
+16
View File
@@ -0,0 +1,16 @@
package client
import "testing"
func TestMigrationsUnambiguousNames(t *testing.T) {
names := make(map[string]bool)
for _, mig := range migrations {
if _, ok := names[mig.Use]; ok {
t.Errorf("duplicate migration name %q", mig.Use)
t.FailNow()
return
} else {
names[mig.Use] = true
}
}
}
+3 -2
View File
@@ -2,11 +2,12 @@ package client
import ( import (
"errors" "errors"
"log"
"os"
"github.com/zrepl/zrepl/cli" "github.com/zrepl/zrepl/cli"
"github.com/zrepl/zrepl/config" "github.com/zrepl/zrepl/config"
"github.com/zrepl/zrepl/daemon" "github.com/zrepl/zrepl/daemon"
"log"
"os"
) )
var pprofArgs struct { var pprofArgs struct {
+3 -2
View File
@@ -2,6 +2,7 @@ package client
import ( import (
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/zrepl/zrepl/cli" "github.com/zrepl/zrepl/cli"
"github.com/zrepl/zrepl/config" "github.com/zrepl/zrepl/config"
"github.com/zrepl/zrepl/daemon" "github.com/zrepl/zrepl/daemon"
@@ -28,10 +29,10 @@ func runSignalCmd(config *config.Config, args []string) error {
err = jsonRequestResponse(httpc, daemon.ControlJobEndpointSignal, err = jsonRequestResponse(httpc, daemon.ControlJobEndpointSignal,
struct { struct {
Name string Name string
Op string Op string
}{ }{
Name: args[1], Name: args[1],
Op: args[0], Op: args[0],
}, },
struct{}{}, struct{}{},
) )
+384 -207
View File
@@ -2,16 +2,6 @@ package client
import ( import (
"fmt" "fmt"
"github.com/gdamore/tcell/termbox"
"github.com/pkg/errors"
"github.com/spf13/pflag"
"github.com/zrepl/yaml-config"
"github.com/zrepl/zrepl/cli"
"github.com/zrepl/zrepl/daemon"
"github.com/zrepl/zrepl/daemon/job"
"github.com/zrepl/zrepl/daemon/pruner"
"github.com/zrepl/zrepl/replication"
"github.com/zrepl/zrepl/replication/fsrep"
"io" "io"
"math" "math"
"net/http" "net/http"
@@ -20,41 +10,70 @@ import (
"strings" "strings"
"sync" "sync"
"time" "time"
// tcell is the termbox-compatible library for abstracting away escape sequences, etc.
// as of tcell#252, the number of default distributed terminals is relatively limited
// additional terminal definitions can be included via side-effect import
// See https://github.com/gdamore/tcell/blob/master/terminfo/base/base.go
// See https://github.com/gdamore/tcell/issues/252#issuecomment-533836078
"github.com/gdamore/tcell/termbox"
_ "github.com/gdamore/tcell/terminfo/s/screen" // tmux on FreeBSD 11 & 12 without ncurses
"github.com/pkg/errors"
"github.com/spf13/pflag"
"github.com/zrepl/yaml-config"
"github.com/zrepl/zrepl/cli"
"github.com/zrepl/zrepl/daemon"
"github.com/zrepl/zrepl/daemon/job"
"github.com/zrepl/zrepl/daemon/pruner"
"github.com/zrepl/zrepl/daemon/snapper"
"github.com/zrepl/zrepl/replication/report"
) )
type byteProgressMeasurement struct {
time time.Time
val int64
}
type bytesProgressHistory struct { type bytesProgressHistory struct {
changeCounter int last *byteProgressMeasurement // pointer as poor man's optional
lastChangeAt time.Time changeCount int
last int64 lastChange time.Time
bpsIncreaseExpAvg float64 bpsAvg float64
} }
func (p *bytesProgressHistory) Update(currentVal int64) (bytesPerSecondAvg int64, changeCount int) { func (p *bytesProgressHistory) Update(currentVal int64) (bytesPerSecondAvg int64, changeCount int) {
if currentVal < p.last {
*p = bytesProgressHistory{ if p.last == nil {
last: currentVal, p.last = &byteProgressMeasurement{
lastChangeAt: time.Now(), time: time.Now(),
val: currentVal,
} }
} return 0, 0
defer func() {
p.last = currentVal
}()
if time.Now().Sub(p.lastChangeAt) > 3 *time.Second { // FIXME depends on refresh frequency
p.changeCounter = 0
p.bpsIncreaseExpAvg = 0
}
if currentVal != p.last {
p.changeCounter++
p.lastChangeAt = time.Now()
} }
byteIncrease := float64(currentVal - p.last) if p.last.val != currentVal {
if byteIncrease < 0 { p.changeCount++
byteIncrease = 0 p.lastChange = time.Now()
} }
const factor = 0.1
p.bpsIncreaseExpAvg = (1-factor) * p.bpsIncreaseExpAvg + factor *byteIncrease if time.Since(p.lastChange) > 3*time.Second {
return int64(p.bpsIncreaseExpAvg), p.changeCounter p.last = nil
return 0, 0
}
deltaV := currentVal - p.last.val
deltaT := time.Since(p.last.time)
rate := float64(deltaV) / deltaT.Seconds()
factor := 0.3
p.bpsAvg = (1-factor)*p.bpsAvg + factor*rate
p.last.time = time.Now()
p.last.val = currentVal
return int64(p.bpsAvg), p.changeCount
} }
type tui struct { type tui struct {
@@ -62,23 +81,20 @@ type tui struct {
indent int indent int
lock sync.Mutex //For report and error lock sync.Mutex //For report and error
report map[string]job.Status report map[string]*job.Status
err error err error
jobFilter string
replicationProgress map[string]*bytesProgressHistory // by job name replicationProgress map[string]*bytesProgressHistory // by job name
} }
func newTui() tui { func newTui() tui {
return tui{ return tui{
replicationProgress: make(map[string]*bytesProgressHistory, 0), replicationProgress: make(map[string]*bytesProgressHistory),
} }
} }
func (t *tui) moveCursor(x, y int) {
t.x += x
t.y += y
}
const INDENT_MULTIPLIER = 4 const INDENT_MULTIPLIER = 4
func (t *tui) moveLine(dl int, col int) { func (t *tui) moveLine(dl int, col int) {
@@ -109,9 +125,9 @@ func wrap(s string, width int) string {
rem = len(s) rem = len(s)
} }
if idx := strings.IndexAny(s, "\n\r"); idx != -1 && idx < rem { if idx := strings.IndexAny(s, "\n\r"); idx != -1 && idx < rem {
rem = idx+1 rem = idx + 1
} }
untilNewline := strings.TrimSpace(s[:rem]) untilNewline := strings.TrimRight(s[:rem], "\n\r")
s = s[rem:] s = s[rem:]
if len(untilNewline) == 0 { if len(untilNewline) == 0 {
continue continue
@@ -119,18 +135,18 @@ func wrap(s string, width int) string {
b.WriteString(untilNewline) b.WriteString(untilNewline)
b.WriteString("\n") b.WriteString("\n")
} }
return strings.TrimSpace(b.String()) return strings.TrimRight(b.String(), "\n\r")
} }
func (t *tui) printfDrawIndentedAndWrappedIfMultiline(format string, a ...interface{}) { func (t *tui) printfDrawIndentedAndWrappedIfMultiline(format string, a ...interface{}) {
whole := fmt.Sprintf(format, a...) whole := fmt.Sprintf(format, a...)
width, _ := termbox.Size() width, _ := termbox.Size()
if !strings.ContainsAny(whole, "\n\r") && t.x + len(whole) <= width { if !strings.ContainsAny(whole, "\n\r") && t.x+len(whole) <= width {
t.printf(format, a...) t.printf(format, a...)
} else { } else {
t.addIndent(1) t.addIndent(1)
t.newline() t.newline()
t.write(wrap(whole, width - INDENT_MULTIPLIER*t.indent)) t.write(wrap(whole, width-INDENT_MULTIPLIER*t.indent))
t.addIndent(-1) t.addIndent(-1)
} }
} }
@@ -149,9 +165,9 @@ func (t *tui) addIndent(indent int) {
t.moveLine(0, 0) t.moveLine(0, 0)
} }
var statusFlags struct { var statusFlags struct {
Raw bool Raw bool
Job string
} }
var StatusCmd = &cli.Subcommand{ var StatusCmd = &cli.Subcommand{
@@ -159,6 +175,7 @@ var StatusCmd = &cli.Subcommand{
Short: "show job activity or dump as JSON for monitoring", Short: "show job activity or dump as JSON for monitoring",
SetupFlags: func(f *pflag.FlagSet) { SetupFlags: func(f *pflag.FlagSet) {
f.BoolVar(&statusFlags.Raw, "raw", false, "dump raw status description from zrepl daemon") f.BoolVar(&statusFlags.Raw, "raw", false, "dump raw status description from zrepl daemon")
f.StringVar(&statusFlags.Job, "job", "", "only dump specified job")
}, },
Run: runStatus, Run: runStatus,
} }
@@ -170,14 +187,17 @@ func runStatus(s *cli.Subcommand, args []string) error {
} }
if statusFlags.Raw { if statusFlags.Raw {
resp, err := httpc.Get("http://unix"+daemon.ControlJobEndpointStatus) resp, err := httpc.Get("http://unix" + daemon.ControlJobEndpointStatus)
if err != nil { if err != nil {
return err return err
} }
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
fmt.Fprintf(os.Stderr, "Received error response:\n") fmt.Fprintf(os.Stderr, "Received error response:\n")
io.CopyN(os.Stderr, resp.Body, 4096) _, err := io.CopyN(os.Stderr, resp.Body, 4096)
if err != nil {
return err
}
return errors.Errorf("exit") return errors.Errorf("exit")
} }
if _, err := io.Copy(os.Stdout, resp.Body); err != nil { if _, err := io.Copy(os.Stdout, resp.Body); err != nil {
@@ -190,6 +210,7 @@ func runStatus(s *cli.Subcommand, args []string) error {
t.lock.Lock() t.lock.Lock()
t.err = errors.New("Got no report yet") t.err = errors.New("Got no report yet")
t.lock.Unlock() t.lock.Unlock()
t.jobFilter = statusFlags.Job
err = termbox.Init() err = termbox.Init()
if err != nil { if err != nil {
@@ -198,7 +219,7 @@ func runStatus(s *cli.Subcommand, args []string) error {
defer termbox.Close() defer termbox.Close()
update := func() { update := func() {
m := make(map[string]job.Status) var m daemon.Status
err2 := jsonRequestResponse(httpc, daemon.ControlJobEndpointStatus, err2 := jsonRequestResponse(httpc, daemon.ControlJobEndpointStatus,
struct{}{}, struct{}{},
@@ -207,7 +228,7 @@ func runStatus(s *cli.Subcommand, args []string) error {
t.lock.Lock() t.lock.Lock()
t.err = err2 t.err = err2
t.report = m t.report = m.Jobs
t.lock.Unlock() t.lock.Unlock()
t.draw() t.draw()
} }
@@ -216,7 +237,7 @@ func runStatus(s *cli.Subcommand, args []string) error {
ticker := time.NewTicker(500 * time.Millisecond) ticker := time.NewTicker(500 * time.Millisecond)
defer ticker.Stop() defer ticker.Stop()
go func() { go func() {
for _ = range ticker.C { for range ticker.C {
update() update()
} }
}() }()
@@ -243,7 +264,7 @@ loop:
} }
func (t *tui) getReplicationProgresHistory(jobName string) *bytesProgressHistory { func (t *tui) getReplicationProgressHistory(jobName string) *bytesProgressHistory {
p, ok := t.replicationProgress[jobName] p, ok := t.replicationProgress[jobName]
if !ok { if !ok {
p = &bytesProgressHistory{} p = &bytesProgressHistory{}
@@ -265,19 +286,29 @@ func (t *tui) draw() {
t.write(t.err.Error()) t.write(t.err.Error())
} else { } else {
//Iterate over map in alphabetical order //Iterate over map in alphabetical order
keys := make([]string, len(t.report)) keys := make([]string, 0, len(t.report))
i := 0 for k := range t.report {
for k, _ := range t.report {
keys[i] = k
i++
}
sort.Strings(keys)
for _, k := range keys {
v := t.report[k]
if len(k) == 0 || daemon.IsInternalJobName(k) { //Internal job if len(k) == 0 || daemon.IsInternalJobName(k) { //Internal job
continue continue
} }
if t.jobFilter != "" && k != t.jobFilter {
continue
}
keys = append(keys, k)
}
sort.Strings(keys)
if len(keys) == 0 {
t.setIndent(0)
t.printf("no jobs to display")
t.newline()
termbox.Flush()
return
}
for _, k := range keys {
v := t.report[k]
t.setIndent(0) t.setIndent(0)
t.printf("Job: %s", k) t.printf("Job: %s", k)
@@ -287,7 +318,66 @@ func (t *tui) draw() {
t.setIndent(1) t.setIndent(1)
t.newline() t.newline()
if v.Type != job.TypePush && v.Type != job.TypePull { if v.Type == job.TypePush || v.Type == job.TypePull {
activeStatus, ok := v.JobSpecific.(*job.ActiveSideStatus)
if !ok || activeStatus == nil {
t.printf("ActiveSideStatus is null")
t.newline()
continue
}
t.printf("Replication:")
t.newline()
t.addIndent(1)
t.renderReplicationReport(activeStatus.Replication, t.getReplicationProgressHistory(k))
t.addIndent(-1)
t.printf("Pruning Sender:")
t.newline()
t.addIndent(1)
t.renderPrunerReport(activeStatus.PruningSender)
t.addIndent(-1)
t.printf("Pruning Receiver:")
t.newline()
t.addIndent(1)
t.renderPrunerReport(activeStatus.PruningReceiver)
t.addIndent(-1)
if v.Type == job.TypePush {
t.printf("Snapshotting:")
t.newline()
t.addIndent(1)
t.renderSnapperReport(activeStatus.Snapshotting)
t.addIndent(-1)
}
} else if v.Type == job.TypeSnap {
snapStatus, ok := v.JobSpecific.(*job.SnapJobStatus)
if !ok || snapStatus == nil {
t.printf("SnapJobStatus is null")
t.newline()
continue
}
t.printf("Pruning snapshots:")
t.newline()
t.addIndent(1)
t.renderPrunerReport(snapStatus.Pruning)
t.addIndent(-1)
t.printf("Snapshotting:")
t.newline()
t.addIndent(1)
t.renderSnapperReport(snapStatus.Snapshotting)
t.addIndent(-1)
} else if v.Type == job.TypeSource {
st := v.JobSpecific.(*job.PassiveStatus)
t.printf("Snapshotting:\n")
t.addIndent(1)
t.renderSnapperReport(st.Snapper)
t.addIndent(-1)
} else {
t.printf("No status representation for job type '%s', dumping as YAML", v.Type) t.printf("No status representation for job type '%s', dumping as YAML", v.Type)
t.newline() t.newline()
asYaml, err := yaml.Marshal(v.JobSpecific) asYaml, err := yaml.Marshal(v.JobSpecific)
@@ -300,105 +390,107 @@ func (t *tui) draw() {
t.newline() t.newline()
continue continue
} }
pushStatus, ok := v.JobSpecific.(*job.ActiveSideStatus)
if !ok || pushStatus == nil {
t.printf("ActiveSideStatus is null")
t.newline()
continue
}
t.printf("Replication:")
t.newline()
t.addIndent(1)
t.renderReplicationReport(pushStatus.Replication, t.getReplicationProgresHistory(k))
t.addIndent(-1)
t.printf("Pruning Sender:")
t.newline()
t.addIndent(1)
t.renderPrunerReport(pushStatus.PruningSender)
t.addIndent(-1)
t.printf("Pruning Receiver:")
t.newline()
t.addIndent(1)
t.renderPrunerReport(pushStatus.PruningReceiver)
t.addIndent(-1)
} }
} }
termbox.Flush() termbox.Flush()
} }
func (t *tui) renderReplicationReport(rep *replication.Report, history *bytesProgressHistory) { func (t *tui) renderReplicationReport(rep *report.Report, history *bytesProgressHistory) {
if rep == nil { if rep == nil {
t.printf("...\n") t.printf("...\n")
return return
} }
all := make([]*fsrep.Report, 0, len(rep.Completed)+len(rep.Pending) + 1) if rep.WaitReconnectError != nil {
all = append(all, rep.Completed...) t.printfDrawIndentedAndWrappedIfMultiline("Connectivity: %s", rep.WaitReconnectError)
all = append(all, rep.Pending...) t.newline()
if rep.Active != nil {
all = append(all, rep.Active)
} }
sort.Slice(all, func(i, j int) bool { if !rep.WaitReconnectSince.IsZero() {
return all[i].Filesystem < all[j].Filesystem delta := time.Until(rep.WaitReconnectUntil).Round(time.Second)
if rep.WaitReconnectUntil.IsZero() || delta > 0 {
var until string
if rep.WaitReconnectUntil.IsZero() {
until = "waiting indefinitely"
} else {
until = fmt.Sprintf("hard fail in %s @ %s", delta, rep.WaitReconnectUntil)
}
t.printfDrawIndentedAndWrappedIfMultiline("Connectivity: reconnecting with exponential backoff (since %s) (%s)",
rep.WaitReconnectSince, until)
} else {
t.printfDrawIndentedAndWrappedIfMultiline("Connectivity: reconnects reached hard-fail timeout @ %s", rep.WaitReconnectUntil)
}
t.newline()
}
// TODO visualize more than the latest attempt by folding all attempts into one
if len(rep.Attempts) == 0 {
t.printf("no attempts made yet")
return
} else {
t.printf("Attempt #%d", len(rep.Attempts))
if len(rep.Attempts) > 1 {
t.printf(". Previous attempts failed with the following statuses:")
t.newline()
t.addIndent(1)
for i, a := range rep.Attempts[:len(rep.Attempts)-1] {
t.printfDrawIndentedAndWrappedIfMultiline("#%d: %s (failed at %s) (ran %s)", i+1, a.State, a.FinishAt, a.FinishAt.Sub(a.StartAt))
t.newline()
}
t.addIndent(-1)
} else {
t.newline()
}
}
latest := rep.Attempts[len(rep.Attempts)-1]
sort.Slice(latest.Filesystems, func(i, j int) bool {
return latest.Filesystems[i].Info.Name < latest.Filesystems[j].Info.Name
}) })
state, err := replication.StateString(rep.Status) t.printf("Status: %s", latest.State)
if err != nil {
t.printf("Status: %q (parse error: %q)\n", rep.Status, err)
return
}
t.printf("Status: %s", state)
t.newline() t.newline()
if rep.Problem != "" { if latest.State == report.AttemptPlanningError {
t.printf("Problem: ") t.printf("Problem: ")
t.printfDrawIndentedAndWrappedIfMultiline("%s", rep.Problem) t.printfDrawIndentedAndWrappedIfMultiline("%s", latest.PlanError)
t.newline()
} else if latest.State == report.AttemptFanOutError {
t.printf("Problem: one or more of the filesystems encountered errors")
t.newline() t.newline()
} }
if rep.SleepUntil.After(time.Now()) && !state.IsTerminal() {
t.printf("Sleeping until %s (%s left)\n", rep.SleepUntil, rep.SleepUntil.Sub(time.Now()))
}
if state != replication.Planning && state != replication.PlanningError { if latest.State != report.AttemptPlanning && latest.State != report.AttemptPlanningError {
// Draw global progress bar
// Progress: [---------------] // Progress: [---------------]
sumUpFSRep := func(rep *fsrep.Report) (transferred, total int64) { expected, replicated, containsInvalidSizeEstimates := latest.BytesSum()
for _, s := range rep.Pending { rate, changeCount := history.Update(replicated)
transferred += s.Bytes eta := time.Duration(0)
total += s.ExpectedBytes if rate > 0 {
} eta = time.Duration((expected-replicated)/rate) * time.Second
for _, s := range rep.Completed {
transferred += s.Bytes
total += s.ExpectedBytes
}
return
} }
var transferred, total int64
for _, fs := range all {
fstx, fstotal := sumUpFSRep(fs)
transferred += fstx
total += fstotal
}
rate, changeCount := history.Update(transferred)
t.write("Progress: ") t.write("Progress: ")
t.drawBar(50, transferred, total, changeCount) t.drawBar(50, replicated, expected, changeCount)
t.write(fmt.Sprintf(" %s / %s @ %s/s", ByteCountBinary(transferred), ByteCountBinary(total), ByteCountBinary(rate))) t.write(fmt.Sprintf(" %s / %s @ %s/s", ByteCountBinary(replicated), ByteCountBinary(expected), ByteCountBinary(rate)))
if eta != 0 {
t.write(fmt.Sprintf(" (%s remaining)", humanizeDuration(eta)))
}
t.newline() t.newline()
if containsInvalidSizeEstimates {
t.write("NOTE: not all steps could be size-estimated, total estimate is likely imprecise!")
t.newline()
}
var maxFSLen int
for _, fs := range latest.Filesystems {
if len(fs.Info.Name) > maxFSLen {
maxFSLen = len(fs.Info.Name)
}
}
for _, fs := range latest.Filesystems {
t.printFilesystemStatus(fs, false, maxFSLen) // FIXME bring 'active' flag back
}
} }
var maxFSLen int
for _, fs := range all {
if len(fs.Filesystem) > maxFSLen {
maxFSLen = len(fs.Filesystem)
}
}
for _, fs := range all {
t.printFilesystemStatus(fs, fs == rep.Active, maxFSLen)
}
} }
func (t *tui) renderPrunerReport(r *pruner.Report) { func (t *tui) renderPrunerReport(r *pruner.Report) {
@@ -419,15 +511,12 @@ func (t *tui) renderPrunerReport(r *pruner.Report) {
if r.Error != "" { if r.Error != "" {
t.printf("Error: %s\n", r.Error) t.printf("Error: %s\n", r.Error)
} }
if r.SleepUntil.After(time.Now()) {
t.printf("Sleeping until %s (%s left)\n", r.SleepUntil, r.SleepUntil.Sub(time.Now()))
}
type commonFS struct { type commonFS struct {
*pruner.FSReport *pruner.FSReport
completed bool completed bool
} }
all := make([]commonFS, 0, len(r.Pending) + len(r.Completed)) all := make([]commonFS, 0, len(r.Pending)+len(r.Completed))
for i := range r.Pending { for i := range r.Pending {
all = append(all, commonFS{&r.Pending[i], false}) all = append(all, commonFS{&r.Pending[i], false})
} }
@@ -436,9 +525,9 @@ func (t *tui) renderPrunerReport(r *pruner.Report) {
} }
switch state { switch state {
case pruner.Plan: fallthrough case pruner.Plan:
case pruner.PlanWait: fallthrough fallthrough
case pruner.ErrPerm: case pruner.PlanErr:
return return
} }
@@ -465,7 +554,7 @@ func (t *tui) renderPrunerReport(r *pruner.Report) {
t.write("[") t.write("[")
t.write(times("=", progress)) t.write(times("=", progress))
t.write(">") t.write(">")
t.write(times("-", 80 - progress)) t.write(times("-", 80-progress))
t.write("]") t.write("]")
t.printf(" %d/%d snapshots", completedDestroyCount, totalDestroyCount) t.printf(" %d/%d snapshots", completedDestroyCount, totalDestroyCount)
t.newline() t.newline()
@@ -478,8 +567,18 @@ func (t *tui) renderPrunerReport(r *pruner.Report) {
for _, fs := range all { for _, fs := range all {
t.write(rightPad(fs.Filesystem, maxFSname, " ")) t.write(rightPad(fs.Filesystem, maxFSname, " "))
t.write(" ") t.write(" ")
if !fs.SkipReason.NotSkipped() {
t.printf("skipped: %s\n", fs.SkipReason)
continue
}
if fs.LastError != "" { if fs.LastError != "" {
t.printf("ERROR (%d): %s\n", fs.ErrorCount, fs.LastError) // whitespace is padding if strings.ContainsAny(fs.LastError, "\r\n") {
t.printf("ERROR:")
t.printfDrawIndentedAndWrappedIfMultiline("%s\n", fs.LastError)
} else {
t.printfDrawIndentedAndWrappedIfMultiline("ERROR: %s\n", fs.LastError)
}
t.newline()
continue continue
} }
@@ -487,7 +586,7 @@ func (t *tui) renderPrunerReport(r *pruner.Report) {
len(fs.DestroyList), len(fs.SnapshotList)) len(fs.DestroyList), len(fs.SnapshotList))
if fs.completed { if fs.completed {
t.printf( "Completed %s\n", pruneRuleActionStr) t.printf("Completed %s\n", pruneRuleActionStr)
continue continue
} }
@@ -502,23 +601,84 @@ func (t *tui) renderPrunerReport(r *pruner.Report) {
} }
const snapshotIndent = 1 func (t *tui) renderSnapperReport(r *snapper.Report) {
func calculateMaxFSLength(all []*fsrep.Report) (maxFS, maxStatus int) { if r == nil {
for _, e := range all { t.printf("<snapshot type does not have a report>\n")
if len(e.Filesystem) > maxFS { return
maxFS = len(e.Filesystem) }
t.printf("Status: %s", r.State)
t.newline()
if r.Error != "" {
t.printf("Error: %s\n", r.Error)
}
if !r.SleepUntil.IsZero() {
t.printf("Sleep until: %s\n", r.SleepUntil)
}
sort.Slice(r.Progress, func(i, j int) bool {
return strings.Compare(r.Progress[i].Path, r.Progress[j].Path) == -1
})
t.addIndent(1)
defer t.addIndent(-1)
dur := func(d time.Duration) string {
return d.Round(100 * time.Millisecond).String()
}
type row struct {
path, state, duration, remainder, hookReport string
}
var widths struct {
path, state, duration int
}
rows := make([]*row, len(r.Progress))
for i, fs := range r.Progress {
r := &row{
path: fs.Path,
state: fs.State.String(),
} }
all2 := make([]*fsrep.StepReport, 0, len(e.Pending) + len(e.Completed)) if fs.HooksHadError {
all2 = append(all2, e.Pending...) r.hookReport = fs.Hooks // FIXME render here, not in daemon
all2 = append(all2, e.Completed...) }
for _, e2 := range all2 { switch fs.State {
elen := len(e2.Problem) + len(e2.From) + len(e2.To) + 60 // random spacing, units, labels, etc case snapper.SnapPending:
if elen > maxStatus { r.duration = "..."
maxStatus = elen r.remainder = ""
} case snapper.SnapStarted:
r.duration = dur(time.Since(fs.StartAt))
r.remainder = fmt.Sprintf("snap name: %q", fs.SnapName)
case snapper.SnapDone:
fallthrough
case snapper.SnapError:
r.duration = dur(fs.DoneAt.Sub(fs.StartAt))
r.remainder = fmt.Sprintf("snap name: %q", fs.SnapName)
}
rows[i] = r
if len(r.path) > widths.path {
widths.path = len(r.path)
}
if len(r.state) > widths.state {
widths.state = len(r.state)
}
if len(r.duration) > widths.duration {
widths.duration = len(r.duration)
} }
} }
return
for _, r := range rows {
path := rightPad(r.path, widths.path, " ")
state := rightPad(r.state, widths.state, " ")
duration := rightPad(r.duration, widths.duration, " ")
t.printf("%s %s %s", path, state, duration)
t.printfDrawIndentedAndWrappedIfMultiline(" %s", r.remainder)
if r.hookReport != "" {
t.printfDrawIndentedAndWrappedIfMultiline("%s", r.hookReport)
}
t.newline()
}
} }
func times(str string, n int) (out string) { func times(str string, n int) (out string) {
@@ -532,20 +692,12 @@ func rightPad(str string, length int, pad string) string {
if len(str) > length { if len(str) > length {
return str[:length] return str[:length]
} }
return str + times(pad, length-len(str)) return str + strings.Repeat(pad, length-len(str))
}
func leftPad(str string, length int, pad string) string {
if len(str) > length {
return str[len(str)-length:]
}
return times(pad, length-len(str)) + str
} }
var arrowPositions = `>\|/` var arrowPositions = `>\|/`
// changeCount = 0 indicates stall / no progresss // changeCount = 0 indicates stall / no progress
func (t *tui) drawBar(length int, bytes, totalBytes int64, changeCount int) { func (t *tui) drawBar(length int, bytes, totalBytes int64, changeCount int) {
var completedLength int var completedLength int
if totalBytes > 0 { if totalBytes > 0 {
@@ -559,40 +711,27 @@ func (t *tui) drawBar(length int, bytes, totalBytes int64, changeCount int) {
t.write("[") t.write("[")
t.write(times("=", completedLength)) t.write(times("=", completedLength))
t.write( string(arrowPositions[changeCount%len(arrowPositions)])) t.write(string(arrowPositions[changeCount%len(arrowPositions)]))
t.write(times("-", length-completedLength)) t.write(times("-", length-completedLength))
t.write("]") t.write("]")
} }
func StringStepState(s fsrep.StepState) string { func (t *tui) printFilesystemStatus(rep *report.FilesystemReport, active bool, maxFS int) {
switch s {
case fsrep.StepReplicationReady: return "Ready"
case fsrep.StepMarkReplicatedReady: return "MarkReady"
case fsrep.StepCompleted: return "Completed"
default:
return fmt.Sprintf("UNKNOWN %d", s)
}
}
func (t *tui) printFilesystemStatus(rep *fsrep.Report, active bool, maxFS int) { expected, replicated, containsInvalidSizeEstimates := rep.BytesSum()
sizeEstimationImpreciseNotice := ""
bytes := int64(0) if containsInvalidSizeEstimates {
totalBytes := int64(0) sizeEstimationImpreciseNotice = " (some steps lack size estimation)"
for _, s := range rep.Pending {
bytes += s.Bytes
totalBytes += s.ExpectedBytes
} }
for _, s := range rep.Completed { if rep.CurrentStep < len(rep.Steps) && rep.Steps[rep.CurrentStep].Info.BytesExpected == 0 {
bytes += s.Bytes sizeEstimationImpreciseNotice = " (step lacks size estimation)"
totalBytes += s.ExpectedBytes
} }
status := fmt.Sprintf("%s (step %d/%d, %s/%s)%s",
status := fmt.Sprintf("%s (step %d/%d, %s/%s)", strings.ToUpper(string(rep.State)),
rep.Status, rep.CurrentStep, len(rep.Steps),
len(rep.Completed), len(rep.Pending) + len(rep.Completed), ByteCountBinary(replicated), ByteCountBinary(expected),
ByteCountBinary(bytes), ByteCountBinary(totalBytes), sizeEstimationImpreciseNotice,
) )
activeIndicator := " " activeIndicator := " "
@@ -601,18 +740,32 @@ func (t *tui) printFilesystemStatus(rep *fsrep.Report, active bool, maxFS int) {
} }
t.printf("%s %s %s ", t.printf("%s %s %s ",
activeIndicator, activeIndicator,
rightPad(rep.Filesystem, maxFS, " "), rightPad(rep.Info.Name, maxFS, " "),
status) status)
next := "" next := ""
if rep.Problem != "" { if err := rep.Error(); err != nil {
next = rep.Problem next = err.Err
} else if len(rep.Pending) > 0 { } else if rep.State != report.FilesystemDone {
if rep.Pending[0].From != "" { if nextStep := rep.NextStep(); nextStep != nil {
next = fmt.Sprintf("next: %s => %s", rep.Pending[0].From, rep.Pending[0].To) if nextStep.IsIncremental() {
next = fmt.Sprintf("next: %s => %s", nextStep.Info.From, nextStep.Info.To)
} else {
next = fmt.Sprintf("next: full send %s", nextStep.Info.To)
}
attribs := []string{}
if nextStep.Info.Resumed {
attribs = append(attribs, "resumed")
}
attribs = append(attribs, fmt.Sprintf("encrypted=%s", nextStep.Info.Encrypted))
next += fmt.Sprintf(" (%s)", strings.Join(attribs, ", "))
} else { } else {
next = fmt.Sprintf("next: %s (full)", rep.Pending[0].To) next = "" // individual FSes may still be in planning state
} }
} }
t.printfDrawIndentedAndWrappedIfMultiline("%s", next) t.printfDrawIndentedAndWrappedIfMultiline("%s", next)
@@ -631,3 +784,27 @@ func ByteCountBinary(b int64) string {
} }
return fmt.Sprintf("%.1f %ciB", float64(b)/float64(div), "KMGTPE"[exp]) return fmt.Sprintf("%.1f %ciB", float64(b)/float64(div), "KMGTPE"[exp])
} }
func humanizeDuration(duration time.Duration) string {
days := int64(duration.Hours() / 24)
hours := int64(math.Mod(duration.Hours(), 24))
minutes := int64(math.Mod(duration.Minutes(), 60))
seconds := int64(math.Mod(duration.Seconds(), 60))
var parts []string
force := false
chunks := []int64{days, hours, minutes, seconds}
for i, chunk := range chunks {
if force || chunk > 0 {
padding := 0
if force {
padding = 2
}
parts = append(parts, fmt.Sprintf("%*d%c", padding, chunk, "dhms"[i]))
force = true
}
}
return strings.Join(parts, " ")
}
+5 -3
View File
@@ -1,13 +1,15 @@
package client package client
import ( import (
"github.com/zrepl/zrepl/cli"
"os" "os"
"github.com/problame/go-netssh"
"github.com/zrepl/zrepl/cli"
"github.com/zrepl/zrepl/config"
"context" "context"
"errors" "errors"
"github.com/problame/go-netssh"
"github.com/zrepl/zrepl/config"
"log" "log"
"path" "path"
) )
+115 -9
View File
@@ -1,29 +1,35 @@
package client package client
import ( import (
"context"
"encoding/json"
"fmt" "fmt"
"os"
"github.com/pkg/errors"
"github.com/spf13/pflag" "github.com/spf13/pflag"
"github.com/zrepl/zrepl/cli" "github.com/zrepl/zrepl/cli"
"github.com/zrepl/zrepl/config" "github.com/zrepl/zrepl/config"
"github.com/zrepl/zrepl/daemon/filters" "github.com/zrepl/zrepl/daemon/filters"
"github.com/zrepl/zrepl/zfs" "github.com/zrepl/zrepl/zfs"
) )
var TestCmd = &cli.Subcommand { var TestCmd = &cli.Subcommand{
Use: "test", Use: "test",
SetupSubcommands: func() []*cli.Subcommand { SetupSubcommands: func() []*cli.Subcommand {
return []*cli.Subcommand{testFilter} return []*cli.Subcommand{testFilter, testPlaceholder, testDecodeResumeToken}
}, },
} }
var testFilterArgs struct { var testFilterArgs struct {
job string job string
all bool all bool
input string input string
} }
var testFilter = &cli.Subcommand{ var testFilter = &cli.Subcommand{
Use: "filesystems --job JOB [--all | --input INPUT]", Use: "filesystems --job JOB [--all | --input INPUT]",
Short: "test filesystems filter specified in push or source job", Short: "test filesystems filter specified in push or source job",
SetupFlags: func(f *pflag.FlagSet) { SetupFlags: func(f *pflag.FlagSet) {
f.StringVar(&testFilterArgs.job, "job", "", "the name of the push or source job") f.StringVar(&testFilterArgs.job, "job", "", "the name of the push or source job")
@@ -43,6 +49,7 @@ func runTestFilterCmd(subcommand *cli.Subcommand, args []string) error {
} }
conf := subcommand.Config() conf := subcommand.Config()
ctx := context.Background()
var confFilter config.FilesystemsFilter var confFilter config.FilesystemsFilter
job, err := conf.Job(testFilterArgs.job) job, err := conf.Job(testFilterArgs.job)
@@ -50,8 +57,12 @@ func runTestFilterCmd(subcommand *cli.Subcommand, args []string) error {
return err return err
} }
switch j := job.Ret.(type) { switch j := job.Ret.(type) {
case *config.SourceJob: confFilter = j.Filesystems case *config.SourceJob:
case *config.PushJob: confFilter = j.Filesystems confFilter = j.Filesystems
case *config.PushJob:
confFilter = j.Filesystems
case *config.SnapJob:
confFilter = j.Filesystems
default: default:
return fmt.Errorf("job type %T does not have filesystems filter", j) return fmt.Errorf("job type %T does not have filesystems filter", j)
} }
@@ -65,7 +76,7 @@ func runTestFilterCmd(subcommand *cli.Subcommand, args []string) error {
if testFilterArgs.input != "" { if testFilterArgs.input != "" {
fsnames = []string{testFilterArgs.input} fsnames = []string{testFilterArgs.input}
} else { } else {
out, err := zfs.ZFSList([]string{"name"}) out, err := zfs.ZFSList(ctx, []string{"name"})
if err != nil { if err != nil {
return fmt.Errorf("could not list ZFS filesystems: %s", err) return fmt.Errorf("could not list ZFS filesystems: %s", err)
} }
@@ -105,4 +116,99 @@ func runTestFilterCmd(subcommand *cli.Subcommand, args []string) error {
return fmt.Errorf("filter errors occurred") return fmt.Errorf("filter errors occurred")
} }
return nil return nil
} }
var testPlaceholderArgs struct {
ds string
all bool
}
var testPlaceholder = &cli.Subcommand{
Use: "placeholder [--all | --dataset DATASET]",
Short: fmt.Sprintf("list received placeholder filesystems (zfs property %q)", zfs.PlaceholderPropertyName),
Example: `
placeholder --all
placeholder --dataset path/to/sink/clientident/fs`,
NoRequireConfig: true,
SetupFlags: func(f *pflag.FlagSet) {
f.StringVar(&testPlaceholderArgs.ds, "dataset", "", "dataset path (not required to exist)")
f.BoolVar(&testPlaceholderArgs.all, "all", false, "list tab-separated placeholder status of all filesystems")
},
Run: runTestPlaceholder,
}
func runTestPlaceholder(subcommand *cli.Subcommand, args []string) error {
var checkDPs []*zfs.DatasetPath
ctx := context.Background()
// all actions first
if testPlaceholderArgs.all {
out, err := zfs.ZFSList(ctx, []string{"name"})
if err != nil {
return errors.Wrap(err, "could not list ZFS filesystems")
}
for _, row := range out {
dp, err := zfs.NewDatasetPath(row[0])
if err != nil {
panic(err)
}
checkDPs = append(checkDPs, dp)
}
} else {
dp, err := zfs.NewDatasetPath(testPlaceholderArgs.ds)
if err != nil {
return err
}
if dp.Empty() {
return fmt.Errorf("must specify --dataset DATASET or --all")
}
checkDPs = append(checkDPs, dp)
}
fmt.Printf("IS_PLACEHOLDER\tDATASET\tzrepl:placeholder\n")
for _, dp := range checkDPs {
ph, err := zfs.ZFSGetFilesystemPlaceholderState(ctx, dp)
if err != nil {
return errors.Wrap(err, "cannot get placeholder state")
}
if !ph.FSExists {
panic("placeholder state inconsistent: filesystem " + ph.FS + " must exist in this context")
}
is := "yes"
if !ph.IsPlaceholder {
is = "no"
}
fmt.Printf("%s\t%s\t%s\n", is, dp.ToString(), ph.RawLocalPropertyValue)
}
return nil
}
var testDecodeResumeTokenArgs struct {
token string
}
var testDecodeResumeToken = &cli.Subcommand{
Use: "decoderesumetoken --token TOKEN",
Short: "decode resume token",
SetupFlags: func(f *pflag.FlagSet) {
f.StringVar(&testDecodeResumeTokenArgs.token, "token", "", "the resume token obtained from the receive_resume_token property")
},
Run: runTestDecodeResumeTokenCmd,
}
func runTestDecodeResumeTokenCmd(subcommand *cli.Subcommand, args []string) error {
if testDecodeResumeTokenArgs.token == "" {
return fmt.Errorf("token argument must be specified")
}
token, err := zfs.ParseResumeToken(context.Background(), testDecodeResumeTokenArgs.token)
if err != nil {
return err
}
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
if err := enc.Encode(&token); err != nil {
panic(err)
}
return nil
}
+7 -5
View File
@@ -2,23 +2,25 @@ package client
import ( import (
"fmt" "fmt"
"os"
"github.com/spf13/pflag" "github.com/spf13/pflag"
"github.com/zrepl/zrepl/cli" "github.com/zrepl/zrepl/cli"
"github.com/zrepl/zrepl/config" "github.com/zrepl/zrepl/config"
"github.com/zrepl/zrepl/daemon" "github.com/zrepl/zrepl/daemon"
"github.com/zrepl/zrepl/version" "github.com/zrepl/zrepl/version"
"os"
) )
var versionArgs struct { var versionArgs struct {
Show string Show string
Config *config.Config Config *config.Config
ConfigErr error ConfigErr error
} }
var VersionCmd = &cli.Subcommand{ var VersionCmd = &cli.Subcommand{
Use: "version", Use: "version",
Short: "print version of zrepl binary and running daemon", Short: "print version of zrepl binary and running daemon",
NoRequireConfig: true, NoRequireConfig: true,
SetupFlags: func(f *pflag.FlagSet) { SetupFlags: func(f *pflag.FlagSet) {
f.StringVar(&versionArgs.Show, "show", "", "version info to show (client|daemon)") f.StringVar(&versionArgs.Show, "show", "", "version info to show (client|daemon)")
+147
View File
@@ -0,0 +1,147 @@
package client
import (
"fmt"
"sort"
"strings"
"github.com/spf13/pflag"
"github.com/zrepl/zrepl/cli"
"github.com/zrepl/zrepl/daemon/filters"
"github.com/zrepl/zrepl/endpoint"
"github.com/zrepl/zrepl/zfs"
)
var (
ZFSAbstractionsCmd = &cli.Subcommand{
Use: "zfs-abstraction",
Short: "manage abstractions that zrepl builds on top of ZFS",
SetupSubcommands: func() []*cli.Subcommand {
return []*cli.Subcommand{
zabsCmdList,
zabsCmdReleaseAll,
zabsCmdReleaseStale,
zabsCmdCreate,
}
},
}
)
// a common set of CLI flags that map to the fields of an
// endpoint.ListZFSHoldsAndBookmarksQuery
type zabsFilterFlags struct {
Filesystems FilesystemsFilterFlag
Job JobIDFlag
Types AbstractionTypesFlag
Concurrency int64
}
// produce a query from the CLI flags
func (f zabsFilterFlags) Query() (endpoint.ListZFSHoldsAndBookmarksQuery, error) {
q := endpoint.ListZFSHoldsAndBookmarksQuery{
FS: f.Filesystems.FlagValue(),
What: f.Types.FlagValue(),
JobID: f.Job.FlagValue(),
Concurrency: f.Concurrency,
}
return q, q.Validate()
}
func (f *zabsFilterFlags) registerZabsFilterFlags(s *pflag.FlagSet, verb string) {
// Note: the default value is defined in the .FlagValue methods
s.Var(&f.Filesystems, "fs", fmt.Sprintf("only %s holds on the specified filesystem [default: all filesystems] [comma-separated list of <dataset-pattern>:<ok|!> pairs]", verb))
s.Var(&f.Job, "job", fmt.Sprintf("only %s holds created by the specified job [default: any job]", verb))
variants := make([]string, 0, len(endpoint.AbstractionTypesAll))
for v := range endpoint.AbstractionTypesAll {
variants = append(variants, string(v))
}
variants = sort.StringSlice(variants)
variantsJoined := strings.Join(variants, "|")
s.Var(&f.Types, "type", fmt.Sprintf("only %s holds of the specified type [default: all] [comma-separated list of %s]", verb, variantsJoined))
s.Int64VarP(&f.Concurrency, "concurrency", "p", 1, "number of concurrently queried filesystems")
}
type JobIDFlag struct{ J *endpoint.JobID }
func (f *JobIDFlag) Set(s string) error {
if len(s) == 0 {
*f = JobIDFlag{J: nil}
return nil
}
jobID, err := endpoint.MakeJobID(s)
if err != nil {
return err
}
*f = JobIDFlag{J: &jobID}
return nil
}
func (f JobIDFlag) Type() string { return "job-ID" }
func (f JobIDFlag) String() string { return fmt.Sprint(f.J) }
func (f JobIDFlag) FlagValue() *endpoint.JobID { return f.J }
type AbstractionTypesFlag map[endpoint.AbstractionType]bool
func (f *AbstractionTypesFlag) Set(s string) error {
ats, err := endpoint.AbstractionTypeSetFromStrings(strings.Split(s, ","))
if err != nil {
return err
}
*f = AbstractionTypesFlag(ats)
return nil
}
func (f AbstractionTypesFlag) Type() string { return "abstraction-type" }
func (f AbstractionTypesFlag) String() string {
return endpoint.AbstractionTypeSet(f).String()
}
func (f AbstractionTypesFlag) FlagValue() map[endpoint.AbstractionType]bool {
if len(f) > 0 {
return f
}
return endpoint.AbstractionTypesAll
}
type FilesystemsFilterFlag struct {
F endpoint.ListZFSHoldsAndBookmarksQueryFilesystemFilter
}
func (flag *FilesystemsFilterFlag) Set(s string) error {
mappings := strings.Split(s, ",")
if len(mappings) == 1 && !strings.Contains(mappings[0], ":") {
flag.F = endpoint.ListZFSHoldsAndBookmarksQueryFilesystemFilter{
FS: &mappings[0],
}
return nil
}
f := filters.NewDatasetMapFilter(len(mappings), true)
for _, m := range mappings {
thisMappingErr := fmt.Errorf("expecting comma-separated list of <dataset-pattern>:<ok|!> pairs, got %q", m)
lhsrhs := strings.SplitN(m, ":", 2)
if len(lhsrhs) != 2 {
return thisMappingErr
}
err := f.Add(lhsrhs[0], lhsrhs[1])
if err != nil {
return fmt.Errorf("%s: %s", thisMappingErr, err)
}
}
flag.F = endpoint.ListZFSHoldsAndBookmarksQueryFilesystemFilter{
Filter: f,
}
return nil
}
func (flag FilesystemsFilterFlag) Type() string { return "filesystem filter spec" }
func (flag FilesystemsFilterFlag) String() string {
return fmt.Sprintf("%v", flag.F)
}
func (flag FilesystemsFilterFlag) FlagValue() endpoint.ListZFSHoldsAndBookmarksQueryFilesystemFilter {
var z FilesystemsFilterFlag
if flag == z {
return endpoint.ListZFSHoldsAndBookmarksQueryFilesystemFilter{Filter: zfs.NoFilter()}
}
return flag.F
}
+14
View File
@@ -0,0 +1,14 @@
package client
import "github.com/zrepl/zrepl/cli"
var zabsCmdCreate = &cli.Subcommand{
Use: "create",
NoRequireConfig: true,
Short: `create zrepl ZFS abstractions (mostly useful for debugging & development, users should not need to use this command)`,
SetupSubcommands: func() []*cli.Subcommand {
return []*cli.Subcommand{
zabsCmdCreateStepHold,
}
},
}
@@ -0,0 +1,60 @@
package client
import (
"context"
"fmt"
"github.com/pkg/errors"
"github.com/spf13/pflag"
"github.com/zrepl/zrepl/cli"
"github.com/zrepl/zrepl/endpoint"
"github.com/zrepl/zrepl/zfs"
)
var zabsCreateStepHoldFlags struct {
target string
jobid JobIDFlag
}
var zabsCmdCreateStepHold = &cli.Subcommand{
Use: "step",
Run: doZabsCreateStep,
NoRequireConfig: true,
Short: `create a step hold or bookmark`,
SetupFlags: func(f *pflag.FlagSet) {
f.StringVarP(&zabsCreateStepHoldFlags.target, "target", "t", "", "snapshot to be held / bookmark to be held")
f.VarP(&zabsCreateStepHoldFlags.jobid, "jobid", "j", "jobid for which the hold is installed")
},
}
func doZabsCreateStep(sc *cli.Subcommand, args []string) error {
if len(args) > 0 {
return errors.New("subcommand takes no arguments")
}
f := &zabsCreateStepHoldFlags
fs, _, _, err := zfs.DecomposeVersionString(f.target)
if err != nil {
return errors.Wrapf(err, "%q invalid target", f.target)
}
if f.jobid.FlagValue() == nil {
return errors.Errorf("jobid must be set")
}
ctx := context.Background()
v, err := zfs.ZFSGetFilesystemVersion(ctx, f.target)
if err != nil {
return errors.Wrapf(err, "get info about target %q", f.target)
}
step, err := endpoint.HoldStep(ctx, fs, v, *f.jobid.FlagValue())
if err != nil {
return errors.Wrap(err, "create step hold")
}
fmt.Println(step.String())
return nil
}
+100
View File
@@ -0,0 +1,100 @@
package client
import (
"context"
"encoding/json"
"fmt"
"os"
"sync"
"github.com/fatih/color"
"github.com/pkg/errors"
"github.com/spf13/pflag"
"github.com/zrepl/zrepl/cli"
"github.com/zrepl/zrepl/endpoint"
"github.com/zrepl/zrepl/util/chainlock"
)
var zabsListFlags struct {
Filter zabsFilterFlags
Json bool
}
var zabsCmdList = &cli.Subcommand{
Use: "list",
Short: `list zrepl ZFS abstractions`,
Run: doZabsList,
NoRequireConfig: true,
SetupFlags: func(f *pflag.FlagSet) {
zabsListFlags.Filter.registerZabsFilterFlags(f, "list")
f.BoolVar(&zabsListFlags.Json, "json", false, "emit JSON")
},
}
func doZabsList(sc *cli.Subcommand, args []string) error {
var err error
ctx := context.Background()
if len(args) > 0 {
return errors.New("this subcommand takes no positional arguments")
}
q, err := zabsListFlags.Filter.Query()
if err != nil {
return errors.Wrap(err, "invalid filter specification on command line")
}
abstractions, errors, err := endpoint.ListAbstractionsStreamed(ctx, q)
if err != nil {
return err // context clear by invocation of command
}
var line chainlock.L
var wg sync.WaitGroup
defer wg.Wait()
wg.Add(1)
// print results
go func() {
defer wg.Done()
enc := json.NewEncoder(os.Stdout)
for a := range abstractions {
func() {
defer line.Lock().Unlock()
if zabsListFlags.Json {
enc.SetIndent("", " ")
if err := enc.Encode(abstractions); err != nil {
panic(err)
}
fmt.Println()
} else {
fmt.Println(a)
}
}()
}
}()
// print errors to stderr
errorColor := color.New(color.FgRed)
var errorsSlice []endpoint.ListAbstractionsError
wg.Add(1)
go func() {
defer wg.Done()
for err := range errors {
func() {
defer line.Lock().Unlock()
errorsSlice = append(errorsSlice, err)
errorColor.Fprintf(os.Stderr, "%s\n", err)
}()
}
}()
wg.Wait()
if len(errorsSlice) > 0 {
errorColor.Add(color.Bold).Fprintf(os.Stderr, "there were errors in listing the abstractions")
return fmt.Errorf("")
} else {
return nil
}
}
+145
View File
@@ -0,0 +1,145 @@
package client
import (
"context"
"encoding/json"
"fmt"
"os"
"github.com/fatih/color"
"github.com/pkg/errors"
"github.com/spf13/pflag"
"github.com/zrepl/zrepl/cli"
"github.com/zrepl/zrepl/endpoint"
)
// shared between release-all and release-step
var zabsReleaseFlags struct {
Filter zabsFilterFlags
Json bool
DryRun bool
}
func registerZabsReleaseFlags(s *pflag.FlagSet) {
zabsReleaseFlags.Filter.registerZabsFilterFlags(s, "release")
s.BoolVar(&zabsReleaseFlags.Json, "json", false, "emit json instead of pretty-printed")
s.BoolVar(&zabsReleaseFlags.DryRun, "dry-run", false, "do a dry-run")
}
var zabsCmdReleaseAll = &cli.Subcommand{
Use: "release-all",
Run: doZabsReleaseAll,
NoRequireConfig: true,
Short: `(DANGEROUS) release ALL zrepl ZFS abstractions (mostly useful for uninstalling zrepl completely or for "de-zrepl-ing" a filesystem)`,
SetupFlags: registerZabsReleaseFlags,
}
var zabsCmdReleaseStale = &cli.Subcommand{
Use: "release-stale",
Run: doZabsReleaseStale,
NoRequireConfig: true,
Short: `release stale zrepl ZFS abstractions (useful if zrepl has a bug and does not do it by itself)`,
SetupFlags: registerZabsReleaseFlags,
}
func doZabsReleaseAll(sc *cli.Subcommand, args []string) error {
var err error
ctx := context.Background()
if len(args) > 0 {
return errors.New("this subcommand takes no positional arguments")
}
q, err := zabsReleaseFlags.Filter.Query()
if err != nil {
return errors.Wrap(err, "invalid filter specification on command line")
}
abstractions, listErrors, err := endpoint.ListAbstractions(ctx, q)
if err != nil {
return err // context clear by invocation of command
}
if len(listErrors) > 0 {
color.New(color.FgRed).Fprintf(os.Stderr, "there were errors in listing the abstractions:\n%s\n", listErrors)
// proceed anyways with rest of abstractions
}
return doZabsRelease_Common(ctx, abstractions)
}
func doZabsReleaseStale(sc *cli.Subcommand, args []string) error {
var err error
ctx := context.Background()
if len(args) > 0 {
return errors.New("this subcommand takes no positional arguments")
}
q, err := zabsReleaseFlags.Filter.Query()
if err != nil {
return errors.Wrap(err, "invalid filter specification on command line")
}
stalenessInfo, err := endpoint.ListStale(ctx, q)
if err != nil {
return err // context clear by invocation of command
}
return doZabsRelease_Common(ctx, stalenessInfo.Stale)
}
func doZabsRelease_Common(ctx context.Context, destroy []endpoint.Abstraction) error {
if zabsReleaseFlags.DryRun {
if zabsReleaseFlags.Json {
m, err := json.MarshalIndent(destroy, "", " ")
if err != nil {
panic(err)
}
if _, err := os.Stdout.Write(m); err != nil {
panic(err)
}
fmt.Println()
} else {
for _, a := range destroy {
fmt.Printf("would destroy %s\n", a)
}
}
return nil
}
outcome := endpoint.BatchDestroy(ctx, destroy)
hadErr := false
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
colorErr := color.New(color.FgRed)
printfSuccess := color.New(color.FgGreen).FprintfFunc()
printfSection := color.New(color.Bold).FprintfFunc()
for res := range outcome {
hadErr = hadErr || res.DestroyErr != nil
if zabsReleaseFlags.Json {
err := enc.Encode(res)
if err != nil {
colorErr.Fprintf(os.Stderr, "cannot marshal there were errors in destroying the abstractions")
}
} else {
printfSection(os.Stdout, "destroy %s ...", res.Abstraction)
if res.DestroyErr != nil {
colorErr.Fprintf(os.Stdout, " failed:\n%s\n", res.DestroyErr)
} else {
printfSuccess(os.Stdout, " OK\n")
}
}
}
if hadErr {
colorErr.Add(color.Bold).Fprintf(os.Stderr, "there were errors in destroying the abstractions")
return fmt.Errorf("")
} else {
return nil
}
}
+243 -69
View File
@@ -2,14 +2,16 @@ package config
import ( import (
"fmt" "fmt"
"github.com/pkg/errors"
"github.com/zrepl/yaml-config"
"io/ioutil" "io/ioutil"
"log/syslog"
"os" "os"
"reflect" "reflect"
"regexp" "regexp"
"strconv" "strconv"
"time" "time"
"github.com/pkg/errors"
"github.com/zrepl/yaml-config"
) )
type Config struct { type Config struct {
@@ -33,52 +35,126 @@ type JobEnum struct {
func (j JobEnum) Name() string { func (j JobEnum) Name() string {
var name string var name string
switch v := j.Ret.(type) { switch v := j.Ret.(type) {
case *PushJob: name = v.Name case *SnapJob:
case *SinkJob: name = v.Name name = v.Name
case *PullJob: name = v.Name case *PushJob:
case *SourceJob: name = v.Name name = v.Name
case *SinkJob:
name = v.Name
case *PullJob:
name = v.Name
case *SourceJob:
name = v.Name
default: default:
panic(fmt.Sprintf("unknownn job type %T", v)) panic(fmt.Sprintf("unknown job type %T", v))
} }
return name return name
} }
type ActiveJob struct { type ActiveJob struct {
Type string `yaml:"type"` Type string `yaml:"type"`
Name string `yaml:"name"` Name string `yaml:"name"`
Connect ConnectEnum `yaml:"connect"` Connect ConnectEnum `yaml:"connect"`
Pruning PruningSenderReceiver `yaml:"pruning"` Pruning PruningSenderReceiver `yaml:"pruning"`
Debug JobDebugSettings `yaml:"debug,optional"` Debug JobDebugSettings `yaml:"debug,optional"`
} }
type PassiveJob struct { type PassiveJob struct {
Type string `yaml:"type"` Type string `yaml:"type"`
Name string `yaml:"name"` Name string `yaml:"name"`
Serve ServeEnum `yaml:"serve"` Serve ServeEnum `yaml:"serve"`
Debug JobDebugSettings `yaml:"debug,optional"` Debug JobDebugSettings `yaml:"debug,optional"`
}
type SnapJob struct {
Type string `yaml:"type"`
Name string `yaml:"name"`
Pruning PruningLocal `yaml:"pruning"`
Debug JobDebugSettings `yaml:"debug,optional"`
Snapshotting SnapshottingEnum `yaml:"snapshotting"`
Filesystems FilesystemsFilter `yaml:"filesystems"`
}
type SendOptions struct {
Encrypted bool `yaml:"encrypted"`
}
var _ yaml.Defaulter = (*SendOptions)(nil)
func (l *SendOptions) SetDefault() {
*l = SendOptions{Encrypted: false}
}
type RecvOptions struct {
// Note: we cannot enforce encrypted recv as the ZFS cli doesn't provide a mechanism for it
// Encrypted bool `yaml:"may_encrypted"`
// Future:
// Reencrypt bool `yaml:"reencrypt"`
}
var _ yaml.Defaulter = (*RecvOptions)(nil)
func (l *RecvOptions) SetDefault() {
*l = RecvOptions{}
} }
type PushJob struct { type PushJob struct {
ActiveJob `yaml:",inline"` ActiveJob `yaml:",inline"`
Snapshotting SnapshottingEnum `yaml:"snapshotting"` Snapshotting SnapshottingEnum `yaml:"snapshotting"`
Filesystems FilesystemsFilter `yaml:"filesystems"` Filesystems FilesystemsFilter `yaml:"filesystems"`
Send *SendOptions `yaml:"send,fromdefaults,optional"`
} }
type PullJob struct { type PullJob struct {
ActiveJob `yaml:",inline"` ActiveJob `yaml:",inline"`
RootFS string `yaml:"root_fs"` RootFS string `yaml:"root_fs"`
Interval time.Duration `yaml:"interval,positive"` Interval PositiveDurationOrManual `yaml:"interval"`
Recv *RecvOptions `yaml:"recv,fromdefaults,optional"`
}
type PositiveDurationOrManual struct {
Interval time.Duration
Manual bool
}
var _ yaml.Unmarshaler = (*PositiveDurationOrManual)(nil)
func (i *PositiveDurationOrManual) UnmarshalYAML(u func(interface{}, bool) error) (err error) {
var s string
if err := u(&s, true); err != nil {
return err
}
switch s {
case "manual":
i.Manual = true
i.Interval = 0
case "":
return fmt.Errorf("value must not be empty")
default:
i.Manual = false
i.Interval, err = time.ParseDuration(s)
if err != nil {
return err
}
if i.Interval <= 0 {
return fmt.Errorf("value must be a positive duration, got %q", s)
}
}
return nil
} }
type SinkJob struct { type SinkJob struct {
PassiveJob `yaml:",inline"` PassiveJob `yaml:",inline"`
RootFS string `yaml:"root_fs"` RootFS string `yaml:"root_fs"`
Recv *RecvOptions `yaml:"recv,optional,fromdefaults"`
} }
type SourceJob struct { type SourceJob struct {
PassiveJob `yaml:",inline"` PassiveJob `yaml:",inline"`
Snapshotting SnapshottingEnum `yaml:"snapshotting"` Snapshotting SnapshottingEnum `yaml:"snapshotting"`
Filesystems FilesystemsFilter `yaml:"filesystems"` Filesystems FilesystemsFilter `yaml:"filesystems"`
Send *SendOptions `yaml:"send,optional,fromdefaults"`
} }
type FilesystemsFilter map[string]bool type FilesystemsFilter map[string]bool
@@ -88,9 +164,10 @@ type SnapshottingEnum struct {
} }
type SnapshottingPeriodic struct { type SnapshottingPeriodic struct {
Type string `yaml:"type"` Type string `yaml:"type"`
Prefix string `yaml:"prefix"` Prefix string `yaml:"prefix"`
Interval time.Duration `yaml:"interval,positive"` Interval time.Duration `yaml:"interval,positive"`
Hooks HookList `yaml:"hooks,optional"`
} }
type SnapshottingManual struct { type SnapshottingManual struct {
@@ -130,7 +207,6 @@ type Global struct {
Monitoring []MonitoringEnum `yaml:"monitoring,optional"` Monitoring []MonitoringEnum `yaml:"monitoring,optional"`
Control *GlobalControl `yaml:"control,optional,fromdefaults"` Control *GlobalControl `yaml:"control,optional,fromdefaults"`
Serve *GlobalServe `yaml:"serve,optional,fromdefaults"` Serve *GlobalServe `yaml:"serve,optional,fromdefaults"`
RPC *RPCConfig `yaml:"rpc,optional,fromdefaults"`
} }
func Default(i interface{}) { func Default(i interface{}) {
@@ -145,39 +221,28 @@ func Default(i interface{}) {
} }
} }
type RPCConfig struct {
Timeout time.Duration `yaml:"timeout,optional,positive,default=10s"`
TxChunkSize uint32 `yaml:"tx_chunk_size,optional,default=32768"`
RxStructuredMaxLen uint32 `yaml:"rx_structured_max,optional,default=16777216"`
RxStreamChunkMaxLen uint32 `yaml:"rx_stream_chunk_max,optional,default=16777216"`
RxHeaderMaxLen uint32 `yaml:"rx_header_max,optional,default=40960"`
SendHeartbeatInterval time.Duration `yaml:"send_heartbeat_interval,optional,positive,default=5s"`
}
type ConnectEnum struct { type ConnectEnum struct {
Ret interface{} Ret interface{}
} }
type ConnectCommon struct { type ConnectCommon struct {
Type string `yaml:"type"` Type string `yaml:"type"`
RPC *RPCConfig `yaml:"rpc,optional"`
} }
type TCPConnect struct { type TCPConnect struct {
ConnectCommon `yaml:",inline"` ConnectCommon `yaml:",inline"`
Address string `yaml:"address"` Address string `yaml:"address,hostport"`
DialTimeout time.Duration `yaml:"dial_timeout,positive,default=10s"` DialTimeout time.Duration `yaml:"dial_timeout,zeropositive,default=10s"`
} }
type TLSConnect struct { type TLSConnect struct {
ConnectCommon `yaml:",inline"` ConnectCommon `yaml:",inline"`
Address string `yaml:"address"` Address string `yaml:"address,hostport"`
Ca string `yaml:"ca"` Ca string `yaml:"ca"`
Cert string `yaml:"cert"` Cert string `yaml:"cert"`
Key string `yaml:"key"` Key string `yaml:"key"`
ServerCN string `yaml:"server_cn"` ServerCN string `yaml:"server_cn"`
DialTimeout time.Duration `yaml:"dial_timeout,positive,default=10s"` DialTimeout time.Duration `yaml:"dial_timeout,zeropositive,default=10s"`
} }
type SSHStdinserverConnect struct { type SSHStdinserverConnect struct {
@@ -189,13 +254,14 @@ type SSHStdinserverConnect struct {
TransportOpenCommand []string `yaml:"transport_open_command,optional"` //TODO unused TransportOpenCommand []string `yaml:"transport_open_command,optional"` //TODO unused
SSHCommand string `yaml:"ssh_command,optional"` //TODO unused SSHCommand string `yaml:"ssh_command,optional"` //TODO unused
Options []string `yaml:"options,optional"` Options []string `yaml:"options,optional"`
DialTimeout time.Duration `yaml:"dial_timeout,positive,default=10s"` DialTimeout time.Duration `yaml:"dial_timeout,zeropositive,default=10s"`
} }
type LocalConnect struct { type LocalConnect struct {
ConnectCommon `yaml:",inline"` ConnectCommon `yaml:",inline"`
ListenerName string `yaml:"listener_name"` ListenerName string `yaml:"listener_name"`
ClientIdentity string `yaml:"client_identity"` ClientIdentity string `yaml:"client_identity"`
DialTimeout time.Duration `yaml:"dial_timeout,zeropositive,default=2s"`
} }
type ServeEnum struct { type ServeEnum struct {
@@ -203,33 +269,34 @@ type ServeEnum struct {
} }
type ServeCommon struct { type ServeCommon struct {
Type string `yaml:"type"` Type string `yaml:"type"`
RPC *RPCConfig `yaml:"rpc,optional"`
} }
type TCPServe struct { type TCPServe struct {
ServeCommon `yaml:",inline"` ServeCommon `yaml:",inline"`
Listen string `yaml:"listen"` Listen string `yaml:"listen,hostport"`
Clients map[string]string `yaml:"clients"` ListenFreeBind bool `yaml:"listen_freebind,default=false"`
Clients map[string]string `yaml:"clients"`
} }
type TLSServe struct { type TLSServe struct {
ServeCommon `yaml:",inline"` ServeCommon `yaml:",inline"`
Listen string `yaml:"listen"` Listen string `yaml:"listen,hostport"`
ListenFreeBind bool `yaml:"listen_freebind,default=false"`
Ca string `yaml:"ca"` Ca string `yaml:"ca"`
Cert string `yaml:"cert"` Cert string `yaml:"cert"`
Key string `yaml:"key"` Key string `yaml:"key"`
ClientCNs []string `yaml:"client_cns"` ClientCNs []string `yaml:"client_cns"`
HandshakeTimeout time.Duration `yaml:"handshake_timeout,positive,default=10s"` HandshakeTimeout time.Duration `yaml:"handshake_timeout,zeropositive,default=10s"`
} }
type StdinserverServer struct { type StdinserverServer struct {
ServeCommon `yaml:",inline"` ServeCommon `yaml:",inline"`
ClientIdentities []string `yaml:"client_identities"` ClientIdentities []string `yaml:"client_identities"`
} }
type LocalServe struct { type LocalServe struct {
ServeCommon `yaml:",inline"` ServeCommon `yaml:",inline"`
ListenerName string `yaml:"listener_name"` ListenerName string `yaml:"listener_name"`
} }
@@ -238,8 +305,8 @@ type PruningEnum struct {
} }
type PruneKeepNotReplicated struct { type PruneKeepNotReplicated struct {
Type string `yaml:"type"` Type string `yaml:"type"`
KeepSnapshotAtCursor bool `yaml:"keep_snapshot_at_cursor,optional,default=true"` KeepSnapshotAtCursor bool `yaml:"keep_snapshot_at_cursor,optional,default=true"`
} }
type PruneKeepLastN struct { type PruneKeepLastN struct {
@@ -248,8 +315,9 @@ type PruneKeepLastN struct {
} }
type PruneKeepRegex struct { // FIXME rename to KeepRegex type PruneKeepRegex struct { // FIXME rename to KeepRegex
Type string `yaml:"type"` Type string `yaml:"type"`
Regex string `yaml:"regex"` Regex string `yaml:"regex"`
Negate bool `yaml:"negate,optional,default=false"`
} }
type LoggingOutletEnum struct { type LoggingOutletEnum struct {
@@ -270,12 +338,13 @@ type StdoutLoggingOutlet struct {
type SyslogLoggingOutlet struct { type SyslogLoggingOutlet struct {
LoggingOutletCommon `yaml:",inline"` LoggingOutletCommon `yaml:",inline"`
RetryInterval time.Duration `yaml:"retry_interval,positive,default=10s"` Facility *SyslogFacility `yaml:"facility,optional,fromdefaults"`
RetryInterval time.Duration `yaml:"retry_interval,positive,default=10s"`
} }
type TCPLoggingOutlet struct { type TCPLoggingOutlet struct {
LoggingOutletCommon `yaml:",inline"` LoggingOutletCommon `yaml:",inline"`
Address string `yaml:"address"` Address string `yaml:"address,hostport"`
Net string `yaml:"net,default=tcp"` Net string `yaml:"net,default=tcp"`
RetryInterval time.Duration `yaml:"retry_interval,positive,default=10s"` RetryInterval time.Duration `yaml:"retry_interval,positive,default=10s"`
TLS *TCPLoggingOutletTLS `yaml:"tls,optional"` TLS *TCPLoggingOutletTLS `yaml:"tls,optional"`
@@ -292,10 +361,19 @@ type MonitoringEnum struct {
} }
type PrometheusMonitoring struct { type PrometheusMonitoring struct {
Type string `yaml:"type"` Type string `yaml:"type"`
Listen string `yaml:"listen"` Listen string `yaml:"listen,hostport"`
ListenFreeBind bool `yaml:"listen_freebind,default=false"`
} }
type SyslogFacility syslog.Priority
func (f *SyslogFacility) SetDefault() {
*f = SyslogFacility(syslog.LOG_LOCAL0)
}
var _ yaml.Defaulter = (*SyslogFacility)(nil)
type GlobalControl struct { type GlobalControl struct {
SockPath string `yaml:"sockpath,default=/var/run/zrepl/control"` SockPath string `yaml:"sockpath,default=/var/run/zrepl/control"`
} }
@@ -316,6 +394,38 @@ type JobDebugSettings struct {
RPCLog bool `yaml:"rpc_log,optional,default=false"` RPCLog bool `yaml:"rpc_log,optional,default=false"`
} }
type HookList []HookEnum
type HookEnum struct {
Ret interface{}
}
type HookCommand struct {
Path string `yaml:"path"`
Timeout time.Duration `yaml:"timeout,optional,positive,default=30s"`
Filesystems FilesystemsFilter `yaml:"filesystems,optional,default={'<': true}"`
HookSettingsCommon `yaml:",inline"`
}
type HookPostgresCheckpoint struct {
HookSettingsCommon `yaml:",inline"`
DSN string `yaml:"dsn"`
Timeout time.Duration `yaml:"timeout,optional,positive,default=30s"`
Filesystems FilesystemsFilter `yaml:"filesystems"` // required, user should not CHECKPOINT for every FS
}
type HookMySQLLockTables struct {
HookSettingsCommon `yaml:",inline"`
DSN string `yaml:"dsn"`
Timeout time.Duration `yaml:"timeout,optional,positive,default=30s"`
Filesystems FilesystemsFilter `yaml:"filesystems"`
}
type HookSettingsCommon struct {
Type string `yaml:"type"`
ErrIsFatal bool `yaml:"err_is_fatal,optional,default=false"`
}
func enumUnmarshal(u func(interface{}, bool) error, types map[string]interface{}) (interface{}, error) { func enumUnmarshal(u func(interface{}, bool) error, types map[string]interface{}) (interface{}, error) {
var in struct { var in struct {
Type string Type string
@@ -339,6 +449,7 @@ func enumUnmarshal(u func(interface{}, bool) error, types map[string]interface{}
func (t *JobEnum) UnmarshalYAML(u func(interface{}, bool) error) (err error) { func (t *JobEnum) UnmarshalYAML(u func(interface{}, bool) error) (err error) {
t.Ret, err = enumUnmarshal(u, map[string]interface{}{ t.Ret, err = enumUnmarshal(u, map[string]interface{}{
"snap": &SnapJob{},
"push": &PushJob{}, "push": &PushJob{},
"sink": &SinkJob{}, "sink": &SinkJob{},
"pull": &PullJob{}, "pull": &PullJob{},
@@ -352,7 +463,7 @@ func (t *ConnectEnum) UnmarshalYAML(u func(interface{}, bool) error) (err error)
"tcp": &TCPConnect{}, "tcp": &TCPConnect{},
"tls": &TLSConnect{}, "tls": &TLSConnect{},
"ssh+stdinserver": &SSHStdinserverConnect{}, "ssh+stdinserver": &SSHStdinserverConnect{},
"local": &LocalConnect{}, "local": &LocalConnect{},
}) })
return return
} }
@@ -362,7 +473,7 @@ func (t *ServeEnum) UnmarshalYAML(u func(interface{}, bool) error) (err error) {
"tcp": &TCPServe{}, "tcp": &TCPServe{},
"tls": &TLSServe{}, "tls": &TLSServe{},
"stdinserver": &StdinserverServer{}, "stdinserver": &StdinserverServer{},
"local" : &LocalServe{}, "local": &LocalServe{},
}) })
return return
} }
@@ -380,7 +491,7 @@ func (t *PruningEnum) UnmarshalYAML(u func(interface{}, bool) error) (err error)
func (t *SnapshottingEnum) UnmarshalYAML(u func(interface{}, bool) error) (err error) { func (t *SnapshottingEnum) UnmarshalYAML(u func(interface{}, bool) error) (err error) {
t.Ret, err = enumUnmarshal(u, map[string]interface{}{ t.Ret, err = enumUnmarshal(u, map[string]interface{}{
"periodic": &SnapshottingPeriodic{}, "periodic": &SnapshottingPeriodic{},
"manual": &SnapshottingManual{}, "manual": &SnapshottingManual{},
}) })
return return
} }
@@ -401,6 +512,69 @@ func (t *MonitoringEnum) UnmarshalYAML(u func(interface{}, bool) error) (err err
return return
} }
func (t *SyslogFacility) UnmarshalYAML(u func(interface{}, bool) error) (err error) {
var s string
if err := u(&s, true); err != nil {
return err
}
var level syslog.Priority
switch s {
case "kern":
level = syslog.LOG_KERN
case "user":
level = syslog.LOG_USER
case "mail":
level = syslog.LOG_MAIL
case "daemon":
level = syslog.LOG_DAEMON
case "auth":
level = syslog.LOG_AUTH
case "syslog":
level = syslog.LOG_SYSLOG
case "lpr":
level = syslog.LOG_LPR
case "news":
level = syslog.LOG_NEWS
case "uucp":
level = syslog.LOG_UUCP
case "cron":
level = syslog.LOG_CRON
case "authpriv":
level = syslog.LOG_AUTHPRIV
case "ftp":
level = syslog.LOG_FTP
case "local0":
level = syslog.LOG_LOCAL0
case "local1":
level = syslog.LOG_LOCAL1
case "local2":
level = syslog.LOG_LOCAL2
case "local3":
level = syslog.LOG_LOCAL3
case "local4":
level = syslog.LOG_LOCAL4
case "local5":
level = syslog.LOG_LOCAL5
case "local6":
level = syslog.LOG_LOCAL6
case "local7":
level = syslog.LOG_LOCAL7
default:
return fmt.Errorf("invalid syslog level: %q", s)
}
*t = SyslogFacility(level)
return nil
}
func (t *HookEnum) UnmarshalYAML(u func(interface{}, bool) error) (err error) {
t.Ret, err = enumUnmarshal(u, map[string]interface{}{
"command": &HookCommand{},
"postgres-checkpoint": &HookPostgresCheckpoint{},
"mysql-lock-tables": &HookMySQLLockTables{},
})
return
}
var ConfigFileDefaultLocations = []string{ var ConfigFileDefaultLocations = []string{
"/etc/zrepl/zrepl.yml", "/etc/zrepl/zrepl.yml",
"/usr/local/etc/zrepl/zrepl.yml", "/usr/local/etc/zrepl/zrepl.yml",
@@ -446,7 +620,7 @@ func ParseConfigBytes(bytes []byte) (*Config, error) {
var durationStringRegex *regexp.Regexp = regexp.MustCompile(`^\s*(\d+)\s*(s|m|h|d|w)\s*$`) var durationStringRegex *regexp.Regexp = regexp.MustCompile(`^\s*(\d+)\s*(s|m|h|d|w)\s*$`)
func parsePostitiveDuration(e string) (d time.Duration, err error) { func parsePositiveDuration(e string) (d time.Duration, err error) {
comps := durationStringRegex.FindStringSubmatch(e) comps := durationStringRegex.FindStringSubmatch(e)
if len(comps) != 3 { if len(comps) != 3 {
err = fmt.Errorf("does not match regex: %s %#v", e, comps) err = fmt.Errorf("does not match regex: %s %#v", e, comps)
+37 -4
View File
@@ -1,10 +1,13 @@
package config package config
import ( import (
"fmt"
"log/syslog"
"testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/zrepl/yaml-config" "github.com/zrepl/yaml-config"
"testing"
) )
func testValidGlobalSection(t *testing.T, s string) *Config { func testValidGlobalSection(t *testing.T, s string) *Config {
@@ -18,11 +21,11 @@ jobs:
clients: { clients: {
"10.0.0.1":"foo" "10.0.0.1":"foo"
} }
root_fs: zoot/foo root_fs: zroot/foo
` `
_, err := ParseConfigBytes([]byte(jobdef)) _, err := ParseConfigBytes([]byte(jobdef))
require.NoError(t, err) require.NoError(t, err)
return testValidConfig(t, s + jobdef) return testValidConfig(t, s+jobdef)
} }
func TestOutletTypes(t *testing.T) { func TestOutletTypes(t *testing.T) {
@@ -69,7 +72,37 @@ global:
- type: prometheus - type: prometheus
listen: ':9091' listen: ':9091'
`) `)
assert.Equal(t, ":9091", conf.Global.Monitoring[0].Ret.(*PrometheusMonitoring).Listen) assert.Equal(t, ":9091", conf.Global.Monitoring[0].Ret.(*PrometheusMonitoring).Listen)
}
func TestSyslogLoggingOutletFacility(t *testing.T) {
type SyslogFacilityPriority struct {
Facility string
Priority syslog.Priority
}
syslogFacilitiesPriorities := []SyslogFacilityPriority{
{"", syslog.LOG_LOCAL0}, // default
{"kern", syslog.LOG_KERN}, {"daemon", syslog.LOG_DAEMON}, {"auth", syslog.LOG_AUTH},
{"syslog", syslog.LOG_SYSLOG}, {"lpr", syslog.LOG_LPR}, {"news", syslog.LOG_NEWS},
{"uucp", syslog.LOG_UUCP}, {"cron", syslog.LOG_CRON}, {"authpriv", syslog.LOG_AUTHPRIV},
{"ftp", syslog.LOG_FTP}, {"local0", syslog.LOG_LOCAL0}, {"local1", syslog.LOG_LOCAL1},
{"local2", syslog.LOG_LOCAL2}, {"local3", syslog.LOG_LOCAL3}, {"local4", syslog.LOG_LOCAL4},
{"local5", syslog.LOG_LOCAL5}, {"local6", syslog.LOG_LOCAL6}, {"local7", syslog.LOG_LOCAL7},
}
for _, sFP := range syslogFacilitiesPriorities {
logcfg := fmt.Sprintf(`
global:
logging:
- type: syslog
level: info
format: human
facility: %s
`, sFP.Facility)
conf := testValidGlobalSection(t, logcfg)
assert.Equal(t, 1, len(*conf.Global.Logging))
assert.True(t, SyslogFacility(sFP.Priority) == *(*conf.Global.Logging)[0].Ret.(*SyslogLoggingOutlet).Facility)
}
} }
func TestLoggingOutletEnumList_SetDefaults(t *testing.T) { func TestLoggingOutletEnumList_SetDefaults(t *testing.T) {
+1
View File
@@ -0,0 +1 @@
package config
+2 -1
View File
@@ -2,6 +2,7 @@ package config
import ( import (
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@@ -36,4 +37,4 @@ jobs:
- type: last_n - type: last_n
count: 1 count: 1
`) `)
} }
@@ -0,0 +1,41 @@
package config
import (
"fmt"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/zrepl/yaml-config"
)
func TestPositiveDurationOrManual(t *testing.T) {
cases := []struct {
Comment, Input string
Result *PositiveDurationOrManual
}{
{"empty is error", "", nil},
{"negative is error", "-1s", nil},
{"zero seconds is error", "0s", nil},
{"zero is error", "0", nil},
{"non-manual is error", "something", nil},
{"positive seconds works", "1s", &PositiveDurationOrManual{Manual: false, Interval: 1 * time.Second}},
{"manual works", "manual", &PositiveDurationOrManual{Manual: true, Interval: 0}},
}
for _, tc := range cases {
t.Run(tc.Comment, func(t *testing.T) {
var out struct {
FieldName PositiveDurationOrManual `yaml:"fieldname"`
}
input := fmt.Sprintf("\nfieldname: %s\n", tc.Input)
err := yaml.UnmarshalStrict([]byte(input), &out)
if tc.Result == nil {
assert.Error(t, err)
t.Logf("%#v", out)
} else {
assert.Equal(t, *tc.Result, out.FieldName)
}
})
}
}
-86
View File
@@ -1,86 +0,0 @@
package config
import (
"github.com/stretchr/testify/assert"
"testing"
"time"
)
func TestRPC(t *testing.T) {
conf := testValidConfig(t, `
jobs:
- name: pull_servers
type: pull
connect:
type: tcp
address: "server1.foo.bar:8888"
rpc:
timeout: 20s # different form default, should merge
root_fs: "pool2/backup_servers"
interval: 10m
pruning:
keep_sender:
- type: not_replicated
keep_receiver:
- type: last_n
count: 100
- name: pull_servers2
type: pull
connect:
type: tcp
address: "server1.foo.bar:8888"
rpc:
tx_chunk_size: 0xabcd # different from default, should merge
root_fs: "pool2/backup_servers"
interval: 10m
pruning:
keep_sender:
- type: not_replicated
keep_receiver:
- type: last_n
count: 100
- type: sink
name: "laptop_sink"
root_fs: "pool2/backup_laptops"
serve:
type: tcp
listen: "192.168.122.189:8888"
clients: {
"10.23.42.23":"client1"
}
rpc:
rx_structured_max: 0x2342
- type: sink
name: "other_sink"
root_fs: "pool2/backup_laptops"
serve:
type: tcp
listen: "192.168.122.189:8888"
clients: {
"10.23.42.23":"client1"
}
rpc:
send_heartbeat_interval: 10s
`)
assert.Equal(t, 20*time.Second, conf.Jobs[0].Ret.(*PullJob).Connect.Ret.(*TCPConnect).RPC.Timeout)
assert.Equal(t, uint32(0xabcd), conf.Jobs[1].Ret.(*PullJob).Connect.Ret.(*TCPConnect).RPC.TxChunkSize)
assert.Equal(t, uint32(0x2342), conf.Jobs[2].Ret.(*SinkJob).Serve.Ret.(*TCPServe).RPC.RxStructuredMaxLen)
assert.Equal(t, 10*time.Second, conf.Jobs[3].Ret.(*SinkJob).Serve.Ret.(*TCPServe).RPC.SendHeartbeatInterval)
defConf := RPCConfig{}
Default(&defConf)
assert.Equal(t, defConf.Timeout, conf.Global.RPC.Timeout)
}
func TestGlobal_DefaultRPCConfig(t *testing.T) {
assert.NotPanics(t, func() {
var c RPCConfig
Default(&c)
assert.NotNil(t, c)
assert.Equal(t, c.TxChunkSize, uint32(1)<<15)
})
}
+74
View File
@@ -0,0 +1,74 @@
package config
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
func TestSendOptions(t *testing.T) {
tmpl := `
jobs:
- name: foo
type: push
connect:
type: local
listener_name: foo
client_identity: bar
filesystems: {"<": true}
%s
snapshotting:
type: manual
pruning:
keep_sender:
- type: last_n
count: 10
keep_receiver:
- type: last_n
count: 10
`
encrypted_false := `
send:
encrypted: false
`
encrypted_true := `
send:
encrypted: true
`
encrypted_unspecified := `
send: {}
`
send_not_specified := `
`
fill := func(s string) string { return fmt.Sprintf(tmpl, s) }
var c *Config
t.Run("encrypted_false", func(t *testing.T) {
c = testValidConfig(t, fill(encrypted_false))
encrypted := c.Jobs[0].Ret.(*PushJob).Send.Encrypted
assert.Equal(t, false, encrypted)
})
t.Run("encrypted_true", func(t *testing.T) {
c = testValidConfig(t, fill(encrypted_true))
encrypted := c.Jobs[0].Ret.(*PushJob).Send.Encrypted
assert.Equal(t, true, encrypted)
})
t.Run("encrypted_unspecified", func(t *testing.T) {
c, err := testConfig(t, fill(encrypted_unspecified))
assert.Error(t, err)
assert.Nil(t, c)
})
t.Run("send_not_specified", func(t *testing.T) {
c, err := testConfig(t, fill(send_not_specified))
assert.NoError(t, err)
assert.NotNil(t, c)
})
}
+36 -3
View File
@@ -2,9 +2,10 @@ package config
import ( import (
"fmt" "fmt"
"github.com/stretchr/testify/assert"
"testing" "testing"
"time" "time"
"github.com/stretchr/testify/assert"
) )
func TestSnapshotting(t *testing.T) { func TestSnapshotting(t *testing.T) {
@@ -37,7 +38,30 @@ jobs:
interval: 10m interval: 10m
` `
fillSnapshotting := func(s string) string {return fmt.Sprintf(tmpl, s)} hooks := `
snapshotting:
type: periodic
prefix: zrepl_
interval: 10m
hooks:
- type: command
path: /tmp/path/to/command
- type: command
path: /tmp/path/to/command
filesystems: { "zroot<": true, "<": false }
- type: postgres-checkpoint
dsn: "host=localhost port=5432 user=postgres sslmode=disable"
filesystems: {
"tank/postgres/data11": true
}
- type: mysql-lock-tables
dsn: "root@tcp(localhost)/"
filesystems: {
"tank/mysql": true
}
`
fillSnapshotting := func(s string) string { return fmt.Sprintf(tmpl, s) }
var c *Config var c *Config
t.Run("manual", func(t *testing.T) { t.Run("manual", func(t *testing.T) {
@@ -51,7 +75,16 @@ jobs:
snp := c.Jobs[0].Ret.(*PushJob).Snapshotting.Ret.(*SnapshottingPeriodic) snp := c.Jobs[0].Ret.(*PushJob).Snapshotting.Ret.(*SnapshottingPeriodic)
assert.Equal(t, "periodic", snp.Type) assert.Equal(t, "periodic", snp.Type)
assert.Equal(t, 10*time.Minute, snp.Interval) assert.Equal(t, 10*time.Minute, snp.Interval)
assert.Equal(t, "zrepl_" , snp.Prefix) assert.Equal(t, "zrepl_", snp.Prefix)
})
t.Run("hooks", func(t *testing.T) {
c = testValidConfig(t, fillSnapshotting(hooks))
hs := c.Jobs[0].Ret.(*PushJob).Snapshotting.Ret.(*SnapshottingPeriodic).Hooks
assert.Equal(t, hs[0].Ret.(*HookCommand).Filesystems["<"], true)
assert.Equal(t, hs[1].Ret.(*HookCommand).Filesystems["zroot<"], true)
assert.Equal(t, hs[2].Ret.(*HookPostgresCheckpoint).Filesystems["tank/postgres/data11"], true)
assert.Equal(t, hs[3].Ret.(*HookMySQLLockTables).Filesystems["tank/mysql"], true)
}) })
} }
+42 -4
View File
@@ -1,11 +1,18 @@
package config package config
import ( import (
"github.com/kr/pretty" "bufio"
"github.com/stretchr/testify/require" "bytes"
"fmt"
"path" "path"
"path/filepath" "path/filepath"
"strings"
"testing" "testing"
"text/template"
"github.com/kr/pretty"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestSampleConfigsAreParsedWithoutErrors(t *testing.T) { func TestSampleConfigsAreParsedWithoutErrors(t *testing.T) {
@@ -35,8 +42,22 @@ func TestSampleConfigsAreParsedWithoutErrors(t *testing.T) {
} }
// template must be a template/text template with a single '{{ . }}' as placeholder for val
//nolint[:deadcode,unused]
func testValidConfigTemplate(t *testing.T, tmpl string, val string) *Config {
tmp, err := template.New("master").Parse(tmpl)
if err != nil {
panic(err)
}
var buf bytes.Buffer
err = tmp.Execute(&buf, val)
if err != nil {
panic(err)
}
return testValidConfig(t, buf.String())
}
func testValidConfig(t *testing.T, input string) (*Config) { func testValidConfig(t *testing.T, input string) *Config {
t.Helper() t.Helper()
conf, err := testConfig(t, input) conf, err := testConfig(t, input)
require.NoError(t, err) require.NoError(t, err)
@@ -47,4 +68,21 @@ func testValidConfig(t *testing.T, input string) (*Config) {
func testConfig(t *testing.T, input string) (*Config, error) { func testConfig(t *testing.T, input string) (*Config, error) {
t.Helper() t.Helper()
return ParseConfigBytes([]byte(input)) return ParseConfigBytes([]byte(input))
} }
func trimSpaceEachLineAndPad(s, pad string) string {
var out strings.Builder
scan := bufio.NewScanner(strings.NewReader(s))
for scan.Scan() {
fmt.Fprintf(&out, "%s%s\n", pad, bytes.TrimSpace(scan.Bytes()))
}
return out.String()
}
func TestTrimSpaceEachLineAndPad(t *testing.T) {
foo := `
foo
bar baz
`
assert.Equal(t, " \n foo\n bar baz\n \n", trimSpaceEachLineAndPad(foo, " "))
}
+99
View File
@@ -0,0 +1,99 @@
package config
import (
"fmt"
"testing"
"github.com/stretchr/testify/require"
)
func TestTransportConnect(t *testing.T) {
tmpl := `
jobs:
- name: foo
type: push
connect:
%s
filesystems: {"<": true}
snapshotting:
type: manual
pruning:
keep_sender:
- type: last_n
count: 10
keep_receiver:
- type: last_n
count: 10
`
mconf := func(s string) string { return fmt.Sprintf(tmpl, s) }
type test struct {
Name string
ExpectError bool
Connect string
}
testTable := []test{
{
Name: "tcp_with_address_and_port",
ExpectError: false,
Connect: `
type: tcp
address: 10.0.0.23:42
`,
},
{
Name: "tls_with_host_and_port",
ExpectError: false,
Connect: `
type: tls
address: "server1.foo.bar:8888"
ca: /etc/zrepl/ca.crt
cert: /etc/zrepl/backupserver.fullchain
key: /etc/zrepl/backupserver.key
server_cn: "server1"
`,
},
{
Name: "tcp_without_port",
ExpectError: true,
Connect: `
type: tcp
address: 10.0.0.23
`,
},
{
Name: "tls_without_port",
ExpectError: true,
Connect: `
type: tls
address: 10.0.0.23
ca: /etc/zrepl/ca.crt
cert: /etc/zrepl/backupserver.fullchain
key: /etc/zrepl/backupserver.key
server_cn: "server1"
`,
},
}
for _, tc := range testTable {
t.Run(tc.Name, func(t *testing.T) {
require.NotEmpty(t, tc.Connect)
connect := trimSpaceEachLineAndPad(tc.Connect, " ")
conf := mconf(connect)
config, err := testConfig(t, conf)
if tc.ExpectError && err == nil {
t.Errorf("expected test failure, but got valid config %v", config)
return
}
if !tc.ExpectError && err != nil {
t.Errorf("not expecint test failure but got error: %s", err)
return
}
t.Logf("error=%v", err)
t.Logf("config=%v", config)
})
}
}
+4 -8
View File
@@ -11,9 +11,9 @@ import (
type RetentionIntervalList []RetentionInterval type RetentionIntervalList []RetentionInterval
type PruneGrid struct { type PruneGrid struct {
Type string `yaml:"type"` Type string `yaml:"type"`
Grid RetentionIntervalList `yaml:"grid"` Grid RetentionIntervalList `yaml:"grid"`
Regex string `yaml:"regex"` Regex string `yaml:"regex"`
} }
type RetentionInterval struct { type RetentionInterval struct {
@@ -31,10 +31,6 @@ func (i *RetentionInterval) KeepCount() int {
const RetentionGridKeepCountAll int = -1 const RetentionGridKeepCountAll int = -1
type RetentionGrid struct {
intervals []RetentionInterval
}
func (t *RetentionIntervalList) UnmarshalYAML(u func(interface{}, bool) error) (err error) { func (t *RetentionIntervalList) UnmarshalYAML(u func(interface{}, bool) error) (err error) {
var in string var in string
if err := u(&in, true); err != nil { if err := u(&in, true); err != nil {
@@ -68,7 +64,7 @@ func parseRetentionGridIntervalString(e string) (intervals []RetentionInterval,
return nil, fmt.Errorf("contains factor <= 0") return nil, fmt.Errorf("contains factor <= 0")
} }
duration, err := parsePostitiveDuration(comps[2]) duration, err := parsePositiveDuration(comps[2])
if err != nil { if err != nil {
return nil, err return nil, err
} }
+22
View File
@@ -0,0 +1,22 @@
#!/usr/bin/env bash
set -e
[ "$ZREPL_DRYRUN" = "true" ] && DRYRUN="echo DRYRUN: "
pre_snapshot() {
$DRYRUN date
}
post_snapshot() {
$DRYRUN date
}
case "$ZREPL_HOOKTYPE" in
pre_snapshot|post_snapshot)
"$ZREPL_HOOKTYPE"
;;
*)
printf 'Unrecognized hook type: %s\n' "$ZREPL_HOOKTYPE"
exit 255
;;
esac
+1 -1
View File
@@ -9,7 +9,7 @@ jobs:
port: 22 port: 22
identity_file: /etc/zrepl/ssh/identity identity_file: /etc/zrepl/ssh/identity
options: # optional, default [], `-o` arguments passed to ssh options: # optional, default [], `-o` arguments passed to ssh
- "Compression=on" - "Compression=yes"
root_fs: "pool2/backup_servers" root_fs: "pool2/backup_servers"
interval: 10m interval: 10m
pruning: pruning:
+2
View File
@@ -10,6 +10,8 @@ jobs:
address: "backup-server.foo.bar:8888" address: "backup-server.foo.bar:8888"
snapshotting: snapshotting:
type: manual type: manual
send:
encrypted: false
pruning: pruning:
keep_sender: keep_sender:
- type: not_replicated - type: not_replicated
+14
View File
@@ -0,0 +1,14 @@
jobs:
- name: snapjob
type: snap
filesystems: {
"tank/frequently_changed<": true,
}
snapshotting:
type: periodic
interval: 2m
prefix: zrepl_snapjob_
pruning:
keep:
- type: last_n
count: 60
+76 -32
View File
@@ -5,16 +5,22 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/pkg/errors"
"github.com/prometheus/client_golang/prometheus"
"github.com/zrepl/zrepl/daemon/job"
"github.com/zrepl/zrepl/daemon/nethelpers"
"github.com/zrepl/zrepl/logger"
"github.com/zrepl/zrepl/version"
"io" "io"
"net" "net"
"net/http" "net/http"
"time" "time"
"github.com/pkg/errors"
"github.com/prometheus/client_golang/prometheus"
"github.com/zrepl/zrepl/daemon/job"
"github.com/zrepl/zrepl/daemon/nethelpers"
"github.com/zrepl/zrepl/endpoint"
"github.com/zrepl/zrepl/logger"
"github.com/zrepl/zrepl/util/envconst"
"github.com/zrepl/zrepl/version"
"github.com/zrepl/zrepl/zfs"
"github.com/zrepl/zrepl/zfs/zfscmd"
) )
type controlJob struct { type controlJob struct {
@@ -38,25 +44,29 @@ func (j *controlJob) Name() string { return jobNameControl }
func (j *controlJob) Status() *job.Status { return &job.Status{Type: job.TypeInternal} } func (j *controlJob) Status() *job.Status { return &job.Status{Type: job.TypeInternal} }
func (j *controlJob) OwnedDatasetSubtreeRoot() (p *zfs.DatasetPath, ok bool) { return nil, false }
func (j *controlJob) SenderConfig() *endpoint.SenderConfig { return nil }
var promControl struct { var promControl struct {
requestBegin *prometheus.CounterVec requestBegin *prometheus.CounterVec
requestFinished *prometheus.HistogramVec requestFinished *prometheus.HistogramVec
} }
func (j *controlJob) RegisterMetrics(registerer prometheus.Registerer) { func (j *controlJob) RegisterMetrics(registerer prometheus.Registerer) {
promControl.requestBegin = prometheus.NewCounterVec(prometheus.CounterOpts{ promControl.requestBegin = prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: "zrepl", Namespace: "zrepl",
Subsystem: "control", Subsystem: "control",
Name: "request_begin", Name: "request_begin",
Help: "number of request we started to handle", Help: "number of request we started to handle",
}, []string{"endpoint"}) }, []string{"endpoint"})
promControl.requestFinished = prometheus.NewHistogramVec(prometheus.HistogramOpts{ promControl.requestFinished = prometheus.NewHistogramVec(prometheus.HistogramOpts{
Namespace: "zrepl", Namespace: "zrepl",
Subsystem: "control", Subsystem: "control",
Name: "request_finished", Name: "request_finished",
Help: "time it took a request to finih", Help: "time it took a request to finish",
Buckets: []float64{1e-6, 10e-6, 100e-6, 500e-6, 1e-3,10e-3, 100e-3, 200e-3,400e-3,800e-3, 1, 10, 20}, Buckets: []float64{1e-6, 10e-6, 100e-6, 500e-6, 1e-3, 10e-3, 100e-3, 200e-3, 400e-3, 800e-3, 1, 10, 20},
}, []string{"endpoint"}) }, []string{"endpoint"})
registerer.MustRegister(promControl.requestBegin) registerer.MustRegister(promControl.requestBegin)
registerer.MustRegister(promControl.requestFinished) registerer.MustRegister(promControl.requestFinished)
@@ -81,10 +91,16 @@ func (j *controlJob) Run(ctx context.Context) {
} }
pprofServer := NewPProfServer(ctx) pprofServer := NewPProfServer(ctx)
if listen := envconst.String("ZREPL_DAEMON_AUTOSTART_PPROF_SERVER", ""); listen != "" {
pprofServer.Control(PprofServerControlMsg{
Run: true,
HttpListenAddress: listen,
})
}
mux := http.NewServeMux() mux := http.NewServeMux()
mux.Handle(ControlJobEndpointPProf, mux.Handle(ControlJobEndpointPProf,
requestLogger{log: log, handler: jsonRequestResponder{func(decoder jsonDecoder) (interface{}, error) { requestLogger{log: log, handler: jsonRequestResponder{log, func(decoder jsonDecoder) (interface{}, error) {
var msg PprofServerControlMsg var msg PprofServerControlMsg
err := decoder(&msg) err := decoder(&msg)
if err != nil { if err != nil {
@@ -95,22 +111,24 @@ func (j *controlJob) Run(ctx context.Context) {
}}}) }}})
mux.Handle(ControlJobEndpointVersion, mux.Handle(ControlJobEndpointVersion,
requestLogger{log: log, handler: jsonResponder{func() (interface{}, error) { requestLogger{log: log, handler: jsonResponder{log, func() (interface{}, error) {
return version.NewZreplVersionInformation(), nil return version.NewZreplVersionInformation(), nil
}}}) }}})
mux.Handle(ControlJobEndpointStatus, mux.Handle(ControlJobEndpointStatus,
// don't log requests to status endpoint, too spammy // don't log requests to status endpoint, too spammy
jsonResponder{func() (interface{}, error) { jsonResponder{log, func() (interface{}, error) {
s := j.jobs.status() jobs := j.jobs.status()
globalZFS := zfscmd.GetReport()
s := Status{Jobs: jobs, Global: GlobalStatus{ZFSCmds: globalZFS}}
return s, nil return s, nil
}}) }})
mux.Handle(ControlJobEndpointSignal, mux.Handle(ControlJobEndpointSignal,
requestLogger{log: log, handler: jsonRequestResponder{func(decoder jsonDecoder) (interface{}, error) { requestLogger{log: log, handler: jsonRequestResponder{log, func(decoder jsonDecoder) (interface{}, error) {
type reqT struct { type reqT struct {
Name string Name string
Op string Op string
} }
var req reqT var req reqT
if decoder(&req) != nil { if decoder(&req) != nil {
@@ -132,8 +150,8 @@ func (j *controlJob) Run(ctx context.Context) {
server := http.Server{ server := http.Server{
Handler: mux, Handler: mux,
// control socket is local, 1s timeout should be more than sufficient, even on a loaded system // control socket is local, 1s timeout should be more than sufficient, even on a loaded system
WriteTimeout: 1*time.Second, WriteTimeout: 1 * time.Second,
ReadTimeout: 1*time.Second, ReadTimeout: 1 * time.Second,
} }
outer: outer:
@@ -148,7 +166,10 @@ outer:
select { select {
case <-ctx.Done(): case <-ctx.Done():
log.WithError(ctx.Err()).Info("context done") log.WithError(ctx.Err()).Info("context done")
server.Shutdown(context.Background()) err := server.Shutdown(context.Background())
if err != nil {
log.WithError(err).Error("cannot shutdown server")
}
break outer break outer
case err = <-served: case err = <-served:
if err != nil { if err != nil {
@@ -162,33 +183,50 @@ outer:
} }
type jsonResponder struct { type jsonResponder struct {
log Logger
producer func() (interface{}, error) producer func() (interface{}, error)
} }
func (j jsonResponder) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (j jsonResponder) ServeHTTP(w http.ResponseWriter, r *http.Request) {
logIoErr := func(err error) {
if err != nil {
j.log.WithError(err).Error("control handler io error")
}
}
res, err := j.producer() res, err := j.producer()
if err != nil { if err != nil {
j.log.WithError(err).Error("control handler error")
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
io.WriteString(w, err.Error()) _, err = io.WriteString(w, err.Error())
logIoErr(err)
return return
} }
var buf bytes.Buffer var buf bytes.Buffer
err = json.NewEncoder(&buf).Encode(res) err = json.NewEncoder(&buf).Encode(res)
if err != nil { if err != nil {
j.log.WithError(err).Error("control handler json marshal error")
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
io.WriteString(w, err.Error()) _, err = io.WriteString(w, err.Error())
} else { } else {
io.Copy(w, &buf) _, err = io.Copy(w, &buf)
} }
logIoErr(err)
} }
type jsonDecoder = func(interface{}) error type jsonDecoder = func(interface{}) error
type jsonRequestResponder struct { type jsonRequestResponder struct {
log Logger
producer func(decoder jsonDecoder) (interface{}, error) producer func(decoder jsonDecoder) (interface{}, error)
} }
func (j jsonRequestResponder) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (j jsonRequestResponder) ServeHTTP(w http.ResponseWriter, r *http.Request) {
logIoErr := func(err error) {
if err != nil {
j.log.WithError(err).Error("control handler io error")
}
}
var decodeError error var decodeError error
decoder := func(i interface{}) error { decoder := func(i interface{}) error {
err := json.NewDecoder(r.Body).Decode(&i) err := json.NewDecoder(r.Body).Decode(&i)
@@ -200,22 +238,28 @@ func (j jsonRequestResponder) ServeHTTP(w http.ResponseWriter, r *http.Request)
//If we had a decode error ignore output of producer and return error //If we had a decode error ignore output of producer and return error
if decodeError != nil { if decodeError != nil {
w.WriteHeader(http.StatusBadRequest) w.WriteHeader(http.StatusBadRequest)
io.WriteString(w, decodeError.Error()) _, err := io.WriteString(w, decodeError.Error())
logIoErr(err)
return return
} }
if producerErr != nil { if producerErr != nil {
j.log.WithError(producerErr).Error("control handler error")
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
io.WriteString(w, producerErr.Error()) _, err := io.WriteString(w, producerErr.Error())
logIoErr(err)
return return
} }
var buf bytes.Buffer var buf bytes.Buffer
encodeErr := json.NewEncoder(&buf).Encode(res) encodeErr := json.NewEncoder(&buf).Encode(res)
if encodeErr != nil { if encodeErr != nil {
j.log.WithError(producerErr).Error("control handler json marshal error")
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
io.WriteString(w, encodeErr.Error()) _, err := io.WriteString(w, encodeErr.Error())
logIoErr(err)
} else { } else {
io.Copy(w, &buf) _, err := io.Copy(w, &buf)
logIoErr(err)
} }
} }
+24 -11
View File
@@ -3,8 +3,16 @@ package daemon
import ( import (
"context" "context"
"fmt" "fmt"
"os"
"os/signal"
"strings"
"sync"
"syscall"
"time"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"github.com/zrepl/zrepl/config" "github.com/zrepl/zrepl/config"
"github.com/zrepl/zrepl/daemon/job" "github.com/zrepl/zrepl/daemon/job"
"github.com/zrepl/zrepl/daemon/job/reset" "github.com/zrepl/zrepl/daemon/job/reset"
@@ -12,12 +20,7 @@ import (
"github.com/zrepl/zrepl/daemon/logging" "github.com/zrepl/zrepl/daemon/logging"
"github.com/zrepl/zrepl/logger" "github.com/zrepl/zrepl/logger"
"github.com/zrepl/zrepl/version" "github.com/zrepl/zrepl/version"
"os" "github.com/zrepl/zrepl/zfs/zfscmd"
"os/signal"
"strings"
"sync"
"syscall"
"time"
) )
func Run(conf *config.Config) error { func Run(conf *config.Config) error {
@@ -74,11 +77,13 @@ func Run(conf *config.Config) error {
return errors.Errorf("unknown monitoring job #%d (type %T)", i, v) return errors.Errorf("unknown monitoring job #%d (type %T)", i, v)
} }
if err != nil { if err != nil {
return errors.Wrapf(err,"cannot build monitorin gjob #%d", i) return errors.Wrapf(err, "cannot build monitoring job #%d", i)
} }
jobs.start(ctx, job, true) jobs.start(ctx, job, true)
} }
// register global (=non job-local) metrics
zfscmd.RegisterMetrics(prometheus.DefaultRegisterer)
log.Info("starting daemon") log.Info("starting daemon")
@@ -103,7 +108,7 @@ type jobs struct {
// m protects all fields below it // m protects all fields below it
m sync.RWMutex m sync.RWMutex
wakeups map[string]wakeup.Func // by Job.Name wakeups map[string]wakeup.Func // by Job.Name
resets map[string]reset.Func // by Job.Name resets map[string]reset.Func // by Job.Name
jobs map[string]job.Job jobs map[string]job.Job
} }
@@ -116,9 +121,7 @@ func newJobs() *jobs {
} }
const ( const (
logJobField string = "job" logJobField string = "job"
logTaskField string = "task"
logSubsysField string = "subsystem"
) )
func (s *jobs) wait() <-chan struct{} { func (s *jobs) wait() <-chan struct{} {
@@ -129,6 +132,15 @@ func (s *jobs) wait() <-chan struct{} {
return ch return ch
} }
type Status struct {
Jobs map[string]*job.Status
Global GlobalStatus
}
type GlobalStatus struct {
ZFSCmds *zfscmd.Report
}
func (s *jobs) status() map[string]*job.Status { func (s *jobs) status() map[string]*job.Status {
s.m.RLock() s.m.RLock()
defer s.m.RUnlock() defer s.m.RUnlock()
@@ -208,6 +220,7 @@ func (s *jobs) start(ctx context.Context, j job.Job, internal bool) {
s.jobs[jobName] = j s.jobs[jobName] = j
ctx = job.WithLogger(ctx, jobLog) ctx = job.WithLogger(ctx, jobLog)
ctx = zfscmd.WithJobID(ctx, j.Name())
ctx, wakeup := wakeup.Context(ctx) ctx, wakeup := wakeup.Context(ctx)
ctx, resetFunc := reset.Context(ctx) ctx, resetFunc := reset.Context(ctx)
s.wakeups[jobName] = wakeup s.wakeups[jobName] = wakeup
+4 -2
View File
@@ -2,10 +2,12 @@ package filters
import ( import (
"fmt" "fmt"
"strings"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/zrepl/zrepl/endpoint" "github.com/zrepl/zrepl/endpoint"
"github.com/zrepl/zrepl/zfs" "github.com/zrepl/zrepl/zfs"
"strings"
) )
type DatasetMapFilter struct { type DatasetMapFilter struct {
@@ -159,7 +161,7 @@ func (m DatasetMapFilter) Filter(p *zfs.DatasetPath) (pass bool, err error) {
} }
// Construct a new filter-only DatasetMapFilter from a mapping // Construct a new filter-only DatasetMapFilter from a mapping
// The new filter allows excactly those paths that were not forbidden by the mapping. // The new filter allows exactly those paths that were not forbidden by the mapping.
func (m DatasetMapFilter) InvertedFilter() (inv *DatasetMapFilter, err error) { func (m DatasetMapFilter) InvertedFilter() (inv *DatasetMapFilter, err error) {
if m.filterMode { if m.filterMode {
+89
View File
@@ -0,0 +1,89 @@
package filters
import (
"testing"
"github.com/zrepl/zrepl/zfs"
)
func TestDatasetMapFilter(t *testing.T) {
type testCase struct {
name string
filter map[string]string
checkPass map[string]bool // each entry is checked to match the filter's `pass` return value
}
tcs := []testCase{
{
"default_no_match",
map[string]string{},
map[string]bool{
"": false,
"foo": false,
"zroot": false,
},
},
{
"more_specific_path_has_precedence",
map[string]string{
"tank<": "ok",
"tank/tmp<": "!",
"tank/home/x<": "!",
"tank/home/x/1": "ok",
},
map[string]bool{
"zroot": false,
"tank": true,
"tank/tmp": false,
"tank/tmp/foo": false,
"tank/home/x": false,
"tank/home/y": true,
"tank/home/x/1": true,
"tank/home/x/2": false,
},
},
{
"precedence_of_specific_over_subtree_wildcard_on_same_path",
map[string]string{
"tank/home/bob": "ok",
"tank/home/bob<": "!",
},
map[string]bool{
"tank/home/bob": true,
"tank/home/bob/downloads": false,
},
},
}
for tc := range tcs {
t.Run(tcs[tc].name, func(t *testing.T) {
c := tcs[tc]
f := NewDatasetMapFilter(len(c.filter), true)
for p, a := range c.filter {
err := f.Add(p, a)
if err != nil {
t.Fatalf("incorrect filter spec: %s", err)
}
}
for p, checkPass := range c.checkPass {
zp, err := zfs.NewDatasetPath(p)
if err != nil {
t.Fatalf("incorrect path spec: %s", err)
}
pass, err := f.Filter(zp)
if err != nil {
t.Fatalf("unexpected filter error: %s", err)
}
ok := pass == checkPass
failstr := "OK"
if !ok {
failstr = "FAIL"
t.Fail()
}
t.Logf("%-40q %5v (exp=%v act=%v)", p, failstr, checkPass, pass)
}
})
}
}
-41
View File
@@ -1,41 +0,0 @@
package filters
import (
"github.com/zrepl/zrepl/zfs"
"strings"
)
type AnyFSVFilter struct{}
func NewAnyFSVFilter() AnyFSVFilter {
return AnyFSVFilter{}
}
var _ zfs.FilesystemVersionFilter = AnyFSVFilter{}
func (AnyFSVFilter) Filter(t zfs.VersionType, name string) (accept bool, err error) {
return true, nil
}
type PrefixFilter struct {
prefix string
fstype zfs.VersionType
fstypeSet bool // optionals anyone?
}
var _ zfs.FilesystemVersionFilter = &PrefixFilter{}
func NewPrefixFilter(prefix string) *PrefixFilter {
return &PrefixFilter{prefix: prefix}
}
func NewTypedPrefixFilter(prefix string, versionType zfs.VersionType) *PrefixFilter {
return &PrefixFilter{prefix, versionType, true}
}
func (f *PrefixFilter) Filter(t zfs.VersionType, name string) (accept bool, err error) {
fstypeMatches := (!f.fstypeSet || t == f.fstype)
prefixMatches := strings.HasPrefix(name, f.prefix)
return fstypeMatches && prefixMatches, nil
}
+35
View File
@@ -0,0 +1,35 @@
// Code generated by "stringer -type=Edge"; DO NOT EDIT.
package hooks
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[Pre-1]
_ = x[Callback-2]
_ = x[Post-4]
}
const (
_Edge_name_0 = "PreCallback"
_Edge_name_1 = "Post"
)
var (
_Edge_index_0 = [...]uint8{0, 3, 11}
)
func (i Edge) String() string {
switch {
case 1 <= i && i <= 2:
i -= 1
return _Edge_name_0[_Edge_index_0[i]:_Edge_index_0[i+1]]
case i == 4:
return _Edge_name_1
default:
return "Edge(" + strconv.FormatInt(int64(i), 10) + ")"
}
}
+52
View File
@@ -0,0 +1,52 @@
package hooks
import (
"fmt"
"github.com/zrepl/zrepl/config"
"github.com/zrepl/zrepl/zfs"
)
type List []Hook
func HookFromConfig(in config.HookEnum) (Hook, error) {
switch v := in.Ret.(type) {
case *config.HookCommand:
return NewCommandHook(v)
case *config.HookPostgresCheckpoint:
return PgChkptHookFromConfig(v)
case *config.HookMySQLLockTables:
return MyLockTablesFromConfig(v)
default:
return nil, fmt.Errorf("unknown hook type %T", v)
}
}
func ListFromConfig(in *config.HookList) (r *List, err error) {
hl := make(List, len(*in))
for i, h := range *in {
hl[i], err = HookFromConfig(h)
if err != nil {
return nil, fmt.Errorf("create hook #%d: %s", i+1, err)
}
}
return &hl, nil
}
func (l List) CopyFilteredForFilesystem(fs *zfs.DatasetPath) (ret List, err error) {
ret = make(List, 0, len(l))
for _, h := range l {
var passFilesystem bool
if passFilesystem, err = h.Filesystems().Filter(fs); err != nil {
return nil, err
}
if passFilesystem {
ret = append(ret, h)
}
}
return ret, nil
}
+34
View File
@@ -0,0 +1,34 @@
// Package hooks implements pre- and post snapshot hooks.
//
// Plan is a generic executor for ExpectStepReports before and after an activity specified in a callback.
// It provides a reporting facility that can be polled while the plan is executing to gather progress information.
//
// This package also provides all supported hook type implementations and abstractions around them.
//
// Use For Other Kinds Of ExpectStepReports
//
// This package REQUIRES REFACTORING before it can be used for other activities than snapshots, e.g. pre- and post-replication:
//
// The Hook interface requires a hook to provide a Filesystems() filter, which doesn't make sense for
// all kinds of activities.
//
// The hook implementations should move out of this package.
// However, there is a lot of tight coupling which to untangle isn't worth it ATM.
//
// How This Package Is Used By Package Snapper
//
// Deserialize a config.List using ListFromConfig().
// Then it MUST filter the list to only contain hooks for a particular filesystem using
// hooksList.CopyFilteredForFilesystem(fs).
//
// Then create a CallbackHook using NewCallbackHookForFilesystem().
//
// Pass all of the above to NewPlan() which provides a Report() and Run() method:
//
// Plan.Run(ctx context.Context,dryRun bool) executes the plan and take a context as argument that should contain a logger added using hooks.WithLogger()).
// The value of dryRun is passed through to the hooks' Run() method.
// Command hooks make it available in the environment variable ZREPL_DRYRUN.
//
// Plan.Report() can be called while Plan.Run() is executing to give an overview of plan execution progress (future use in "zrepl status").
//
package hooks
+292
View File
@@ -0,0 +1,292 @@
package hooks
import (
"context"
"fmt"
"strings"
"sync"
"time"
"github.com/zrepl/zrepl/zfs"
)
// Re-export type here so that
// every file in package hooks doesn't
// have to import github.com/zrepl/zrepl/zfs
type Filter zfs.DatasetFilter
type Hook interface {
Filesystems() Filter
// If true and the Pre edge invocation of Run fails, Post edge will not run and other Pre edges will not run.
ErrIsFatal() bool
// Run is invoked by HookPlan for a Pre edge.
// If HookReport.HadError() == false, the Post edge will be invoked, too.
Run(ctx context.Context, edge Edge, phase Phase, dryRun bool, extra Env, state map[interface{}]interface{}) HookReport
String() string
}
type Phase string
const (
PhaseSnapshot = Phase("snapshot")
PhaseTesting = Phase("testing")
)
func (p Phase) String() string {
return string(p)
}
//go:generate stringer -type=Edge
type Edge uint
const (
Pre = Edge(1 << iota)
Callback
Post
)
func (e Edge) StringForPhase(phase Phase) string {
return fmt.Sprintf("%s_%s", e.String(), phase.String())
}
//go:generate enumer -type=StepStatus -trimprefix=Step
type StepStatus int
const (
StepPending StepStatus = 1 << iota
StepExec
StepOk
StepErr
StepSkippedDueToFatalErr
StepSkippedDueToPreErr
)
type HookReport interface {
String() string
HadError() bool
Error() string
}
type Step struct {
Hook Hook
Edge Edge
Status StepStatus
Begin, End time.Time
// Report may be nil
// FIXME cannot serialize this for client status, but contains interesting info (like what error happened)
Report HookReport
state map[interface{}]interface{}
}
func (s Step) String() (out string) {
fatal := "~"
if s.Hook.ErrIsFatal() && s.Edge == Pre {
fatal = "!"
}
runTime := "..."
if s.Status != StepPending {
t := s.End.Sub(s.Begin)
runTime = t.Round(time.Millisecond).String()
}
return fmt.Sprintf("[%s] [%5s] %s [%s] %s", s.Status, runTime, fatal, s.Edge, s.Hook)
}
type Plan struct {
mtx sync.RWMutex
steps []*Step
pre []*Step // protected by mtx
cb *Step
post []*Step // not reversed, i.e. entry at index i corresponds to pre-edge in pre[i]
phase Phase
env Env
}
func NewPlan(hooks *List, phase Phase, cb *CallbackHook, extra Env) (*Plan, error) {
var pre, post []*Step
// TODO sanity check unique name of hook?
for _, hook := range *hooks {
state := make(map[interface{}]interface{})
preE := &Step{
Hook: hook,
Edge: Pre,
Status: StepPending,
state: state,
}
pre = append(pre, preE)
postE := &Step{
Hook: hook,
Edge: Post,
Status: StepPending,
state: state,
}
post = append(post, postE)
}
cbE := &Step{
Hook: cb,
Edge: Callback,
Status: StepPending,
}
steps := make([]*Step, 0, len(pre)+len(post)+1)
steps = append(steps, pre...)
steps = append(steps, cbE)
for i := len(post) - 1; i >= 0; i-- {
steps = append(steps, post[i])
}
plan := &Plan{
phase: phase,
env: extra,
steps: steps,
pre: pre,
post: post,
cb: cbE,
}
return plan, nil
}
type PlanReport []Step
func (p *Plan) Report() PlanReport {
p.mtx.RLock()
defer p.mtx.RUnlock()
rep := make([]Step, len(p.steps))
for i := range rep {
rep[i] = *p.steps[i]
}
return rep
}
func (r PlanReport) HadError() bool {
for _, e := range r {
if e.Status == StepErr {
return true
}
}
return false
}
func (r PlanReport) HadFatalError() bool {
for _, e := range r {
if e.Status == StepSkippedDueToFatalErr {
return true
}
}
return false
}
func (r PlanReport) String() string {
stepStrings := make([]string, len(r))
for i, e := range r {
stepStrings[i] = fmt.Sprintf("%02d %s", i+1, e)
}
return strings.Join(stepStrings, "\n")
}
func (p *Plan) Run(ctx context.Context, dryRun bool) {
p.mtx.RLock()
defer p.mtx.RUnlock()
w := func(f func()) {
p.mtx.RUnlock()
defer p.mtx.RLock()
p.mtx.Lock()
defer p.mtx.Unlock()
f()
}
runHook := func(s *Step, ctx context.Context, edge Edge) HookReport {
w(func() { s.Status = StepExec })
begin := time.Now()
r := s.Hook.Run(ctx, edge, p.phase, dryRun, p.env, s.state)
end := time.Now()
w(func() {
s.Report = r
s.Status = StepOk
if r.HadError() {
s.Status = StepErr
}
s.Begin, s.End = begin, end
})
return r
}
l := getLogger(ctx)
// it's a stack, execute until we reach the end of the list (last item in)
// or fail inbetween
l.Info("run pre-edges in configuration order")
next := 0
for ; next < len(p.pre); next++ {
e := p.pre[next]
l := l.WithField("hook", e.Hook)
r := runHook(e, ctx, Pre)
if r.HadError() {
l.WithError(r).Error("hook invocation failed for pre-edge")
if e.Hook.ErrIsFatal() {
l.Error("the hook run was aborted due to a fatal error in this hook")
break
}
}
}
hadFatalErr := next != len(p.pre)
if hadFatalErr {
l.Error("fatal error in a pre-snapshot hook invocation")
l.Error("no snapshot will be taken")
l.Error("only running post-edges for successful pre-edges")
w(func() {
p.post[next].Status = StepSkippedDueToFatalErr
for i := next + 1; i < len(p.pre); i++ {
p.pre[i].Status = StepSkippedDueToFatalErr
p.post[i].Status = StepSkippedDueToFatalErr
}
p.cb.Status = StepSkippedDueToFatalErr
})
return
}
l.Info("running callback")
cbR := runHook(p.cb, ctx, Callback)
if cbR.HadError() {
l.WithError(cbR).Error("callback failed")
}
l.Info("run post-edges for successful pre-edges in reverse configuration order")
// the constructor produces pre and post entries
// post is NOT reversed
next-- // now at index of last executed pre-edge
for ; next >= 0; next-- {
e := p.post[next]
l := l.WithField("hook", e.Hook)
if p.pre[next].Status != StepOk {
if p.pre[next].Status != StepErr {
panic(fmt.Sprintf("expecting a pre-edge hook report to be either Ok or Err, got %s", p.pre[next].Status))
}
l.Info("skip post-edge because pre-edge failed")
w(func() {
e.Status = StepSkippedDueToPreErr
})
continue
}
report := runHook(e, ctx, Post)
if report.HadError() {
l.WithError(report).Error("hook invocation failed for post-edge")
l.Error("subsequent post-edges run regardless of this post-edge failure")
}
// ErrIsFatal is only relevant for Pre
}
}
+103
View File
@@ -0,0 +1,103 @@
package hooks
import (
"bufio"
"bytes"
"context"
"sync"
"github.com/zrepl/zrepl/logger"
"github.com/zrepl/zrepl/util/envconst"
)
type contextKey int
const (
contextKeyLog contextKey = 0
)
type Logger = logger.Logger
func WithLogger(ctx context.Context, log Logger) context.Context {
return context.WithValue(ctx, contextKeyLog, log)
}
func GetLogger(ctx context.Context) Logger { return getLogger(ctx) }
func getLogger(ctx context.Context) Logger {
if log, ok := ctx.Value(contextKeyLog).(Logger); ok {
return log
}
return logger.NewNullLogger()
}
const MAX_HOOK_LOG_SIZE_DEFAULT int = 1 << 20
type logWriter struct {
/*
Mutex prevents:
concurrent writes to buf, scanner in Write([]byte)
data race on scanner vs Write([]byte)
and concurrent write to buf (call to buf.Reset())
in Close()
(Also, Close() should generally block until any Write() call completes.)
*/
mtx *sync.Mutex
buf bytes.Buffer
scanner *bufio.Scanner
logger Logger
level logger.Level
field string
}
func NewLogWriter(mtx *sync.Mutex, logger Logger, level logger.Level, field string) *logWriter {
w := new(logWriter)
w.mtx = mtx
w.scanner = bufio.NewScanner(&w.buf)
w.logger = logger
w.level = level
w.field = field
return w
}
func (w *logWriter) log(line string) {
w.logger.WithField(w.field, line).Log(w.level, "hook output")
}
func (w *logWriter) logUnreadBytes() error {
for w.scanner.Scan() {
w.log(w.scanner.Text())
}
if w.buf.Cap() > envconst.Int("ZREPL_MAX_HOOK_LOG_SIZE", MAX_HOOK_LOG_SIZE_DEFAULT) {
w.buf.Reset()
}
return nil
}
func (w *logWriter) Write(in []byte) (int, error) {
w.mtx.Lock()
defer w.mtx.Unlock()
n, bufErr := w.buf.Write(in)
if bufErr != nil {
return n, bufErr
}
err := w.logUnreadBytes()
if err != nil {
return n, err
}
// Always reset the scanner for the next Write
w.scanner = bufio.NewScanner(&w.buf)
return n, nil
}
func (w *logWriter) Close() (err error) {
w.mtx.Lock()
defer w.mtx.Unlock()
return w.logUnreadBytes()
}
+65
View File
@@ -0,0 +1,65 @@
package hooks
import (
"context"
"fmt"
"github.com/zrepl/zrepl/daemon/filters"
"github.com/zrepl/zrepl/zfs"
)
type HookJobCallback func(ctx context.Context) error
type CallbackHook struct {
cb HookJobCallback
filter Filter
displayString string
}
func NewCallbackHookForFilesystem(displayString string, fs *zfs.DatasetPath, cb HookJobCallback) *CallbackHook {
filter, _ := filters.DatasetMapFilterFromConfig(map[string]bool{fs.ToString(): true})
return NewCallbackHook(displayString, cb, filter)
}
func NewCallbackHook(displayString string, cb HookJobCallback, filter Filter) *CallbackHook {
return &CallbackHook{
cb: cb,
filter: filter,
displayString: displayString,
}
}
func (h *CallbackHook) Filesystems() Filter {
return h.filter
}
func (h *CallbackHook) ErrIsFatal() bool {
return false // callback is by definition
}
func (h *CallbackHook) String() string {
return h.displayString
}
type CallbackHookReport struct {
Name string
Err error
}
func (r *CallbackHookReport) String() string {
if r.HadError() {
return r.Error()
}
return r.Name
}
func (r *CallbackHookReport) HadError() bool { return r.Err != nil }
func (r *CallbackHookReport) Error() string {
return fmt.Sprintf("%s error: %s", r.Name, r.Err)
}
func (h *CallbackHook) Run(ctx context.Context, edge Edge, phase Phase, dryRun bool, extra Env, state map[interface{}]interface{}) HookReport {
err := h.cb(ctx)
return &CallbackHookReport{h.displayString, err}
}
+192
View File
@@ -0,0 +1,192 @@
package hooks
import (
"context"
"fmt"
"io"
"math"
"os"
"os/exec"
"sort"
"strings"
"sync"
"time"
"github.com/zrepl/zrepl/config"
"github.com/zrepl/zrepl/daemon/filters"
"github.com/zrepl/zrepl/logger"
"github.com/zrepl/zrepl/util/circlog"
"github.com/zrepl/zrepl/util/envconst"
)
type HookEnvVar string
const (
EnvType HookEnvVar = "ZREPL_HOOKTYPE"
EnvDryRun HookEnvVar = "ZREPL_DRYRUN"
EnvFS HookEnvVar = "ZREPL_FS"
EnvSnapshot HookEnvVar = "ZREPL_SNAPNAME"
EnvTimeout HookEnvVar = "ZREPL_TIMEOUT"
)
type Env map[HookEnvVar]string
func NewHookEnv(edge Edge, phase Phase, dryRun bool, timeout time.Duration, extra Env) Env {
r := Env{
EnvTimeout: fmt.Sprintf("%.f", math.Floor(timeout.Seconds())),
}
edgeString := edge.StringForPhase(phase)
r[EnvType] = strings.ToLower(edgeString)
var dryRunString string
if dryRun {
dryRunString = "true"
} else {
dryRunString = ""
}
r[EnvDryRun] = dryRunString
for k, v := range extra {
r[k] = v
}
return r
}
type CommandHook struct {
edge Edge
filter Filter
errIsFatal bool
command string
timeout time.Duration
}
type CommandHookReport struct {
Command string
Args []string // currently always empty
Env Env
Err error
CapturedStdoutStderrCombined []byte
}
func (r *CommandHookReport) String() string {
// Reproduces a POSIX shell-compatible command line
var cmdLine strings.Builder
sep := ""
// Make sure environment variables are always
// printed in the same order
var hookEnvKeys []HookEnvVar
for k := range r.Env {
hookEnvKeys = append(hookEnvKeys, k)
}
sort.Slice(hookEnvKeys, func(i, j int) bool { return string(hookEnvKeys[i]) < string(hookEnvKeys[j]) })
for _, k := range hookEnvKeys {
cmdLine.WriteString(fmt.Sprintf("%s%s='%s'", sep, k, r.Env[k]))
sep = " "
}
cmdLine.WriteString(fmt.Sprintf("%s%s", sep, r.Command))
for _, a := range r.Args {
cmdLine.WriteString(fmt.Sprintf("%s'%s'", sep, a))
}
return fmt.Sprintf("command hook invocation: \"%s\"", cmdLine.String()) // no %q to make copy-pastable
}
func (r *CommandHookReport) Error() string {
if r.Err == nil {
return ""
}
return fmt.Sprintf("%s FAILED with error: %s", r.String(), r.Err)
}
func (r *CommandHookReport) HadError() bool {
return r.Err != nil
}
func NewCommandHook(in *config.HookCommand) (r *CommandHook, err error) {
r = &CommandHook{
errIsFatal: in.ErrIsFatal,
command: in.Path,
timeout: in.Timeout,
}
r.filter, err = filters.DatasetMapFilterFromConfig(in.Filesystems)
if err != nil {
return nil, fmt.Errorf("cannot parse filesystem filter: %s", err)
}
r.edge = Pre | Post
return r, nil
}
func (h *CommandHook) Filesystems() Filter {
return h.filter
}
func (h *CommandHook) ErrIsFatal() bool {
return h.errIsFatal
}
func (h *CommandHook) String() string {
return h.command
}
func (h *CommandHook) Run(ctx context.Context, edge Edge, phase Phase, dryRun bool, extra Env, state map[interface{}]interface{}) HookReport {
l := getLogger(ctx).WithField("command", h.command)
cmdCtx, cancel := context.WithTimeout(ctx, h.timeout)
defer cancel()
cmdExec := exec.CommandContext(cmdCtx, h.command)
hookEnv := NewHookEnv(edge, phase, dryRun, h.timeout, extra)
cmdEnv := os.Environ()
for k, v := range hookEnv {
cmdEnv = append(cmdEnv, fmt.Sprintf("%s=%s", k, v))
}
cmdExec.Env = cmdEnv
var scanMutex sync.Mutex
combinedOutput, err := circlog.NewCircularLog(envconst.Int("ZREPL_MAX_HOOK_LOG_SIZE", MAX_HOOK_LOG_SIZE_DEFAULT))
if err != nil {
return &CommandHookReport{Err: err}
}
logErrWriter := NewLogWriter(&scanMutex, l, logger.Warn, "stderr")
logOutWriter := NewLogWriter(&scanMutex, l, logger.Info, "stdout")
defer logErrWriter.Close()
defer logOutWriter.Close()
cmdExec.Stderr = io.MultiWriter(logErrWriter, combinedOutput)
cmdExec.Stdout = io.MultiWriter(logOutWriter, combinedOutput)
report := &CommandHookReport{
Command: h.command,
Env: hookEnv,
// no report.Args
}
err = cmdExec.Start()
if err != nil {
report.Err = err
return report
}
err = cmdExec.Wait()
combinedOutputBytes := combinedOutput.Bytes()
report.CapturedStdoutStderrCombined = make([]byte, len(combinedOutputBytes))
copy(report.CapturedStdoutStderrCombined, combinedOutputBytes)
if err != nil {
if cmdCtx.Err() == context.DeadlineExceeded {
report.Err = fmt.Errorf("timed out after %s: %s", h.timeout, err)
return report
}
report.Err = err
return report
}
return report
}
+145
View File
@@ -0,0 +1,145 @@
package hooks
import (
"context"
"database/sql"
"fmt"
"strings"
sqldriver "database/sql/driver"
"github.com/go-sql-driver/mysql"
"github.com/pkg/errors"
"github.com/zrepl/zrepl/config"
"github.com/zrepl/zrepl/daemon/filters"
"github.com/zrepl/zrepl/zfs"
)
// https://dev.mysql.com/doc/mysql-backup-excerpt/5.7/en/backup-methods.html
//
// Making Backups Using a File System Snapshot:
//
// If you are using a Veritas file system, you can make a backup like this:
//
// From a client program, execute FLUSH TABLES WITH READ LOCK.
// From another shell, execute mount vxfs snapshot.
// From the first client, execute UNLOCK TABLES.
// Copy files from the snapshot.
// Unmount the snapshot.
//
// Similar snapshot capabilities may be available in other file systems, such as LVM or ZFS.
type MySQLLockTables struct {
errIsFatal bool
connector sqldriver.Connector
filesystems Filter
}
type myLockTablesStateKey int
const (
myLockTablesConnection myLockTablesStateKey = 1 + iota
)
func MyLockTablesFromConfig(in *config.HookMySQLLockTables) (*MySQLLockTables, error) {
conf, err := mysql.ParseDSN(in.DSN)
if err != nil {
return nil, errors.Wrap(err, "`dsn` invalid")
}
cn, err := mysql.NewConnector(conf)
if err != nil {
return nil, errors.Wrap(err, "`connect` invalid")
}
filesystems, err := filters.DatasetMapFilterFromConfig(in.Filesystems)
if err != nil {
return nil, errors.Wrap(err, "`filesystems` invalid")
}
return &MySQLLockTables{
in.ErrIsFatal,
cn,
filesystems,
}, nil
}
func (h *MySQLLockTables) ErrIsFatal() bool { return h.errIsFatal }
func (h *MySQLLockTables) Filesystems() Filter { return h.filesystems }
func (h *MySQLLockTables) String() string { return "MySQL FLUSH TABLES WITH READ LOCK" }
type MyLockTablesReport struct {
What string
Err error
}
func (r *MyLockTablesReport) HadError() bool { return r.Err != nil }
func (r *MyLockTablesReport) Error() string { return r.String() }
func (r *MyLockTablesReport) String() string {
var s strings.Builder
s.WriteString(r.What)
if r.Err != nil {
fmt.Fprintf(&s, ": %s", r.Err)
}
return s.String()
}
func (h *MySQLLockTables) Run(ctx context.Context, edge Edge, phase Phase, dryRun bool, extra Env, state map[interface{}]interface{}) HookReport {
fs, ok := extra[EnvFS]
if !ok {
panic(extra)
}
dp, err := zfs.NewDatasetPath(fs)
if err != nil {
panic(err)
}
if pass, err := h.filesystems.Filter(dp); err != nil {
return &MyLockTablesReport{What: "filesystem filter", Err: err}
} else if !pass {
getLogger(ctx).Debug("filesystem does not match filter, skipping")
return &MyLockTablesReport{What: "filesystem filter skipped this filesystem", Err: nil}
}
switch edge {
case Pre:
err := h.doRunPre(ctx, dp, dryRun, state)
return &MyLockTablesReport{"FLUSH TABLES WITH READ LOCK", err}
case Post:
err := h.doRunPost(ctx, dp, dryRun, state)
return &MyLockTablesReport{"UNLOCK TABLES", err}
}
return &MyLockTablesReport{What: "skipped this edge", Err: nil}
}
func (h *MySQLLockTables) doRunPre(ctx context.Context, fs *zfs.DatasetPath, dry bool, state map[interface{}]interface{}) (err error) {
db := sql.OpenDB(h.connector)
defer func(err *error) {
if *err != nil {
db.Close()
}
}(&err)
getLogger(ctx).Debug("do FLUSH TABLES WITH READ LOCK")
_, err = db.ExecContext(ctx, "FLUSH TABLES WITH READ LOCK")
if err != nil {
return
}
state[myLockTablesConnection] = db
return nil
}
func (h *MySQLLockTables) doRunPost(ctx context.Context, fs *zfs.DatasetPath, dry bool, state map[interface{}]interface{}) error {
db := state[myLockTablesConnection].(*sql.DB)
defer db.Close()
getLogger(ctx).Debug("do UNLOCK TABLES")
_, err := db.ExecContext(ctx, "UNLOCK TABLES")
if err != nil {
return err
}
return nil
}
@@ -0,0 +1,98 @@
package hooks
import (
"context"
"database/sql"
"fmt"
"math"
"time"
"github.com/lib/pq"
"github.com/pkg/errors"
"github.com/zrepl/zrepl/config"
"github.com/zrepl/zrepl/daemon/filters"
"github.com/zrepl/zrepl/zfs"
)
type PgChkptHook struct {
errIsFatal bool
connector *pq.Connector
filesystems Filter
}
func PgChkptHookFromConfig(in *config.HookPostgresCheckpoint) (*PgChkptHook, error) {
filesystems, err := filters.DatasetMapFilterFromConfig(in.Filesystems)
if err != nil {
return nil, errors.Wrap(err, "`filesystems` invalid")
}
cn, err := pq.NewConnector(in.DSN)
if err != nil {
return nil, errors.Wrap(err, "`dsn` invalid")
}
return &PgChkptHook{
in.ErrIsFatal,
cn,
filesystems,
}, nil
}
func (h *PgChkptHook) ErrIsFatal() bool { return h.errIsFatal }
func (h *PgChkptHook) Filesystems() Filter { return h.filesystems }
func (h *PgChkptHook) String() string { return "postgres checkpoint" }
type PgChkptHookReport struct{ Err error }
func (r *PgChkptHookReport) HadError() bool { return r.Err != nil }
func (r *PgChkptHookReport) Error() string { return r.Err.Error() }
func (r *PgChkptHookReport) String() string {
if r.Err != nil {
return fmt.Sprintf("postgres CHECKPOINT failed: %s", r.Err)
} else {
return "postgres CHECKPOINT completed"
}
}
func (h *PgChkptHook) Run(ctx context.Context, edge Edge, phase Phase, dryRun bool, extra Env, state map[interface{}]interface{}) HookReport {
if edge != Pre {
return &PgChkptHookReport{nil}
}
fs, ok := extra[EnvFS]
if !ok {
panic(extra)
}
dp, err := zfs.NewDatasetPath(fs)
if err != nil {
panic(err)
}
err = h.doRunPre(ctx, dp, dryRun)
return &PgChkptHookReport{err}
}
func (h *PgChkptHook) doRunPre(ctx context.Context, fs *zfs.DatasetPath, dry bool) error {
if pass, err := h.filesystems.Filter(fs); err != nil || !pass {
getLogger(ctx).Debug("filesystem does not match filter, skipping")
return err
}
db := sql.OpenDB(h.connector)
defer db.Close()
dl, ok := ctx.Deadline()
if ok {
timeout := uint64(math.Floor(time.Until(dl).Seconds() * 1000)) // TODO go1.13 milliseconds
getLogger(ctx).WithField("statement_timeout", timeout).Debug("setting statement timeout for CHECKPOINT")
_, err := db.ExecContext(ctx, "SET statement_timeout TO ?", timeout)
if err != nil {
return err
}
}
if dry {
getLogger(ctx).Debug("dry-run - use ping instead of CHECKPOINT")
return db.PingContext(ctx)
}
getLogger(ctx).Info("execute CHECKPOINT command")
_, err := db.ExecContext(ctx, "CHECKPOINT")
return err
}
+484
View File
@@ -0,0 +1,484 @@
package hooks_test
import (
"bytes"
"context"
"fmt"
"os"
"regexp"
"testing"
"text/template"
"github.com/stretchr/testify/require"
"github.com/zrepl/zrepl/config"
"github.com/zrepl/zrepl/daemon/hooks"
"github.com/zrepl/zrepl/logger"
"github.com/zrepl/zrepl/zfs"
)
type comparisonAssertionFunc func(require.TestingT, interface{}, interface{}, ...interface{})
type valueAssertionFunc func(require.TestingT, interface{}, ...interface{})
type expectStep struct {
ExpectedEdge hooks.Edge
ExpectStatus hooks.StepStatus
OutputTest valueAssertionFunc
ErrorTest valueAssertionFunc
}
type testCase struct {
Name string
Config []string
IsSlow bool
SuppressOutput bool
ExpectCallbackSkipped bool
ExpectHadFatalErr bool
ExpectHadError bool
ExpectStepReports []expectStep
}
func curryLeft(f comparisonAssertionFunc, expected interface{}) valueAssertionFunc {
return curry(f, expected, false)
}
func curryRight(f comparisonAssertionFunc, expected interface{}) valueAssertionFunc {
return curry(f, expected, true)
}
func curry(f comparisonAssertionFunc, expected interface{}, right bool) (ret valueAssertionFunc) {
ret = func(t require.TestingT, s interface{}, v ...interface{}) {
var x interface{}
var y interface{}
if right {
x = s
y = expected
} else {
x = expected
y = s
}
if len(v) > 0 {
f(t, x, y, v)
} else {
f(t, x, y)
}
}
return
}
func TestHooks(t *testing.T) {
testFSName := "testpool/testdataset"
testSnapshotName := "testsnap"
tmpl, err := template.New("TestHooks").Parse(`
jobs:
- name: TestHooks
type: snap
filesystems: {"<": true}
snapshotting:
type: periodic
interval: 1m
prefix: zrepl_snapjob_
hooks:
{{- template "List" . }}
pruning:
keep:
- type: last_n
count: 10
`)
if err != nil {
panic(err)
}
regexpTest := func(s string) valueAssertionFunc {
return curryLeft(require.Regexp, regexp.MustCompile(s))
}
containsTest := func(s string) valueAssertionFunc {
return curryRight(require.Contains, s)
}
testTable := []testCase{
testCase{
Name: "no_hooks",
ExpectStepReports: []expectStep{
{ExpectedEdge: hooks.Callback, ExpectStatus: hooks.StepOk},
},
},
testCase{
Name: "timeout",
IsSlow: true,
ExpectHadError: true,
Config: []string{`{type: command, path: {{.WorkDir}}/test/test-timeout.sh, timeout: 2s}`},
ExpectStepReports: []expectStep{
expectStep{
ExpectedEdge: hooks.Pre,
ExpectStatus: hooks.StepErr,
OutputTest: containsTest(fmt.Sprintf("TEST pre_testing %s@%s ZREPL_TIMEOUT=2", testFSName, testSnapshotName)),
ErrorTest: regexpTest(`timed out after 2(.\d+)?s`),
},
expectStep{ExpectedEdge: hooks.Callback, ExpectStatus: hooks.StepOk},
expectStep{
ExpectedEdge: hooks.Post,
ExpectStatus: hooks.StepSkippedDueToPreErr,
},
},
},
testCase{
Name: "check_env",
Config: []string{`{type: command, path: {{.WorkDir}}/test/test-report-env.sh}`},
ExpectHadError: false,
ExpectStepReports: []expectStep{
expectStep{
ExpectedEdge: hooks.Pre,
ExpectStatus: hooks.StepOk,
OutputTest: containsTest(fmt.Sprintf("TEST pre_testing %s@%s", testFSName, testSnapshotName)),
},
expectStep{ExpectedEdge: hooks.Callback, ExpectStatus: hooks.StepOk},
expectStep{
ExpectedEdge: hooks.Post,
ExpectStatus: hooks.StepOk,
OutputTest: containsTest(fmt.Sprintf("TEST post_testing %s@%s", testFSName, testSnapshotName)),
},
},
},
testCase{
Name: "nonfatal_pre_error_continues",
ExpectCallbackSkipped: false,
ExpectHadError: true,
Config: []string{`{type: command, path: {{.WorkDir}}/test/test-error.sh}`},
ExpectStepReports: []expectStep{
expectStep{
ExpectedEdge: hooks.Pre,
ExpectStatus: hooks.StepErr,
OutputTest: containsTest(fmt.Sprintf("TEST ERROR pre_testing %s@%s", testFSName, testSnapshotName)),
ErrorTest: regexpTest("^command hook invocation.*exit status 1$"),
},
expectStep{ExpectedEdge: hooks.Callback, ExpectStatus: hooks.StepOk},
expectStep{
ExpectedEdge: hooks.Post,
ExpectStatus: hooks.StepSkippedDueToPreErr, // post-edge is not executed for failing pre-edge
},
},
},
testCase{
Name: "pre_error_fatal_skips_subsequent_pre_edges_and_callback_and_its_post_edge_and_post_edges",
ExpectCallbackSkipped: true,
ExpectHadFatalErr: true,
ExpectHadError: true,
Config: []string{
`{type: command, path: {{.WorkDir}}/test/test-error.sh, err_is_fatal: true}`,
`{type: command, path: {{.WorkDir}}/test/test-report-env.sh}`,
},
ExpectStepReports: []expectStep{
expectStep{
ExpectedEdge: hooks.Pre,
ExpectStatus: hooks.StepErr,
OutputTest: containsTest(fmt.Sprintf("TEST ERROR pre_testing %s@%s", testFSName, testSnapshotName)),
ErrorTest: regexpTest("^command hook invocation.*exit status 1$"),
},
expectStep{
ExpectedEdge: hooks.Pre,
ExpectStatus: hooks.StepSkippedDueToFatalErr,
},
expectStep{ExpectedEdge: hooks.Callback, ExpectStatus: hooks.StepSkippedDueToFatalErr},
expectStep{
ExpectedEdge: hooks.Post,
ExpectStatus: hooks.StepSkippedDueToFatalErr,
},
expectStep{
ExpectedEdge: hooks.Post,
ExpectStatus: hooks.StepSkippedDueToFatalErr,
},
},
},
testCase{
Name: "post_error_fails_are_ignored_even_if_fatal",
ExpectHadFatalErr: false, // only occurs during Post, so it's not a fatal error
ExpectHadError: true,
Config: []string{
`{type: command, path: {{.WorkDir}}/test/test-post-error.sh, err_is_fatal: true}`,
`{type: command, path: {{.WorkDir}}/test/test-report-env.sh}`,
},
ExpectStepReports: []expectStep{
expectStep{
// No-action run of test-post-error.sh
ExpectedEdge: hooks.Pre,
ExpectStatus: hooks.StepOk,
OutputTest: require.Empty,
},
expectStep{
// Pre run of test-report-env.sh
ExpectedEdge: hooks.Pre,
ExpectStatus: hooks.StepOk,
OutputTest: containsTest(fmt.Sprintf("TEST pre_testing %s@%s", testFSName, testSnapshotName)),
},
expectStep{ExpectedEdge: hooks.Callback, ExpectStatus: hooks.StepOk},
expectStep{
ExpectedEdge: hooks.Post,
ExpectStatus: hooks.StepOk,
OutputTest: containsTest(fmt.Sprintf("TEST post_testing %s@%s", testFSName, testSnapshotName)),
},
expectStep{
ExpectedEdge: hooks.Post,
ExpectStatus: hooks.StepErr,
OutputTest: containsTest(fmt.Sprintf("TEST ERROR post_testing %s@%s", testFSName, testSnapshotName)),
ErrorTest: regexpTest("^command hook invocation.*exit status 1$"),
},
},
},
testCase{
Name: "cleanup_check_env",
Config: []string{`{type: command, path: {{.WorkDir}}/test/test-report-env.sh}`},
ExpectStepReports: []expectStep{
expectStep{
ExpectedEdge: hooks.Pre,
ExpectStatus: hooks.StepOk,
OutputTest: containsTest(fmt.Sprintf("TEST pre_testing %s@%s", testFSName, testSnapshotName)),
},
expectStep{ExpectedEdge: hooks.Callback, ExpectStatus: hooks.StepOk},
expectStep{
ExpectedEdge: hooks.Post,
ExpectStatus: hooks.StepOk,
OutputTest: containsTest(fmt.Sprintf("TEST post_testing %s@%s", testFSName, testSnapshotName)),
},
},
},
testCase{
Name: "pre_error_cancels_post",
Config: []string{`{type: command, path: {{.WorkDir}}/test/test-pre-error-post-ok.sh}`},
ExpectHadError: true,
ExpectHadFatalErr: false,
ExpectStepReports: []expectStep{
expectStep{
ExpectedEdge: hooks.Pre,
ExpectStatus: hooks.StepErr,
OutputTest: containsTest(fmt.Sprintf("TEST ERROR pre_testing %s@%s", testFSName, testSnapshotName)),
ErrorTest: regexpTest("^command hook invocation.*exit status 1$"),
},
expectStep{ExpectedEdge: hooks.Callback, ExpectStatus: hooks.StepOk},
expectStep{
ExpectedEdge: hooks.Post,
ExpectStatus: hooks.StepSkippedDueToPreErr,
},
},
},
testCase{
Name: "pre_error_does_not_cancel_other_posts_but_itself",
Config: []string{
`{type: command, path: {{.WorkDir}}/test/test-report-env.sh}`,
`{type: command, path: {{.WorkDir}}/test/test-pre-error-post-ok.sh}`,
},
ExpectHadError: true,
ExpectHadFatalErr: false,
ExpectStepReports: []expectStep{
expectStep{
ExpectedEdge: hooks.Pre,
ExpectStatus: hooks.StepOk,
OutputTest: containsTest(fmt.Sprintf("TEST pre_testing %s@%s", testFSName, testSnapshotName)),
},
expectStep{
ExpectedEdge: hooks.Pre,
ExpectStatus: hooks.StepErr,
OutputTest: containsTest(fmt.Sprintf("TEST ERROR pre_testing %s@%s", testFSName, testSnapshotName)),
ErrorTest: regexpTest("^command hook invocation.*exit status 1$"),
},
expectStep{ExpectedEdge: hooks.Callback, ExpectStatus: hooks.StepOk},
expectStep{
ExpectedEdge: hooks.Post,
ExpectStatus: hooks.StepSkippedDueToPreErr,
},
expectStep{
ExpectedEdge: hooks.Post,
ExpectStatus: hooks.StepOk,
OutputTest: containsTest(fmt.Sprintf("TEST post_testing %s@%s", testFSName, testSnapshotName)),
},
},
},
testCase{
Name: "exceed_buffer_limit",
SuppressOutput: true,
Config: []string{`{type: command, path: {{.WorkDir}}/test/test-large-stdout.sh}`},
ExpectHadError: false,
ExpectHadFatalErr: false,
ExpectStepReports: []expectStep{
expectStep{
ExpectedEdge: hooks.Pre,
ExpectStatus: hooks.StepOk,
OutputTest: func(t require.TestingT, s interface{}, v ...interface{}) {
require.Len(t, s, 1<<20)
},
},
expectStep{ExpectedEdge: hooks.Callback, ExpectStatus: hooks.StepOk},
expectStep{
// No-action run of above hook
ExpectedEdge: hooks.Post,
ExpectStatus: hooks.StepOk,
OutputTest: require.Empty,
},
},
},
/*
Following not intended to test functionality of
filter package. Only to demonstrate that hook
filters are being applied. The following should
result in NO hooks running. If it does run, a
fatal hooks.RunReport will be returned.
*/
testCase{
Name: "exclude_all_filesystems",
Config: []string{
`{type: command, path: {{.WorkDir}}/test/test-error.sh, err_is_fatal: true, filesystems: {"<": false}}`,
},
ExpectStepReports: []expectStep{
expectStep{
ExpectedEdge: hooks.Callback,
ExpectStatus: hooks.StepOk,
},
},
},
}
parseHookConfig := func(t *testing.T, in string) *config.Config {
t.Helper()
conf, err := config.ParseConfigBytes([]byte(in))
require.NoError(t, err)
require.NotNil(t, conf)
return conf
}
fillHooks := func(tt *testCase) string {
// make hook path absolute
cwd, err := os.Getwd()
if err != nil {
panic("os.Getwd() failed")
}
var hooksTmpl string = "\n"
for _, l := range tt.Config {
hooksTmpl += fmt.Sprintf(" - %s\n", l)
}
tmpl.New("List").Parse(hooksTmpl)
var outBytes bytes.Buffer
data := struct {
WorkDir string
}{
WorkDir: cwd,
}
if err := tmpl.Execute(&outBytes, data); err != nil {
panic(err)
}
return outBytes.String()
}
var c *config.Config
fs, err := zfs.NewDatasetPath(testFSName)
require.NoError(t, err)
log := logger.NewTestLogger(t)
var cbReached bool
cb := hooks.NewCallbackHookForFilesystem("testcallback", fs, func(_ context.Context) error {
cbReached = true
return nil
})
hookEnvExtra := hooks.Env{
hooks.EnvFS: fs.ToString(),
hooks.EnvSnapshot: testSnapshotName,
}
for _, tt := range testTable {
if testing.Short() && tt.IsSlow {
continue
}
t.Run(tt.Name, func(t *testing.T) {
c = parseHookConfig(t, fillHooks(&tt))
snp := c.Jobs[0].Ret.(*config.SnapJob).Snapshotting.Ret.(*config.SnapshottingPeriodic)
hookList, err := hooks.ListFromConfig(&snp.Hooks)
require.NoError(t, err)
filteredHooks, err := hookList.CopyFilteredForFilesystem(fs)
require.NoError(t, err)
plan, err := hooks.NewPlan(&filteredHooks, hooks.PhaseTesting, cb, hookEnvExtra)
require.NoError(t, err)
t.Logf("REPORT PRE EXECUTION:\n%s", plan.Report())
cbReached = false
ctx := context.Background()
if testing.Verbose() && !tt.SuppressOutput {
ctx = hooks.WithLogger(ctx, log)
}
plan.Run(ctx, false)
report := plan.Report()
t.Logf("REPORT POST EXECUTION:\n%s", report)
/*
* TEST ASSERTIONS
*/
t.Logf("len(runReports)=%v", len(report))
t.Logf("len(tt.ExpectStepReports)=%v", len(tt.ExpectStepReports))
require.Equal(t, len(tt.ExpectStepReports), len(report), "ExpectStepReports must be same length as expected number of hook runs, excluding possible Callback")
// Check if callback ran, when required
if tt.ExpectCallbackSkipped {
require.False(t, cbReached, "callback ran but should not have run")
} else {
require.True(t, cbReached, "callback should have run but did not")
}
// Check if a fatal run error occurred and was expected
require.Equal(t, tt.ExpectHadFatalErr, report.HadFatalError(), "non-matching HadFatalError")
require.Equal(t, tt.ExpectHadError, report.HadError(), "non-matching HadError")
if tt.ExpectHadFatalErr {
require.True(t, tt.ExpectHadError, "ExpectHadFatalErr implies ExpectHadError")
}
if !tt.ExpectHadError {
require.False(t, tt.ExpectHadFatalErr, "!ExpectHadError implies !ExpectHadFatalErr")
}
// Iterate through each expected hook run
for i, hook := range tt.ExpectStepReports {
t.Logf("expecting report conforming to %v", hook)
exp, act := hook.ExpectStatus, report[i].Status
require.Equal(t, exp, act, "%s != %s", exp, act)
// Check for required ExpectedEdge
require.NotZero(t, hook.ExpectedEdge, "each hook must have an ExpectedEdge")
require.Equal(t, hook.ExpectedEdge, report[i].Edge,
"incorrect edge: expected %q, actual %q", hook.ExpectedEdge.String(), report[i].Edge.String(),
)
// Check for expected output
if hook.OutputTest != nil {
require.IsType(t, (*hooks.CommandHookReport)(nil), report[i].Report)
chr := report[i].Report.(*hooks.CommandHookReport)
hook.OutputTest(t, string(chr.CapturedStdoutStderrCombined))
}
// Check for expected errors
if hook.ErrorTest != nil {
hook.ErrorTest(t, string(report[i].Report.Error()))
}
}
})
}
}
+77
View File
@@ -0,0 +1,77 @@
// Code generated by "enumer -type=StepStatus -trimprefix=Step"; DO NOT EDIT.
//
package hooks
import (
"fmt"
)
const (
_StepStatusName_0 = "PendingExec"
_StepStatusName_1 = "Ok"
_StepStatusName_2 = "Err"
_StepStatusName_3 = "SkippedDueToFatalErr"
_StepStatusName_4 = "SkippedDueToPreErr"
)
var (
_StepStatusIndex_0 = [...]uint8{0, 7, 11}
_StepStatusIndex_1 = [...]uint8{0, 2}
_StepStatusIndex_2 = [...]uint8{0, 3}
_StepStatusIndex_3 = [...]uint8{0, 20}
_StepStatusIndex_4 = [...]uint8{0, 18}
)
func (i StepStatus) String() string {
switch {
case 1 <= i && i <= 2:
i -= 1
return _StepStatusName_0[_StepStatusIndex_0[i]:_StepStatusIndex_0[i+1]]
case i == 4:
return _StepStatusName_1
case i == 8:
return _StepStatusName_2
case i == 16:
return _StepStatusName_3
case i == 32:
return _StepStatusName_4
default:
return fmt.Sprintf("StepStatus(%d)", i)
}
}
var _StepStatusValues = []StepStatus{1, 2, 4, 8, 16, 32}
var _StepStatusNameToValueMap = map[string]StepStatus{
_StepStatusName_0[0:7]: 1,
_StepStatusName_0[7:11]: 2,
_StepStatusName_1[0:2]: 4,
_StepStatusName_2[0:3]: 8,
_StepStatusName_3[0:20]: 16,
_StepStatusName_4[0:18]: 32,
}
// StepStatusString retrieves an enum value from the enum constants string name.
// Throws an error if the param is not part of the enum.
func StepStatusString(s string) (StepStatus, error) {
if val, ok := _StepStatusNameToValueMap[s]; ok {
return val, nil
}
return 0, fmt.Errorf("%s does not belong to StepStatus values", s)
}
// StepStatusValues returns all values of the enum
func StepStatusValues() []StepStatus {
return _StepStatusValues
}
// IsAStepStatus returns "true" if the value is listed in the enum definition. "false" otherwise
func (i StepStatus) IsAStepStatus() bool {
for _, v := range _StepStatusValues {
if i == v {
return true
}
}
return false
}
+5
View File
@@ -0,0 +1,5 @@
#!/bin/sh -eu
>&2 echo "TEST ERROR $ZREPL_HOOKTYPE $ZREPL_FS@$ZREPL_SNAPNAME"
exit 1
+18
View File
@@ -0,0 +1,18 @@
#!/bin/sh -eu
# Exceed default hook log size of 1<<20 bytes by double
# The Go test should fail if buffer size exceeds 1MB
pre_testing() {
ln=0
while :; do printf '%06d: 012345678901234567890123456789012345678901234567890123456789012345678901\n' "$ln"; ln="$(($ln + 1))"; done \
| head -c$(( 2 << 20 ))
}
case "$ZREPL_HOOKTYPE" in
pre_testing)
"$ZREPL_HOOKTYPE";;
*)
# Not handled by this script
exit 0;;
esac
+15
View File
@@ -0,0 +1,15 @@
#!/bin/sh -eu
post_testing() {
>&2 echo "TEST ERROR $ZREPL_HOOKTYPE $ZREPL_FS@$ZREPL_SNAPNAME"
exit 1
}
case "$ZREPL_HOOKTYPE" in
post_testing)
"$ZREPL_HOOKTYPE";;
*)
# Not handled by this script
exit 0;;
esac
+11
View File
@@ -0,0 +1,11 @@
#!/bin/sh -eu
if [ "$ZREPL_HOOKTYPE" = "pre_testing" ]; then
>&2 echo "TEST ERROR $ZREPL_HOOKTYPE $ZREPL_FS@$ZREPL_SNAPNAME"
exit 1
elif [ "$ZREPL_HOOKTYPE" = "post_testing" ]; then
echo "TEST $ZREPL_HOOKTYPE $ZREPL_FS@$ZREPL_SNAPNAME"
else
printf "Unknown hook type: %s" "$ZREPL_HOOKTYPE"
exit 255
fi
+3
View File
@@ -0,0 +1,3 @@
#!/bin/sh -eu
echo "TEST $ZREPL_HOOKTYPE $ZREPL_FS@$ZREPL_SNAPNAME ${ZREPL_DRYRUN:+(dry run)}"
+5
View File
@@ -0,0 +1,5 @@
#!/bin/sh -eu
echo "TEST $ZREPL_HOOKTYPE $ZREPL_FS@$ZREPL_SNAPNAME ZREPL_TIMEOUT=$ZREPL_TIMEOUT"
exec sleep $(($ZREPL_TIMEOUT + 1))
+202 -133
View File
@@ -2,9 +2,13 @@ package job
import ( import (
"context" "context"
"fmt"
"sync"
"time"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/problame/go-streamrpc"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"github.com/zrepl/zrepl/config" "github.com/zrepl/zrepl/config"
"github.com/zrepl/zrepl/daemon/filters" "github.com/zrepl/zrepl/daemon/filters"
"github.com/zrepl/zrepl/daemon/job/reset" "github.com/zrepl/zrepl/daemon/job/reset"
@@ -12,32 +16,32 @@ import (
"github.com/zrepl/zrepl/daemon/logging" "github.com/zrepl/zrepl/daemon/logging"
"github.com/zrepl/zrepl/daemon/pruner" "github.com/zrepl/zrepl/daemon/pruner"
"github.com/zrepl/zrepl/daemon/snapper" "github.com/zrepl/zrepl/daemon/snapper"
"github.com/zrepl/zrepl/daemon/transport/connecter"
"github.com/zrepl/zrepl/endpoint" "github.com/zrepl/zrepl/endpoint"
"github.com/zrepl/zrepl/replication" "github.com/zrepl/zrepl/replication"
"github.com/zrepl/zrepl/util/envconst" "github.com/zrepl/zrepl/replication/driver"
"github.com/zrepl/zrepl/replication/logic"
"github.com/zrepl/zrepl/replication/report"
"github.com/zrepl/zrepl/rpc"
"github.com/zrepl/zrepl/transport"
"github.com/zrepl/zrepl/transport/fromconfig"
"github.com/zrepl/zrepl/zfs" "github.com/zrepl/zrepl/zfs"
"sync"
"time"
) )
type ActiveSide struct { type ActiveSide struct {
mode activeMode mode activeMode
name string name endpoint.JobID
clientFactory *connecter.ClientFactory connecter transport.Connecter
prunerFactory *pruner.PrunerFactory prunerFactory *pruner.PrunerFactory
promRepStateSecs *prometheus.HistogramVec // labels: state
promRepStateSecs *prometheus.HistogramVec // labels: state promPruneSecs *prometheus.HistogramVec // labels: prune_side
promPruneSecs *prometheus.HistogramVec // labels: prune_side promBytesReplicated *prometheus.CounterVec // labels: filesystem
promBytesReplicated *prometheus.CounterVec // labels: filesystem
tasksMtx sync.Mutex tasksMtx sync.Mutex
tasks activeSideTasks tasks activeSideTasks
} }
//go:generate enumer -type=ActiveSideState //go:generate enumer -type=ActiveSideState
type ActiveSideState int type ActiveSideState int
@@ -48,12 +52,11 @@ const (
ActiveSideDone // also errors ActiveSideDone // also errors
) )
type activeSideTasks struct { type activeSideTasks struct {
state ActiveSideState state ActiveSideState
// valid for state ActiveSideReplicating, ActiveSidePruneSender, ActiveSidePruneReceiver, ActiveSideDone // valid for state ActiveSideReplicating, ActiveSidePruneSender, ActiveSidePruneReceiver, ActiveSideDone
replication *replication.Replication replicationReport driver.ReportFunc
replicationCancel context.CancelFunc replicationCancel context.CancelFunc
// valid for state ActiveSidePruneSender, ActiveSidePruneReceiver, ActiveSideDone // valid for state ActiveSidePruneSender, ActiveSidePruneReceiver, ActiveSideDone
@@ -66,8 +69,7 @@ type activeSideTasks struct {
func (a *ActiveSide) updateTasks(u func(*activeSideTasks)) activeSideTasks { func (a *ActiveSide) updateTasks(u func(*activeSideTasks)) activeSideTasks {
a.tasksMtx.Lock() a.tasksMtx.Lock()
defer a.tasksMtx.Unlock() defer a.tasksMtx.Unlock()
var copy activeSideTasks copy := a.tasks
copy = a.tasks
if u == nil { if u == nil {
return copy return copy
} }
@@ -77,36 +79,85 @@ func (a *ActiveSide) updateTasks(u func(*activeSideTasks)) activeSideTasks {
} }
type activeMode interface { type activeMode interface {
SenderReceiver(client *streamrpc.Client) (replication.Sender, replication.Receiver, error) ConnectEndpoints(rpcLoggers rpc.Loggers, connecter transport.Connecter)
DisconnectEndpoints()
SenderReceiver() (logic.Sender, logic.Receiver)
Type() Type Type() Type
PlannerPolicy() logic.PlannerPolicy
RunPeriodic(ctx context.Context, wakeUpCommon chan<- struct{}) RunPeriodic(ctx context.Context, wakeUpCommon chan<- struct{})
SnapperReport() *snapper.Report
ResetConnectBackoff()
} }
type modePush struct { type modePush struct {
fsfilter endpoint.FSFilter setupMtx sync.Mutex
snapper *snapper.PeriodicOrManual sender *endpoint.Sender
receiver *rpc.Client
senderConfig *endpoint.SenderConfig
plannerPolicy *logic.PlannerPolicy
snapper *snapper.PeriodicOrManual
} }
func (m *modePush) SenderReceiver(client *streamrpc.Client) (replication.Sender, replication.Receiver, error) { func (m *modePush) ConnectEndpoints(loggers rpc.Loggers, connecter transport.Connecter) {
sender := endpoint.NewSender(m.fsfilter) m.setupMtx.Lock()
receiver := endpoint.NewRemote(client) defer m.setupMtx.Unlock()
return sender, receiver, nil if m.receiver != nil || m.sender != nil {
panic("inconsistent use of ConnectEndpoints and DisconnectEndpoints")
}
m.sender = endpoint.NewSender(*m.senderConfig)
m.receiver = rpc.NewClient(connecter, loggers)
}
func (m *modePush) DisconnectEndpoints() {
m.setupMtx.Lock()
defer m.setupMtx.Unlock()
m.receiver.Close()
m.sender = nil
m.receiver = nil
}
func (m *modePush) SenderReceiver() (logic.Sender, logic.Receiver) {
m.setupMtx.Lock()
defer m.setupMtx.Unlock()
return m.sender, m.receiver
} }
func (m *modePush) Type() Type { return TypePush } func (m *modePush) Type() Type { return TypePush }
func (m *modePush) RunPeriodic(ctx context.Context, wakeUpCommon chan <- struct{}) { func (m *modePush) PlannerPolicy() logic.PlannerPolicy { return *m.plannerPolicy }
func (m *modePush) RunPeriodic(ctx context.Context, wakeUpCommon chan<- struct{}) {
m.snapper.Run(ctx, wakeUpCommon) m.snapper.Run(ctx, wakeUpCommon)
} }
func (m *modePush) SnapperReport() *snapper.Report {
return m.snapper.Report()
}
func modePushFromConfig(g *config.Global, in *config.PushJob) (*modePush, error) { func (m *modePush) ResetConnectBackoff() {
m.setupMtx.Lock()
defer m.setupMtx.Unlock()
if m.receiver != nil {
m.receiver.ResetConnectBackoff()
}
}
func modePushFromConfig(g *config.Global, in *config.PushJob, jobID endpoint.JobID) (*modePush, error) {
m := &modePush{} m := &modePush{}
fsf, err := filters.DatasetMapFilterFromConfig(in.Filesystems) fsf, err := filters.DatasetMapFilterFromConfig(in.Filesystems)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "cannnot build filesystem filter") return nil, errors.Wrap(err, "cannot build filesystem filter")
}
m.senderConfig = &endpoint.SenderConfig{
FSF: fsf,
Encrypt: &zfs.NilBool{B: in.Send.Encrypted},
JobID: jobID,
}
m.plannerPolicy = &logic.PlannerPolicy{
EncryptedSend: logic.TriFromBool(in.Send.Encrypted),
} }
m.fsfilter = fsf
if m.snapper, err = snapper.FromConfig(g, fsf, in.Snapshotting); err != nil { if m.snapper, err = snapper.FromConfig(g, fsf, in.Snapshotting); err != nil {
return nil, errors.Wrap(err, "cannot build snapper") return nil, errors.Wrap(err, "cannot build snapper")
@@ -116,20 +167,50 @@ func modePushFromConfig(g *config.Global, in *config.PushJob) (*modePush, error)
} }
type modePull struct { type modePull struct {
rootFS *zfs.DatasetPath setupMtx sync.Mutex
interval time.Duration receiver *endpoint.Receiver
receiverConfig endpoint.ReceiverConfig
sender *rpc.Client
rootFS *zfs.DatasetPath
plannerPolicy *logic.PlannerPolicy
interval config.PositiveDurationOrManual
} }
func (m *modePull) SenderReceiver(client *streamrpc.Client) (replication.Sender, replication.Receiver, error) { func (m *modePull) ConnectEndpoints(loggers rpc.Loggers, connecter transport.Connecter) {
sender := endpoint.NewRemote(client) m.setupMtx.Lock()
receiver, err := endpoint.NewReceiver(m.rootFS) defer m.setupMtx.Unlock()
return sender, receiver, err if m.receiver != nil || m.sender != nil {
panic("inconsistent use of ConnectEndpoints and DisconnectEndpoints")
}
m.receiver = endpoint.NewReceiver(m.receiverConfig)
m.sender = rpc.NewClient(connecter, loggers)
}
func (m *modePull) DisconnectEndpoints() {
m.setupMtx.Lock()
defer m.setupMtx.Unlock()
m.sender.Close()
m.sender = nil
m.receiver = nil
}
func (m *modePull) SenderReceiver() (logic.Sender, logic.Receiver) {
m.setupMtx.Lock()
defer m.setupMtx.Unlock()
return m.sender, m.receiver
} }
func (*modePull) Type() Type { return TypePull } func (*modePull) Type() Type { return TypePull }
func (m *modePull) PlannerPolicy() logic.PlannerPolicy { return *m.plannerPolicy }
func (m *modePull) RunPeriodic(ctx context.Context, wakeUpCommon chan<- struct{}) { func (m *modePull) RunPeriodic(ctx context.Context, wakeUpCommon chan<- struct{}) {
t := time.NewTicker(m.interval) if m.interval.Manual {
GetLogger(ctx).Info("manual pull configured, periodic pull disabled")
// "waiting for wakeups" is printed in common ActiveSide.do
return
}
t := time.NewTicker(m.interval.Interval)
defer t.Stop() defer t.Stop()
for { for {
select { select {
@@ -148,11 +229,20 @@ func (m *modePull) RunPeriodic(ctx context.Context, wakeUpCommon chan<- struct{}
} }
} }
func modePullFromConfig(g *config.Global, in *config.PullJob) (m *modePull, err error) { func (m *modePull) SnapperReport() *snapper.Report {
m = &modePull{} return nil
if in.Interval <= 0 { }
return nil, errors.New("interval must be positive")
func (m *modePull) ResetConnectBackoff() {
m.setupMtx.Lock()
defer m.setupMtx.Unlock()
if m.sender != nil {
m.sender.ResetConnectBackoff()
} }
}
func modePullFromConfig(g *config.Global, in *config.PullJob, jobID endpoint.JobID) (m *modePull, err error) {
m = &modePull{}
m.interval = in.Interval m.interval = in.Interval
m.rootFS, err = zfs.NewDatasetPath(in.RootFS) m.rootFS, err = zfs.NewDatasetPath(in.RootFS)
@@ -163,29 +253,59 @@ func modePullFromConfig(g *config.Global, in *config.PullJob) (m *modePull, err
return nil, errors.New("RootFS must not be empty") // duplicates error check of receiver return nil, errors.New("RootFS must not be empty") // duplicates error check of receiver
} }
m.plannerPolicy = &logic.PlannerPolicy{
EncryptedSend: logic.DontCare,
}
m.receiverConfig = endpoint.ReceiverConfig{
JobID: jobID,
RootWithoutClientComponent: m.rootFS,
AppendClientIdentity: false, // !
UpdateLastReceivedHold: true,
}
if err := m.receiverConfig.Validate(); err != nil {
return nil, errors.Wrap(err, "cannot build receiver config")
}
return m, nil return m, nil
} }
func activeSide(g *config.Global, in *config.ActiveJob, mode activeMode) (j *ActiveSide, err error) { func activeSide(g *config.Global, in *config.ActiveJob, configJob interface{}) (j *ActiveSide, err error) {
j = &ActiveSide{}
j.name, err = endpoint.MakeJobID(in.Name)
if err != nil {
return nil, errors.Wrap(err, "invalid job name")
}
switch v := configJob.(type) {
case *config.PushJob:
j.mode, err = modePushFromConfig(g, v, j.name) // shadow
case *config.PullJob:
j.mode, err = modePullFromConfig(g, v, j.name) // shadow
default:
panic(fmt.Sprintf("implementation error: unknown job type %T", v))
}
if err != nil {
return nil, err // no wrapping required
}
j = &ActiveSide{mode: mode}
j.name = in.Name
j.promRepStateSecs = prometheus.NewHistogramVec(prometheus.HistogramOpts{ j.promRepStateSecs = prometheus.NewHistogramVec(prometheus.HistogramOpts{
Namespace: "zrepl", Namespace: "zrepl",
Subsystem: "replication", Subsystem: "replication",
Name: "state_time", Name: "state_time",
Help: "seconds spent during replication", Help: "seconds spent during replication",
ConstLabels: prometheus.Labels{"zrepl_job":j.name}, ConstLabels: prometheus.Labels{"zrepl_job": j.name.String()},
}, []string{"state"}) }, []string{"state"})
j.promBytesReplicated = prometheus.NewCounterVec(prometheus.CounterOpts{ j.promBytesReplicated = prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: "zrepl", Namespace: "zrepl",
Subsystem: "replication", Subsystem: "replication",
Name: "bytes_replicated", Name: "bytes_replicated",
Help: "number of bytes replicated from sender to receiver per filesystem", Help: "number of bytes replicated from sender to receiver per filesystem",
ConstLabels: prometheus.Labels{"zrepl_job":j.name}, ConstLabels: prometheus.Labels{"zrepl_job": j.name.String()},
}, []string{"filesystem"}) }, []string{"filesystem"})
j.clientFactory, err = connecter.FromConfig(g, in.Connect) j.connecter, err = fromconfig.ConnecterFromConfig(g, in.Connect)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "cannot build client") return nil, errors.Wrap(err, "cannot build client")
} }
@@ -195,7 +315,7 @@ func activeSide(g *config.Global, in *config.ActiveJob, mode activeMode) (j *Act
Subsystem: "pruning", Subsystem: "pruning",
Name: "time", Name: "time",
Help: "seconds spent in pruner", Help: "seconds spent in pruner",
ConstLabels: prometheus.Labels{"zrepl_job":j.name}, ConstLabels: prometheus.Labels{"zrepl_job": j.name.String()},
}, []string{"prune_side"}) }, []string{"prune_side"})
j.prunerFactory, err = pruner.NewPrunerFactory(in.Pruning, j.promPruneSecs) j.prunerFactory, err = pruner.NewPrunerFactory(in.Pruning, j.promPruneSecs)
if err != nil { if err != nil {
@@ -211,11 +331,12 @@ func (j *ActiveSide) RegisterMetrics(registerer prometheus.Registerer) {
registerer.MustRegister(j.promBytesReplicated) registerer.MustRegister(j.promBytesReplicated)
} }
func (j *ActiveSide) Name() string { return j.name } func (j *ActiveSide) Name() string { return j.name.String() }
type ActiveSideStatus struct { type ActiveSideStatus struct {
Replication *replication.Report Replication *report.Report
PruningSender, PruningReceiver *pruner.Report PruningSender, PruningReceiver *pruner.Report
Snapshotting *snapper.Report
} }
func (j *ActiveSide) Status() *Status { func (j *ActiveSide) Status() *Status {
@@ -223,8 +344,8 @@ func (j *ActiveSide) Status() *Status {
s := &ActiveSideStatus{} s := &ActiveSideStatus{}
t := j.mode.Type() t := j.mode.Type()
if tasks.replication != nil { if tasks.replicationReport != nil {
s.Replication = tasks.replication.Report() s.Replication = tasks.replicationReport()
} }
if tasks.prunerSender != nil { if tasks.prunerSender != nil {
s.PruningSender = tasks.prunerSender.Report() s.PruningSender = tasks.prunerSender.Report()
@@ -232,9 +353,28 @@ func (j *ActiveSide) Status() *Status {
if tasks.prunerReceiver != nil { if tasks.prunerReceiver != nil {
s.PruningReceiver = tasks.prunerReceiver.Report() s.PruningReceiver = tasks.prunerReceiver.Report()
} }
s.Snapshotting = j.mode.SnapperReport()
return &Status{Type: t, JobSpecific: s} return &Status{Type: t, JobSpecific: s}
} }
func (j *ActiveSide) OwnedDatasetSubtreeRoot() (rfs *zfs.DatasetPath, ok bool) {
pull, ok := j.mode.(*modePull)
if !ok {
_ = j.mode.(*modePush) // make sure we didn't introduce a new job type
return nil, false
}
return pull.rootFS.Copy(), true
}
func (j *ActiveSide) SenderConfig() *endpoint.SenderConfig {
push, ok := j.mode.(*modePush)
if !ok {
_ = j.mode.(*modePull) // make sure we didn't introduce a new job type
return nil
}
return push.senderConfig
}
func (j *ActiveSide) Run(ctx context.Context) { func (j *ActiveSide) Run(ctx context.Context) {
log := GetLogger(ctx) log := GetLogger(ctx)
ctx = logging.WithSubsystemLoggers(ctx, log) ctx = logging.WithSubsystemLoggers(ctx, log)
@@ -256,6 +396,7 @@ outer:
break outer break outer
case <-wakeup.Wait(ctx): case <-wakeup.Wait(ctx):
j.mode.ResetConnectBackoff()
case <-periodicDone: case <-periodicDone:
} }
invocationCount++ invocationCount++
@@ -268,6 +409,9 @@ func (j *ActiveSide) do(ctx context.Context) {
log := GetLogger(ctx) log := GetLogger(ctx)
ctx = logging.WithSubsystemLoggers(ctx, log) ctx = logging.WithSubsystemLoggers(ctx, log)
loggers := rpc.GetLoggersOrPanic(ctx) // filled by WithSubsystemLoggers
j.mode.ConnectEndpoints(loggers, j.connecter)
defer j.mode.DisconnectEndpoints()
// allow cancellation of an invocation (this function) // allow cancellation of an invocation (this function)
ctx, cancelThisRun := context.WithCancel(ctx) ctx, cancelThisRun := context.WithCancel(ctx)
@@ -281,85 +425,7 @@ func (j *ActiveSide) do(ctx context.Context) {
} }
}() }()
// The code after this watchdog goroutine is sequential and transitions the state from sender, receiver := j.mode.SenderReceiver()
// ActiveSideReplicating -> ActiveSidePruneSender -> ActiveSidePruneReceiver -> ActiveSideDone
// If any of those sequential tasks 'gets stuck' (livelock, no progress), the watchdog will eventually
// cancel its context.
// If the task is written to support context cancellation, it will return immediately (in permanent error state),
// and the sequential code above transitions to the next state.
go func() {
wdto := envconst.Duration("ZREPL_JOB_WATCHDOG_TIMEOUT", 10*time.Minute)
jitter := envconst.Duration("ZREPL_JOB_WATCHDOG_JITTER", 1*time.Second)
// shadowing!
log := log.WithField("watchdog_timeout", wdto.String())
log.Debug("starting watchdog")
defer log.Debug("watchdog stopped")
t := time.NewTicker(wdto)
defer t.Stop()
for {
select {
case <-ctx.Done():
return
case <-t.C: // fall
}
j.updateTasks(func(tasks *activeSideTasks) {
// Since cancelling a task will cause the sequential code to transition to the next state immediately,
// we cannot check for its progress right then (no fallthrough).
// Instead, we return (not continue because we are in a closure) and give the new state another
// ZREPL_JOB_WATCHDOG_TIMEOUT interval to try make some progress.
log.WithField("state", tasks.state).Debug("watchdog firing")
const WATCHDOG_ENVCONST_NOTICE = " (adjust ZREPL_JOB_WATCHDOG_TIMEOUT env variable if inappropriate)"
switch tasks.state {
case ActiveSideReplicating:
log.WithField("replication_progress", tasks.replication.Progress.String()).
Debug("check replication progress")
if tasks.replication.Progress.CheckTimeout(wdto, jitter) {
log.Error("replication did not make progress, cancelling" + WATCHDOG_ENVCONST_NOTICE)
tasks.replicationCancel()
return
}
case ActiveSidePruneSender:
log.WithField("prune_sender_progress", tasks.replication.Progress.String()).
Debug("check pruner_sender progress")
if tasks.prunerSender.Progress.CheckTimeout(wdto, jitter) {
log.Error("pruner_sender did not make progress, cancelling" + WATCHDOG_ENVCONST_NOTICE)
tasks.prunerSenderCancel()
return
}
case ActiveSidePruneReceiver:
log.WithField("prune_receiver_progress", tasks.replication.Progress.String()).
Debug("check pruner_receiver progress")
if tasks.prunerReceiver.Progress.CheckTimeout(wdto, jitter) {
log.Error("pruner_receiver did not make progress, cancelling" + WATCHDOG_ENVCONST_NOTICE)
tasks.prunerReceiverCancel()
return
}
case ActiveSideDone:
// ignore, ctx will be Done() in a few milliseconds and the watchdog will exit
default:
log.WithField("state", tasks.state).
Error("watchdog implementation error: unknown active side state")
}
})
}
}()
client, err := j.clientFactory.NewClient()
if err != nil {
log.WithError(err).Error("factory cannot instantiate streamrpc client")
}
defer client.Close(ctx)
sender, receiver, err := j.mode.SenderReceiver(client)
{ {
select { select {
@@ -368,16 +434,19 @@ func (j *ActiveSide) do(ctx context.Context) {
default: default:
} }
ctx, repCancel := context.WithCancel(ctx) ctx, repCancel := context.WithCancel(ctx)
tasks := j.updateTasks(func(tasks *activeSideTasks) { var repWait driver.WaitFunc
j.updateTasks(func(tasks *activeSideTasks) {
// reset it // reset it
*tasks = activeSideTasks{} *tasks = activeSideTasks{}
tasks.replicationCancel = repCancel tasks.replicationCancel = repCancel
tasks.replication = replication.NewReplication(j.promRepStateSecs, j.promBytesReplicated) tasks.replicationReport, repWait = replication.Do(
ctx, logic.NewPlanner(j.promRepStateSecs, j.promBytesReplicated, sender, receiver, j.mode.PlannerPolicy()),
)
tasks.state = ActiveSideReplicating tasks.state = ActiveSideReplicating
}) })
log.Info("start replication") log.Info("start replication")
tasks.replication.Drive(ctx, sender, receiver) repWait(true) // wait blocking
repCancel() // always cancel to free up context resources repCancel() // always cancel to free up context resources
} }
{ {
+1
View File
@@ -1,5 +1,6 @@
// Code generated by "enumer -type=ActiveSideState"; DO NOT EDIT. // Code generated by "enumer -type=ActiveSideState"; DO NOT EDIT.
//
package job package job
import ( import (
+55 -17
View File
@@ -2,7 +2,11 @@ package job
import ( import (
"fmt" "fmt"
"sort"
"strings"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/zrepl/zrepl/config" "github.com/zrepl/zrepl/config"
) )
@@ -18,6 +22,22 @@ func JobsFromConfig(c *config.Config) ([]Job, error) {
} }
js[i] = j js[i] = j
} }
// receiving-side root filesystems must not overlap
{
rfss := make([]string, 0, len(js))
for _, j := range js {
jrfs, ok := j.OwnedDatasetSubtreeRoot()
if !ok {
continue
}
rfss = append(rfss, jrfs.ToString())
}
if err := validateReceivingSidesDoNotOverlap(rfss); err != nil {
return nil, err
}
}
return js, nil return js, nil
} }
@@ -28,38 +48,27 @@ func buildJob(c *config.Global, in config.JobEnum) (j Job, err error) {
// FIXME prettify this // FIXME prettify this
switch v := in.Ret.(type) { switch v := in.Ret.(type) {
case *config.SinkJob: case *config.SinkJob:
m, err := modeSinkFromConfig(c, v) j, err = passiveSideFromConfig(c, &v.PassiveJob, v)
if err != nil {
return cannotBuildJob(err, v.Name)
}
j, err = passiveSideFromConfig(c, &v.PassiveJob, m)
if err != nil { if err != nil {
return cannotBuildJob(err, v.Name) return cannotBuildJob(err, v.Name)
} }
case *config.SourceJob: case *config.SourceJob:
m, err := modeSourceFromConfig(c, v) j, err = passiveSideFromConfig(c, &v.PassiveJob, v)
if err != nil { if err != nil {
return cannotBuildJob(err, v.Name) return cannotBuildJob(err, v.Name)
} }
j, err = passiveSideFromConfig(c, &v.PassiveJob, m) case *config.SnapJob:
j, err = snapJobFromConfig(c, v)
if err != nil { if err != nil {
return cannotBuildJob(err, v.Name) return cannotBuildJob(err, v.Name)
} }
case *config.PushJob: case *config.PushJob:
m, err := modePushFromConfig(c, v) j, err = activeSide(c, &v.ActiveJob, v)
if err != nil {
return cannotBuildJob(err, v.Name)
}
j, err = activeSide(c, &v.ActiveJob, m)
if err != nil { if err != nil {
return cannotBuildJob(err, v.Name) return cannotBuildJob(err, v.Name)
} }
case *config.PullJob: case *config.PullJob:
m, err := modePullFromConfig(c, v) j, err = activeSide(c, &v.ActiveJob, v)
if err != nil {
return cannotBuildJob(err, v.Name)
}
j, err = activeSide(c, &v.ActiveJob, m)
if err != nil { if err != nil {
return cannotBuildJob(err, v.Name) return cannotBuildJob(err, v.Name)
} }
@@ -69,3 +78,32 @@ func buildJob(c *config.Global, in config.JobEnum) (j Job, err error) {
return j, nil return j, nil
} }
func validateReceivingSidesDoNotOverlap(receivingRootFSs []string) error {
if len(receivingRootFSs) == 0 {
return nil
}
rfss := make([]string, len(receivingRootFSs))
copy(rfss, receivingRootFSs)
sort.Slice(rfss, func(i, j int) bool {
return strings.Compare(rfss[i], rfss[j]) == -1
})
// add tailing slash because of hierarchy-simulation
// rootfs/ is not root of rootfs2/
for i := 0; i < len(rfss); i++ {
rfss[i] += "/"
}
// idea:
// no path in rfss must be prefix of another
//
// rfss is now lexicographically sorted, which means that
// if i is prefix of j, i < j (in lexicographical order)
// thus,
// if any i is prefix of i+n (n >= 1), there is overlap
for i := 0; i < len(rfss)-1; i++ {
if strings.HasPrefix(rfss[i+1], rfss[i]) {
return fmt.Errorf("receiving jobs with overlapping root filesystems are forbidden")
}
}
return nil
}
+110
View File
@@ -0,0 +1,110 @@
package job
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zrepl/zrepl/config"
)
func TestValidateReceivingSidesDoNotOverlap(t *testing.T) {
type testCase struct {
err bool
input []string
}
tcs := []testCase{
{false, nil},
{false, []string{}},
{false, []string{""}}, // not our job to determine valid paths
{false, []string{"a"}},
{false, []string{"some/path"}},
{false, []string{"zroot/sink1", "zroot/sink2", "zroot/sink3"}},
{false, []string{"zroot/foo", "zroot/foobar"}},
{true, []string{"zroot/b", "zroot/b"}},
{true, []string{"zroot/foo", "zroot/foo/bar", "zroot/baz"}},
{false, []string{"a/x", "b/x"}},
{false, []string{"a", "b"}},
{true, []string{"a", "a"}},
{true, []string{"a/x/y", "a/x"}},
{true, []string{"a/x", "a/x/y"}},
{true, []string{"a/x", "b/x", "a/x/y"}},
{true, []string{"a", "a/b", "a/c", "a/b"}},
{true, []string{"a/b", "a/c", "a/b", "a/d", "a/c"}},
}
for _, tc := range tcs {
t.Logf("input: %v", tc.input)
err := validateReceivingSidesDoNotOverlap(tc.input)
if tc.err {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
}
}
func TestJobIDErrorHandling(t *testing.T) {
tmpl := `
jobs:
- name: %s
type: push
connect:
type: local
listener_name: foo
client_identity: bar
filesystems: {"<": true}
snapshotting:
type: manual
pruning:
keep_sender:
- type: last_n
count: 10
keep_receiver:
- type: last_n
count: 10
`
fill := func(s string) string { return fmt.Sprintf(tmpl, s) }
type Case struct {
jobName string
valid bool
}
cases := []Case{
{"validjobname", true},
{"valid with spaces", true},
{"invalid\twith\ttabs", false},
{"invalid#withdelimiter", false},
{"invalid@withdelimiter", false},
{"withnewline\\nmiddle", false},
{"withnewline\\n", false},
{"withslash/", false},
{"withslash/inthemiddle", false},
{"/", false},
}
for i := range cases {
t.Run(cases[i].jobName, func(t *testing.T) {
c := cases[i]
conf, err := config.ParseConfigBytes([]byte(fill(c.jobName)))
require.NoError(t, err, "not expecting yaml-config to know about job ids")
require.NotNil(t, conf)
jobs, err := JobsFromConfig(conf)
if c.valid {
assert.NoError(t, err)
require.Len(t, jobs, 1)
assert.Equal(t, c.jobName, jobs[0].Name())
} else {
t.Logf("error: %s", err)
assert.Error(t, err)
assert.Nil(t, jobs)
}
})
}
}
+27 -10
View File
@@ -4,8 +4,12 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"github.com/zrepl/zrepl/endpoint"
"github.com/zrepl/zrepl/logger" "github.com/zrepl/zrepl/logger"
"github.com/zrepl/zrepl/zfs"
) )
type Logger = logger.Logger type Logger = logger.Logger
@@ -27,26 +31,30 @@ func WithLogger(ctx context.Context, l Logger) context.Context {
return context.WithValue(ctx, contextKeyLog, l) return context.WithValue(ctx, contextKeyLog, l)
} }
type Job interface { type Job interface {
Name() string Name() string
Run(ctx context.Context) Run(ctx context.Context)
Status() *Status Status() *Status
RegisterMetrics(registerer prometheus.Registerer) RegisterMetrics(registerer prometheus.Registerer)
// Jobs that return a subtree of the dataset hierarchy
// must return the root of that subtree as rfs and ok = true
OwnedDatasetSubtreeRoot() (rfs *zfs.DatasetPath, ok bool)
SenderConfig() *endpoint.SenderConfig
} }
type Type string type Type string
const ( const (
TypeInternal Type = "internal" TypeInternal Type = "internal"
TypePush Type = "push" TypeSnap Type = "snap"
TypeSink Type = "sink" TypePush Type = "push"
TypePull Type = "pull" TypeSink Type = "sink"
TypeSource Type = "source" TypePull Type = "pull"
TypeSource Type = "source"
) )
type Status struct { type Status struct {
Type Type Type Type
JobSpecific interface{} JobSpecific interface{}
} }
@@ -59,8 +67,8 @@ func (s *Status) MarshalJSON() ([]byte, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
m := map[string]json.RawMessage { m := map[string]json.RawMessage{
"type": typeJson, "type": typeJson,
string(s.Type): jobJSON, string(s.Type): jobJSON,
} }
return json.Marshal(m) return json.Marshal(m)
@@ -84,16 +92,25 @@ func (s *Status) UnmarshalJSON(in []byte) (err error) {
return fmt.Errorf("field '%s', not found", key) return fmt.Errorf("field '%s', not found", key)
} }
switch s.Type { switch s.Type {
case TypePull: fallthrough case TypeSnap:
var st SnapJobStatus
err = json.Unmarshal(jobJSON, &st)
s.JobSpecific = &st
case TypePull:
fallthrough
case TypePush: case TypePush:
var st ActiveSideStatus var st ActiveSideStatus
err = json.Unmarshal(jobJSON, &st) err = json.Unmarshal(jobJSON, &st)
s.JobSpecific = &st s.JobSpecific = &st
case TypeSource: fallthrough
case TypeSource:
fallthrough
case TypeSink: case TypeSink:
var st PassiveStatus var st PassiveStatus
err = json.Unmarshal(jobJSON, &st) err = json.Unmarshal(jobJSON, &st)
s.JobSpecific = &st s.JobSpecific = &st
case TypeInternal: case TypeInternal:
// internal jobs do not report specifics // internal jobs do not report specifics
default: default:
+107 -107
View File
@@ -2,87 +2,86 @@ package job
import ( import (
"context" "context"
"fmt"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/problame/go-streamrpc"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"github.com/zrepl/zrepl/config" "github.com/zrepl/zrepl/config"
"github.com/zrepl/zrepl/daemon/filters" "github.com/zrepl/zrepl/daemon/filters"
"github.com/zrepl/zrepl/daemon/logging" "github.com/zrepl/zrepl/daemon/logging"
"github.com/zrepl/zrepl/daemon/transport/serve"
"github.com/zrepl/zrepl/daemon/snapper" "github.com/zrepl/zrepl/daemon/snapper"
"github.com/zrepl/zrepl/endpoint" "github.com/zrepl/zrepl/endpoint"
"github.com/zrepl/zrepl/rpc"
"github.com/zrepl/zrepl/transport"
"github.com/zrepl/zrepl/transport/fromconfig"
"github.com/zrepl/zrepl/zfs" "github.com/zrepl/zrepl/zfs"
"path"
) )
type PassiveSide struct { type PassiveSide struct {
mode passiveMode mode passiveMode
name string name endpoint.JobID
l serve.ListenerFactory listen transport.AuthenticatedListenerFactory
rpcConf *streamrpc.ConnConfig
} }
type passiveMode interface { type passiveMode interface {
ConnHandleFunc(ctx context.Context, conn serve.AuthenticatedConn) streamrpc.HandlerFunc Handler() rpc.Handler
RunPeriodic(ctx context.Context) RunPeriodic(ctx context.Context)
SnapperReport() *snapper.Report // may be nil
Type() Type Type() Type
} }
type modeSink struct { type modeSink struct {
rootDataset *zfs.DatasetPath receiverConfig endpoint.ReceiverConfig
} }
func (m *modeSink) Type() Type { return TypeSink } func (m *modeSink) Type() Type { return TypeSink }
func (m *modeSink) ConnHandleFunc(ctx context.Context, conn serve.AuthenticatedConn) streamrpc.HandlerFunc { func (m *modeSink) Handler() rpc.Handler {
log := GetLogger(ctx) return endpoint.NewReceiver(m.receiverConfig)
clientRootStr := path.Join(m.rootDataset.ToString(), conn.ClientIdentity())
clientRoot, err := zfs.NewDatasetPath(clientRootStr)
if err != nil {
log.WithError(err).
WithField("client_identity", conn.ClientIdentity()).
Error("cannot build client filesystem map (client identity must be a valid ZFS FS name")
}
log.WithField("client_root", clientRoot).Debug("client root")
local, err := endpoint.NewReceiver(clientRoot)
if err != nil {
log.WithError(err).Error("unexpected error: cannot convert mapping to filter")
return nil
}
h := endpoint.NewHandler(local)
return h.Handle
} }
func (m *modeSink) RunPeriodic(_ context.Context) {} func (m *modeSink) RunPeriodic(_ context.Context) {}
func (m *modeSink) SnapperReport() *snapper.Report { return nil }
func modeSinkFromConfig(g *config.Global, in *config.SinkJob) (m *modeSink, err error) { func modeSinkFromConfig(g *config.Global, in *config.SinkJob, jobID endpoint.JobID) (m *modeSink, err error) {
m = &modeSink{} m = &modeSink{}
m.rootDataset, err = zfs.NewDatasetPath(in.RootFS)
rootDataset, err := zfs.NewDatasetPath(in.RootFS)
if err != nil { if err != nil {
return nil, errors.New("root dataset is not a valid zfs filesystem path") return nil, errors.New("root dataset is not a valid zfs filesystem path")
} }
if m.rootDataset.Length() <= 0 {
return nil, errors.New("root dataset must not be empty") // duplicates error check of receiver m.receiverConfig = endpoint.ReceiverConfig{
JobID: jobID,
RootWithoutClientComponent: rootDataset,
AppendClientIdentity: true, // !
UpdateLastReceivedHold: true,
} }
if err := m.receiverConfig.Validate(); err != nil {
return nil, errors.Wrap(err, "cannot build receiver config")
}
return m, nil return m, nil
} }
type modeSource struct { type modeSource struct {
fsfilter zfs.DatasetFilter senderConfig *endpoint.SenderConfig
snapper *snapper.PeriodicOrManual snapper *snapper.PeriodicOrManual
} }
func modeSourceFromConfig(g *config.Global, in *config.SourceJob) (m *modeSource, err error) { func modeSourceFromConfig(g *config.Global, in *config.SourceJob, jobID endpoint.JobID) (m *modeSource, err error) {
// FIXME exact dedup of modePush // FIXME exact dedup of modePush
m = &modeSource{} m = &modeSource{}
fsf, err := filters.DatasetMapFilterFromConfig(in.Filesystems) fsf, err := filters.DatasetMapFilterFromConfig(in.Filesystems)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "cannnot build filesystem filter") return nil, errors.Wrap(err, "cannot build filesystem filter")
}
m.senderConfig = &endpoint.SenderConfig{
FSF: fsf,
Encrypt: &zfs.NilBool{B: in.Send.Encrypted},
JobID: jobID,
} }
m.fsfilter = fsf
if m.snapper, err = snapper.FromConfig(g, fsf, in.Snapshotting); err != nil { if m.snapper, err = snapper.FromConfig(g, fsf, in.Snapshotting); err != nil {
return nil, errors.Wrap(err, "cannot build snapper") return nil, errors.Wrap(err, "cannot build snapper")
@@ -93,32 +92,73 @@ func modeSourceFromConfig(g *config.Global, in *config.SourceJob) (m *modeSource
func (m *modeSource) Type() Type { return TypeSource } func (m *modeSource) Type() Type { return TypeSource }
func (m *modeSource) ConnHandleFunc(ctx context.Context, conn serve.AuthenticatedConn) streamrpc.HandlerFunc { func (m *modeSource) Handler() rpc.Handler {
sender := endpoint.NewSender(m.fsfilter) return endpoint.NewSender(*m.senderConfig)
h := endpoint.NewHandler(sender)
return h.Handle
} }
func (m *modeSource) RunPeriodic(ctx context.Context) { func (m *modeSource) RunPeriodic(ctx context.Context) {
m.snapper.Run(ctx, nil) m.snapper.Run(ctx, nil)
} }
func passiveSideFromConfig(g *config.Global, in *config.PassiveJob, mode passiveMode) (s *PassiveSide, err error) { func (m *modeSource) SnapperReport() *snapper.Report {
return m.snapper.Report()
}
s = &PassiveSide{mode: mode, name: in.Name} func passiveSideFromConfig(g *config.Global, in *config.PassiveJob, configJob interface{}) (s *PassiveSide, err error) {
if s.l, s.rpcConf, err = serve.FromConfig(g, in.Serve); err != nil {
return nil, errors.Wrap(err, "cannot build server") s = &PassiveSide{}
s.name, err = endpoint.MakeJobID(in.Name)
if err != nil {
return nil, errors.Wrap(err, "invalid job name")
}
switch v := configJob.(type) {
case *config.SinkJob:
s.mode, err = modeSinkFromConfig(g, v, s.name) // shadow
case *config.SourceJob:
s.mode, err = modeSourceFromConfig(g, v, s.name) // shadow
}
if err != nil {
return nil, err // no wrapping necessary
}
if s.listen, err = fromconfig.ListenerFactoryFromConfig(g, in.Serve); err != nil {
return nil, errors.Wrap(err, "cannot build listener factory")
} }
return s, nil return s, nil
} }
func (j *PassiveSide) Name() string { return j.name } func (j *PassiveSide) Name() string { return j.name.String() }
type PassiveStatus struct {} type PassiveStatus struct {
Snapper *snapper.Report
}
func (s *PassiveSide) Status() *Status { func (s *PassiveSide) Status() *Status {
return &Status{Type: s.mode.Type()} // FIXME PassiveStatus st := &PassiveStatus{
Snapper: s.mode.SnapperReport(),
}
return &Status{Type: s.mode.Type(), JobSpecific: st}
}
func (j *PassiveSide) OwnedDatasetSubtreeRoot() (rfs *zfs.DatasetPath, ok bool) {
sink, ok := j.mode.(*modeSink)
if !ok {
_ = j.mode.(*modeSource) // make sure we didn't introduce a new job type
return nil, false
}
return sink.receiverConfig.RootWithoutClientComponent.Copy(), true
}
func (j *PassiveSide) SenderConfig() *endpoint.SenderConfig {
source, ok := j.mode.(*modeSource)
if !ok {
_ = j.mode.(*modeSink) // make sure we didn't introduce a new job type
return nil
}
return source.senderConfig
} }
func (*PassiveSide) RegisterMetrics(registerer prometheus.Registerer) {} func (*PassiveSide) RegisterMetrics(registerer prometheus.Registerer) {}
@@ -127,70 +167,30 @@ func (j *PassiveSide) Run(ctx context.Context) {
log := GetLogger(ctx) log := GetLogger(ctx)
defer log.Info("job exiting") defer log.Info("job exiting")
ctx = logging.WithSubsystemLoggers(ctx, log)
l, err := j.l.Listen()
if err != nil {
log.WithError(err).Error("cannot listen")
return
}
defer l.Close()
{ {
ctx, cancel := context.WithCancel(logging.WithSubsystemLoggers(ctx, log)) // shadowing ctx, cancel := context.WithCancel(ctx) // shadowing
defer cancel() defer cancel()
go j.mode.RunPeriodic(ctx) go j.mode.RunPeriodic(ctx)
} }
log.WithField("addr", l.Addr()).Debug("accepting connections") handler := j.mode.Handler()
var connId int if handler == nil {
outer: panic(fmt.Sprintf("implementation error: j.mode.Handler() returned nil: %#v", j))
for {
select {
case res := <-accept(ctx, l):
if res.err != nil {
log.WithError(res.err).Info("accept error")
continue
}
conn := res.conn
connId++
connLog := log.
WithField("connID", connId)
connLog.
WithField("addr", conn.RemoteAddr()).
WithField("client_identity", conn.ClientIdentity()).
Info("handling connection")
go func() {
defer connLog.Info("finished handling connection")
defer conn.Close()
ctx := logging.WithSubsystemLoggers(ctx, connLog)
handleFunc := j.mode.ConnHandleFunc(ctx, conn)
if handleFunc == nil {
return
}
if err := streamrpc.ServeConn(ctx, conn, j.rpcConf, handleFunc); err != nil {
log.WithError(err).Error("error serving client")
}
}()
case <-ctx.Done():
break outer
}
} }
} ctxInterceptor := func(handlerCtx context.Context) context.Context {
return logging.WithSubsystemLoggers(handlerCtx, log)
}
type acceptResult struct { rpcLoggers := rpc.GetLoggersOrPanic(ctx) // WithSubsystemLoggers above
conn serve.AuthenticatedConn server := rpc.NewServer(handler, rpcLoggers, ctxInterceptor)
err error
}
func accept(ctx context.Context, listener serve.AuthenticatedListener) <-chan acceptResult { listener, err := j.listen()
c := make(chan acceptResult, 1) if err != nil {
go func() { log.WithError(err).Error("cannot listen")
conn, err := listener.Accept(ctx) return
c <- acceptResult{conn, err} }
}()
return c server.Serve(ctx, listener)
} }
+176
View File
@@ -0,0 +1,176 @@
package job
import (
"context"
"sort"
"github.com/pkg/errors"
"github.com/prometheus/client_golang/prometheus"
"github.com/zrepl/zrepl/config"
"github.com/zrepl/zrepl/daemon/filters"
"github.com/zrepl/zrepl/daemon/job/wakeup"
"github.com/zrepl/zrepl/daemon/logging"
"github.com/zrepl/zrepl/daemon/pruner"
"github.com/zrepl/zrepl/daemon/snapper"
"github.com/zrepl/zrepl/endpoint"
"github.com/zrepl/zrepl/replication/logic/pdu"
"github.com/zrepl/zrepl/zfs"
)
type SnapJob struct {
name endpoint.JobID
fsfilter zfs.DatasetFilter
snapper *snapper.PeriodicOrManual
prunerFactory *pruner.LocalPrunerFactory
promPruneSecs *prometheus.HistogramVec // labels: prune_side
pruner *pruner.Pruner
}
func (j *SnapJob) Name() string { return j.name.String() }
func (j *SnapJob) Type() Type { return TypeSnap }
func snapJobFromConfig(g *config.Global, in *config.SnapJob) (j *SnapJob, err error) {
j = &SnapJob{}
fsf, err := filters.DatasetMapFilterFromConfig(in.Filesystems)
if err != nil {
return nil, errors.Wrap(err, "cannot build filesystem filter")
}
j.fsfilter = fsf
if j.snapper, err = snapper.FromConfig(g, fsf, in.Snapshotting); err != nil {
return nil, errors.Wrap(err, "cannot build snapper")
}
j.name, err = endpoint.MakeJobID(in.Name)
if err != nil {
return nil, errors.Wrap(err, "invalid job name")
}
j.promPruneSecs = prometheus.NewHistogramVec(prometheus.HistogramOpts{
Namespace: "zrepl",
Subsystem: "pruning",
Name: "time",
Help: "seconds spent in pruner",
ConstLabels: prometheus.Labels{"zrepl_job": j.name.String()},
}, []string{"prune_side"})
j.prunerFactory, err = pruner.NewLocalPrunerFactory(in.Pruning, j.promPruneSecs)
if err != nil {
return nil, errors.Wrap(err, "cannot build snapjob pruning rules")
}
return j, nil
}
func (j *SnapJob) RegisterMetrics(registerer prometheus.Registerer) {
registerer.MustRegister(j.promPruneSecs)
}
type SnapJobStatus struct {
Pruning *pruner.Report
Snapshotting *snapper.Report // may be nil
}
func (j *SnapJob) Status() *Status {
s := &SnapJobStatus{}
t := j.Type()
if j.pruner != nil {
s.Pruning = j.pruner.Report()
}
s.Snapshotting = j.snapper.Report()
return &Status{Type: t, JobSpecific: s}
}
func (j *SnapJob) OwnedDatasetSubtreeRoot() (rfs *zfs.DatasetPath, ok bool) {
return nil, false
}
func (j *SnapJob) SenderConfig() *endpoint.SenderConfig { return nil }
func (j *SnapJob) Run(ctx context.Context) {
log := GetLogger(ctx)
ctx = logging.WithSubsystemLoggers(ctx, log)
defer log.Info("job exiting")
periodicDone := make(chan struct{})
ctx, cancel := context.WithCancel(ctx)
defer cancel()
go j.snapper.Run(ctx, periodicDone)
invocationCount := 0
outer:
for {
log.Info("wait for wakeups")
select {
case <-ctx.Done():
log.WithError(ctx.Err()).Info("context")
break outer
case <-wakeup.Wait(ctx):
case <-periodicDone:
}
invocationCount++
invLog := log.WithField("invocation", invocationCount)
j.doPrune(WithLogger(ctx, invLog))
}
}
// Adaptor that implements pruner.History around a pruner.Target.
// The ReplicationCursor method is Get-op only and always returns
// the filesystem's most recent version's GUID.
//
// TODO:
// This is a work-around for the current package daemon/pruner
// and package pruning.Snapshot limitation: they require the
// `Replicated` getter method be present, but obviously,
// a local job like SnapJob can't deliver on that.
// But the pruner.Pruner gives up on an FS if no replication
// cursor is present, which is why this pruner returns the
// most recent filesystem version.
type alwaysUpToDateReplicationCursorHistory struct {
// the Target passed as Target to BuildLocalPruner
target pruner.Target
}
var _ pruner.History = (*alwaysUpToDateReplicationCursorHistory)(nil)
func (h alwaysUpToDateReplicationCursorHistory) ReplicationCursor(ctx context.Context, req *pdu.ReplicationCursorReq) (*pdu.ReplicationCursorRes, error) {
fsvReq := &pdu.ListFilesystemVersionsReq{
Filesystem: req.GetFilesystem(),
}
res, err := h.target.ListFilesystemVersions(ctx, fsvReq)
if err != nil {
return nil, err
}
fsvs := res.GetVersions()
if len(fsvs) <= 0 {
return &pdu.ReplicationCursorRes{Result: &pdu.ReplicationCursorRes_Notexist{Notexist: true}}, nil
}
// always return must recent version
sort.Slice(fsvs, func(i, j int) bool {
return fsvs[i].CreateTXG < fsvs[j].CreateTXG
})
mostRecent := fsvs[len(fsvs)-1]
return &pdu.ReplicationCursorRes{Result: &pdu.ReplicationCursorRes_Guid{Guid: mostRecent.GetGuid()}}, nil
}
func (h alwaysUpToDateReplicationCursorHistory) ListFilesystems(ctx context.Context, req *pdu.ListFilesystemReq) (*pdu.ListFilesystemRes, error) {
return h.target.ListFilesystems(ctx, req)
}
func (j *SnapJob) doPrune(ctx context.Context) {
log := GetLogger(ctx)
ctx = logging.WithSubsystemLoggers(ctx, log)
sender := endpoint.NewSender(endpoint.SenderConfig{
JobID: j.name,
FSF: j.fsfilter,
// FIXME encryption setting is irrelevant for SnapJob because the endpoint is only used as pruner.Target
Encrypt: &zfs.NilBool{B: true},
})
j.pruner = j.prunerFactory.BuildLocalPruner(ctx, sender, alwaysUpToDateReplicationCursorHistory{sender})
log.Info("start pruning")
j.pruner.Prune()
log.Info("finished pruning")
}
-32
View File
@@ -1,32 +0,0 @@
package logging
import (
"fmt"
"github.com/problame/go-streamrpc"
"github.com/zrepl/zrepl/logger"
"strings"
)
type streamrpcLogAdaptor = twoClassLogAdaptor
type twoClassLogAdaptor struct {
logger.Logger
}
var _ streamrpc.Logger = twoClassLogAdaptor{}
func (a twoClassLogAdaptor) Errorf(fmtStr string, args ...interface{}) {
const errorSuffix = ": %s"
if len(args) == 1 {
if err, ok := args[0].(error); ok && strings.HasSuffix(fmtStr, errorSuffix) {
msg := strings.TrimSuffix(fmtStr, errorSuffix)
a.WithError(err).Error(msg)
return
}
}
a.Logger.Error(fmt.Sprintf(fmtStr, args...))
}
func (a twoClassLogAdaptor) Infof(fmtStr string, args ...interface{}) {
a.Logger.Debug(fmt.Sprintf(fmtStr, args...))
}
+48 -16
View File
@@ -4,18 +4,25 @@ import (
"context" "context"
"crypto/tls" "crypto/tls"
"crypto/x509" "crypto/x509"
"log/syslog"
"os"
"github.com/mattn/go-isatty" "github.com/mattn/go-isatty"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/problame/go-streamrpc"
"github.com/zrepl/zrepl/config" "github.com/zrepl/zrepl/config"
"github.com/zrepl/zrepl/daemon/hooks"
"github.com/zrepl/zrepl/daemon/pruner" "github.com/zrepl/zrepl/daemon/pruner"
"github.com/zrepl/zrepl/daemon/snapper"
"github.com/zrepl/zrepl/endpoint" "github.com/zrepl/zrepl/endpoint"
"github.com/zrepl/zrepl/logger" "github.com/zrepl/zrepl/logger"
"github.com/zrepl/zrepl/replication" "github.com/zrepl/zrepl/replication/driver"
"github.com/zrepl/zrepl/replication/logic"
"github.com/zrepl/zrepl/rpc"
"github.com/zrepl/zrepl/rpc/transportmux"
"github.com/zrepl/zrepl/tlsconf" "github.com/zrepl/zrepl/tlsconf"
"os" "github.com/zrepl/zrepl/transport"
"github.com/zrepl/zrepl/daemon/snapper" "github.com/zrepl/zrepl/zfs/zfscmd"
"github.com/zrepl/zrepl/daemon/transport/serve"
) )
func OutletsFromConfig(in config.LoggingOutletEnumList) (*logger.Outlets, error) { func OutletsFromConfig(in config.LoggingOutletEnumList) (*logger.Outlets, error) {
@@ -32,7 +39,7 @@ func OutletsFromConfig(in config.LoggingOutletEnumList) (*logger.Outlets, error)
var syslogOutlets, stdoutOutlets int var syslogOutlets, stdoutOutlets int
for lei, le := range in { for lei, le := range in {
outlet, minLevel, err := parseOutlet(le) outlet, minLevel, err := ParseOutlet(le)
if err != nil { if err != nil {
return nil, errors.Wrapf(err, "cannot parse outlet #%d", lei) return nil, errors.Wrapf(err, "cannot parse outlet #%d", lei)
} }
@@ -60,22 +67,46 @@ func OutletsFromConfig(in config.LoggingOutletEnumList) (*logger.Outlets, error)
} }
type Subsystem string
const ( const (
SubsysReplication = "repl" SubsysReplication Subsystem = "repl"
SubsysStreamrpc = "rpc" SubsysEndpoint Subsystem = "endpoint"
SubsyEndpoint = "endpoint" SubsysPruning Subsystem = "pruning"
SubsysSnapshot Subsystem = "snapshot"
SubsysHooks Subsystem = "hook"
SubsysTransport Subsystem = "transport"
SubsysTransportMux Subsystem = "transportmux"
SubsysRPC Subsystem = "rpc"
SubsysRPCControl Subsystem = "rpc.ctrl"
SubsysRPCData Subsystem = "rpc.data"
SubsysZFSCmd Subsystem = "zfs.cmd"
) )
func WithSubsystemLoggers(ctx context.Context, log logger.Logger) context.Context { func WithSubsystemLoggers(ctx context.Context, log logger.Logger) context.Context {
ctx = replication.WithLogger(ctx, log.WithField(SubsysField, "repl")) ctx = logic.WithLogger(ctx, log.WithField(SubsysField, SubsysReplication))
ctx = streamrpc.ContextWithLogger(ctx, streamrpcLogAdaptor{log.WithField(SubsysField, "rpc")}) ctx = driver.WithLogger(ctx, log.WithField(SubsysField, SubsysReplication))
ctx = endpoint.WithLogger(ctx, log.WithField(SubsysField, "endpoint")) ctx = endpoint.WithLogger(ctx, log.WithField(SubsysField, SubsysEndpoint))
ctx = pruner.WithLogger(ctx, log.WithField(SubsysField, "pruning")) ctx = pruner.WithLogger(ctx, log.WithField(SubsysField, SubsysPruning))
ctx = snapper.WithLogger(ctx, log.WithField(SubsysField, "snapshot")) ctx = snapper.WithLogger(ctx, log.WithField(SubsysField, SubsysSnapshot))
ctx = serve.WithLogger(ctx, log.WithField(SubsysField, "serve")) ctx = hooks.WithLogger(ctx, log.WithField(SubsysField, SubsysHooks))
ctx = transport.WithLogger(ctx, log.WithField(SubsysField, SubsysTransport))
ctx = transportmux.WithLogger(ctx, log.WithField(SubsysField, SubsysTransportMux))
ctx = zfscmd.WithLogger(ctx, log.WithField(SubsysField, SubsysZFSCmd))
ctx = rpc.WithLoggers(ctx,
rpc.Loggers{
General: log.WithField(SubsysField, SubsysRPC),
Control: log.WithField(SubsysField, SubsysRPCControl),
Data: log.WithField(SubsysField, SubsysRPCData),
},
)
return ctx return ctx
} }
func LogSubsystem(log logger.Logger, subsys Subsystem) logger.Logger {
return log.ReplaceField(SubsysField, subsys)
}
func parseLogFormat(i interface{}) (f EntryFormatter, err error) { func parseLogFormat(i interface{}) (f EntryFormatter, err error) {
var is string var is string
switch j := i.(type) { switch j := i.(type) {
@@ -98,7 +129,7 @@ func parseLogFormat(i interface{}) (f EntryFormatter, err error) {
} }
func parseOutlet(in config.LoggingOutletEnum) (o logger.Outlet, level logger.Level, err error) { func ParseOutlet(in config.LoggingOutletEnum) (o logger.Outlet, level logger.Level, err error) {
parseCommon := func(common config.LoggingOutletCommon) (logger.Level, EntryFormatter, error) { parseCommon := func(common config.LoggingOutletCommon) (logger.Level, EntryFormatter, error) {
if common.Level == "" || common.Format == "" { if common.Level == "" || common.Format == "" {
@@ -200,6 +231,7 @@ func parseSyslogOutlet(in *config.SyslogLoggingOutlet, formatter EntryFormatter)
out = &SyslogOutlet{} out = &SyslogOutlet{}
out.Formatter = formatter out.Formatter = formatter
out.Formatter.SetMetadataFlags(MetadataNone) out.Formatter.SetMetadataFlags(MetadataNone)
out.Facility = syslog.Priority(*in.Facility)
out.RetryInterval = in.RetryInterval out.RetryInterval = in.RetryInterval
return out, nil return out, nil
} }
+20 -7
View File
@@ -4,11 +4,13 @@ import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"fmt" "fmt"
"time"
"github.com/fatih/color" "github.com/fatih/color"
"github.com/go-logfmt/logfmt" "github.com/go-logfmt/logfmt"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/zrepl/zrepl/logger" "github.com/zrepl/zrepl/logger"
"time"
) )
const ( const (
@@ -86,7 +88,7 @@ func (f *HumanFormatter) Format(e *logger.Entry) (out []byte, err error) {
prefixFields := []string{JobField, SubsysField} prefixFields := []string{JobField, SubsysField}
prefixed := make(map[string]bool, len(prefixFields)+2) prefixed := make(map[string]bool, len(prefixFields)+2)
for _, field := range prefixFields { for _, field := range prefixFields {
val, ok := e.Fields[field].(string) val, ok := e.Fields[field]
if !ok { if !ok {
continue continue
} }
@@ -159,10 +161,16 @@ func (f *LogfmtFormatter) Format(e *logger.Entry) ([]byte, error) {
enc := logfmt.NewEncoder(&buf) enc := logfmt.NewEncoder(&buf)
if f.metadataFlags&MetadataTime != 0 { if f.metadataFlags&MetadataTime != 0 {
enc.EncodeKeyval(FieldTime, e.Time) err := enc.EncodeKeyval(FieldTime, e.Time)
if err != nil {
return nil, errors.Wrap(err, "logfmt: encode time")
}
} }
if f.metadataFlags&MetadataLevel != 0 { if f.metadataFlags&MetadataLevel != 0 {
enc.EncodeKeyval(FieldLevel, e.Level) err := enc.EncodeKeyval(FieldLevel, e.Level)
if err != nil {
return nil, errors.Wrap(err, "logfmt: encode level")
}
} }
// at least try and put job and task in front // at least try and put job and task in front
@@ -179,8 +187,10 @@ func (f *LogfmtFormatter) Format(e *logger.Entry) ([]byte, error) {
prefixed[pf] = true prefixed[pf] = true
} }
enc.EncodeKeyval(FieldMessage, e.Message) err := enc.EncodeKeyval(FieldMessage, e.Message)
if err != nil {
return nil, errors.Wrap(err, "logfmt: encode message")
}
for k, v := range e.Fields { for k, v := range e.Fields {
if !prefixed[k] { if !prefixed[k] {
if err := logfmtTryEncodeKeyval(enc, k, v); err != nil { if err := logfmtTryEncodeKeyval(enc, k, v); err != nil {
@@ -199,7 +209,10 @@ func logfmtTryEncodeKeyval(enc *logfmt.Encoder, field, value interface{}) error
case nil: // ok case nil: // ok
return nil return nil
case logfmt.ErrUnsupportedValueType: case logfmt.ErrUnsupportedValueType:
enc.EncodeKeyval(field, fmt.Sprintf("<%T>", value)) err := enc.EncodeKeyval(field, fmt.Sprintf("<%T>", value))
if err != nil {
return errors.Wrap(err, "cannot encode unsupported value type Go type")
}
return nil return nil
} }
return errors.Wrapf(err, "cannot encode field '%s'", field) return errors.Wrapf(err, "cannot encode field '%s'", field)
+15 -7
View File
@@ -4,12 +4,14 @@ import (
"bytes" "bytes"
"context" "context"
"crypto/tls" "crypto/tls"
"github.com/pkg/errors"
"github.com/zrepl/zrepl/logger"
"io" "io"
"log/syslog" "log/syslog"
"net" "net"
"time" "time"
"github.com/pkg/errors"
"github.com/zrepl/zrepl/logger"
) )
type EntryFormatter interface { type EntryFormatter interface {
@@ -28,7 +30,10 @@ func (h WriterOutlet) WriteEntry(entry logger.Entry) error {
return err return err
} }
_, err = h.writer.Write(bytes) _, err = h.writer.Write(bytes)
h.writer.Write([]byte("\n")) if err != nil {
return err
}
_, err = h.writer.Write([]byte("\n"))
return err return err
} }
@@ -58,7 +63,7 @@ func NewTCPOutlet(formatter EntryFormatter, network, address string, tlsConfig *
return return
} }
entryChan := make(chan *bytes.Buffer, 1) // allow one message in flight while previos is in io.Copy() entryChan := make(chan *bytes.Buffer, 1) // allow one message in flight while previous is in io.Copy()
o := &TCPOutlet{ o := &TCPOutlet{
formatter: formatter, formatter: formatter,
@@ -92,8 +97,10 @@ func (h *TCPOutlet) outLoop(retryInterval time.Duration) {
conn = nil conn = nil
} }
} }
conn.SetWriteDeadline(time.Now().Add(retryInterval)) err = conn.SetWriteDeadline(time.Now().Add(retryInterval))
_, err = io.Copy(conn, msg) if err == nil {
_, err = io.Copy(conn, msg)
}
if err != nil { if err != nil {
retry = time.Now().Add(retryInterval) retry = time.Now().Add(retryInterval)
conn.Close() conn.Close()
@@ -124,6 +131,7 @@ func (h *TCPOutlet) WriteEntry(e logger.Entry) error {
type SyslogOutlet struct { type SyslogOutlet struct {
Formatter EntryFormatter Formatter EntryFormatter
RetryInterval time.Duration RetryInterval time.Duration
Facility syslog.Priority
writer *syslog.Writer writer *syslog.Writer
lastConnectAttempt time.Time lastConnectAttempt time.Time
} }
@@ -142,7 +150,7 @@ func (o *SyslogOutlet) WriteEntry(entry logger.Entry) error {
if now.Sub(o.lastConnectAttempt) < o.RetryInterval { if now.Sub(o.lastConnectAttempt) < o.RetryInterval {
return nil // not an error toward logger return nil // not an error toward logger
} }
o.writer, err = syslog.New(syslog.LOG_LOCAL0, "zrepl") o.writer, err = syslog.New(o.Facility, "zrepl")
o.lastConnectAttempt = time.Now() o.lastConnectAttempt = time.Now()
if err != nil { if err != nil {
o.writer = nil o.writer = nil
+1 -1
View File
@@ -7,7 +7,7 @@ import (
type Logger = logger.Logger type Logger = logger.Logger
var DaemonCmd = &cli.Subcommand { var DaemonCmd = &cli.Subcommand{
Use: "daemon", Use: "daemon",
Short: "run the zrepl daemon", Short: "run the zrepl daemon",
Run: func(subcommand *cli.Subcommand, args []string) error { Run: func(subcommand *cli.Subcommand, args []string) error {
+2 -1
View File
@@ -1,10 +1,11 @@
package nethelpers package nethelpers
import ( import (
"github.com/pkg/errors"
"net" "net"
"os" "os"
"path/filepath" "path/filepath"
"github.com/pkg/errors"
) )
func PreparePrivateSockpath(sockpath string) error { func PreparePrivateSockpath(sockpath string) error {
+10 -2
View File
@@ -7,11 +7,12 @@ import (
"context" "context"
"net" "net"
"net/http/pprof" "net/http/pprof"
"github.com/zrepl/zrepl/daemon/job"
) )
type pprofServer struct { type pprofServer struct {
cc chan PprofServerControlMsg cc chan PprofServerControlMsg
state PprofServerControlMsg
listener net.Listener listener net.Listener
} }
@@ -63,7 +64,14 @@ outer:
mux.Handle("/debug/pprof/profile", http.HandlerFunc(pprof.Profile)) mux.Handle("/debug/pprof/profile", http.HandlerFunc(pprof.Profile))
mux.Handle("/debug/pprof/symbol", http.HandlerFunc(pprof.Symbol)) mux.Handle("/debug/pprof/symbol", http.HandlerFunc(pprof.Symbol))
mux.Handle("/debug/pprof/trace", http.HandlerFunc(pprof.Trace)) mux.Handle("/debug/pprof/trace", http.HandlerFunc(pprof.Trace))
go http.Serve(s.listener, mux) go func() {
err := http.Serve(s.listener, mux)
if ctx.Err() != nil {
return
} else if err != nil {
job.GetLogger(ctx).WithError(err).Error("pprof server serve error")
}
}()
continue continue
} }
+28 -16
View File
@@ -2,29 +2,35 @@ package daemon
import ( import (
"context" "context"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/zrepl/zrepl/config"
"github.com/zrepl/zrepl/daemon/job"
"github.com/zrepl/zrepl/logger"
"github.com/zrepl/zrepl/zfs"
"net" "net"
"net/http" "net/http"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/zrepl/zrepl/config"
"github.com/zrepl/zrepl/daemon/job"
"github.com/zrepl/zrepl/endpoint"
"github.com/zrepl/zrepl/logger"
"github.com/zrepl/zrepl/rpc/dataconn/frameconn"
"github.com/zrepl/zrepl/util/tcpsock"
"github.com/zrepl/zrepl/zfs"
) )
type prometheusJob struct { type prometheusJob struct {
listen string listen string
freeBind bool
} }
func newPrometheusJobFromConfig(in *config.PrometheusMonitoring) (*prometheusJob, error) { func newPrometheusJobFromConfig(in *config.PrometheusMonitoring) (*prometheusJob, error) {
if _, _, err := net.SplitHostPort(in.Listen); err != nil { if _, _, err := net.SplitHostPort(in.Listen); err != nil {
return nil, err return nil, err
} }
return &prometheusJob{in.Listen}, nil return &prometheusJob{in.Listen, in.ListenFreeBind}, nil
} }
var prom struct { var prom struct {
taskLogEntries *prometheus.CounterVec taskLogEntries *prometheus.CounterVec
} }
func init() { func init() {
@@ -41,6 +47,10 @@ func (j *prometheusJob) Name() string { return jobNamePrometheus }
func (j *prometheusJob) Status() *job.Status { return &job.Status{Type: job.TypeInternal} } func (j *prometheusJob) Status() *job.Status { return &job.Status{Type: job.TypeInternal} }
func (j *prometheusJob) OwnedDatasetSubtreeRoot() (p *zfs.DatasetPath, ok bool) { return nil, false }
func (j *prometheusJob) SenderConfig() *endpoint.SenderConfig { return nil }
func (j *prometheusJob) RegisterMetrics(registerer prometheus.Registerer) {} func (j *prometheusJob) RegisterMetrics(registerer prometheus.Registerer) {}
func (j *prometheusJob) Run(ctx context.Context) { func (j *prometheusJob) Run(ctx context.Context) {
@@ -49,24 +59,27 @@ func (j *prometheusJob) Run(ctx context.Context) {
panic(err) panic(err)
} }
if err := frameconn.PrometheusRegister(prometheus.DefaultRegisterer); err != nil {
panic(err)
}
log := job.GetLogger(ctx) log := job.GetLogger(ctx)
l, err := net.Listen("tcp", j.listen) l, err := tcpsock.Listen(j.listen, j.freeBind)
if err != nil { if err != nil {
log.WithError(err).Error("cannot listen") log.WithError(err).Error("cannot listen")
return
} }
go func() { go func() {
select { <-ctx.Done()
case <-ctx.Done(): l.Close()
l.Close()
}
}() }()
mux := http.NewServeMux() mux := http.NewServeMux()
mux.Handle("/metrics", promhttp.Handler()) mux.Handle("/metrics", promhttp.Handler())
err = http.Serve(l, mux) err = http.Serve(l, mux)
if err != nil { if err != nil && ctx.Err() == nil {
log.WithError(err).Error("error while serving") log.WithError(err).Error("error while serving")
} }
@@ -86,4 +99,3 @@ func (o prometheusJobOutlet) WriteEntry(entry logger.Entry) error {
prom.taskLogEntries.WithLabelValues(o.jobName, entry.Level.String()).Inc() prom.taskLogEntries.WithLabelValues(o.jobName, entry.Level.String()).Inc()
return nil return nil
} }
+213 -218
View File
@@ -3,30 +3,31 @@ package pruner
import ( import (
"context" "context"
"fmt" "fmt"
"github.com/pkg/errors"
"github.com/prometheus/client_golang/prometheus"
"github.com/zrepl/zrepl/config"
"github.com/zrepl/zrepl/logger"
"github.com/zrepl/zrepl/pruning"
"github.com/zrepl/zrepl/replication/pdu"
"github.com/zrepl/zrepl/util/envconst"
"github.com/zrepl/zrepl/util/watchdog"
"github.com/problame/go-streamrpc"
"net"
"sort" "sort"
"strings" "strings"
"sync" "sync"
"time" "time"
"github.com/pkg/errors"
"github.com/prometheus/client_golang/prometheus"
"github.com/zrepl/zrepl/config"
"github.com/zrepl/zrepl/logger"
"github.com/zrepl/zrepl/pruning"
"github.com/zrepl/zrepl/replication/logic/pdu"
"github.com/zrepl/zrepl/util/envconst"
) )
// Try to keep it compatible with gitub.com/zrepl/zrepl/replication.Endpoint // Try to keep it compatible with github.com/zrepl/zrepl/endpoint.Endpoint
type History interface { type History interface {
ReplicationCursor(ctx context.Context, req *pdu.ReplicationCursorReq) (*pdu.ReplicationCursorRes, error) ReplicationCursor(ctx context.Context, req *pdu.ReplicationCursorReq) (*pdu.ReplicationCursorRes, error)
ListFilesystems(ctx context.Context, req *pdu.ListFilesystemReq) (*pdu.ListFilesystemRes, error)
} }
// Try to keep it compatible with github.com/zrepl/zrepl/endpoint.Endpoint
type Target interface { type Target interface {
ListFilesystems(ctx context.Context) ([]*pdu.Filesystem, error) ListFilesystems(ctx context.Context, req *pdu.ListFilesystemReq) (*pdu.ListFilesystemRes, error)
ListFilesystemVersions(ctx context.Context, fs string) ([]*pdu.FilesystemVersion, error) // fix depS ListFilesystemVersions(ctx context.Context, req *pdu.ListFilesystemVersionsReq) (*pdu.ListFilesystemVersionsRes, error)
DestroySnapshots(ctx context.Context, req *pdu.DestroySnapshotsReq) (*pdu.DestroySnapshotsRes, error) DestroySnapshots(ctx context.Context, req *pdu.DestroySnapshotsReq) (*pdu.DestroySnapshotsRes, error)
} }
@@ -54,21 +55,18 @@ type args struct {
rules []pruning.KeepRule rules []pruning.KeepRule
retryWait time.Duration retryWait time.Duration
considerSnapAtCursorReplicated bool considerSnapAtCursorReplicated bool
promPruneSecs prometheus.Observer promPruneSecs prometheus.Observer
} }
type Pruner struct { type Pruner struct {
args args args args
Progress watchdog.KeepAlive
mtx sync.RWMutex mtx sync.RWMutex
state State state State
// State ErrWait|ErrPerm // State PlanErr
sleepUntil time.Time err error
err error
// State Exec // State Exec
execQueue *execQueue execQueue *execQueue
@@ -79,20 +77,33 @@ type PrunerFactory struct {
receiverRules []pruning.KeepRule receiverRules []pruning.KeepRule
retryWait time.Duration retryWait time.Duration
considerSnapAtCursorReplicated bool considerSnapAtCursorReplicated bool
promPruneSecs *prometheus.HistogramVec
}
type LocalPrunerFactory struct {
keepRules []pruning.KeepRule
retryWait time.Duration
promPruneSecs *prometheus.HistogramVec promPruneSecs *prometheus.HistogramVec
} }
func checkContainsKeep1(rules []pruning.KeepRule) error { func NewLocalPrunerFactory(in config.PruningLocal, promPruneSecs *prometheus.HistogramVec) (*LocalPrunerFactory, error) {
if len(rules) == 0 { rules, err := pruning.RulesFromConfig(in.Keep)
return nil //No keep rules means keep all - ok if err != nil {
return nil, errors.Wrap(err, "cannot build pruning rules")
} }
for _, e := range rules { for _, r := range in.Keep {
switch e.(type) { if _, ok := r.Ret.(*config.PruneKeepNotReplicated); ok {
case *pruning.KeepLastN: // rule NotReplicated for a local pruner doesn't make sense
return nil // because no replication happens with that job type
return nil, fmt.Errorf("single-site pruner cannot support `not_replicated` keep rule")
} }
} }
return errors.New("sender keep rules must contain last_n or be empty so that the last snapshot is definitely kept") f := &LocalPrunerFactory{
keepRules: rules,
retryWait: envconst.Duration("ZREPL_PRUNER_RETRY_INTERVAL", 10*time.Second),
promPruneSecs: promPruneSecs,
}
return f, nil
} }
func NewPrunerFactory(in config.PruningSenderReceiver, promPruneSecs *prometheus.HistogramVec) (*PrunerFactory, error) { func NewPrunerFactory(in config.PruningSenderReceiver, promPruneSecs *prometheus.HistogramVec) (*PrunerFactory, error) {
@@ -115,11 +126,11 @@ func NewPrunerFactory(in config.PruningSenderReceiver, promPruneSecs *prometheus
considerSnapAtCursorReplicated = considerSnapAtCursorReplicated || !knr.KeepSnapshotAtCursor considerSnapAtCursorReplicated = considerSnapAtCursorReplicated || !knr.KeepSnapshotAtCursor
} }
f := &PrunerFactory{ f := &PrunerFactory{
senderRules: keepRulesSender, senderRules: keepRulesSender,
receiverRules: keepRulesReceiver, receiverRules: keepRulesReceiver,
retryWait: envconst.Duration("ZREPL_PRUNER_RETRY_INTERVAL", 10 * time.Second), retryWait: envconst.Duration("ZREPL_PRUNER_RETRY_INTERVAL", 10*time.Second),
considerSnapAtCursorReplicated: considerSnapAtCursorReplicated, considerSnapAtCursorReplicated: considerSnapAtCursorReplicated,
promPruneSecs: promPruneSecs, promPruneSecs: promPruneSecs,
} }
return f, nil return f, nil
} }
@@ -156,82 +167,70 @@ func (f *PrunerFactory) BuildReceiverPruner(ctx context.Context, target Target,
return p return p
} }
func (f *LocalPrunerFactory) BuildLocalPruner(ctx context.Context, target Target, receiver History) *Pruner {
p := &Pruner{
args: args{
ctx,
target,
receiver,
f.keepRules,
f.retryWait,
false, // considerSnapAtCursorReplicated is not relevant for local pruning
f.promPruneSecs.WithLabelValues("local"),
},
state: Plan,
}
return p
}
//go:generate enumer -type=State //go:generate enumer -type=State
type State int type State int
const ( const (
Plan State = 1 << iota Plan State = 1 << iota
PlanWait PlanErr
Exec Exec
ExecWait ExecErr
ErrPerm
Done Done
) )
func (s State) statefunc() state { type updater func(func(*Pruner))
var statemap = map[State]state{
Plan: statePlan,
PlanWait: statePlanWait,
Exec: stateExec,
ExecWait: stateExecWait,
ErrPerm: nil,
Done: nil,
}
return statemap[s]
}
func (s State) IsTerminal() bool {
return s.statefunc() == nil
}
type updater func(func(*Pruner)) State
type state func(args *args, u updater) state
func (p *Pruner) Prune() { func (p *Pruner) Prune() {
p.prune(p.args) p.prune(p.args)
} }
func (p *Pruner) prune(args args) { func (p *Pruner) prune(args args) {
s := p.state.statefunc() u := func(f func(*Pruner)) {
for s != nil { p.mtx.Lock()
pre := p.state defer p.mtx.Unlock()
s = s(&args, func(f func(*Pruner)) State { f(p)
p.mtx.Lock()
defer p.mtx.Unlock()
f(p)
return p.state
})
post := p.state
GetLogger(args.ctx).
WithField("transition", fmt.Sprintf("%s=>%s", pre, post)).
Debug("state transition")
if err := p.Error(); err != nil {
GetLogger(args.ctx).
WithError(p.err).
WithField("state", post.String()).
Error("entering error state after error")
}
} }
// TODO support automatic retries
// It is advisable to merge this code with package replication/driver before
// That will likely require re-modelling struct fs like replication/driver.attempt,
// including figuring out how to resume a plan after being interrupted by network errors
// The non-retrying code in this package should move straight to replication/logic.
doOneAttempt(&args, u)
} }
type Report struct { type Report struct {
State string State string
SleepUntil time.Time Error string
Error string
Pending, Completed []FSReport Pending, Completed []FSReport
} }
type FSReport struct { type FSReport struct {
Filesystem string Filesystem string
SnapshotList, DestroyList []SnapshotReport SnapshotList, DestroyList []SnapshotReport
ErrorCount int SkipReason FSSkipReason
LastError string LastError string
} }
type SnapshotReport struct { type SnapshotReport struct {
Name string Name string
Replicated bool Replicated bool
Date time.Time Date time.Time
} }
func (p *Pruner) Report() *Report { func (p *Pruner) Report() *Report {
@@ -240,13 +239,8 @@ func (p *Pruner) Report() *Report {
r := Report{State: p.state.String()} r := Report{State: p.state.String()}
if p.state & (PlanWait|ExecWait) != 0 { if p.err != nil {
r.SleepUntil = p.sleepUntil r.Error = p.err.Error()
}
if p.state & (PlanWait|ExecWait|ErrPerm) != 0 {
if p.err != nil {
r.Error = p.err.Error()
}
} }
if p.execQueue != nil { if p.execQueue != nil {
@@ -262,17 +256,16 @@ func (p *Pruner) State() State {
return p.state return p.state
} }
func (p *Pruner) Error() error {
p.mtx.Lock()
defer p.mtx.Unlock()
if p.state & (PlanWait|ExecWait|ErrPerm) != 0 {
return p.err
}
return nil
}
type fs struct { type fs struct {
path string path string
// permanent error during planning
planErr error
planErrContext string
// if != "", the fs was skipped for planning and the field
// contains the reason
skipReason FSSkipReason
// snapshots presented by target // snapshots presented by target
// (type snapshot) // (type snapshot)
@@ -285,8 +278,18 @@ type fs struct {
// only during Exec state, also used by execQueue // only during Exec state, also used by execQueue
execErrLast error execErrLast error
execErrCount int }
type FSSkipReason string
const (
NotSkipped = ""
SkipPlaceholder = "filesystem is placeholder"
SkipNoCorrespondenceOnSender = "filesystem has no correspondence on sender"
)
func (r FSSkipReason) NotSkipped() bool {
return r == NotSkipped
} }
func (f *fs) Report() FSReport { func (f *fs) Report() FSReport {
@@ -295,8 +298,14 @@ func (f *fs) Report() FSReport {
r := FSReport{} r := FSReport{}
r.Filesystem = f.path r.Filesystem = f.path
r.ErrorCount = f.execErrCount r.SkipReason = f.skipReason
if f.execErrLast != nil { if !r.SkipReason.NotSkipped() {
return r
}
if f.planErr != nil {
r.LastError = f.planErr.Error()
} else if f.execErrLast != nil {
r.LastError = f.execErrLast.Error() r.LastError = f.execErrLast.Error()
} }
@@ -306,7 +315,7 @@ func (f *fs) Report() FSReport {
} }
r.DestroyList = make([]SnapshotReport, len(f.destroyList)) r.DestroyList = make([]SnapshotReport, len(f.destroyList))
for i, snap := range f.destroyList{ for i, snap := range f.destroyList {
r.DestroyList[i] = snap.(snapshot).Report() r.DestroyList[i] = snap.(snapshot).Report()
} }
@@ -335,90 +344,85 @@ func (s snapshot) Replicated() bool { return s.replicated }
func (s snapshot) Date() time.Time { return s.date } func (s snapshot) Date() time.Time { return s.date }
type Error interface { func doOneAttempt(a *args, u updater) {
error
Temporary() bool
}
var _ Error = net.Error(nil)
var _ Error = streamrpc.Error(nil)
func shouldRetry(e error) bool {
if neterr, ok := e.(net.Error); ok {
return neterr.Temporary()
}
return false
}
func onErr(u updater, e error) state {
return u(func(p *Pruner) {
p.err = e
if !shouldRetry(e) {
p.state = ErrPerm
return
}
switch p.state {
case Plan:
p.state = PlanWait
case Exec:
p.state = ExecWait
default:
panic(p.state)
}
}).statefunc()
}
func statePlan(a *args, u updater) state {
ctx, target, receiver := a.ctx, a.target, a.receiver ctx, target, receiver := a.ctx, a.target, a.receiver
var ka *watchdog.KeepAlive
u(func(pruner *Pruner) {
ka = &pruner.Progress
})
tfss, err := target.ListFilesystems(ctx) sfssres, err := receiver.ListFilesystems(ctx, &pdu.ListFilesystemReq{})
if err != nil { if err != nil {
return onErr(u, err) u(func(p *Pruner) {
p.state = PlanErr
p.err = err
})
return
}
sfss := make(map[string]*pdu.Filesystem)
for _, sfs := range sfssres.GetFilesystems() {
sfss[sfs.GetPath()] = sfs
} }
tfssres, err := target.ListFilesystems(ctx, &pdu.ListFilesystemReq{})
if err != nil {
u(func(p *Pruner) {
p.state = PlanErr
p.err = err
})
return
}
tfss := tfssres.GetFilesystems()
pfss := make([]*fs, len(tfss)) pfss := make([]*fs, len(tfss))
tfss_loop:
for i, tfs := range tfss { for i, tfs := range tfss {
l := GetLogger(ctx).WithField("fs", tfs.Path) l := GetLogger(ctx).WithField("fs", tfs.Path)
l.Debug("plan filesystem") l.Debug("plan filesystem")
pfs := &fs{ pfs := &fs{
path: tfs.Path, path: tfs.Path,
} }
pfss[i] = pfs pfss[i] = pfs
tfsvs, err := target.ListFilesystemVersions(ctx, tfs.Path) if tfs.GetIsPlaceholder() {
if err != nil { pfs.skipReason = SkipPlaceholder
l.WithError(err).Error("cannot list filesystem versions") l.WithField("skip_reason", pfs.skipReason).Debug("skipping filesystem")
return onErr(u, err) continue
} else if sfs := sfss[tfs.GetPath()]; sfs == nil {
pfs.skipReason = SkipNoCorrespondenceOnSender
l.WithField("skip_reason", pfs.skipReason).WithField("sfs", sfs.GetPath()).Debug("skipping filesystem")
continue
} }
pfsPlanErrAndLog := func(err error, message string) {
t := fmt.Sprintf("%T", err)
pfs.planErr = err
pfs.planErrContext = message
l.WithField("orig_err_type", t).WithError(err).Error(fmt.Sprintf("%s: plan error, skipping filesystem", message))
}
tfsvsres, err := target.ListFilesystemVersions(ctx, &pdu.ListFilesystemVersionsReq{Filesystem: tfs.Path})
if err != nil {
pfsPlanErrAndLog(err, "cannot list filesystem versions")
continue tfss_loop
}
tfsvs := tfsvsres.GetVersions()
// no progress here since we could run in a live-lock (must have used target AND receiver before progress) // no progress here since we could run in a live-lock (must have used target AND receiver before progress)
pfs.snaps = make([]pruning.Snapshot, 0, len(tfsvs)) pfs.snaps = make([]pruning.Snapshot, 0, len(tfsvs))
rcReq := &pdu.ReplicationCursorReq{ rcReq := &pdu.ReplicationCursorReq{
Filesystem: tfs.Path, Filesystem: tfs.Path,
Op: &pdu.ReplicationCursorReq_Get{
Get: &pdu.ReplicationCursorReq_GetOp{},
},
} }
rc, err := receiver.ReplicationCursor(ctx, rcReq) rc, err := receiver.ReplicationCursor(ctx, rcReq)
if err != nil { if err != nil {
l.WithError(err).Error("cannot get replication cursor") pfsPlanErrAndLog(err, "cannot get replication cursor bookmark")
return onErr(u, err) continue tfss_loop
} }
if rc.GetError() != "" { if rc.GetNotexist() {
l.WithField("reqErr", rc.GetError()).Error("cannot get replication cursor") err := errors.New("replication cursor bookmark does not exist (one successful replication is required before pruning works)")
return onErr(u, err) pfsPlanErrAndLog(err, "")
continue tfss_loop
} }
ka.MadeProgress()
// scan from older to newer, all snapshots older than cursor are interpreted as replicated // scan from older to newer, all snapshots older than cursor are interpreted as replicated
sort.Slice(tfsvs, func(i, j int) bool { sort.Slice(tfsvs, func(i, j int) bool {
@@ -441,11 +445,9 @@ func statePlan(a *args, u updater) state {
} }
creation, err := tfsv.CreationAsTime() creation, err := tfsv.CreationAsTime()
if err != nil { if err != nil {
err := fmt.Errorf("%s%s has invalid creation date: %s", tfs, tfsv.RelName(), err) err := fmt.Errorf("%s: %s", tfsv.RelName(), err)
l.WithError(err). pfsPlanErrAndLog(err, "fs version with invalid creation date")
WithField("tfsv", tfsv.RelName()). continue tfss_loop
Error("error with fileesystem version")
return onErr(u, err)
} }
// note that we cannot use CreateTXG because target and receiver could be on different pools // note that we cannot use CreateTXG because target and receiver could be on different pools
atCursor := tfsv.Guid == rc.GetGuid() atCursor := tfsv.Guid == rc.GetGuid()
@@ -457,43 +459,61 @@ func statePlan(a *args, u updater) state {
}) })
} }
if preCursor { if preCursor {
err := fmt.Errorf("replication cursor not found in prune target filesystem versions") pfsPlanErrAndLog(fmt.Errorf("replication cursor not found in prune target filesystem versions"), "")
l.Error(err.Error()) continue tfss_loop
return onErr(u, err)
} }
// Apply prune rules // Apply prune rules
pfs.destroyList = pruning.PruneSnapshots(pfs.snaps, a.rules) pfs.destroyList = pruning.PruneSnapshots(pfs.snaps, a.rules)
ka.MadeProgress()
} }
return u(func(pruner *Pruner) { u(func(pruner *Pruner) {
pruner.Progress.MadeProgress()
pruner.execQueue = newExecQueue(len(pfss)) pruner.execQueue = newExecQueue(len(pfss))
for _, pfs := range pfss { for _, pfs := range pfss {
pruner.execQueue.Put(pfs, nil, false) pruner.execQueue.Put(pfs, nil, false)
} }
pruner.state = Exec pruner.state = Exec
}).statefunc() })
}
func stateExec(a *args, u updater) state { for {
var pfs *fs
var pfs *fs u(func(pruner *Pruner) {
state := u(func(pruner *Pruner) { pfs = pruner.execQueue.Pop()
pfs = pruner.execQueue.Pop() })
if pfs == nil { if pfs == nil {
nextState := Done break
if pruner.execQueue.HasCompletedFSWithErrors() { }
nextState = ErrPerm doOneAttemptExec(a, u, pfs)
} }
pruner.state = nextState
return var rep *Report
{
// must not hold lock for report
var pruner *Pruner
u(func(p *Pruner) {
pruner = p
})
rep = pruner.Report()
}
u(func(p *Pruner) {
if len(rep.Pending) > 0 {
panic("queue should not have pending items at this point")
}
hadErr := false
for _, fsr := range rep.Completed {
hadErr = hadErr || fsr.SkipReason.NotSkipped() && fsr.LastError != ""
}
if hadErr {
p.state = ExecErr
} else {
p.state = Done
} }
}) })
if state != Exec {
return state.statefunc() }
}
// attempts to exec pfs, puts it back into the queue with the result
func doOneAttemptExec(a *args, u updater, pfs *fs) {
destroyList := make([]*pdu.FilesystemVersion, len(pfs.destroyList)) destroyList := make([]*pdu.FilesystemVersion, len(pfs.destroyList))
for i := range destroyList { for i := range destroyList {
@@ -513,7 +533,7 @@ func stateExec(a *args, u updater) state {
u(func(pruner *Pruner) { u(func(pruner *Pruner) {
pruner.execQueue.Put(pfs, err, false) pruner.execQueue.Put(pfs, err, false)
}) })
return onErr(u, err) return
} }
// check if all snapshots were destroyed // check if all snapshots were destroyed
destroyResults := make(map[string]*pdu.DestroySnapshotRes) destroyResults := make(map[string]*pdu.DestroySnapshotRes)
@@ -523,20 +543,20 @@ func stateExec(a *args, u updater) state {
err = nil err = nil
destroyFails := make([]*pdu.DestroySnapshotRes, 0) destroyFails := make([]*pdu.DestroySnapshotRes, 0)
for _, reqDestroy := range destroyList { for _, reqDestroy := range destroyList {
res, ok := destroyResults[reqDestroy.Name] res, ok := destroyResults[reqDestroy.Name]
if !ok { if !ok {
err = fmt.Errorf("missing destroy-result for %s", reqDestroy.RelName()) err = fmt.Errorf("missing destroy-result for %s", reqDestroy.RelName())
break break
} else if res.Error != "" { } else if res.Error != "" {
destroyFails = append(destroyFails, res) destroyFails = append(destroyFails, res)
} }
} }
if err == nil && len(destroyFails) > 0 { if err == nil && len(destroyFails) > 0 {
names := make([]string, len(destroyFails)) names := make([]string, len(destroyFails))
pairs := make([]string, len(destroyFails)) pairs := make([]string, len(destroyFails))
allSame := true allSame := true
lastMsg := destroyFails[0].Error lastMsg := destroyFails[0].Error
for i := 0; i < len(destroyFails); i++{ for i := 0; i < len(destroyFails); i++ {
allSame = allSame && destroyFails[i].Error == lastMsg allSame = allSame && destroyFails[i].Error == lastMsg
relname := destroyFails[i].Snapshot.RelName() relname := destroyFails[i].Snapshot.RelName()
names[i] = relname names[i] = relname
@@ -554,31 +574,6 @@ func stateExec(a *args, u updater) state {
}) })
if err != nil { if err != nil {
GetLogger(a.ctx).WithError(err).Error("target could not destroy snapshots") GetLogger(a.ctx).WithError(err).Error("target could not destroy snapshots")
return onErr(u, err) return
}
return u(func(pruner *Pruner) {
pruner.Progress.MadeProgress()
}).statefunc()
}
func stateExecWait(a *args, u updater) state {
return doWait(Exec, a, u)
}
func statePlanWait(a *args, u updater) state {
return doWait(Plan, a, u)
}
func doWait(goback State, a *args, u updater) state {
timer := time.NewTimer(a.retryWait)
defer timer.Stop()
select {
case <-timer.C:
return u(func(pruner *Pruner) {
pruner.state = goback
}).statefunc()
case <-a.ctx.Done():
return onErr(u, a.ctx.Err())
} }
} }
+5 -12
View File
@@ -7,13 +7,13 @@ import (
) )
type execQueue struct { type execQueue struct {
mtx sync.Mutex mtx sync.Mutex
pending, completed []*fs pending, completed []*fs
} }
func newExecQueue(cap int) *execQueue { func newExecQueue(cap int) *execQueue {
q := execQueue{ q := execQueue{
pending: make([]*fs, 0, cap), pending: make([]*fs, 0, cap),
completed: make([]*fs, 0, cap), completed: make([]*fs, 0, cap),
} }
return &q return &q
@@ -55,13 +55,10 @@ func (q *execQueue) Pop() *fs {
return fs return fs
} }
func(q *execQueue) Put(fs *fs, err error, done bool) { func (q *execQueue) Put(fs *fs, err error, done bool) {
fs.mtx.Lock() fs.mtx.Lock()
fs.execErrLast = err fs.execErrLast = err
if err != nil { if done || err != nil {
fs.execErrCount++
}
if done || (err != nil && !shouldRetry(fs.execErrLast)) {
fs.mtx.Unlock() fs.mtx.Unlock()
q.mtx.Lock() q.mtx.Lock()
q.completed = append(q.completed, fs) q.completed = append(q.completed, fs)
@@ -78,12 +75,8 @@ func(q *execQueue) Put(fs *fs, err error, done bool) {
defer q.pending[i].mtx.Unlock() defer q.pending[i].mtx.Unlock()
q.pending[j].mtx.Lock() q.pending[j].mtx.Lock()
defer q.pending[j].mtx.Unlock() defer q.pending[j].mtx.Unlock()
if q.pending[i].execErrCount != q.pending[j].execErrCount {
return q.pending[i].execErrCount < q.pending[j].execErrCount
}
return strings.Compare(q.pending[i].path, q.pending[j].path) == -1 return strings.Compare(q.pending[i].path, q.pending[j].path) == -1
}) })
q.mtx.Unlock() q.mtx.Unlock()
}
}
-205
View File
@@ -1,205 +0,0 @@
package pruner
import (
"context"
"fmt"
"github.com/stretchr/testify/assert"
"github.com/zrepl/zrepl/logger"
"github.com/zrepl/zrepl/pruning"
"github.com/zrepl/zrepl/replication/pdu"
"net"
"testing"
"time"
)
type mockFS struct {
path string
snaps []string
}
func (m *mockFS) Filesystem() *pdu.Filesystem {
return &pdu.Filesystem{
Path: m.path,
}
}
func (m *mockFS) FilesystemVersions() []*pdu.FilesystemVersion {
versions := make([]*pdu.FilesystemVersion, len(m.snaps))
for i, v := range m.snaps {
versions[i] = &pdu.FilesystemVersion{
Type: pdu.FilesystemVersion_Snapshot,
Name: v,
Creation: pdu.FilesystemVersionCreation(time.Unix(0, 0)),
Guid: uint64(i),
}
}
return versions
}
type mockTarget struct {
fss []mockFS
destroyed map[string][]string
listVersionsErrs map[string][]error
listFilesystemsErr []error
destroyErrs map[string][]error
}
func (t *mockTarget) ListFilesystems(ctx context.Context) ([]*pdu.Filesystem, error) {
if len(t.listFilesystemsErr) > 0 {
e := t.listFilesystemsErr[0]
t.listFilesystemsErr = t.listFilesystemsErr[1:]
return nil, e
}
fss := make([]*pdu.Filesystem, len(t.fss))
for i := range fss {
fss[i] = t.fss[i].Filesystem()
}
return fss, nil
}
func (t *mockTarget) ListFilesystemVersions(ctx context.Context, fs string) ([]*pdu.FilesystemVersion, error) {
if len(t.listVersionsErrs[fs]) != 0 {
e := t.listVersionsErrs[fs][0]
t.listVersionsErrs[fs] = t.listVersionsErrs[fs][1:]
return nil, e
}
for _, mfs := range t.fss {
if mfs.path != fs {
continue
}
return mfs.FilesystemVersions(), nil
}
return nil, fmt.Errorf("filesystem %s does not exist", fs)
}
func (t *mockTarget) DestroySnapshots(ctx context.Context, req *pdu.DestroySnapshotsReq) (*pdu.DestroySnapshotsRes, error) {
fs, snaps := req.Filesystem, req.Snapshots
if len(t.destroyErrs[fs]) != 0 {
e := t.destroyErrs[fs][0]
t.destroyErrs[fs] = t.destroyErrs[fs][1:]
return nil, e
}
destroyed := t.destroyed[fs]
res := make([]*pdu.DestroySnapshotRes, len(snaps))
for i, s := range snaps {
destroyed = append(destroyed, s.Name)
res[i] = &pdu.DestroySnapshotRes{Error: "", Snapshot: s}
}
t.destroyed[fs] = destroyed
return &pdu.DestroySnapshotsRes{Results: res}, nil
}
type mockCursor struct {
snapname string
guid uint64
}
type mockHistory struct {
errs map[string][]error
cursors map[string]*mockCursor
}
func (r *mockHistory) ReplicationCursor(ctx context.Context, req *pdu.ReplicationCursorReq) (*pdu.ReplicationCursorRes, error) {
fs := req.Filesystem
if len(r.errs[fs]) > 0 {
e := r.errs[fs][0]
r.errs[fs] = r.errs[fs][1:]
return nil, e
}
return &pdu.ReplicationCursorRes{Result: &pdu.ReplicationCursorRes_Guid{Guid: 0}}, nil
}
type stubNetErr struct {
msg string
temporary, timeout bool
}
var _ net.Error = stubNetErr{}
func (e stubNetErr) Error() string {
return e.msg
}
func (e stubNetErr) Temporary() bool { return e.temporary }
func (e stubNetErr) Timeout() bool { return e.timeout }
func TestPruner_Prune(t *testing.T) {
var _ net.Error = &net.OpError{} // we use it below
target := &mockTarget{
listFilesystemsErr: []error{
stubNetErr{msg: "fakerror0", temporary: true},
},
listVersionsErrs: map[string][]error{
"zroot/foo": {
stubNetErr{msg: "fakeerror1", temporary: true},
stubNetErr{msg: "fakeerror2", temporary: true,},
},
},
destroyErrs: map[string][]error{
"zroot/baz": {
stubNetErr{msg: "fakeerror3", temporary: true}, // first error puts it back in the queue
stubNetErr{msg:"permanent error"}, // so it will be last when pruner gives up due to permanent err
},
},
destroyed: make(map[string][]string),
fss: []mockFS{
{
path: "zroot/foo",
snaps: []string{
"keep_a",
"keep_b",
"drop_c",
"keep_d",
},
},
{
path: "zroot/bar",
snaps: []string{
"keep_e",
"keep_f",
"drop_g",
},
},
{
path: "zroot/baz",
snaps: []string{
"keep_h",
"drop_i",
},
},
},
}
history := &mockHistory{
errs: map[string][]error{
"zroot/foo": {
stubNetErr{msg: "fakeerror4", temporary: true},
},
},
}
keepRules := []pruning.KeepRule{pruning.MustKeepRegex("^keep")}
p := Pruner{
args: args{
ctx: WithLogger(context.Background(), logger.NewTestLogger(t)),
target: target,
receiver: history,
rules: keepRules,
retryWait: 10*time.Millisecond,
},
state: Plan,
}
p.Prune()
exp := map[string][]string{
"zroot/foo": {"drop_c"},
"zroot/bar": {"drop_g"},
}
assert.Equal(t, exp, target.destroyed)
//assert.Equal(t, map[string][]error{}, target.listVersionsErrs, "retried")
}
+11 -15
View File
@@ -1,5 +1,6 @@
// Code generated by "enumer -type=State"; DO NOT EDIT. // Code generated by "enumer -type=State"; DO NOT EDIT.
//
package pruner package pruner
import ( import (
@@ -7,19 +8,17 @@ import (
) )
const ( const (
_StateName_0 = "PlanPlanWait" _StateName_0 = "PlanPlanErr"
_StateName_1 = "Exec" _StateName_1 = "Exec"
_StateName_2 = "ExecWait" _StateName_2 = "ExecErr"
_StateName_3 = "ErrPerm" _StateName_3 = "Done"
_StateName_4 = "Done"
) )
var ( var (
_StateIndex_0 = [...]uint8{0, 4, 12} _StateIndex_0 = [...]uint8{0, 4, 11}
_StateIndex_1 = [...]uint8{0, 4} _StateIndex_1 = [...]uint8{0, 4}
_StateIndex_2 = [...]uint8{0, 8} _StateIndex_2 = [...]uint8{0, 7}
_StateIndex_3 = [...]uint8{0, 7} _StateIndex_3 = [...]uint8{0, 4}
_StateIndex_4 = [...]uint8{0, 4}
) )
func (i State) String() string { func (i State) String() string {
@@ -33,22 +32,19 @@ func (i State) String() string {
return _StateName_2 return _StateName_2
case i == 16: case i == 16:
return _StateName_3 return _StateName_3
case i == 32:
return _StateName_4
default: default:
return fmt.Sprintf("State(%d)", i) return fmt.Sprintf("State(%d)", i)
} }
} }
var _StateValues = []State{1, 2, 4, 8, 16, 32} var _StateValues = []State{1, 2, 4, 8, 16}
var _StateNameToValueMap = map[string]State{ var _StateNameToValueMap = map[string]State{
_StateName_0[0:4]: 1, _StateName_0[0:4]: 1,
_StateName_0[4:12]: 2, _StateName_0[4:11]: 2,
_StateName_1[0:4]: 4, _StateName_1[0:4]: 4,
_StateName_2[0:8]: 8, _StateName_2[0:7]: 8,
_StateName_3[0:7]: 16, _StateName_3[0:4]: 16,
_StateName_4[0:4]: 32,
} }
// StateString retrieves an enum value from the enum constants string name. // StateString retrieves an enum value from the enum constants string name.
+229 -85
View File
@@ -1,18 +1,21 @@
package snapper package snapper
import ( import (
"github.com/zrepl/zrepl/config"
"github.com/pkg/errors"
"time"
"context" "context"
"github.com/zrepl/zrepl/daemon/filters"
"fmt" "fmt"
"github.com/zrepl/zrepl/zfs"
"sort" "sort"
"github.com/zrepl/zrepl/logger"
"sync" "sync"
) "time"
"github.com/pkg/errors"
"github.com/zrepl/zrepl/config"
"github.com/zrepl/zrepl/daemon/filters"
"github.com/zrepl/zrepl/daemon/hooks"
"github.com/zrepl/zrepl/logger"
"github.com/zrepl/zrepl/util/envconst"
"github.com/zrepl/zrepl/zfs"
)
//go:generate stringer -type=SnapState //go:generate stringer -type=SnapState
type SnapState uint type SnapState uint
@@ -24,18 +27,20 @@ const (
SnapError SnapError
) )
// All fields protected by Snapper.mtx
type snapProgress struct { type snapProgress struct {
state SnapState state SnapState
// SnapStarted, SnapDone, SnapError // SnapStarted, SnapDone, SnapError
name string name string
startAt time.Time startAt time.Time
hookPlan *hooks.Plan
// SnapDone // SnapDone
doneAt time.Time doneAt time.Time
// SnapErr // SnapErr TODO disambiguate state
err error runResults hooks.PlanReport
} }
type args struct { type args struct {
@@ -44,20 +49,22 @@ type args struct {
prefix string prefix string
interval time.Duration interval time.Duration
fsf *filters.DatasetMapFilter fsf *filters.DatasetMapFilter
snapshotsTaken chan<-struct{} snapshotsTaken chan<- struct{}
hooks *hooks.List
dryRun bool
} }
type Snapper struct { type Snapper struct {
args args args args
mtx sync.Mutex mtx sync.Mutex
state State state State
// set in state Plan, used in Waiting // set in state Plan, used in Waiting
lastInvocation time.Time lastInvocation time.Time
// valid for state Snapshotting // valid for state Snapshotting
plan map[*zfs.DatasetPath]snapProgress plan map[*zfs.DatasetPath]*snapProgress
// valid for state SyncUp and Waiting // valid for state SyncUp and Waiting
sleepUntil time.Time sleepUntil time.Time
@@ -70,7 +77,8 @@ type Snapper struct {
type State uint type State uint
const ( const (
SyncUp State = 1<<iota SyncUp State = 1 << iota
SyncUpErrWait
Planning Planning
Snapshotting Snapshotting
Waiting Waiting
@@ -80,12 +88,13 @@ const (
func (s State) sf() state { func (s State) sf() state {
m := map[State]state{ m := map[State]state{
SyncUp: syncUp, SyncUp: syncUp,
Planning: plan, SyncUpErrWait: wait,
Snapshotting: snapshot, Planning: plan,
Waiting: wait, Snapshotting: snapshot,
ErrorWait: wait, Waiting: wait,
Stopped: nil, ErrorWait: wait,
Stopped: nil,
} }
return m[s] return m[s]
} }
@@ -120,10 +129,16 @@ func PeriodicFromConfig(g *config.Global, fsf *filters.DatasetMapFilter, in *con
return nil, errors.New("interval must be positive") return nil, errors.New("interval must be positive")
} }
hookList, err := hooks.ListFromConfig(&in.Hooks)
if err != nil {
return nil, errors.Wrap(err, "hook config error")
}
args := args{ args := args{
prefix: in.Prefix, prefix: in.Prefix,
interval: in.Interval, interval: in.Interval,
fsf: fsf, fsf: fsf,
hooks: hookList,
// ctx and log is set in Run() // ctx and log is set in Run()
} }
@@ -138,6 +153,7 @@ func (s *Snapper) Run(ctx context.Context, snapshotsTaken chan<- struct{}) {
s.args.snapshotsTaken = snapshotsTaken s.args.snapshotsTaken = snapshotsTaken
s.args.ctx = ctx s.args.ctx = ctx
s.args.log = getLogger(ctx) s.args.log = getLogger(ctx)
s.args.dryRun = false // for future expansion
u := func(u func(*Snapper)) State { u := func(u func(*Snapper)) State {
s.mtx.Lock() s.mtx.Lock()
@@ -165,7 +181,16 @@ func (s *Snapper) Run(ctx context.Context, snapshotsTaken chan<- struct{}) {
func onErr(err error, u updater) state { func onErr(err error, u updater) state {
return u(func(s *Snapper) { return u(func(s *Snapper) {
s.err = err s.err = err
s.state = ErrorWait preState := s.state
switch s.state {
case SyncUp:
s.state = SyncUpErrWait
case Planning:
fallthrough
case Snapshotting:
s.state = ErrorWait
}
s.args.log.WithError(err).WithField("pre_state", preState).WithField("post_state", s.state).Error("snapshotting error")
}).sf() }).sf()
} }
@@ -177,7 +202,10 @@ func onMainCtxDone(ctx context.Context, u updater) state {
} }
func syncUp(a args, u updater) state { func syncUp(a args, u updater) state {
fss, err := listFSes(a.fsf) u(func(snapper *Snapper) {
snapper.lastInvocation = time.Now()
})
fss, err := listFSes(a.ctx, a.fsf)
if err != nil { if err != nil {
return onErr(err, u) return onErr(err, u)
} }
@@ -185,10 +213,10 @@ func syncUp(a args, u updater) state {
if err != nil { if err != nil {
return onErr(err, u) return onErr(err, u)
} }
u(func(s *Snapper){ u(func(s *Snapper) {
s.sleepUntil = syncPoint s.sleepUntil = syncPoint
}) })
t := time.NewTimer(syncPoint.Sub(time.Now())) t := time.NewTimer(time.Until(syncPoint))
defer t.Stop() defer t.Stop()
select { select {
case <-t.C: case <-t.C:
@@ -204,29 +232,35 @@ func plan(a args, u updater) state {
u(func(snapper *Snapper) { u(func(snapper *Snapper) {
snapper.lastInvocation = time.Now() snapper.lastInvocation = time.Now()
}) })
fss, err := listFSes(a.fsf) fss, err := listFSes(a.ctx, a.fsf)
if err != nil { if err != nil {
return onErr(err, u) return onErr(err, u)
} }
plan := make(map[*zfs.DatasetPath]snapProgress, len(fss)) plan := make(map[*zfs.DatasetPath]*snapProgress, len(fss))
for _, fs := range fss { for _, fs := range fss {
plan[fs] = snapProgress{state: SnapPending} plan[fs] = &snapProgress{state: SnapPending}
} }
return u(func(s *Snapper) { return u(func(s *Snapper) {
s.state = Snapshotting s.state = Snapshotting
s.plan = plan s.plan = plan
s.err = nil
}).sf() }).sf()
} }
func snapshot(a args, u updater) state { func snapshot(a args, u updater) state {
var plan map[*zfs.DatasetPath]snapProgress var plan map[*zfs.DatasetPath]*snapProgress
u(func(snapper *Snapper) { u(func(snapper *Snapper) {
plan = snapper.plan plan = snapper.plan
}) })
hadErr := false hookMatchCount := make(map[hooks.Hook]int, len(*a.hooks))
for _, h := range *a.hooks {
hookMatchCount[h] = 0
}
anyFsHadErr := false
// TODO channel programs -> allow a little jitter? // TODO channel programs -> allow a little jitter?
for fs, progress := range plan { for fs, progress := range plan {
suffix := time.Now().In(time.UTC).Format("20060102_150405_000") suffix := time.Now().In(time.UTC).Format("20060102_150405_000")
@@ -236,27 +270,71 @@ func snapshot(a args, u updater) state {
WithField("fs", fs.ToString()). WithField("fs", fs.ToString()).
WithField("snap", snapname) WithField("snap", snapname)
hookEnvExtra := hooks.Env{
hooks.EnvFS: fs.ToString(),
hooks.EnvSnapshot: snapname,
}
jobCallback := hooks.NewCallbackHookForFilesystem("snapshot", fs, func(_ context.Context) (err error) {
l.Debug("create snapshot")
err = zfs.ZFSSnapshot(a.ctx, fs, snapname, false) // TODO propagate context to ZFSSnapshot
if err != nil {
l.WithError(err).Error("cannot create snapshot")
}
return
})
fsHadErr := false
var planReport hooks.PlanReport
var plan *hooks.Plan
{
filteredHooks, err := a.hooks.CopyFilteredForFilesystem(fs)
if err != nil {
l.WithError(err).Error("unexpected filter error")
fsHadErr = true
goto updateFSState
}
// account for running hooks
for _, h := range filteredHooks {
hookMatchCount[h] = hookMatchCount[h] + 1
}
var planErr error
plan, planErr = hooks.NewPlan(&filteredHooks, hooks.PhaseSnapshot, jobCallback, hookEnvExtra)
if planErr != nil {
fsHadErr = true
l.WithError(planErr).Error("cannot create job hook plan")
goto updateFSState
}
}
u(func(snapper *Snapper) { u(func(snapper *Snapper) {
progress.name = snapname progress.name = snapname
progress.startAt = time.Now() progress.startAt = time.Now()
progress.hookPlan = plan
progress.state = SnapStarted progress.state = SnapStarted
}) })
{
l.Debug("create snapshot") l := hooks.GetLogger(a.ctx).WithField("fs", fs.ToString()).WithField("snap", snapname)
err := zfs.ZFSSnapshot(fs, snapname, false) l.WithField("report", plan.Report().String()).Debug("begin run job plan")
if err != nil { plan.Run(hooks.WithLogger(a.ctx, l), a.dryRun)
hadErr = true planReport = plan.Report()
l.WithError(err).Error("cannot create snapshot") fsHadErr = planReport.HadError() // not just fatal errors
} if fsHadErr {
doneAt := time.Now() l.WithField("report", planReport.String()).Error("end run job plan with error")
} else {
u(func(snapper *Snapper) { l.WithField("report", planReport.String()).Info("end run job plan successful")
progress.doneAt = doneAt
progress.state = SnapDone
if err != nil {
progress.state = SnapError
progress.err = err
} }
}
updateFSState:
anyFsHadErr = anyFsHadErr || fsHadErr
u(func(snapper *Snapper) {
progress.doneAt = time.Now()
progress.state = SnapDone
if fsHadErr {
progress.state = SnapError
}
progress.runResults = planReport
}) })
} }
@@ -268,12 +346,26 @@ func snapshot(a args, u updater) state {
} }
} }
for h, mc := range hookMatchCount {
if mc == 0 {
hookIdx := -1
for idx, ah := range *a.hooks {
if ah == h {
hookIdx = idx
break
}
}
a.log.WithField("hook", h.String()).WithField("hook_number", hookIdx+1).Warn("hook did not match any snapshotted filesystems")
}
}
return u(func(snapper *Snapper) { return u(func(snapper *Snapper) {
if hadErr { if anyFsHadErr {
snapper.state = ErrorWait snapper.state = ErrorWait
snapper.err = errors.New("one or more snapshots could not be created, check logs for details") snapper.err = errors.New("one or more snapshots could not be created, check logs for details")
} else { } else {
snapper.state = Waiting snapper.state = Waiting
snapper.err = nil
} }
}).sf() }).sf()
} }
@@ -284,9 +376,15 @@ func wait(a args, u updater) state {
lastTick := snapper.lastInvocation lastTick := snapper.lastInvocation
snapper.sleepUntil = lastTick.Add(a.interval) snapper.sleepUntil = lastTick.Add(a.interval)
sleepUntil = snapper.sleepUntil sleepUntil = snapper.sleepUntil
log := a.log.WithField("sleep_until", sleepUntil).WithField("duration", a.interval)
logFunc := log.Debug
if snapper.state == ErrorWait || snapper.state == SyncUpErrWait {
logFunc = log.Error
}
logFunc("enter wait-state after error")
}) })
t := time.NewTimer(sleepUntil.Sub(time.Now())) t := time.NewTimer(time.Until(sleepUntil))
defer t.Stop() defer t.Stop()
select { select {
@@ -299,13 +397,23 @@ func wait(a args, u updater) state {
} }
} }
func listFSes(mf *filters.DatasetMapFilter) (fss []*zfs.DatasetPath, err error) { func listFSes(ctx context.Context, mf *filters.DatasetMapFilter) (fss []*zfs.DatasetPath, err error) {
return zfs.ZFSListMapping(mf) return zfs.ZFSListMapping(ctx, mf)
} }
var syncUpWarnNoSnapshotUntilSyncupMinDuration = envconst.Duration("ZREPL_SNAPPER_SYNCUP_WARN_MIN_DURATION", 1*time.Second)
// see docs/snapshotting.rst
func findSyncPoint(log Logger, fss []*zfs.DatasetPath, prefix string, interval time.Duration) (syncPoint time.Time, err error) { func findSyncPoint(log Logger, fss []*zfs.DatasetPath, prefix string, interval time.Duration) (syncPoint time.Time, err error) {
const (
prioHasVersions int = iota
prioNoVersions
)
type snapTime struct { type snapTime struct {
ds *zfs.DatasetPath ds *zfs.DatasetPath
prio int // lower is higher
time time.Time time time.Time
} }
@@ -314,56 +422,92 @@ func findSyncPoint(log Logger, fss []*zfs.DatasetPath, prefix string, interval t
} }
snaptimes := make([]snapTime, 0, len(fss)) snaptimes := make([]snapTime, 0, len(fss))
hardErrs := 0
now := time.Now() now := time.Now()
log.Debug("examine filesystem state") log.Debug("examine filesystem state to find sync point")
for _, d := range fss { for _, d := range fss {
l := log.WithField("fs", d.ToString()) l := log.WithField("fs", d.ToString())
syncPoint, err := findSyncPointFSNextOptimalSnapshotTime(l, now, interval, prefix, d)
fsvs, err := zfs.ZFSListFilesystemVersions(d, filters.NewTypedPrefixFilter(prefix, zfs.Snapshot)) if err == findSyncPointFSNoFilesystemVersionsErr {
if err != nil { snaptimes = append(snaptimes, snapTime{
l.WithError(err).Error("cannot list filesystem versions") ds: d,
continue prio: prioNoVersions,
} time: now,
if len(fsvs) <= 0 { })
l.WithField("prefix", prefix).Debug("no filesystem versions with prefix") } else if err != nil {
continue hardErrs++
l.WithError(err).Error("cannot determine optimal sync point for this filesystem")
} else {
l.WithField("syncPoint", syncPoint).Debug("found optimal sync point for this filesystem")
snaptimes = append(snaptimes, snapTime{
ds: d,
prio: prioHasVersions,
time: syncPoint,
})
} }
}
// Sort versions by creation if hardErrs == len(fss) {
sort.SliceStable(fsvs, func(i, j int) bool { return time.Time{}, fmt.Errorf("hard errors in determining sync point for every matching filesystem")
return fsvs[i].CreateTXG < fsvs[j].CreateTXG
})
latest := fsvs[len(fsvs)-1]
l.WithField("creation", latest.Creation).
Debug("found latest snapshot")
since := now.Sub(latest.Creation)
if since < 0 {
l.WithField("snapshot", latest.Name).
WithField("creation", latest.Creation).
Error("snapshot is from the future")
continue
}
next := now
if since < interval {
next = latest.Creation.Add(interval)
}
snaptimes = append(snaptimes, snapTime{d, next})
} }
if len(snaptimes) == 0 { if len(snaptimes) == 0 {
snaptimes = append(snaptimes, snapTime{nil, now}) panic("implementation error: loop must either inc hardErrs or add result to snaptimes")
} }
// sort ascending by (prio,time)
// => those filesystems with versions win over those without any
sort.Slice(snaptimes, func(i, j int) bool { sort.Slice(snaptimes, func(i, j int) bool {
return snaptimes[i].time.Before(snaptimes[j].time) if snaptimes[i].prio == snaptimes[j].prio {
return snaptimes[i].time.Before(snaptimes[j].time)
}
return snaptimes[i].prio < snaptimes[j].prio
}) })
winnerSyncPoint := snaptimes[0].time
l := log.WithField("syncPoint", winnerSyncPoint.String())
l.Info("determined sync point")
if winnerSyncPoint.Sub(now) > syncUpWarnNoSnapshotUntilSyncupMinDuration {
for _, st := range snaptimes {
if st.prio == prioNoVersions {
l.WithField("fs", st.ds.ToString()).Warn("filesystem will not be snapshotted until sync point")
}
}
}
return snaptimes[0].time, nil return snaptimes[0].time, nil
} }
var findSyncPointFSNoFilesystemVersionsErr = fmt.Errorf("no filesystem versions")
func findSyncPointFSNextOptimalSnapshotTime(l Logger, now time.Time, interval time.Duration, prefix string, d *zfs.DatasetPath) (time.Time, error) {
fsvs, err := zfs.ZFSListFilesystemVersions(context.TODO(), d, zfs.ListFilesystemVersionsOptions{
Types: zfs.Snapshots,
ShortnamePrefix: prefix,
})
if err != nil {
return time.Time{}, errors.Wrap(err, "list filesystem versions")
}
if len(fsvs) <= 0 {
return time.Time{}, findSyncPointFSNoFilesystemVersionsErr
}
// Sort versions by creation
sort.SliceStable(fsvs, func(i, j int) bool {
return fsvs[i].CreateTXG < fsvs[j].CreateTXG
})
latest := fsvs[len(fsvs)-1]
l.WithField("creation", latest.Creation).Debug("found latest snapshot")
since := now.Sub(latest.Creation)
if since < 0 {
return time.Time{}, fmt.Errorf("snapshot %q is from the future: creation=%q now=%q", latest.ToAbsPath(d), latest.Creation, now)
}
return latest.Creation.Add(interval), nil
}
+10 -1
View File
@@ -3,6 +3,7 @@ package snapper
import ( import (
"context" "context"
"fmt" "fmt"
"github.com/zrepl/zrepl/config" "github.com/zrepl/zrepl/config"
"github.com/zrepl/zrepl/daemon/filters" "github.com/zrepl/zrepl/daemon/filters"
) )
@@ -17,12 +18,20 @@ type PeriodicOrManual struct {
s *Snapper s *Snapper
} }
func (s *PeriodicOrManual) Run(ctx context.Context, wakeUpCommon chan <- struct{}) { func (s *PeriodicOrManual) Run(ctx context.Context, wakeUpCommon chan<- struct{}) {
if s.s != nil { if s.s != nil {
s.s.Run(ctx, wakeUpCommon) s.s.Run(ctx, wakeUpCommon)
} }
} }
// Returns nil if manual
func (s *PeriodicOrManual) Report() *Report {
if s.s != nil {
return s.s.Report()
}
return nil
}
func FromConfig(g *config.Global, fsf *filters.DatasetMapFilter, in config.SnapshottingEnum) (*PeriodicOrManual, error) { func FromConfig(g *config.Global, fsf *filters.DatasetMapFilter, in config.SnapshottingEnum) (*PeriodicOrManual, error) {
switch v := in.Ret.(type) { switch v := in.Ret.(type) {
case *config.SnapshottingPeriodic: case *config.SnapshottingPeriodic:
+118
View File
@@ -0,0 +1,118 @@
package snapper
import (
"fmt"
"sort"
"strings"
"time"
"github.com/zrepl/zrepl/daemon/hooks"
)
type Report struct {
State State
// valid in state SyncUp and Waiting
SleepUntil time.Time
// valid in state Err
Error string
// valid in state Snapshotting
Progress []*ReportFilesystem
}
type ReportFilesystem struct {
Path string
State SnapState
// Valid in SnapStarted and later
SnapName string
StartAt time.Time
Hooks string
HooksHadError bool
// Valid in SnapDone | SnapError
DoneAt time.Time
}
func errOrEmptyString(e error) string {
if e != nil {
return e.Error()
}
return ""
}
func (s *Snapper) Report() *Report {
s.mtx.Lock()
defer s.mtx.Unlock()
pReps := make([]*ReportFilesystem, 0, len(s.plan))
for fs, p := range s.plan {
var hooksStr string
var hooksHadError bool
if p.hookPlan != nil {
hr := p.hookPlan.Report()
// FIXME: technically this belongs into client
// but we can't serialize hooks.Step ATM
rightPad := func(str string, length int, pad string) string {
if len(str) > length {
return str[:length]
}
return str + strings.Repeat(pad, length-len(str))
}
hooksHadError = hr.HadError()
rows := make([][]string, len(hr))
const numCols = 4
lens := make([]int, numCols)
for i, e := range hr {
rows[i] = make([]string, numCols)
rows[i][0] = fmt.Sprintf("%d", i+1)
rows[i][1] = e.Status.String()
runTime := "..."
if e.Status != hooks.StepPending {
runTime = e.End.Sub(e.Begin).Round(time.Millisecond).String()
}
rows[i][2] = runTime
rows[i][3] = ""
if e.Report != nil {
rows[i][3] = e.Report.String()
}
for j, col := range lens {
if len(rows[i][j]) > col {
lens[j] = len(rows[i][j])
}
}
}
rowsFlat := make([]string, len(hr))
for i, r := range rows {
colsPadded := make([]string, len(r))
for j, c := range r[:len(r)-1] {
colsPadded[j] = rightPad(c, lens[j], " ")
}
colsPadded[len(r)-1] = r[len(r)-1]
rowsFlat[i] = strings.Join(colsPadded, " ")
}
hooksStr = strings.Join(rowsFlat, "\n")
}
pReps = append(pReps, &ReportFilesystem{
Path: fs.ToString(),
State: p.state,
SnapName: p.name,
StartAt: p.startAt,
DoneAt: p.doneAt,
Hooks: hooksStr,
HooksHadError: hooksHadError,
})
}
sort.Slice(pReps, func(i, j int) bool {
return strings.Compare(pReps[i].Path, pReps[j].Path) == -1
})
r := &Report{
State: s.state,
SleepUntil: s.sleepUntil,
Error: errOrEmptyString(s.err),
Progress: pReps,
}
return r
}
+10
View File
@@ -4,6 +4,16 @@ package snapper
import "strconv" import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[SnapPending-1]
_ = x[SnapStarted-2]
_ = x[SnapDone-4]
_ = x[SnapError-8]
}
const ( const (
_SnapState_name_0 = "SnapPendingSnapStarted" _SnapState_name_0 = "SnapPendingSnapStarted"
_SnapState_name_1 = "SnapDone" _SnapState_name_1 = "SnapDone"
+22 -6
View File
@@ -4,16 +4,30 @@ package snapper
import "strconv" import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[SyncUp-1]
_ = x[SyncUpErrWait-2]
_ = x[Planning-4]
_ = x[Snapshotting-8]
_ = x[Waiting-16]
_ = x[ErrorWait-32]
_ = x[Stopped-64]
}
const ( const (
_State_name_0 = "SyncUpPlanning" _State_name_0 = "SyncUpSyncUpErrWait"
_State_name_1 = "Snapshotting" _State_name_1 = "Planning"
_State_name_2 = "Waiting" _State_name_2 = "Snapshotting"
_State_name_3 = "ErrorWait" _State_name_3 = "Waiting"
_State_name_4 = "Stopped" _State_name_4 = "ErrorWait"
_State_name_5 = "Stopped"
) )
var ( var (
_State_index_0 = [...]uint8{0, 6, 14} _State_index_0 = [...]uint8{0, 6, 19}
) )
func (i State) String() string { func (i State) String() string {
@@ -29,6 +43,8 @@ func (i State) String() string {
return _State_name_3 return _State_name_3
case i == 32: case i == 32:
return _State_name_4 return _State_name_4
case i == 64:
return _State_name_5
default: default:
return "State(" + strconv.FormatInt(int64(i), 10) + ")" return "State(" + strconv.FormatInt(int64(i), 10) + ")"
} }
-25
View File
@@ -1,25 +0,0 @@
package streamrpcconfig
import (
"github.com/problame/go-streamrpc"
"github.com/zrepl/zrepl/config"
)
func FromDaemonConfig(g *config.Global, in *config.RPCConfig) (*streamrpc.ConnConfig, error) {
conf := in
if conf == nil {
conf = g.RPC
}
srpcConf := &streamrpc.ConnConfig{
RxHeaderMaxLen: conf.RxHeaderMaxLen,
RxStructuredMaxLen: conf.RxStructuredMaxLen,
RxStreamMaxChunkSize: conf.RxStreamChunkMaxLen,
TxChunkSize: conf.TxChunkSize,
Timeout: conf.Timeout,
SendHeartbeatInterval: conf.SendHeartbeatInterval,
}
if err := srpcConf.Validate(); err != nil {
return nil, err
}
return srpcConf, nil
}
@@ -1,30 +0,0 @@
package connecter
import (
"context"
"fmt"
"github.com/zrepl/zrepl/config"
"github.com/zrepl/zrepl/daemon/transport/serve"
"net"
)
type LocalConnecter struct {
listenerName string
clientIdentity string
}
func LocalConnecterFromConfig(in *config.LocalConnect) (*LocalConnecter, error) {
if in.ClientIdentity == "" {
return nil, fmt.Errorf("ClientIdentity must not be empty")
}
if in.ListenerName == "" {
return nil, fmt.Errorf("ListenerName must not be empty")
}
return &LocalConnecter{listenerName: in.ListenerName, clientIdentity: in.ClientIdentity}, nil
}
func (c *LocalConnecter) Connect(dialCtx context.Context) (conn net.Conn, err error) {
l := serve.GetLocalListener(c.listenerName)
return l.Connect(dialCtx, c.clientIdentity)
}
-84
View File
@@ -1,84 +0,0 @@
package connecter
import (
"context"
"fmt"
"github.com/problame/go-streamrpc"
"github.com/zrepl/zrepl/config"
"github.com/zrepl/zrepl/daemon/streamrpcconfig"
"github.com/zrepl/zrepl/daemon/transport"
"net"
"time"
)
type HandshakeConnecter struct {
connecter streamrpc.Connecter
}
func (c HandshakeConnecter) Connect(ctx context.Context) (net.Conn, error) {
conn, err := c.connecter.Connect(ctx)
if err != nil {
return nil, err
}
dl, ok := ctx.Deadline()
if !ok {
dl = time.Now().Add(10 * time.Second) // FIXME constant
}
if err := transport.DoHandshakeCurrentVersion(conn, dl); err != nil {
conn.Close()
return nil, err
}
return conn, nil
}
func FromConfig(g *config.Global, in config.ConnectEnum) (*ClientFactory, error) {
var (
connecter streamrpc.Connecter
errConnecter, errRPC error
connConf *streamrpc.ConnConfig
)
switch v := in.Ret.(type) {
case *config.SSHStdinserverConnect:
connecter, errConnecter = SSHStdinserverConnecterFromConfig(v)
connConf, errRPC = streamrpcconfig.FromDaemonConfig(g, v.RPC)
case *config.TCPConnect:
connecter, errConnecter = TCPConnecterFromConfig(v)
connConf, errRPC = streamrpcconfig.FromDaemonConfig(g, v.RPC)
case *config.TLSConnect:
connecter, errConnecter = TLSConnecterFromConfig(v)
connConf, errRPC = streamrpcconfig.FromDaemonConfig(g, v.RPC)
case *config.LocalConnect:
connecter, errConnecter = LocalConnecterFromConfig(v)
connConf, errRPC = streamrpcconfig.FromDaemonConfig(g, v.RPC)
default:
panic(fmt.Sprintf("implementation error: unknown connecter type %T", v))
}
if errConnecter != nil {
return nil, errConnecter
}
if errRPC != nil {
return nil, errRPC
}
config := streamrpc.ClientConfig{ConnConfig: connConf}
if err := config.Validate(); err != nil {
return nil, err
}
connecter = HandshakeConnecter{connecter}
return &ClientFactory{connecter: connecter, config: &config}, nil
}
type ClientFactory struct {
connecter streamrpc.Connecter
config *streamrpc.ClientConfig
}
func (f ClientFactory) NewClient() (*streamrpc.Client, error) {
return streamrpc.NewClient(f.connecter, f.config)
}
-136
View File
@@ -1,136 +0,0 @@
package transport
import (
"bytes"
"fmt"
"io"
"net"
"strings"
"time"
"unicode/utf8"
)
type HandshakeMessage struct {
ProtocolVersion int
Extensions []string
}
func (m *HandshakeMessage) Encode() ([]byte, error) {
if m.ProtocolVersion <= 0 || m.ProtocolVersion > 9999 {
return nil, fmt.Errorf("protocol version must be in [1, 9999]")
}
if len(m.Extensions) >= 9999 {
return nil, fmt.Errorf("protocol only supports [0, 9999] extensions")
}
// EXTENSIONS is a count of subsequent \n separated lines that contain protocol extensions
var extensions strings.Builder
for i, ext := range m.Extensions {
if strings.ContainsAny(ext, "\n") {
return nil, fmt.Errorf("Extension #%d contains forbidden newline character", i)
}
if !utf8.ValidString(ext) {
return nil, fmt.Errorf("Extension #%d is not valid UTF-8", i)
}
extensions.WriteString(ext)
extensions.WriteString("\n")
}
withoutLen := fmt.Sprintf("ZREPL_ZFS_REPLICATION PROTOVERSION=%04d EXTENSIONS=%04d\n%s",
m.ProtocolVersion, len(m.Extensions), extensions.String())
withLen := fmt.Sprintf("%010d %s", len(withoutLen), withoutLen)
return []byte(withLen), nil
}
func (m *HandshakeMessage) DecodeReader(r io.Reader, maxLen int) error {
var lenAndSpace [11]byte
if _, err := io.ReadFull(r, lenAndSpace[:]); err != nil {
return err
}
if !utf8.Valid(lenAndSpace[:]) {
return fmt.Errorf("invalid start of handshake message: not valid UTF-8")
}
var followLen int
n, err := fmt.Sscanf(string(lenAndSpace[:]), "%010d ", &followLen)
if n != 1 || err != nil {
return fmt.Errorf("could not parse handshake message length")
}
if followLen > maxLen {
return fmt.Errorf("handshake message length exceeds max length (%d vs %d)",
followLen, maxLen)
}
var buf bytes.Buffer
_, err = io.Copy(&buf, io.LimitReader(r, int64(followLen)))
if err != nil {
return err
}
var (
protoVersion, extensionCount int
)
n, err = fmt.Fscanf(&buf, "ZREPL_ZFS_REPLICATION PROTOVERSION=%04d EXTENSIONS=%4d\n",
&protoVersion, &extensionCount)
if n != 2 || err != nil {
return fmt.Errorf("could not parse handshake message: %s", err)
}
if protoVersion < 1 {
return fmt.Errorf("invalid protocol version %q", protoVersion)
}
m.ProtocolVersion = protoVersion
if extensionCount < 0 {
return fmt.Errorf("invalid extension count %q", extensionCount)
}
if extensionCount == 0 {
if buf.Len() != 0 {
return fmt.Errorf("unexpected data trailing after header")
}
m.Extensions = nil
return nil
}
s := buf.String()
if strings.Count(s, "\n") != extensionCount {
return fmt.Errorf("inconsistent extension count: found %d, header says %d", len(m.Extensions), extensionCount)
}
exts := strings.Split(s, "\n")
if exts[len(exts)-1] != "" {
return fmt.Errorf("unexpected data trailing after last extension newline")
}
m.Extensions = exts[0:len(exts)-1]
return nil
}
func DoHandshakeCurrentVersion(conn net.Conn, deadline time.Time) error {
// current protocol version is hardcoded here
return DoHandshakeVersion(conn, deadline, 1)
}
func DoHandshakeVersion(conn net.Conn, deadline time.Time, version int) error {
ours := HandshakeMessage{
ProtocolVersion: version,
Extensions: nil,
}
hsb, err := ours.Encode()
if err != nil {
return fmt.Errorf("could not encode protocol banner: %s", err)
}
conn.SetDeadline(deadline)
_, err = io.Copy(conn, bytes.NewBuffer(hsb))
if err != nil {
return fmt.Errorf("could not send protocol banner: %s", err)
}
theirs := HandshakeMessage{}
if err := theirs.DecodeReader(conn, 16 * 4096); err != nil { // FIXME constant
return fmt.Errorf("could not decode protocol banner: %s", err)
}
if theirs.ProtocolVersion != ours.ProtocolVersion {
return fmt.Errorf("protocol versions do not match: ours is %d, theirs is %d",
ours.ProtocolVersion, theirs.ProtocolVersion)
}
// ignore extensions, we don't use them
return nil
}
-147
View File
@@ -1,147 +0,0 @@
package serve
import (
"github.com/pkg/errors"
"github.com/zrepl/zrepl/config"
"github.com/zrepl/zrepl/daemon/transport"
"net"
"github.com/zrepl/zrepl/daemon/streamrpcconfig"
"github.com/problame/go-streamrpc"
"context"
"github.com/zrepl/zrepl/logger"
"github.com/zrepl/zrepl/zfs"
"time"
)
type contextKey int
const contextKeyLog contextKey = 0
type Logger = logger.Logger
func WithLogger(ctx context.Context, log Logger) context.Context {
return context.WithValue(ctx, contextKeyLog, log)
}
func getLogger(ctx context.Context) Logger {
if log, ok := ctx.Value(contextKeyLog).(Logger); ok {
return log
}
return logger.NewNullLogger()
}
type AuthenticatedConn interface {
net.Conn
// ClientIdentity must be a string that satisfies ValidateClientIdentity
ClientIdentity() string
}
// A client identity must be a single component in a ZFS filesystem path
func ValidateClientIdentity(in string) (err error) {
path, err := zfs.NewDatasetPath(in)
if err != nil {
return err
}
if path.Length() != 1 {
return errors.New("client identity must be a single path comonent (not empty, no '/')")
}
return nil
}
type authConn struct {
net.Conn
clientIdentity string
}
var _ AuthenticatedConn = authConn{}
func (c authConn) ClientIdentity() string {
if err := ValidateClientIdentity(c.clientIdentity); err != nil {
panic(err)
}
return c.clientIdentity
}
// like net.Listener, but with an AuthenticatedConn instead of net.Conn
type AuthenticatedListener interface {
Addr() (net.Addr)
Accept(ctx context.Context) (AuthenticatedConn, error)
Close() error
}
type ListenerFactory interface {
Listen() (AuthenticatedListener, error)
}
type HandshakeListenerFactory struct {
lf ListenerFactory
}
func (lf HandshakeListenerFactory) Listen() (AuthenticatedListener, error) {
l, err := lf.lf.Listen()
if err != nil {
return nil, err
}
return HandshakeListener{l}, nil
}
type HandshakeListener struct {
l AuthenticatedListener
}
func (l HandshakeListener) Addr() (net.Addr) { return l.l.Addr() }
func (l HandshakeListener) Close() error { return l.l.Close() }
func (l HandshakeListener) Accept(ctx context.Context) (AuthenticatedConn, error) {
conn, err := l.l.Accept(ctx)
if err != nil {
return nil, err
}
dl, ok := ctx.Deadline()
if !ok {
dl = time.Now().Add(10*time.Second) // FIXME constant
}
if err := transport.DoHandshakeCurrentVersion(conn, dl); err != nil {
conn.Close()
return nil, err
}
return conn, nil
}
func FromConfig(g *config.Global, in config.ServeEnum) (lf ListenerFactory, conf *streamrpc.ConnConfig, _ error) {
var (
lfError, rpcErr error
)
switch v := in.Ret.(type) {
case *config.TCPServe:
lf, lfError = TCPListenerFactoryFromConfig(g, v)
conf, rpcErr = streamrpcconfig.FromDaemonConfig(g, v.RPC)
case *config.TLSServe:
lf, lfError = TLSListenerFactoryFromConfig(g, v)
conf, rpcErr = streamrpcconfig.FromDaemonConfig(g, v.RPC)
case *config.StdinserverServer:
lf, lfError = MultiStdinserverListenerFactoryFromConfig(g, v)
conf, rpcErr = streamrpcconfig.FromDaemonConfig(g, v.RPC)
case *config.LocalServe:
lf, lfError = LocalListenerFactoryFromConfig(g, v)
conf, rpcErr = streamrpcconfig.FromDaemonConfig(g, v.RPC)
default:
return nil, nil, errors.Errorf("internal error: unknown serve type %T", v)
}
if lfError != nil {
return nil, nil, lfError
}
if rpcErr != nil {
return nil, nil, rpcErr
}
lf = HandshakeListenerFactory{lf}
return lf, conf, nil
}
-83
View File
@@ -1,83 +0,0 @@
package serve
import (
"crypto/tls"
"crypto/x509"
"fmt"
"github.com/pkg/errors"
"github.com/zrepl/zrepl/config"
"github.com/zrepl/zrepl/tlsconf"
"net"
"time"
"context"
)
type TLSListenerFactory struct {
address string
clientCA *x509.CertPool
serverCert tls.Certificate
handshakeTimeout time.Duration
clientCNs map[string]struct{}
}
func TLSListenerFactoryFromConfig(c *config.Global, in *config.TLSServe) (lf *TLSListenerFactory, err error) {
lf = &TLSListenerFactory{
address: in.Listen,
handshakeTimeout: in.HandshakeTimeout,
}
if in.Ca == "" || in.Cert == "" || in.Key == "" {
return nil, errors.New("fields 'ca', 'cert' and 'key'must be specified")
}
lf.clientCA, err = tlsconf.ParseCAFile(in.Ca)
if err != nil {
return nil, errors.Wrap(err, "cannot parse ca file")
}
lf.serverCert, err = tls.LoadX509KeyPair(in.Cert, in.Key)
if err != nil {
return nil, errors.Wrap(err, "cannot parse cer/key pair")
}
lf.clientCNs = make(map[string]struct{}, len(in.ClientCNs))
for i, cn := range in.ClientCNs {
if err := ValidateClientIdentity(cn); err != nil {
return nil, errors.Wrapf(err, "unsuitable client_cn #%d %q", i, cn)
}
// dupes are ok fr now
lf.clientCNs[cn] = struct{}{}
}
return lf, nil
}
func (f *TLSListenerFactory) Listen() (AuthenticatedListener, error) {
l, err := net.Listen("tcp", f.address)
if err != nil {
return nil, err
}
tl := tlsconf.NewClientAuthListener(l, f.clientCA, f.serverCert, f.handshakeTimeout)
return tlsAuthListener{tl, f.clientCNs}, nil
}
type tlsAuthListener struct {
*tlsconf.ClientAuthListener
clientCNs map[string]struct{}
}
func (l tlsAuthListener) Accept(ctx context.Context) (AuthenticatedConn, error) {
c, cn, err := l.ClientAuthListener.Accept()
if err != nil {
return nil, err
}
if _, ok := l.clientCNs[cn]; !ok {
if err := c.Close(); err != nil {
getLogger(ctx).WithError(err).Error("error closing connection with unauthorized common name")
}
return nil, fmt.Errorf("unauthorized client common name %q from %s", cn, c.RemoteAddr())
}
return authConn{c, cn}, nil
}
+595
View File
@@ -0,0 +1,595 @@
{
"annotations": {
"list": [
{
"$$hashKey": "object:3351",
"builtIn": 1,
"datasource": "-- Grafana --",
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard"
}
]
},
"editable": true,
"gnetId": null,
"graphTooltip": 1,
"id": 7,
"iteration": 1552746418826,
"links": [],
"panels": [
{
"content": "# zrepl Prometheus Metrics\n\nzrepl exposes Prometheus metrics and ships with this Grafana dashboard.\nThe exported metrics are suitable for health checks:\n\n* The log should generally be warning & error-free\n * The `Log Messages that require attention` graph visualizes log message counts indicating problems.\n* The number of goroutines should not grow unboundedly over time.\n * During replication, the number of goroutines can be way higher than during idle time.\n * If the goroutine count grows with each replication, there is clearly a goroutine leak. Please open a bug report.\n* The sys memory consumption should not grow unboundedly over time.\n * Note that the Go runtime pre-allocates some of its heap from the OS.\n * zrepl actually uses much less memory than allocated from the OS.\n * Since Go 1.11, Go pre-allocates more aggressively.\n* Monitor that some data is replicated, although that metric does not guarantee that replication was successful.\n\n**In general, note that the exported metrics are not stable unless declared otherwise.**",
"gridPos": {
"h": 9,
"w": 24,
"x": 0,
"y": 0
},
"id": 35,
"mode": "markdown",
"title": "Panel Title",
"type": "text"
},
{
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": null,
"fill": 1,
"gridPos": {
"h": 9,
"w": 12,
"x": 0,
"y": 9
},
"id": 15,
"legend": {
"avg": false,
"current": false,
"max": false,
"min": false,
"show": true,
"total": false,
"values": false
},
"lines": true,
"linewidth": 1,
"links": [],
"nullPointMode": "null",
"percentage": false,
"pointradius": 5,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"spaceLength": 10,
"stack": true,
"steppedLine": false,
"targets": [
{
"$$hashKey": "object:3436",
"expr": "up{job='$prom_job_name'}",
"format": "time_series",
"intervalFactor": 1,
"refId": "A"
}
],
"thresholds": [],
"timeFrom": null,
"timeShift": null,
"title": "zrepl Instances Up",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"buckets": null,
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "short",
"label": null,
"logBase": 1,
"max": "5",
"min": "0",
"show": true
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
]
},
{
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": null,
"fill": 1,
"gridPos": {
"h": 9,
"w": 12,
"x": 12,
"y": 9
},
"id": 22,
"legend": {
"avg": false,
"current": false,
"max": false,
"min": false,
"show": true,
"total": false,
"values": false
},
"lines": true,
"linewidth": 1,
"links": [],
"nullPointMode": "null",
"percentage": false,
"pointradius": 5,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"spaceLength": 10,
"stack": false,
"steppedLine": false,
"targets": [
{
"expr": "increase(zrepl_daemon_log_entries{job='$prom_job_name',level=~'warn|error'}[$__interval])",
"format": "time_series",
"intervalFactor": 1,
"refId": "A"
}
],
"thresholds": [],
"timeFrom": null,
"timeShift": null,
"title": "Log Messages that require attention",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"buckets": null,
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": "0",
"show": true
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
]
},
{
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": null,
"fill": 1,
"gridPos": {
"h": 9,
"w": 12,
"x": 0,
"y": 18
},
"id": 33,
"legend": {
"avg": false,
"current": false,
"max": false,
"min": false,
"show": true,
"total": false,
"values": false
},
"lines": true,
"linewidth": 1,
"links": [],
"nullPointMode": "null",
"percentage": false,
"pointradius": 5,
"points": false,
"renderer": "flot",
"seriesOverrides": [
{
"alias": "/replicated bytes in last.*/",
"yaxis": 2
}
],
"spaceLength": 10,
"stack": false,
"steppedLine": false,
"targets": [
{
"expr": "sum(rate(zrepl_replication_bytes_replicated{job='$prom_job_name'}[$__interval])) by (zrepl_job)",
"format": "time_series",
"hide": false,
"interval": "",
"intervalFactor": 1,
"legendFormat": "replication data rate zrepl_job={{zrepl_job}}",
"refId": "A"
},
{
"expr": "sum(increase(zrepl_replication_bytes_replicated{job='$prom_job_name'}[10m])) by (zrepl_job)",
"format": "time_series",
"hide": false,
"interval": "",
"intervalFactor": 1,
"legendFormat": "replicated bytes in last 10min zrepl_job={{zrepl_job}}",
"refId": "B"
}
],
"thresholds": [],
"timeFrom": null,
"timeShift": null,
"title": "Replication Data Rate and Volume(integrates last 10min)",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"buckets": null,
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "Bps",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
},
{
"format": "bytes",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
]
},
{
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": null,
"fill": 1,
"gridPos": {
"h": 9,
"w": 12,
"x": 12,
"y": 18
},
"id": 23,
"legend": {
"avg": false,
"current": false,
"max": false,
"min": false,
"show": true,
"total": false,
"values": false
},
"lines": true,
"linewidth": 1,
"links": [],
"nullPointMode": "null",
"percentage": false,
"pointradius": 5,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"spaceLength": 10,
"stack": false,
"steppedLine": false,
"targets": [
{
"expr": "sum(increase(zrepl_daemon_log_entries{job='$prom_job_name',zrepl_job=~\"^[^_].*\"}[$__interval])) by (instance,zrepl_job)",
"format": "time_series",
"intervalFactor": 1,
"refId": "A"
}
],
"thresholds": [],
"timeFrom": null,
"timeShift": null,
"title": "Log Activity (without internal jobs)",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"buckets": null,
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": "0",
"show": true
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
]
},
{
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": null,
"fill": 1,
"gridPos": {
"h": 9,
"w": 12,
"x": 0,
"y": 27
},
"id": 17,
"legend": {
"avg": false,
"current": false,
"max": false,
"min": false,
"show": true,
"total": false,
"values": false
},
"lines": true,
"linewidth": 1,
"links": [],
"nullPointMode": "null",
"percentage": false,
"pointradius": 5,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"spaceLength": 10,
"stack": false,
"steppedLine": false,
"targets": [
{
"$$hashKey": "object:3535",
"expr": "go_memstats_sys_bytes{job='$prom_job_name'}",
"format": "time_series",
"hide": false,
"intervalFactor": 1,
"refId": "A"
}
],
"thresholds": [],
"timeFrom": null,
"timeShift": null,
"title": "Memory Allocated by the Go runtime from the OS (should not grow unboundedly)",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"buckets": null,
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "bytes",
"label": null,
"logBase": 1,
"max": null,
"min": "0",
"show": true
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
]
},
{
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": null,
"fill": 1,
"gridPos": {
"h": 9,
"w": 12,
"x": 12,
"y": 27
},
"id": 19,
"legend": {
"avg": false,
"current": false,
"max": false,
"min": false,
"show": true,
"total": false,
"values": false
},
"lines": true,
"linewidth": 1,
"links": [],
"nullPointMode": "null",
"percentage": false,
"pointradius": 5,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"spaceLength": 10,
"stack": false,
"steppedLine": false,
"targets": [
{
"expr": "go_goroutines{job='$prom_job_name'}",
"format": "time_series",
"intervalFactor": 1,
"refId": "A"
}
],
"thresholds": [],
"timeFrom": null,
"timeShift": null,
"title": "number of goroutines (should not grow unboundedly)",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"buckets": null,
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": "0",
"show": true
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
]
}
],
"refresh": "1m",
"schemaVersion": 16,
"style": "dark",
"tags": [],
"templating": {
"list": [
{
"allValue": null,
"current": {
"text": "zrepl",
"value": "zrepl"
},
"datasource": "prometheus",
"hide": 0,
"includeAll": false,
"label": "Prometheus Job Name",
"multi": false,
"name": "prom_job_name",
"options": [],
"query": "label_values(up, job)",
"refresh": 1,
"regex": "",
"sort": 1,
"tagValuesQuery": "",
"tags": [],
"tagsQuery": "",
"type": "query",
"useTags": false
}
]
},
"time": {
"from": "now-2d",
"to": "now"
},
"timepicker": {
"refresh_intervals": [
"5s",
"10s",
"30s",
"1m",
"5m",
"15m",
"30m",
"1h",
"2h",
"1d"
],
"time_options": [
"5m",
"15m",
"1h",
"6h",
"12h",
"24h",
"2d",
"7d",
"30d"
]
},
"timezone": "",
"title": "zrepl 0.1",
"uid": "xTljn4qmk",
"version": 6
}
+33
View File
@@ -0,0 +1,33 @@
[Unit]
Description=zrepl daemon
Documentation=https://zrepl.github.io
[Service]
Type=simple
ExecStartPre=/usr/local/bin/zrepl --config /etc/zrepl/zrepl.yml configcheck
ExecStart=/usr/local/bin/zrepl --config /etc/zrepl/zrepl.yml daemon
RuntimeDirectory=zrepl zrepl/stdinserver
RuntimeDirectoryMode=0700
ProtectSystem=strict
#PrivateDevices=yes # TODO ZFS needs access to /dev/zfs, could we limit this?
ProtectKernelTunables=yes
ProtectControlGroups=yes
PrivateTmp=yes
#PrivateUsers=yes # TODO Does not work, why?
ProtectKernelModules=true
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
RestrictNamespaces=true
RestrictRealtime=yes
SystemCallArchitectures=native
ProtectHome=read-only
# ProtectHome=tmpfs totally possible, not by default though because of Debian stretch
# SystemCallFilter
# ~@privileged doesn't work with Ubuntu 18.04 ssh
SystemCallFilter=~ @mount @cpu-emulation @keyring @module @obsolete @raw-io @debug @clock @resources
[Install]
WantedBy=multi-user.target
+130 -16
View File
@@ -1,8 +1,11 @@
.. include:: global.rst.inc
.. |break_config| replace:: **[CONFIG]** .. |break_config| replace:: **[CONFIG]**
.. |break| replace:: **[BREAK]** .. |break| replace:: **[BREAK]**
.. |bugfix| replace:: [BUG] .. |bugfix| replace:: [BUG]
.. |docs| replace:: [DOCS] .. |docs| replace:: [DOCS]
.. |feature| replace:: [FEATURE] .. |feature| replace:: [FEATURE]
.. |mig| replace:: **[MIGRATION]**
.. _changelog: .. _changelog:
@@ -19,10 +22,102 @@ We use the following annotations for classifying changes:
* |break| Change that breaks interoperability or persistent state representation with previous releases. * |break| Change that breaks interoperability or persistent state representation with previous releases.
As a package maintainer, make sure to warn your users about config breakage somehow. As a package maintainer, make sure to warn your users about config breakage somehow.
Note that even updating the package on both sides might not be sufficient, e.g. if persistent state needs to be migrated to a new format. Note that even updating the package on both sides might not be sufficient, e.g. if persistent state needs to be migrated to a new format.
* |mig| Migration that must be run by the user.
* |feature| Change that introduces new functionality. * |feature| Change that introduces new functionality.
* |bugfix| Change that fixes a bug, no regressions or incompatibilities expected. * |bugfix| Change that fixes a bug, no regressions or incompatibilities expected.
* |docs| Change to the documentation. * |docs| Change to the documentation.
0.3
---
This is a big one! Headlining features:
* **Resumable Send & Recv Support**
No knobs required, automatically used where supported.
* **Hold-Protected Send & Recv**
Automatic ZFS holds to ensure that we can always resume a replication step.
* **Encrypted Send & Recv Support** for OpenZFS native encryption.
:ref:`Configurable <job-send-options>` at the job level, i.e., for all filesystems a job is responsible for.
* **Receive-side hold on last received dataset**
The counterpart to the replication cursor bookmark on the send-side.
Ensures that incremental replication will always be possible between a sender and receiver.
Actual changelog:
* |break_config| **more restrictive job names than in prior zrepl versions**
Starting with this version, job names are going to be embedded into ZFS holds and bookmark names.
* |break| |mig| replication cursor representation changed
* zrepl now manages the :ref:`replication cursor bookmark <replication-cursor-and-last-received-hold>` per job-filesystem tuple instead of a single replication cursor per filesystem.
In the future, this will permit multiple sending jobs to send from the same filesystems.
* ZFS does not allow bookmark renaming, thus we cannot migrate the old replication cursors.
* zrepl 0.3 will automatically create cursors in the new format for new replications, and warn if it still finds ones in the old format.
* Run ``zrepl migrate replication-cursor:v1-v2`` to safely destroy old-format cursors.
The migration will ensure that only those old-format cursors are destroyed that have been superseeded by new-format cursors.
* |bugfix| missing logger context vars in control connection handlers
* |bugfix| improved error messages on ``zfs send`` errors
* |feature| New option ``listen_freebind`` (tcp, tls, prometheus listener)
* |bugfix| |docs| snapshotting: clarify sync-up behavior and warn about filesystems
that will not be snapshotted until the sync-up phase is over
* |docs| Document new replication features in the :ref:`config overview <overview-how-replication-works>` and :repomasterlink:`replication/design.md`.
* |feature| documented subcommand to generate ``bash`` and ``zsh`` completions
* **[MAINTAINER NOTICE]** New platform tests in this version, please make sure you run them for your distro!
* **[MAINTAINER NOTICE]** Please add the shell completions to the zrepl packages.
0.2.1
-----
* |feature| Illumos (and Solaris) compatibility and binary builds (thanks, `MNX.io <https://mnx.io>`_ )
* |feature| 32bit binaries for Linux and FreeBSD (untested, though)
* |bugfix| better error messages in ``ssh+stdinserver`` transport
* |bugfix| systemd + ``ssh+stdinserver``: automatically create ``/var/run/zrepl/stdinserver``
* |bugfix| crash if Prometheus listening socket cannot be opened
* [MAINTAINER NOTICE] ``Makefile`` refactoring, see :commit:`080f2c0`
0.2
---
* |feature| :ref:`Pre- and Post-Snapshot Hooks <job-snapshotting-hooks>`
with built-in support for MySQL and Postgres checkpointing
as well as custom scripts (thanks, `@overhacked <https://github.com/overhacked>`_!)
* |feature| Use ``zfs destroy pool/fs@snap1,snap2,...`` CLI feature if available
* |feature| Linux ARM64 Docker build support & binary builds
* |feature| ``zrepl status`` now displays snapshotting reports
* |feature| ``zrepl status --job <JOBNAME>`` filter flag
* |bugfix| i386 build
* |bugfix| early validation of host:port tuples in config
* |bugfix| ``zrepl status`` now supports ``TERM=screen`` (tmux on FreeBSD / FreeNAS)
* |bugfix| ignore *connection reset by peer* errors when shutting down connections
* |bugfix| correct error messages when receive-side pool or ``root_fs`` dataset is not imported
* |bugfix| fail fast for misconfigured local transport
* |bugfix| race condition in replication report generation would crash the daemon when running ``zrepl status``
* |bugfix| rpc goroutine leak in ``push`` mode if zfs recv fails on the ``sink`` side
* [MAINTAINER NOTICE] Go modules for dependency management both inside and outside of GOPATH
(``lazy.sh`` and ``Makefile`` force ``GO111MODULE=on``)
* [MAINTAINER NOTICE] ``make platformtest`` target to check zrepl's ZFS abstractions (screen scraping, etc.).
These tests only work on a system with ZFS installed, and must be run as root because they create a file-backed pool for each test case.
The pool name ``zreplplatformtest`` is reserved for this use case.
Only run ``make platformtest`` on test systems, e.g. a FreeBSD VM image.
.. NOTE::
| zrepl is a spare-time project primarily developed by `Christian Schwarz <https://cschwarz.com>`_.
| You can support maintenance and feature development through one of the following services:
| |Donate via Patreon| |Donate via Liberapay| |Donate via PayPal|
| Note that PayPal processing fees are relatively high for small donations.
| For SEPA wire transfer and **commercial support**, please `contact Christian directly <https://cschwarz.com>`_.
0.1.1
-----
* |bugfix| :issue:`162` :commit:`d6304f4` : fix I/O timeout errors on variable receive rate
* A significant reduction or sudden stall of the receive rate (e.g. recv pool has other I/O to do)
would cause a ``writev I/O timeout`` error after approximately ten seconds.
0.1 0.1
--- ---
@@ -40,6 +135,7 @@ It breaks both configuration and transport format, and thus requires manual inte
Notes to Package Maintainers Notes to Package Maintainers
~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* Notify users about config changes and migrations (see changes attributed with |break| and |mig| below)
* If the daemon crashes, the stack trace produced by the Go runtime and possibly diagnostic output of zrepl will be written to stderr. * If the daemon crashes, the stack trace produced by the Go runtime and possibly diagnostic output of zrepl will be written to stderr.
This behavior is independent from the ``stdout`` outlet type. This behavior is independent from the ``stdout`` outlet type.
Please make sure the stderr output of the daemon is captured somewhere. Please make sure the stderr output of the daemon is captured somewhere.
@@ -48,26 +144,43 @@ Notes to Package Maintainers
This functionality will cause SIGABRT on panics and can be used to capture a coredump of the panicking process. This functionality will cause SIGABRT on panics and can be used to capture a coredump of the panicking process.
To that extend, make sure that your package build system, your OS's coredump collection and the Go delve debugger work together. To that extend, make sure that your package build system, your OS's coredump collection and the Go delve debugger work together.
Use your build system to package the Go program in `this tutorial on Go coredumps and the delve debugger <https://rakyll.org/coredumps/>`_ , and make sure the symbol resolution etc. work on coredumps captured from the binary produced by your build system. (Special focus on symbol stripping, etc.) Use your build system to package the Go program in `this tutorial on Go coredumps and the delve debugger <https://rakyll.org/coredumps/>`_ , and make sure the symbol resolution etc. work on coredumps captured from the binary produced by your build system. (Special focus on symbol stripping, etc.)
* Use of ``ssh+stdinserver`` :ref:`transport <transport-ssh+stdinserver>` is no longer encouraged. * Consider using the ``zrepl configcheck`` subcommand in startup scripts to abort a restart that would fail due to an invalid config.
Please encourage users to use the new ``tcp`` or ``tls`` transports.
You might as well mention some of the :ref:`tunneling options listed here <transport-tcp-tunneling>`.
Changes Changes
~~~~~~~ ~~~~~~~
* |break| |mig| Placeholder property representation changed
* The :ref:`placeholder property <replication-placeholder-property>` now uses ``on|off`` as values
instead of hashes of the dataset path. This permits renames of the sink filesystem without
updating all placeholder properties.
* Relevant for 0.0.X-0.1-rc* to 0.1 migrations
* Make sure your config is valid with ``zrepl configcheck``
* Run ``zrepl migrate 0.0.X:0.1:placeholder``
* |feature| :issue:`55` : Push replication (see :ref:`push job <job-push>` and :ref:`sink job <job-sink>`) * |feature| :issue:`55` : Push replication (see :ref:`push job <job-push>` and :ref:`sink job <job-sink>`)
* |feature| :ref:`TCP Transport <transport-tcp>` * |feature| :ref:`TCP Transport <transport-tcp>`
* |feature| :ref:`TCP + TLS client authentication transport <transport-tcp+tlsclientauth>` * |feature| :ref:`TCP + TLS client authentication transport <transport-tcp+tlsclientauth>`
* |feature| :issue:`78` :commit:`074f989` : Replication protocol rewrite * |feature| :issue:`111`: RPC protocol rewrite
* Uses ``github.com/problame/go-streamrpc`` for RPC layer * |break| Protocol breakage; Update and restart of all zrepl daemons is required.
* |break| Protocol breakage, update and restart of all zrepl daemons is required * Use `gRPC <https://grpc.io/>`_ for control RPCs and a custom protocol for bulk data transfer.
* |feature| :issue:`83`: Improved error handling of network-level errors (zrepl retries instead of failing the entire job) * Automatic retries for network-temporary errors
* |bugfix| :issue:`75` :issue:`81`: use connection timeouts and protocol-level heartbeats
* |break| |break_config|: mappings are no longer supported
* Receiving sides (``pull`` and ``sink`` job) specify a single ``root_fs``. * Limited to errors during replication for this release.
Received filesystems are then stored *per client* in ``${root_fs}/${client_identity}``. Addresses the common problem of ISP-forced reconnection at night, but will become
way more useful with resumable send & recv support.
Pruning errors are handled per FS, i.e., a prune RPC is attempted at least once per FS.
* |feature| Proper timeout handling for the :ref:`SSH transport <transport-ssh+stdinserver>`
* |break| Requires Go 1.11 or later.
* |break| |break_config|: mappings are no longer supported
* Receiving sides (``pull`` and ``sink`` job) specify a single ``root_fs``.
Received filesystems are then stored *per client* in ``${root_fs}/${client_identity}``.
See :ref:`job-overview` for details.
* |feature| |break| |break_config| Manual snapshotting + triggering of replication * |feature| |break| |break_config| Manual snapshotting + triggering of replication
@@ -92,12 +205,18 @@ Changes
* |break_config| ``keep_bookmarks`` parameter of the ``grid`` keep rule has been removed * |break_config| ``keep_bookmarks`` parameter of the ``grid`` keep rule has been removed
* |feature| ``zrepl status`` for live-updating replication progress (it's really cool!) * |feature| ``zrepl status`` for live-updating replication progress (it's really cool!)
* |feature| :ref:`Snapshot- & pruning-only job type <job-snap>` (for local snapshot management)
* |feature| :issue:`67`: Expose `Prometheus <https://prometheus.io>`_ metrics via HTTP (:ref:`config docs <monitoring-prometheus>`) * |feature| :issue:`67`: Expose `Prometheus <https://prometheus.io>`_ metrics via HTTP (:ref:`config docs <monitoring-prometheus>`)
* Compatible Grafana dashboard shipping in ``dist/grafana``
* |break_config| Logging outlet types must be specified using the ``type`` instead of ``outlet`` key * |break_config| Logging outlet types must be specified using the ``type`` instead of ``outlet`` key
* |break| :issue:`53`: CLI: ``zrepl control *`` subcommands have been made direct subcommands of ``zrepl *`` * |break| :issue:`53`: CLI: ``zrepl control *`` subcommands have been made direct subcommands of ``zrepl *``
* |bugfix| Goroutine leak on ssh transport connection timeouts * |bugfix| Goroutine leak on ssh transport connection timeouts
* |bugfix| :issue:`81` :issue:`77` : handle failed accepts correctly (``source`` job) * |bugfix| :issue:`81` :issue:`77` : handle failed accepts correctly (``source`` job)
* |bugfix| :issue:`100`: fix incompatibility with ZoL 0.8
* |feature| :issue:`115`: logging: configurable syslog facility
* |feature| Systemd unit file in ``dist/systemd``
.. |lastrelease| replace:: 0.0.3 .. |lastrelease| replace:: 0.0.3
@@ -107,8 +226,3 @@ Previous Releases
.. NOTE:: .. NOTE::
Due to limitations in our documentation system, we only show the changelog since the last release and the time this documentation is built. Due to limitations in our documentation system, we only show the changelog since the last release and the time this documentation is built.
For the changelog of previous releases, use the version selection in the hosted version of these docs at `zrepl.github.io <https://zrepl.github.io>`_. For the changelog of previous releases, use the version selection in the hosted version of these docs at `zrepl.github.io <https://zrepl.github.io>`_.
W
+3 -1
View File
@@ -7,10 +7,12 @@ Configuration
.. toctree:: .. toctree::
configuration/preface configuration/overview
configuration/jobs configuration/jobs
configuration/transports configuration/transports
configuration/filter_syntax configuration/filter_syntax
configuration/sendrecvoptions
configuration/snapshotting
configuration/prune configuration/prune
configuration/logging configuration/logging
configuration/monitoring configuration/monitoring

Some files were not shown because too many files have changed in this diff Show More