Compare commits

..

11 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
41 changed files with 696 additions and 1163 deletions
+1 -5
View File
@@ -5,11 +5,7 @@ jobs:
type: tcp type: tcp
listen: "0.0.0.0:8888" listen: "0.0.0.0:8888"
clients: { clients: {
"192.168.122.123" : "mysql01", "192.168.122.123" : "client1"
"192.168.122.42" : "mx01",
"2001:0db8:85a3::8a2e:0370:7334": "gateway",
"10.23.42.0/24": "cluster-*",
"fde4:8dba:82e1::/64": "san-*",
} }
filesystems: { filesystems: {
"<": true, "<": true,
+2 -9
View File
@@ -52,15 +52,8 @@ Serve
listen: ":8888" listen: ":8888"
listen_freebind: true # optional, default false listen_freebind: true # optional, default false
clients: { clients: {
"192.168.122.123" : "mysql01", "192.168.122.123" : "mysql01"
"192.168.122.42" : "mx01", "192.168.122.123" : "mx01"
"2001:0db8:85a3::8a2e:0370:7334": "gateway",
# CIDR masks require a '*' in the client identity string
# that is expanded to the client's IP address
"10.23.42.0/24": "cluster-*"
"fde4:8dba:82e1::/64": "san-*"
} }
... ...
+129 -10
View File
@@ -1,19 +1,138 @@
.. _binary releases: https://github.com/zrepl/zrepl/releases
.. _installation_toc: .. _installation:
************
Installation Installation
************ ============
.. TIP:: .. TIP::
Note: check out the :ref:`tutorial` if you want a first impression of zrepl. Note: check out the :ref:`tutorial` if you want a first impression of zrepl.
.. toctree:: User Privileges
---------------
installation/user-privileges It is possible to run zrepl as an unprivileged user in combination with
installation/packages `ZFS delegation <https://www.freebsd.org/doc/handbook/zfs-zfs-allow.html>`_.
installation/apt-repos Also, there is the possibility to run it in a jail on FreeBSD by delegating a dataset to the jail.
installation/compile-from-source However, until we get around documenting those setups, you will have to run zrepl as root or experiment yourself :)
installation/freebsd-jail-with-iocage
installation/what-next Packages
--------
zrepl source releases are signed & tagged by the author in the git repository.
Your OS vendor may provide binary packages of zrepl through the package manager.
Additionally, `binary releases`_ are provided on GitHub.
The following list may be incomplete, feel free to submit a PR with an update:
.. list-table::
:header-rows: 1
* - OS / Distro
- Install Command
- Link
* - FreeBSD
- ``pkg install zrepl``
- `<https://www.freshports.org/sysutils/zrepl/>`_
* - MacOS
- ``brew install zrepl``
- Available on `homebrew <https://brew.sh>`_
* - Arch Linux
- ``yay install zrepl``
- Available on `AUR <https://aur.archlinux.org/packages/zrepl>`_
* - Fedora
- ``dnf install zrepl``
- Available on `COPR <https://copr.fedorainfracloud.org/coprs/poettlerric/zrepl/>`_
* - CentOS/RHEL
- ``yum install zrepl``
- Available on `COPR <https://copr.fedorainfracloud.org/coprs/poettlerric/zrepl/>`_
* - Debian + Ubuntu
- ``apt install zrepl``
- APT repository config :ref:`see below <installation-apt-repos>`
* - OmniOS
- ``pkg install zrepl``
- Available since `r151030 <https://pkg.omniosce.org/r151030/extra/en/search.shtml?token=zrepl&action=Search>`_
* - Void Linux
- ``xbps-install zrepl``
- Available since `a88a2a4 <https://github.com/void-linux/void-packages/commit/a88a2a4d7bf56072dadf61ab56b8424e39155890>`_
* - Others
-
- Use `binary releases`_ or build from source.
.. _installation-apt-repos:
Debian / Ubuntu APT repositories
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
We maintain APT repositories for Debian, Ubuntu and derivatives.
The fingerprint of the signing key is ``E101 418F D3D6 FBCB 9D65 A62D 7086 99FC 5F2E BF16``.
It is available at `<https://zrepl.cschwarz.com/apt/apt-key.asc>`_ .
Please open an issue `in the packaging repository <https://github.com/zrepl/debian-binary-packaging>`_ if you encounter any issues with the repository.
The following snippet configure the repository for your Debian or Ubuntu release:
::
apt update && apt install curl gnupg lsb-release; \
ARCH="$(dpkg --print-architecture)"; \
CODENAME="$(lsb_release -i -s | tr '[:upper:]' '[:lower:]') $(lsb_release -c -s | tr '[:upper:]' '[:lower:]')"; \
echo "Using Distro and Codename: $CODENAME"; \
(curl https://zrepl.cschwarz.com/apt/apt-key.asc | apt-key add -) && \
(echo "deb [arch=$ARCH] https://zrepl.cschwarz.com/apt/$CODENAME main" > /etc/apt/sources.list.d/zrepl.list) && \
apt update
.. NOTE::
Until zrepl reaches 1.0, all APT repositories will be updated to the latest zrepl release immediately.
This includes breaking changes between zrepl versions.
Use ``apt-mark hold zrepl`` to prevent upgrades of zrepl.
Compile From Source
~~~~~~~~~~~~~~~~~~~
Producing a release requires **Go 1.11** or newer and **Python 3** + **pip3** + ``docs/requirements.txt`` for the Sphinx documentation.
A tutorial to install Go is available over at `golang.org <https://golang.org/doc/install>`_.
Python and pip3 should probably be installed via your distro's package manager.
Alternatively, you can use the Docker build process:
it is used to produce the official zrepl `binary releases`_
and serves as a reference for build dependencies and procedure:
::
git clone https://github.com/zrepl/zrepl.git && \
cd zrepl && \
sudo docker build -t zrepl_build -f build.Dockerfile . && \
sudo docker run -it --rm \
-v "${PWD}:/src" \
--user "$(id -u):$(id -g)" \
zrepl_build make release
Alternatively, you can install build dependencies on your local system and then build in your ``$GOPATH``:
::
mkdir -p "${GOPATH}/src/github.com/zrepl/zrepl"
git clone https://github.com/zrepl/zrepl.git "${GOPATH}/src/github.com/zrepl/zrepl"
cd "${GOPATH}/src/github.com/zrepl/zrepl"
python3 -m venv3
source venv3/bin/activate
./lazy.sh devsetup
make release
The Python venv is used for the documentation build dependencies.
If you just want to build the zrepl binary, leave it out and use `./lazy.sh godep` instead.
Either way, all build results are located in the ``artifacts/`` directory.
.. NOTE::
It is your job to install the appropriate binary in the zrepl users's ``$PATH``, e.g. ``/usr/local/bin/zrepl``.
Otherwise, the examples in the :ref:`tutorial` may need to be adjusted.
What next?
----------
Read the :ref:`configuration chapter<configuration_toc>` and then continue with the :ref:`usage chapter<usage>`.
**Reminder**: If you want a quick introduction, please read the :ref:`tutorial`.
-29
View File
@@ -1,29 +0,0 @@
.. _installation-apt-repos:
Debian / Ubuntu APT repositories
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
We maintain APT repositories for Debian, Ubuntu and derivatives.
The fingerprint of the signing key is ``E101 418F D3D6 FBCB 9D65 A62D 7086 99FC 5F2E BF16``.
It is available at `<https://zrepl.cschwarz.com/apt/apt-key.asc>`_ .
Please open an issue `in the packaging repository <https://github.com/zrepl/debian-binary-packaging>`_ if you encounter any issues with the repository.
The following snippet configure the repository for your Debian or Ubuntu release:
::
apt update && apt install curl gnupg lsb-release; \
ARCH="$(dpkg --print-architecture)"; \
CODENAME="$(lsb_release -i -s | tr '[:upper:]' '[:lower:]') $(lsb_release -c -s | tr '[:upper:]' '[:lower:]')"; \
echo "Using Distro and Codename: $CODENAME"; \
(curl https://zrepl.cschwarz.com/apt/apt-key.asc | apt-key add -) && \
(echo "deb [arch=$ARCH] https://zrepl.cschwarz.com/apt/$CODENAME main" > /etc/apt/sources.list.d/zrepl.list) && \
apt update
.. NOTE::
Until zrepl reaches 1.0, all APT repositories will be updated to the latest zrepl release immediately.
This includes breaking changes between zrepl versions.
Use ``apt-mark hold zrepl`` to prevent upgrades of zrepl.
-45
View File
@@ -1,45 +0,0 @@
.. _binary releases: https://github.com/zrepl/zrepl/releases
.. _installation-compile-from-source:
Compile From Source
~~~~~~~~~~~~~~~~~~~
Producing a release requires **Go 1.11** or newer and **Python 3** + **pip3** + ``docs/requirements.txt`` for the Sphinx documentation.
A tutorial to install Go is available over at `golang.org <https://golang.org/doc/install>`_.
Python and pip3 should probably be installed via your distro's package manager.
Alternatively, you can use the Docker build process:
it is used to produce the official zrepl `binary releases`_
and serves as a reference for build dependencies and procedure:
::
git clone https://github.com/zrepl/zrepl.git && \
cd zrepl && \
sudo docker build -t zrepl_build -f build.Dockerfile . && \
sudo docker run -it --rm \
-v "${PWD}:/src" \
--user "$(id -u):$(id -g)" \
zrepl_build make release
Alternatively, you can install build dependencies on your local system and then build in your ``$GOPATH``:
::
mkdir -p "${GOPATH}/src/github.com/zrepl/zrepl"
git clone https://github.com/zrepl/zrepl.git "${GOPATH}/src/github.com/zrepl/zrepl"
cd "${GOPATH}/src/github.com/zrepl/zrepl"
python3 -m venv3
source venv3/bin/activate
./lazy.sh devsetup
make release
The Python venv is used for the documentation build dependencies.
If you just want to build the zrepl binary, leave it out and use `./lazy.sh godep` instead.
Either way, all build results are located in the ``artifacts/`` directory.
.. NOTE::
It is your job to install the appropriate binary in the zrepl users's ``$PATH``, e.g. ``/usr/local/bin/zrepl``.
Otherwise, the examples in the :ref:`tutorial` may need to be adjusted.
@@ -1,140 +0,0 @@
.. include:: ../global.rst.inc
.. _installation-freebsd-jail-with-iocage:
FreeBSD Jail With iocage
========================
This tutorial shows how zrepl can be installed on FreeBSD, or FreeNAS in a jail using iocage.
While this tutorial focuses on using iocage, much of the setup would be similar
using a different jail manager.
.. NOTE::
From a security perspective, just keep in mind that ``zfs send``/``recv`` was never designed with
jails in mind, an attacker could probably crash the receive-side kernel or worse induce stateful
damage to the receive-side pool if they were able to get access to the jail.
The jail doesn't provide security benefits, but only management ones.
Requirements
------------
A dataset that will be delegated to the jail needs to be created if one does not already exist.
For the tutorial ``tank/zrepl`` will be used.
.. code-block:: bash
zfs create -o mountpoint=none tank/zrepl
The only software requirements on the host system are ``iocage``, which can be installed
from ports or packages.
.. code-block:: bash
pkg install py37-iocage
.. NOTE::
By default ``iocage`` will "activate" on first use which will set up some defaults such as
which pool will be used. To activate ``iocage`` manually the ``iocage activate`` command can be used.
Jail Creation
-------------
There are two options for jail creation using FreeBSD.
1. Manually set up the jail from scratch
2. Create the jail using the ``zrepl`` plugin. On FreeNAS this is possible from the user interface using the community index.
Manual Jail
###########
Create a jail, using the same release as the host, called ``zrepl`` that will be automatically started at boot.
The jail will have ``tank/zrepl`` delegated into it.
.. code-block:: bash
iocage create --release "$(freebsd-version -k | cut -d '-' -f '1,2')" --name zrepl \
boot=on nat=1 \
jail_zfs=on \
jail_zfs_dataset=zrepl \
jail_zfs_mountpoint='none'
Enter the jail:
.. code-block:: bash
iocage console zrepl
Install ``zrepl``
.. code-block:: bash
pkg update && pkg upgrade
pkg install zrepl
Create the log file ``/var/log/zrepl.log``
.. code-block:: bash
touch /var/log/zrepl.log && service newsyslog restart
Tell syslogd to redirect facility local0 to the ``zrepl.log`` file:
.. code-block:: bash
service syslogd reload
Enable the zrepl daemon to start automatically at boot:
.. code-block:: bash
sysrc zrepl_enable="YES"
Plugin
######
When using the plugin, ``zrepl`` will be installed for you in a jail using the following ``iocage`` properties.
* ``nat=1``
* ``jail_zfs=on``
* ``jail_zfs_mountpoint=none``
Additionally the delegated dataset should be specified upon creation, and optionally start on boot can be set.
This can also be done from the FreeNAS webui.
.. code-block:: bash
fetch https://raw.githubusercontent.com/ix-plugin-hub/iocage-plugin-index/master/zrepl.json -o /tmp/zrepl.json
iocage fetch -P /tmp/zrepl.json --name zrepl jail_zfs_dataset=zrepl boot=on
Configuration
-------------
Now ``zrepl`` can be configured.
Enter the jail.
.. code-block:: bash
iocage console zrepl
Modify the ``/usr/local/etc/zrepl/zrepl.yml`` configuration file.
.. TIP::
Note: check out the :ref:`tutorial` for examples of a ``sink`` job.
Now ``zrepl`` can be started.
.. code-block:: bash
service zrepl start
Summary
-------
Congratulations, you have a working jail!
-50
View File
@@ -1,50 +0,0 @@
.. _installation-packages:
.. _binary releases: https://github.com/zrepl/zrepl/releases
Packages
--------
zrepl source releases are signed & tagged by the author in the git repository.
Your OS vendor may provide binary packages of zrepl through the package manager.
Additionally, `binary releases`_ are provided on GitHub.
The following list may be incomplete, feel free to submit a PR with an update:
.. list-table::
:header-rows: 1
* - OS / Distro
- Install Command
- Link
* - FreeBSD
- ``pkg install zrepl``
- `<https://www.freshports.org/sysutils/zrepl/>`_
:ref:`installation-freebsd-jail-with-iocage`
* - FreeNAS
-
- :ref:`installation-freebsd-jail-with-iocage`
* - MacOS
- ``brew install zrepl``
- Available on `homebrew <https://brew.sh>`_
* - Arch Linux
- ``yay install zrepl``
- Available on `AUR <https://aur.archlinux.org/packages/zrepl>`_
* - Fedora
- ``dnf install zrepl``
- Available on `COPR <https://copr.fedorainfracloud.org/coprs/poettlerric/zrepl/>`_
* - CentOS/RHEL
- ``yum install zrepl``
- Available on `COPR <https://copr.fedorainfracloud.org/coprs/poettlerric/zrepl/>`_
* - Debian + Ubuntu
- ``apt install zrepl``
- APT repository config :ref:`see below <installation-apt-repos>`
* - OmniOS
- ``pkg install zrepl``
- Available since `r151030 <https://pkg.omniosce.org/r151030/extra/en/search.shtml?token=zrepl&action=Search>`_
* - Void Linux
- ``xbps-install zrepl``
- Available since `a88a2a4 <https://github.com/void-linux/void-packages/commit/a88a2a4d7bf56072dadf61ab56b8424e39155890>`_
* - Others
-
- Use `binary releases`_ or build from source.
-12
View File
@@ -1,12 +0,0 @@
.. _installation-user-privileges:
User Privileges
---------------
It is possible to run zrepl as an unprivileged user in combination with
`ZFS delegation <https://www.freebsd.org/doc/handbook/zfs-zfs-allow.html>`_.
Also, there is the possibility to run it in a jail on FreeBSD by delegating a dataset to the jail.
.. TIP::
Note: check out the :ref:`installation-freebsd-jail-with-iocage` for FreeBSD jail setup instructions.
-8
View File
@@ -1,8 +0,0 @@
.. _installation-what-next:
What next?
----------
Read the :ref:`configuration chapter<configuration_toc>` and then continue with the :ref:`usage chapter<usage>`.
**Reminder**: If you want a quick introduction, please read the :ref:`tutorial`.
+1 -1
View File
@@ -47,7 +47,7 @@ We can model this situation as two jobs:
Install zrepl Install zrepl
------------- -------------
Follow the :ref:`OS-specific installation instructions <installation_toc>` and come back here. Follow the :ref:`OS-specific installation instructions <installation>` and come back here.
Generate TLS Certificates Generate TLS Certificates
------------------------- -------------------------
+101 -14
View File
@@ -2,14 +2,17 @@
package endpoint package endpoint
import ( import (
"bytes"
"context" "context"
"fmt" "fmt"
"io"
"path" "path"
"sync" "sync"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/zrepl/zrepl/replication/logic/pdu" "github.com/zrepl/zrepl/replication/logic/pdu"
"github.com/zrepl/zrepl/util/chainedio"
"github.com/zrepl/zrepl/util/chainlock" "github.com/zrepl/zrepl/util/chainlock"
"github.com/zrepl/zrepl/util/envconst" "github.com/zrepl/zrepl/util/envconst"
"github.com/zrepl/zrepl/util/semaphore" "github.com/zrepl/zrepl/util/semaphore"
@@ -241,7 +244,7 @@ func sendArgsFromPDUAndValidateExistsAndGetVersion(ctx context.Context, fs strin
return version, nil return version, nil
} }
func (s *Sender) Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, zfs.StreamCopier, error) { func (s *Sender) Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, io.ReadCloser, error) {
_, err := s.filterCheckFS(r.Filesystem) _, err := s.filterCheckFS(r.Filesystem)
if err != nil { if err != nil {
@@ -339,11 +342,11 @@ func (s *Sender) Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, zfs.St
// step holds & replication cursor released / moved forward in s.SendCompleted => s.moveCursorAndReleaseSendHolds // step holds & replication cursor released / moved forward in s.SendCompleted => s.moveCursorAndReleaseSendHolds
streamCopier, err := zfs.ZFSSend(ctx, sendArgs) sendStream, err := zfs.ZFSSend(ctx, sendArgs)
if err != nil { if err != nil {
return nil, nil, errors.Wrap(err, "zfs send failed") return nil, nil, errors.Wrap(err, "zfs send failed")
} }
return res, streamCopier, nil return res, sendStream, nil
} }
func (p *Sender) SendCompleted(ctx context.Context, r *pdu.SendCompletedReq) (*pdu.SendCompletedRes, error) { func (p *Sender) SendCompleted(ctx context.Context, r *pdu.SendCompletedReq) (*pdu.SendCompletedRes, error) {
@@ -476,7 +479,7 @@ func (p *Sender) ReplicationCursor(ctx context.Context, req *pdu.ReplicationCurs
return &pdu.ReplicationCursorRes{Result: &pdu.ReplicationCursorRes_Guid{Guid: cursor.Guid}}, nil return &pdu.ReplicationCursorRes{Result: &pdu.ReplicationCursorRes_Guid{Guid: cursor.Guid}}, nil
} }
func (p *Sender) Receive(ctx context.Context, r *pdu.ReceiveReq, receive zfs.StreamCopier) (*pdu.ReceiveRes, error) { func (p *Sender) Receive(ctx context.Context, r *pdu.ReceiveReq, _ io.ReadCloser) (*pdu.ReceiveRes, error) {
return nil, fmt.Errorf("sender does not implement Receive()") return nil, fmt.Errorf("sender does not implement Receive()")
} }
@@ -680,13 +683,13 @@ func (s *Receiver) ReplicationCursor(context.Context, *pdu.ReplicationCursorReq)
return nil, fmt.Errorf("ReplicationCursor not implemented for Receiver") return nil, fmt.Errorf("ReplicationCursor not implemented for Receiver")
} }
func (s *Receiver) Send(ctx context.Context, req *pdu.SendReq) (*pdu.SendRes, zfs.StreamCopier, error) { func (s *Receiver) Send(ctx context.Context, req *pdu.SendReq) (*pdu.SendRes, io.ReadCloser, error) {
return nil, nil, fmt.Errorf("receiver does not implement Send()") return nil, nil, fmt.Errorf("receiver does not implement Send()")
} }
var maxConcurrentZFSRecvSemaphore = semaphore.New(envconst.Int64("ZREPL_ENDPOINT_MAX_CONCURRENT_RECV", 10)) var maxConcurrentZFSRecvSemaphore = semaphore.New(envconst.Int64("ZREPL_ENDPOINT_MAX_CONCURRENT_RECV", 10))
func (s *Receiver) Receive(ctx context.Context, req *pdu.ReceiveReq, receive zfs.StreamCopier) (*pdu.ReceiveRes, error) { func (s *Receiver) Receive(ctx context.Context, req *pdu.ReceiveReq, receive io.ReadCloser) (*pdu.ReceiveRes, error) {
getLogger(ctx).Debug("incoming Receive") getLogger(ctx).Debug("incoming Receive")
defer receive.Close() defer receive.Close()
@@ -765,21 +768,30 @@ func (s *Receiver) Receive(ctx context.Context, req *pdu.ReceiveReq, receive zfs
return nil, visitErr return nil, visitErr
} }
log := getLogger(ctx).WithField("proto_fs", req.GetFilesystem()).WithField("local_fs", lp.ToString())
// determine whether we need to rollback the filesystem / change its placeholder state // determine whether we need to rollback the filesystem / change its placeholder state
var clearPlaceholderProperty bool var clearPlaceholderProperty bool
var recvOpts zfs.RecvOptions var recvOpts zfs.RecvOptions
ph, err := zfs.ZFSGetFilesystemPlaceholderState(ctx, lp) ph, err := zfs.ZFSGetFilesystemPlaceholderState(ctx, lp)
if err == nil && ph.FSExists && ph.IsPlaceholder { if err != nil {
return nil, errors.Wrap(err, "cannot get placeholder state")
}
log.WithField("placeholder_state", fmt.Sprintf("%#v", ph)).Debug("placeholder state")
if ph.FSExists && ph.IsPlaceholder {
recvOpts.RollbackAndForceRecv = true recvOpts.RollbackAndForceRecv = true
clearPlaceholderProperty = true clearPlaceholderProperty = true
} }
if clearPlaceholderProperty { if clearPlaceholderProperty {
log.Info("clearing placeholder property")
if err := zfs.ZFSSetPlaceholder(ctx, lp, false); err != nil { if err := zfs.ZFSSetPlaceholder(ctx, lp, false); err != nil {
return nil, fmt.Errorf("cannot clear placeholder property for forced receive: %s", err) return nil, fmt.Errorf("cannot clear placeholder property for forced receive: %s", err)
} }
} }
if req.ClearResumeToken && ph.FSExists { if req.ClearResumeToken && ph.FSExists {
log.Info("clearing resume token")
if err := zfs.ZFSRecvClearResumeToken(ctx, lp.ToString()); err != nil { if err := zfs.ZFSRecvClearResumeToken(ctx, lp.ToString()); err != nil {
return nil, errors.Wrap(err, "cannot clear resume token") return nil, errors.Wrap(err, "cannot clear resume token")
} }
@@ -790,7 +802,7 @@ func (s *Receiver) Receive(ctx context.Context, req *pdu.ReceiveReq, receive zfs
return nil, errors.Wrap(err, "cannot determine whether we can use resumable send & recv") return nil, errors.Wrap(err, "cannot determine whether we can use resumable send & recv")
} }
getLogger(ctx).Debug("acquire concurrent recv semaphore") log.Debug("acquire concurrent recv semaphore")
// TODO use try-acquire and fail with resource-exhaustion rpc status // TODO use try-acquire and fail with resource-exhaustion rpc status
// => would require handling on the client-side // => would require handling on the client-side
// => this is a dataconn endpoint, doesn't have the status code semantics of gRPC // => this is a dataconn endpoint, doesn't have the status code semantics of gRPC
@@ -800,14 +812,89 @@ func (s *Receiver) Receive(ctx context.Context, req *pdu.ReceiveReq, receive zfs
} }
defer guard.Release() defer guard.Release()
getLogger(ctx).WithField("opts", fmt.Sprintf("%#v", recvOpts)).Debug("start receive command") log.Info("peeking 1M ahead")
var peek bytes.Buffer
var MaxPeek = envconst.Int64("ZREPL_ENDPOINT_RECV_PEEK_SIZE", 1<<20)
if _, err := io.Copy(&peek, io.LimitReader(receive, MaxPeek)); err != nil {
log.WithError(err).Error("cannot read peek-buffer from send stream")
}
var peekCopy bytes.Buffer
if n, err := peekCopy.Write(peek.Bytes()); err != nil || n != peek.Len() {
panic(peek.Len())
}
log.WithField("opts", fmt.Sprintf("%#v", recvOpts)).Debug("start receive command")
snapFullPath := to.FullPath(lp.ToString()) snapFullPath := to.FullPath(lp.ToString())
if err := zfs.ZFSRecv(ctx, lp.ToString(), to, receive, recvOpts); err != nil { if err := zfs.ZFSRecv(ctx, lp.ToString(), to, chainedio.NewChainedReader(&peek, receive), recvOpts); err != nil {
getLogger(ctx).
// best-effort rollback of placeholder state if the recv didn't start
_, resumableStatePresent := err.(*zfs.RecvFailedWithResumeTokenErr)
disablePlaceholderRestoration := envconst.Bool("ZREPL_ENDPOINT_DISABLE_PLACEHOLDER_RESTORATION", false)
placeholderRestored := !ph.IsPlaceholder
if !disablePlaceholderRestoration && !resumableStatePresent && recvOpts.RollbackAndForceRecv && ph.FSExists && ph.IsPlaceholder && clearPlaceholderProperty {
log.Info("restoring placeholder property")
if phErr := zfs.ZFSSetPlaceholder(ctx, lp, true); phErr != nil {
log.WithError(phErr).Error("cannot restore placeholder property after failed receive, subsequent replications will likely fail with a different error")
// fallthrough
} else {
placeholderRestored = true
}
// fallthrough
}
// deal with failing initial encrypted send & recv
if _, ok := err.(*zfs.RecvDestroyOrOverwriteEncryptedErr); ok && ph.IsPlaceholder && placeholderRestored {
msg := `cannot automatically replace placeholder filesystem with incoming send stream - please see receive-side log for details`
err := errors.New(msg)
log.Error(msg)
log.Error(`zrepl creates placeholder filesystems on the receiving side of a replication to match the sending side's dataset hierarchy`)
log.Error(`zrepl uses zfs receive -F to replace those placeholders with incoming full sends`)
log.Error(`OpenZFS native encryption prohibits zfs receive -F for encrypted filesystems`)
log.Error(`the current zrepl placeholder filesystem concept is thus incompatible with OpenZFS native encryption`)
tempStartFullRecvFS := lp.Copy().ToString() + ".zrepl.initial-recv"
tempStartFullRecvFSDP, dpErr := zfs.NewDatasetPath(tempStartFullRecvFS)
if dpErr != nil {
log.WithError(dpErr).Error("cannot determine temporary filesystem name for initial encrypted recv workaround")
return nil, err // yes, err, not dpErr
}
log := log.WithField("temp_recv_fs", tempStartFullRecvFS)
log.Error(`as a workaround, zrepl will now attempt to re-receive the beginning of the stream into a temporary filesystem temp_recv_fs`)
log.Error(`if that step succeeds: shut down zrepl and use 'zfs rename' to swap temp_recv_fs with local_fs, then restart zrepl`)
log.Error(`replication will then resume using resumable send+recv`)
tempPH, phErr := zfs.ZFSGetFilesystemPlaceholderState(ctx, tempStartFullRecvFSDP)
if phErr != nil {
log.WithError(phErr).Error("cannot determine placeholder state of temp_recv_fs")
return nil, err // yes, err, not dpErr
}
if tempPH.FSExists {
log.Error("temp_recv_fs already exists, assuming a (partial) initial recv to that filesystem has already been done")
return nil, err
}
recvOpts.RollbackAndForceRecv = false
recvOpts.SavePartialRecvState = true
rerecvErr := zfs.ZFSRecv(ctx, tempStartFullRecvFS, to, chainedio.NewChainedReader(&peekCopy), recvOpts)
if _, isResumable := rerecvErr.(*zfs.RecvFailedWithResumeTokenErr); rerecvErr == nil || isResumable {
log.Error("completed re-receive into temporary filesystem temp_recv_fs, now shut down zrepl and use zfs rename to swap temp_recv_fs with local_fs")
} else {
log.WithError(rerecvErr).Error("failed to receive the beginning of the stream into temporary filesystem temp_recv_fs")
log.Error("we advise you to collect the error log and current configuration, open an issue on GitHub, and revert to your previous configuration in the meantime")
}
log.Error(`if you would like to see improvements to this situation, please open an issue on GitHub`)
return nil, err
}
log.
WithError(err). WithError(err).
WithField("opts", fmt.Sprintf("%#v", recvOpts)). WithField("opts", fmt.Sprintf("%#v", recvOpts)).
Error("zfs receive failed") Error("zfs receive failed")
return nil, err return nil, err
} }
@@ -815,13 +902,13 @@ func (s *Receiver) Receive(ctx context.Context, req *pdu.ReceiveReq, receive zfs
toRecvd, err := to.ValidateExistsAndGetVersion(ctx, lp.ToString()) toRecvd, err := to.ValidateExistsAndGetVersion(ctx, lp.ToString())
if err != nil { if err != nil {
msg := "receive request's `To` version does not match what we received in the stream" msg := "receive request's `To` version does not match what we received in the stream"
getLogger(ctx).WithError(err).WithField("snap", snapFullPath).Error(msg) log.WithError(err).WithField("snap", snapFullPath).Error(msg)
getLogger(ctx).Error("aborting recv request, but keeping received snapshot for inspection") log.Error("aborting recv request, but keeping received snapshot for inspection")
return nil, errors.Wrap(err, msg) return nil, errors.Wrap(err, msg)
} }
if s.conf.UpdateLastReceivedHold { if s.conf.UpdateLastReceivedHold {
getLogger(ctx).Debug("move last-received-hold") log.Debug("move last-received-hold")
if err := MoveLastReceivedHold(ctx, lp.ToString(), toRecvd, s.conf.JobID); err != nil { if err := MoveLastReceivedHold(ctx, lp.ToString(), toRecvd, s.conf.JobID); err != nil {
return nil, errors.Wrap(err, "cannot move last-received-hold") return nil, errors.Wrap(err, "cannot move last-received-hold")
} }
+1 -4
View File
@@ -467,7 +467,7 @@ func (e ListAbstractionsErrors) Error() string {
} }
msgs := make([]string, len(e)) msgs := make([]string, len(e))
for i := range e { for i := range e {
msgs[i] = e[i].Error() msgs[i] = e.Error()
} }
return fmt.Sprintf("list endpoint abstractions: multiple errors:\n%s", strings.Join(msgs, "\n")) return fmt.Sprintf("list endpoint abstractions: multiple errors:\n%s", strings.Join(msgs, "\n"))
} }
@@ -731,9 +731,6 @@ func listStaleFiltering(abs []Abstraction, sinceBound *CreateTXGRangeBound) *Sta
} }
stepFirstNotStaleCandidates := make(map[fsAndJobId]stepFirstNotStaleCandidate) // empty map => will always return nil stepFirstNotStaleCandidates := make(map[fsAndJobId]stepFirstNotStaleCandidate) // empty map => will always return nil
for _, a := range abs { for _, a := range abs {
if a.GetJobID() == nil {
continue // already put those into always-live list noJobId in above loop
}
key := fsAndJobId{a.GetFS(), *a.GetJobID()} key := fsAndJobId{a.GetFS(), *a.GetJobID()}
c := stepFirstNotStaleCandidates[key] c := stepFirstNotStaleCandidates[key]
+1 -1
View File
@@ -21,7 +21,7 @@ require (
github.com/onsi/gomega v1.7.0 // indirect github.com/onsi/gomega v1.7.0 // indirect
github.com/pkg/errors v0.8.1 github.com/pkg/errors v0.8.1
github.com/pkg/profile v1.2.1 github.com/pkg/profile v1.2.1
github.com/problame/go-netssh v0.0.0-20200601114649-26439f9f0dc5 github.com/problame/go-netssh v0.0.0-20191209123953-18d8aa6923c7
github.com/prometheus/client_golang v1.2.1 github.com/prometheus/client_golang v1.2.1
github.com/sergi/go-diff v1.0.1-0.20180205163309-da645544ed44 // go1.12 thinks it needs this github.com/sergi/go-diff v1.0.1-0.20180205163309-da645544ed44 // go1.12 thinks it needs this
github.com/spf13/cobra v0.0.2 github.com/spf13/cobra v0.0.2
-3
View File
@@ -208,8 +208,6 @@ github.com/problame/go-netssh v0.0.0-20191026123024-f34099f4f6b1 h1:HH8yzlaZq/A8
github.com/problame/go-netssh v0.0.0-20191026123024-f34099f4f6b1/go.mod h1:JRs3nyBjvOv/9YHC+Vlw9z1Jfzl5s5t0BNnTzmclj1c= github.com/problame/go-netssh v0.0.0-20191026123024-f34099f4f6b1/go.mod h1:JRs3nyBjvOv/9YHC+Vlw9z1Jfzl5s5t0BNnTzmclj1c=
github.com/problame/go-netssh v0.0.0-20191209123953-18d8aa6923c7 h1:Oik8tLrLhoMq4fduDRuNNOAQ+q0M0dXkjAYLUf4+mAc= github.com/problame/go-netssh v0.0.0-20191209123953-18d8aa6923c7 h1:Oik8tLrLhoMq4fduDRuNNOAQ+q0M0dXkjAYLUf4+mAc=
github.com/problame/go-netssh v0.0.0-20191209123953-18d8aa6923c7/go.mod h1:Ba6RaFpK1+IHWs1wLZ6sCBuXkt4iAVLeOG5GinXHusM= github.com/problame/go-netssh v0.0.0-20191209123953-18d8aa6923c7/go.mod h1:Ba6RaFpK1+IHWs1wLZ6sCBuXkt4iAVLeOG5GinXHusM=
github.com/problame/go-netssh v0.0.0-20200601114649-26439f9f0dc5 h1:1eSrO2WAeSXjMol8WRKW9LekL1252VIMaKQwWkmEdvs=
github.com/problame/go-netssh v0.0.0-20200601114649-26439f9f0dc5/go.mod h1:Ba6RaFpK1+IHWs1wLZ6sCBuXkt4iAVLeOG5GinXHusM=
github.com/prometheus/client_golang v0.0.0-20180410130117-e11c6ff8170b/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.0.0-20180410130117-e11c6ff8170b/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
@@ -265,7 +263,6 @@ github.com/spf13/viper v1.0.2/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7Sr
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= 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.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/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.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= 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.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
-1
View File
@@ -27,7 +27,6 @@ func (c *Context) Logf(format string, args ...interface{}) {
func (c *Context) Errorf(format string, args ...interface{}) { func (c *Context) Errorf(format string, args ...interface{}) {
GetLog(c).Printf(format, args...) GetLog(c).Printf(format, args...)
c.FailNow()
} }
func (c *Context) FailNow() { func (c *Context) FailNow() {
+1 -1
View File
@@ -136,7 +136,7 @@ func makeResumeSituation(ctx *platformtest.Context, src dummySnapshotSituation,
return situation return situation
} }
limitedCopier := zfs.NewReadCloserCopier(limitio.ReadCloser(copier, src.dummyDataLen/2)) limitedCopier := limitio.ReadCloser(copier, src.dummyDataLen/2)
defer limitedCopier.Close() defer limitedCopier.Close()
require.NotNil(ctx, sendArgs.To) require.NotNil(ctx, sendArgs.To)
+1 -1
View File
@@ -35,7 +35,7 @@ func SendArgsValidationEncryptedSendOfUnencryptedDatasetForbidden(ctx *platformt
ResumeToken: "", ResumeToken: "",
}.Validate(ctx) }.Validate(ctx)
var stream *zfs.ReadCloserCopier var stream *zfs.SendStream
if err == nil { if err == nil {
stream, err = zfs.ZFSSend(ctx, sendArgs) // no shadow stream, err = zfs.ZFSSend(ctx, sendArgs) // no shadow
if err == nil { if err == nil {
+131 -7
View File
@@ -146,6 +146,12 @@ type fs struct {
l *chainlock.L l *chainlock.L
// ordering relationship that must be maintained for initial replication
initialRepOrd struct {
parents, children []*fs
parentDidUpdate chan struct{}
}
planning struct { planning struct {
done bool done bool
err *timedError err *timedError
@@ -296,6 +302,7 @@ func (a *attempt) do(ctx context.Context, prev *attempt) {
fs: pfs, fs: pfs,
l: a.l, l: a.l,
} }
fs.initialRepOrd.parentDidUpdate = make(chan struct{}, 1)
a.fss = append(a.fss, fs) a.fss = append(a.fss, fs)
} }
@@ -354,6 +361,18 @@ func (a *attempt) do(ctx context.Context, prev *attempt) {
} }
// invariant: prevs contains an entry for each unambiguous correspondence // invariant: prevs contains an entry for each unambiguous correspondence
// build up parent-child relationship (FIXME (O(n^2), but who's going to have that many filesystems...))
for _, f1 := range a.fss {
fs1 := f1.fs.ReportInfo().Name
for _, f2 := range a.fss {
fs2 := f2.fs.ReportInfo().Name
if strings.HasPrefix(fs1, fs2) && fs1 != fs2 {
f1.initialRepOrd.parents = append(f1.initialRepOrd.parents, f2)
f2.initialRepOrd.children = append(f2.initialRepOrd.children, f1)
}
}
}
stepQueue := newStepQueue() stepQueue := newStepQueue()
defer stepQueue.Start(envconst.Int("ZREPL_REPLICATION_EXPERIMENTAL_REPLICATION_CONCURRENCY", 1))() // TODO parallel replication defer stepQueue.Start(envconst.Int("ZREPL_REPLICATION_EXPERIMENTAL_REPLICATION_CONCURRENCY", 1))() // TODO parallel replication
var fssesDone sync.WaitGroup var fssesDone sync.WaitGroup
@@ -374,9 +393,27 @@ func (f *fs) debug(format string, args ...interface{}) {
debugPrefix("fs=%s", f.fs.ReportInfo().Name)(format, args...) debugPrefix("fs=%s", f.fs.ReportInfo().Name)(format, args...)
} }
// wake up children that watch for f.{planning.{err,done},planned.{step,stepErr}}
func (f *fs) initialRepOrdWakeupChildren() {
var children []string
for _, c := range f.initialRepOrd.children {
// no locking required, c.fs does not change
children = append(children, c.fs.ReportInfo().Name)
}
f.debug("wakeup children %s", children)
for _, child := range f.initialRepOrd.children {
select {
// no locking required, child.initialRepOrd does not change
case child.initialRepOrd.parentDidUpdate <- struct{}{}:
default:
}
}
}
func (f *fs) do(ctx context.Context, pq *stepQueue, prev *fs) { func (f *fs) do(ctx context.Context, pq *stepQueue, prev *fs) {
defer f.l.Lock().Unlock() defer f.l.Lock().Unlock()
defer f.initialRepOrdWakeupChildren()
// get planned steps from replication logic // get planned steps from replication logic
var psteps []Step var psteps []Step
@@ -390,7 +427,6 @@ func (f *fs) do(ctx context.Context, pq *stepQueue, prev *fs) {
psteps, err = f.fs.PlanFS(ctx) // no shadow psteps, err = f.fs.PlanFS(ctx) // no shadow
errTime = time.Now() // no shadow errTime = time.Now() // no shadow
}) })
f.planning.done = true
if err != nil { if err != nil {
f.planning.err = newTimedError(err, errTime) f.planning.err = newTimedError(err, errTime)
return return
@@ -402,6 +438,8 @@ func (f *fs) do(ctx context.Context, pq *stepQueue, prev *fs) {
} }
f.planned.steps = append(f.planned.steps, step) f.planned.steps = append(f.planned.steps, step)
} }
// we're not done planning yet, f.planned.steps might still be changed by next block
// => don't set f.planning.done just yet
f.debug("initial len(fs.planned.steps) = %d", len(f.planned.steps)) f.debug("initial len(fs.planned.steps) = %d", len(f.planned.steps))
// for not-first attempts, only allow fs.planned.steps // for not-first attempts, only allow fs.planned.steps
@@ -456,24 +494,110 @@ func (f *fs) do(ctx context.Context, pq *stepQueue, prev *fs) {
} }
f.debug("post-prev-merge len(fs.planned.steps) = %d", len(f.planned.steps)) f.debug("post-prev-merge len(fs.planned.steps) = %d", len(f.planned.steps))
// now we are done planning (f.planned.steps won't change from now on)
f.planning.done = true
// wait for parents' initial replication
var parents []string
for _, p := range f.initialRepOrd.parents {
parents = append(parents, p.fs.ReportInfo().Name)
}
f.debug("wait for parents %s", parents)
for {
var initialReplicatingParentsWithErrors []string
allParentsPresentOnReceiver := true
f.l.DropWhile(func() {
for _, p := range f.initialRepOrd.parents {
p.l.HoldWhile(func() {
// (get the preconditions that allow us to inspect p.planned)
parentHasPlanningDone := p.planning.done && p.planning.err == nil
if !parentHasPlanningDone {
// if the parent couldn't be planned, we cannot know whether it needs initial replication
// or incremental replication => be conservative and assume it was initial replication
allParentsPresentOnReceiver = false
if p.planning.err != nil {
initialReplicatingParentsWithErrors = append(initialReplicatingParentsWithErrors, p.fs.ReportInfo().Name)
}
return
}
// now allowed to inspect p.planned
// if there are no steps to be done, the filesystem must exist on the receiving side
// (otherwise we'd replicate it, and there would be a step for that)
// (FIXME hardcoded initial replication policy, assuming the policy will always do _some_ initial replication)
parentHasNoSteps := len(p.planned.steps) == 0
// OR if it has completed at least one step
// (remember that .step points to the next step to be done)
// (TODO technically, we could make this step ready in the moment the recv-side
// dataset exists, i.e. after the first few megabytes of transferred data, but we'd have to ask the receiver for that -> poll ListFilesystems RPC)
parentHasTakenAtLeastOneSuccessfulStep := !parentHasNoSteps && p.planned.step >= 1
parentFirstStepIsIncremental := // no need to lock for .report() because step.l == it's fs.l
len(p.planned.steps) > 0 && p.planned.steps[0].report().IsIncremental()
f.debug("parentHasNoSteps=%v parentFirstStepIsIncremental=%v parentHasTakenAtLeastOneSuccessfulStep=%v",
parentHasNoSteps, parentFirstStepIsIncremental, parentHasTakenAtLeastOneSuccessfulStep)
parentPresentOnReceiver := parentHasNoSteps || parentFirstStepIsIncremental || parentHasTakenAtLeastOneSuccessfulStep
allParentsPresentOnReceiver = allParentsPresentOnReceiver && parentPresentOnReceiver // no shadow
if !parentPresentOnReceiver && p.planned.stepErr != nil {
initialReplicatingParentsWithErrors = append(initialReplicatingParentsWithErrors, p.fs.ReportInfo().Name)
}
})
}
})
if len(initialReplicatingParentsWithErrors) > 0 {
f.planned.stepErr = newTimedError(fmt.Errorf("parent(s) failed during initial replication: %s", initialReplicatingParentsWithErrors), time.Now())
return
}
if allParentsPresentOnReceiver {
break // good to go
}
// wait for wakeups from parents, then check again
// lock must not be held while waiting in order for reporting to work
f.l.DropWhile(func() {
select {
case <-ctx.Done():
f.planned.stepErr = newTimedError(ctx.Err(), time.Now())
return
case <-f.initialRepOrd.parentDidUpdate:
// loop
}
})
if f.planned.stepErr != nil {
return
}
}
f.debug("all parents ready, start replication %s", parents)
// do our steps
for i, s := range f.planned.steps { for i, s := range f.planned.steps {
var (
err error
errTime time.Time
)
// lock must not be held while executing step in order for reporting to work // lock must not be held while executing step in order for reporting to work
f.l.DropWhile(func() { f.l.DropWhile(func() {
// wait for parallel replication
targetDate := s.step.TargetDate() targetDate := s.step.TargetDate()
defer pq.WaitReady(f, targetDate)() defer pq.WaitReady(f, targetDate)()
err = s.step.Step(ctx) // no shadow // do the step
errTime = time.Now() // no shadow err, errTime = s.step.Step(ctx), time.Now() // no shadow
}) })
if err != nil { if err != nil {
f.planned.stepErr = newTimedError(err, errTime) f.planned.stepErr = newTimedError(err, errTime)
break break
} }
f.planned.step = i + 1 // fs.planned.step must be == len(fs.planned.steps) if all went OK f.planned.step = i + 1 // fs.planned.step must be == len(fs.planned.steps) if all went OK
f.initialRepOrdWakeupChildren()
} }
} }
// caller must hold lock l // caller must hold lock l
@@ -26,6 +26,6 @@ type debugFunc func(format string, args ...interface{})
func debugPrefix(prefixFormat string, prefixFormatArgs ...interface{}) debugFunc { func debugPrefix(prefixFormat string, prefixFormatArgs ...interface{}) debugFunc {
prefix := fmt.Sprintf(prefixFormat, prefixFormatArgs...) prefix := fmt.Sprintf(prefixFormat, prefixFormatArgs...)
return func(format string, args ...interface{}) { return func(format string, args ...interface{}) {
debug("%s: %s", prefix, fmt.Sprintf(format, args)) debug("%s: %s", prefix, fmt.Sprintf(format, args...))
} }
} }
+9 -8
View File
@@ -4,6 +4,7 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"io"
"sync" "sync"
"time" "time"
@@ -38,7 +39,7 @@ type Sender interface {
// If a non-nil io.ReadCloser is returned, it is guaranteed to be closed before // If a non-nil io.ReadCloser is returned, it is guaranteed to be closed before
// any next call to the parent github.com/zrepl/zrepl/replication.Endpoint. // any next call to the parent github.com/zrepl/zrepl/replication.Endpoint.
// If the send request is for dry run the io.ReadCloser will be nil // If the send request is for dry run the io.ReadCloser will be nil
Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, zfs.StreamCopier, error) Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, io.ReadCloser, error)
SendCompleted(ctx context.Context, r *pdu.SendCompletedReq) (*pdu.SendCompletedRes, error) SendCompleted(ctx context.Context, r *pdu.SendCompletedReq) (*pdu.SendCompletedRes, error)
ReplicationCursor(ctx context.Context, req *pdu.ReplicationCursorReq) (*pdu.ReplicationCursorRes, error) ReplicationCursor(ctx context.Context, req *pdu.ReplicationCursorReq) (*pdu.ReplicationCursorRes, error)
} }
@@ -47,7 +48,7 @@ type Receiver interface {
Endpoint Endpoint
// Receive sends r and sendStream (the latter containing a ZFS send stream) // Receive sends r and sendStream (the latter containing a ZFS send stream)
// to the parent github.com/zrepl/zrepl/replication.Endpoint. // to the parent github.com/zrepl/zrepl/replication.Endpoint.
Receive(ctx context.Context, req *pdu.ReceiveReq, receive zfs.StreamCopier) (*pdu.ReceiveRes, error) Receive(ctx context.Context, req *pdu.ReceiveReq, receive io.ReadCloser) (*pdu.ReceiveRes, error)
} }
type PlannerPolicy struct { type PlannerPolicy struct {
@@ -162,7 +163,7 @@ type Step struct {
// byteCounter is nil initially, and set later in Step.doReplication // byteCounter is nil initially, and set later in Step.doReplication
// => concurrent read of that pointer from Step.ReportInfo must be protected // => concurrent read of that pointer from Step.ReportInfo must be protected
byteCounter bytecounter.StreamCopier byteCounter bytecounter.ReadCloser
byteCounterMtx chainlock.L byteCounterMtx chainlock.L
} }
@@ -602,23 +603,23 @@ func (s *Step) doReplication(ctx context.Context) error {
fs := s.parent.Path fs := s.parent.Path
log := getLogger(ctx).WithField("filesystem", fs) log := getLogger(ctx)
sr := s.buildSendRequest(false) sr := s.buildSendRequest(false)
log.Debug("initiate send request") log.Debug("initiate send request")
sres, sstreamCopier, err := s.sender.Send(ctx, sr) sres, stream, err := s.sender.Send(ctx, sr)
if err != nil { if err != nil {
log.WithError(err).Error("send request failed") log.WithError(err).Error("send request failed")
return err return err
} }
if sstreamCopier == nil { if stream == nil {
err := errors.New("send request did not return a stream, broken endpoint implementation") err := errors.New("send request did not return a stream, broken endpoint implementation")
return err return err
} }
defer sstreamCopier.Close() defer stream.Close()
// Install a byte counter to track progress + for status report // Install a byte counter to track progress + for status report
byteCountingStream := bytecounter.NewStreamCopier(sstreamCopier) byteCountingStream := bytecounter.NewReadCloser(stream)
s.byteCounterMtx.Lock() s.byteCounterMtx.Lock()
s.byteCounter = byteCountingStream s.byteCounter = byteCountingStream
s.byteCounterMtx.Unlock() s.byteCounterMtx.Unlock()
+13 -10
View File
@@ -4,6 +4,7 @@ import (
"bytes" "bytes"
"context" "context"
"fmt" "fmt"
"io"
"strings" "strings"
"github.com/golang/protobuf/proto" "github.com/golang/protobuf/proto"
@@ -11,7 +12,6 @@ import (
"github.com/zrepl/zrepl/replication/logic/pdu" "github.com/zrepl/zrepl/replication/logic/pdu"
"github.com/zrepl/zrepl/rpc/dataconn/stream" "github.com/zrepl/zrepl/rpc/dataconn/stream"
"github.com/zrepl/zrepl/transport" "github.com/zrepl/zrepl/transport"
"github.com/zrepl/zrepl/zfs"
) )
type Client struct { type Client struct {
@@ -26,7 +26,7 @@ func NewClient(connecter transport.Connecter, log Logger) *Client {
} }
} }
func (c *Client) send(ctx context.Context, conn *stream.Conn, endpoint string, req proto.Message, streamCopier zfs.StreamCopier) error { func (c *Client) send(ctx context.Context, conn *stream.Conn, endpoint string, req proto.Message, stream io.ReadCloser) error {
var buf bytes.Buffer var buf bytes.Buffer
_, memErr := buf.WriteString(endpoint) _, memErr := buf.WriteString(endpoint)
@@ -46,8 +46,8 @@ func (c *Client) send(ctx context.Context, conn *stream.Conn, endpoint string, r
return err return err
} }
if streamCopier != nil { if stream != nil {
return conn.SendStream(ctx, streamCopier, ZFSStream) return conn.SendStream(ctx, stream, ZFSStream)
} else { } else {
return nil return nil
} }
@@ -109,7 +109,7 @@ func (c *Client) putWire(conn *stream.Conn) {
} }
} }
func (c *Client) ReqSend(ctx context.Context, req *pdu.SendReq) (*pdu.SendRes, zfs.StreamCopier, error) { func (c *Client) ReqSend(ctx context.Context, req *pdu.SendReq) (*pdu.SendRes, io.ReadCloser, error) {
conn, err := c.getWire(ctx) conn, err := c.getWire(ctx)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
@@ -130,16 +130,19 @@ func (c *Client) ReqSend(ctx context.Context, req *pdu.SendReq) (*pdu.SendRes, z
return nil, nil, err return nil, nil, err
} }
var copier zfs.StreamCopier = nil var stream io.ReadCloser
if !req.DryRun { if !req.DryRun {
putWireOnReturn = false putWireOnReturn = false
copier = &streamCopier{streamConn: conn, closeStreamOnClose: true} stream, err = conn.ReadStream(ZFSStream, true) // no shadow
if err != nil {
return nil, nil, err
}
} }
return &res, copier, nil return &res, stream, nil
} }
func (c *Client) ReqRecv(ctx context.Context, req *pdu.ReceiveReq, streamCopier zfs.StreamCopier) (*pdu.ReceiveRes, error) { func (c *Client) ReqRecv(ctx context.Context, req *pdu.ReceiveReq, stream io.ReadCloser) (*pdu.ReceiveRes, error) {
defer c.log.Debug("ReqRecv returns") defer c.log.Debug("ReqRecv returns")
conn, err := c.getWire(ctx) conn, err := c.getWire(ctx)
@@ -166,7 +169,7 @@ func (c *Client) ReqRecv(ctx context.Context, req *pdu.ReceiveReq, streamCopier
sendErrChan := make(chan error) sendErrChan := make(chan error)
go func() { go func() {
if err := c.send(ctx, conn, EndpointRecv, req, streamCopier); err != nil { if err := c.send(ctx, conn, EndpointRecv, req, stream); err != nil {
sendErrChan <- err sendErrChan <- err
} else { } else {
sendErrChan <- nil sendErrChan <- nil
+10 -5
View File
@@ -4,6 +4,7 @@ import (
"bytes" "bytes"
"context" "context"
"fmt" "fmt"
"io"
"github.com/golang/protobuf/proto" "github.com/golang/protobuf/proto"
@@ -11,7 +12,6 @@ import (
"github.com/zrepl/zrepl/replication/logic/pdu" "github.com/zrepl/zrepl/replication/logic/pdu"
"github.com/zrepl/zrepl/rpc/dataconn/stream" "github.com/zrepl/zrepl/rpc/dataconn/stream"
"github.com/zrepl/zrepl/transport" "github.com/zrepl/zrepl/transport"
"github.com/zrepl/zrepl/zfs"
) )
// WireInterceptor has a chance to exchange the context and connection on each client connection. // WireInterceptor has a chance to exchange the context and connection on each client connection.
@@ -21,11 +21,11 @@ type WireInterceptor func(ctx context.Context, rawConn *transport.AuthConn) (con
type Handler interface { type Handler interface {
// Send handles a SendRequest. // Send handles a SendRequest.
// The returned io.ReadCloser is allowed to be nil, for example if the requested Send is a dry-run. // The returned io.ReadCloser is allowed to be nil, for example if the requested Send is a dry-run.
Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, zfs.StreamCopier, error) Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, io.ReadCloser, error)
// Receive handles a ReceiveRequest. // Receive handles a ReceiveRequest.
// It is guaranteed that Server calls Receive with a stream that holds the IdleConnTimeout // It is guaranteed that Server calls Receive with a stream that holds the IdleConnTimeout
// configured in ServerConfig.Shared.IdleConnTimeout. // configured in ServerConfig.Shared.IdleConnTimeout.
Receive(ctx context.Context, r *pdu.ReceiveReq, receive zfs.StreamCopier) (*pdu.ReceiveRes, error) Receive(ctx context.Context, r *pdu.ReceiveReq, receive io.ReadCloser) (*pdu.ReceiveRes, error)
// PingDataconn handles a PingReq // PingDataconn handles a PingReq
PingDataconn(ctx context.Context, r *pdu.PingReq) (*pdu.PingRes, error) PingDataconn(ctx context.Context, r *pdu.PingReq) (*pdu.PingRes, error)
} }
@@ -111,7 +111,7 @@ func (s *Server) serveConn(nc *transport.AuthConn) {
s.log.WithField("endpoint", endpoint).Debug("calling handler") s.log.WithField("endpoint", endpoint).Debug("calling handler")
var res proto.Message var res proto.Message
var sendStream zfs.StreamCopier var sendStream io.ReadCloser
var handlerErr error var handlerErr error
switch endpoint { switch endpoint {
case EndpointSend: case EndpointSend:
@@ -127,7 +127,12 @@ func (s *Server) serveConn(nc *transport.AuthConn) {
s.log.WithError(err).Error("cannot unmarshal receive request") s.log.WithError(err).Error("cannot unmarshal receive request")
return return
} }
res, handlerErr = s.h.Receive(ctx, &req, &streamCopier{streamConn: c, closeStreamOnClose: false}) // SHADOWING stream, err := c.ReadStream(ZFSStream, false)
if err != nil {
s.log.WithError(err).Error("cannot open stream in receive request")
return
}
res, handlerErr = s.h.Receive(ctx, &req, stream) // SHADOWING
case EndpointPing: case EndpointPing:
var req pdu.PingReq var req pdu.PingReq
if err := proto.Unmarshal(reqStructured, &req); err != nil { if err := proto.Unmarshal(reqStructured, &req); err != nil {
-35
View File
@@ -1,12 +1,7 @@
package dataconn package dataconn
import ( import (
"io"
"sync"
"time" "time"
"github.com/zrepl/zrepl/rpc/dataconn/stream"
"github.com/zrepl/zrepl/zfs"
) )
const ( const (
@@ -39,33 +34,3 @@ const (
responseHeaderHandlerOk = "HANDLER OK\n" responseHeaderHandlerOk = "HANDLER OK\n"
responseHeaderHandlerErrorPrefix = "HANDLER ERROR:\n" responseHeaderHandlerErrorPrefix = "HANDLER ERROR:\n"
) )
type streamCopier struct {
mtx sync.Mutex
used bool
streamConn *stream.Conn
closeStreamOnClose bool
}
// WriteStreamTo implements zfs.StreamCopier
func (s *streamCopier) WriteStreamTo(w io.Writer) zfs.StreamCopierError {
s.mtx.Lock()
defer s.mtx.Unlock()
if s.used {
panic("streamCopier used multiple times")
}
s.used = true
return s.streamConn.ReadStreamInto(w, ZFSStream)
}
// Close implements zfs.StreamCopier
func (s *streamCopier) Close() error {
// only record the close here, what we do actually depends on whether
// the streamCopier is instantiated server-side or client-side
s.mtx.Lock()
defer s.mtx.Unlock()
if s.closeStreamOnClose {
return s.streamConn.Close()
}
return nil
}
+4 -19
View File
@@ -29,7 +29,6 @@ import (
"github.com/zrepl/zrepl/rpc/dataconn/timeoutconn" "github.com/zrepl/zrepl/rpc/dataconn/timeoutconn"
"github.com/zrepl/zrepl/transport" "github.com/zrepl/zrepl/transport"
"github.com/zrepl/zrepl/util/devnoop" "github.com/zrepl/zrepl/util/devnoop"
"github.com/zrepl/zrepl/zfs"
) )
func orDie(err error) { func orDie(err error) {
@@ -42,23 +41,9 @@ type readerStreamCopier struct{ io.Reader }
func (readerStreamCopier) Close() error { return nil } func (readerStreamCopier) Close() error { return nil }
type readerStreamCopierErr struct {
error
}
func (readerStreamCopierErr) IsReadError() bool { return false }
func (readerStreamCopierErr) IsWriteError() bool { return true }
func (c readerStreamCopier) WriteStreamTo(w io.Writer) zfs.StreamCopierError {
var buf [1 << 21]byte
_, err := io.CopyBuffer(w, c.Reader, buf[:])
// always assume write error
return readerStreamCopierErr{err}
}
type devNullHandler struct{} type devNullHandler struct{}
func (devNullHandler) Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, zfs.StreamCopier, error) { func (devNullHandler) Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, io.ReadCloser, error) {
var res pdu.SendRes var res pdu.SendRes
if args.devnoopReader { if args.devnoopReader {
return &res, readerStreamCopier{devnoop.Get()}, nil return &res, readerStreamCopier{devnoop.Get()}, nil
@@ -67,12 +52,12 @@ func (devNullHandler) Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, z
} }
} }
func (devNullHandler) Receive(ctx context.Context, r *pdu.ReceiveReq, stream zfs.StreamCopier) (*pdu.ReceiveRes, error) { func (devNullHandler) Receive(ctx context.Context, r *pdu.ReceiveReq, stream io.ReadCloser) (*pdu.ReceiveRes, error) {
var out io.Writer = os.Stdout var out io.Writer = os.Stdout
if args.devnoopWriter { if args.devnoopWriter {
out = devnoop.Get() out = devnoop.Get()
} }
err := stream.WriteStreamTo(out) _, err := io.Copy(out, stream)
var res pdu.ReceiveRes var res pdu.ReceiveRes
return &res, err return &res, err
} }
@@ -172,7 +157,7 @@ func client() {
req := pdu.SendReq{} req := pdu.SendReq{}
_, stream, err := client.ReqSend(ctx, &req) _, stream, err := client.ReqSend(ctx, &req)
orDie(err) orDie(err)
err = stream.WriteStreamTo(os.Stdout) _, err = io.Copy(os.Stdout, stream)
orDie(err) orDie(err)
case "recv": case "recv":
var r io.Reader = os.Stdin var r io.Reader = os.Stdin
+1 -2
View File
@@ -14,7 +14,6 @@ import (
"github.com/zrepl/zrepl/rpc/dataconn/base2bufpool" "github.com/zrepl/zrepl/rpc/dataconn/base2bufpool"
"github.com/zrepl/zrepl/rpc/dataconn/frameconn" "github.com/zrepl/zrepl/rpc/dataconn/frameconn"
"github.com/zrepl/zrepl/rpc/dataconn/heartbeatconn" "github.com/zrepl/zrepl/rpc/dataconn/heartbeatconn"
"github.com/zrepl/zrepl/zfs"
) )
type Logger = logger.Logger type Logger = logger.Logger
@@ -198,7 +197,7 @@ func (e ReadStreamError) Temporary() bool {
return false return false
} }
var _ zfs.StreamCopierError = &ReadStreamError{} var _ net.Error = &ReadStreamError{}
func (e ReadStreamError) IsReadError() bool { func (e ReadStreamError) IsReadError() bool {
return e.Kind != ReadStreamErrorKindWrite return e.Kind != ReadStreamErrorKindWrite
+45 -71
View File
@@ -14,7 +14,6 @@ import (
"github.com/zrepl/zrepl/rpc/dataconn/heartbeatconn" "github.com/zrepl/zrepl/rpc/dataconn/heartbeatconn"
"github.com/zrepl/zrepl/rpc/dataconn/timeoutconn" "github.com/zrepl/zrepl/rpc/dataconn/timeoutconn"
"github.com/zrepl/zrepl/zfs"
) )
type Conn struct { type Conn struct {
@@ -40,15 +39,7 @@ type Conn struct {
var readMessageSentinel = fmt.Errorf("read stream complete") var readMessageSentinel = fmt.Errorf("read stream complete")
type writeStreamToErrorUnknownState struct{} var errWriteStreamToErrorUnknownState = fmt.Errorf("dataconn read stream: connection is in unknown state")
func (e writeStreamToErrorUnknownState) Error() string {
return "dataconn read stream: connection is in unknown state"
}
func (e writeStreamToErrorUnknownState) IsReadError() bool { return true }
func (e writeStreamToErrorUnknownState) IsWriteError() bool { return false }
func Wrap(nc timeoutconn.Wire, sendHeartbeatInterval, peerTimeout time.Duration) *Conn { func Wrap(nc timeoutconn.Wire, sendHeartbeatInterval, peerTimeout time.Duration) *Conn {
hc := heartbeatconn.Wrap(nc, sendHeartbeatInterval, peerTimeout) hc := heartbeatconn.Wrap(nc, sendHeartbeatInterval, peerTimeout)
@@ -123,14 +114,28 @@ func (c *Conn) ReadStreamedMessage(ctx context.Context, maxSize uint32, frameTyp
} }
} }
type StreamReader struct {
*io.PipeReader
conn *Conn
closeConnOnClose bool
}
func (r *StreamReader) Close() error {
err := r.PipeReader.Close()
if r.closeConnOnClose {
r.conn.Close() // TODO error logging
}
return err
}
// WriteStreamTo reads a stream from Conn and writes it to w. // WriteStreamTo reads a stream from Conn and writes it to w.
func (c *Conn) ReadStreamInto(w io.Writer, frameType uint32) (err zfs.StreamCopierError) { func (c *Conn) ReadStream(frameType uint32, closeConnOnClose bool) (_ *StreamReader, err error) {
// if we are closed while writing, return that as an error // if we are closed while writing, return that as an error
if closeGuard, cse := c.closeState.RWEntry(); cse != nil { if closeGuard, cse := c.closeState.RWEntry(); cse != nil {
return cse return nil, cse
} else { } else {
defer func(err *zfs.StreamCopierError) { defer func(err *error) {
if closed := closeGuard.RWExit(); closed != nil { if closed := closeGuard.RWExit(); closed != nil {
*err = closed *err = closed
} }
@@ -138,18 +143,23 @@ func (c *Conn) ReadStreamInto(w io.Writer, frameType uint32) (err zfs.StreamCopi
} }
c.readMtx.Lock() c.readMtx.Lock()
defer c.readMtx.Unlock()
if !c.readClean { if !c.readClean {
return writeStreamToErrorUnknownState{} return nil, errWriteStreamToErrorUnknownState
} }
var rse *ReadStreamError = readStream(c.frameReads, c.hc, w, frameType)
c.readClean = isConnCleanAfterRead(rse)
// https://golang.org/doc/faq#nil_error r, w := io.Pipe()
if rse == nil { go func() {
return nil defer c.readMtx.Unlock()
} var err *ReadStreamError = readStream(c.frameReads, c.hc, w, frameType)
return rse if err != nil {
_ = w.CloseWithError(err) // doc guarantees that error will always be nil
} else {
w.Close()
}
c.readClean = isConnCleanAfterRead(err)
}()
return &StreamReader{PipeReader: r, conn: c, closeConnOnClose: closeConnOnClose}, nil
} }
func (c *Conn) WriteStreamedMessage(ctx context.Context, buf io.Reader, frameType uint32) (err error) { func (c *Conn) WriteStreamedMessage(ctx context.Context, buf io.Reader, frameType uint32) (err error) {
@@ -178,7 +188,7 @@ func (c *Conn) WriteStreamedMessage(ctx context.Context, buf io.Reader, frameTyp
return errConn return errConn
} }
func (c *Conn) SendStream(ctx context.Context, src zfs.StreamCopier, frameType uint32) (err error) { func (c *Conn) SendStream(ctx context.Context, stream io.ReadCloser, frameType uint32) (err error) {
// if we are closed while reading, return that as an error // if we are closed while reading, return that as an error
if closeGuard, cse := c.closeState.RWEntry(); cse != nil { if closeGuard, cse := c.closeState.RWEntry(); cse != nil {
@@ -197,49 +207,17 @@ func (c *Conn) SendStream(ctx context.Context, src zfs.StreamCopier, frameType u
return fmt.Errorf("dataconn send stream: connection is in unknown state") return fmt.Errorf("dataconn send stream: connection is in unknown state")
} }
// avoid io.Pipe if zfs.StreamCopier is an io.Reader errStream, errConn := writeStream(ctx, c.hc, stream, frameType)
var r io.Reader
var w *io.PipeWriter
streamCopierErrChan := make(chan zfs.StreamCopierError, 1)
if reader, ok := src.(io.Reader); ok {
r = reader
streamCopierErrChan <- nil
close(streamCopierErrChan)
} else {
r, w = io.Pipe()
go func() {
streamCopierErrChan <- src.WriteStreamTo(w)
w.Close()
}()
}
type writeStreamRes struct { c.writeClean = isConnCleanAfterWrite(errConn) // TODO correct?
errStream, errConn error
}
writeStreamErrChan := make(chan writeStreamRes, 1)
go func() {
var res writeStreamRes
res.errStream, res.errConn = writeStream(ctx, c.hc, r, frameType)
if w != nil {
_ = w.CloseWithError(res.errStream) // always returns nil
}
writeStreamErrChan <- res
}()
writeRes := <-writeStreamErrChan if errStream != nil {
streamCopierErr := <-streamCopierErrChan return errStream
c.writeClean = isConnCleanAfterWrite(writeRes.errConn) // TODO correct? } else if errConn != nil {
if streamCopierErr != nil && streamCopierErr.IsReadError() { return errConn
return streamCopierErr // something on our side is bad
} else {
if writeRes.errStream != nil {
return writeRes.errStream
} else if writeRes.errConn != nil {
return writeRes.errConn
}
// TODO combined error?
return streamCopierErr
} }
// TODO combined error?
return nil
} }
type closeState struct { type closeState struct {
@@ -248,17 +226,13 @@ type closeState struct {
type closeStateErrConnectionClosed struct{} type closeStateErrConnectionClosed struct{}
var _ zfs.StreamCopierError = (*closeStateErrConnectionClosed)(nil)
var _ error = (*closeStateErrConnectionClosed)(nil)
var _ net.Error = (*closeStateErrConnectionClosed)(nil) var _ net.Error = (*closeStateErrConnectionClosed)(nil)
func (e *closeStateErrConnectionClosed) Error() string { func (e *closeStateErrConnectionClosed) Error() string {
return "connection closed" return "connection closed"
} }
func (e *closeStateErrConnectionClosed) IsReadError() bool { return true } func (e *closeStateErrConnectionClosed) Timeout() bool { return false }
func (e *closeStateErrConnectionClosed) IsWriteError() bool { return true } func (e *closeStateErrConnectionClosed) Temporary() bool { return false }
func (e *closeStateErrConnectionClosed) Timeout() bool { return false }
func (e *closeStateErrConnectionClosed) Temporary() bool { return false }
func (s *closeState) CloseEntry() error { func (s *closeState) CloseEntry() error {
firstCloser := atomic.AddUint32(&s.closeCount, 1) == 1 firstCloser := atomic.AddUint32(&s.closeCount, 1) == 1
@@ -273,7 +247,7 @@ type closeStateEntry struct {
entryCount uint32 entryCount uint32
} }
func (s *closeState) RWEntry() (e *closeStateEntry, err zfs.StreamCopierError) { func (s *closeState) RWEntry() (e *closeStateEntry, err net.Error) {
entry := &closeStateEntry{s, atomic.LoadUint32(&s.closeCount)} entry := &closeStateEntry{s, atomic.LoadUint32(&s.closeCount)}
if entry.entryCount > 0 { if entry.entryCount > 0 {
return nil, &closeStateErrConnectionClosed{} return nil, &closeStateErrConnectionClosed{}
@@ -281,7 +255,7 @@ func (s *closeState) RWEntry() (e *closeStateEntry, err zfs.StreamCopierError) {
return entry, nil return entry, nil
} }
func (e *closeStateEntry) RWExit() zfs.StreamCopierError { func (e *closeStateEntry) RWExit() net.Error {
if atomic.LoadUint32(&e.entryCount) == e.entryCount { if atomic.LoadUint32(&e.entryCount) == e.entryCount {
// no calls to Close() while running rw operation // no calls to Close() while running rw operation
return nil return nil
+7 -7
View File
@@ -4,6 +4,7 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"io"
"net" "net"
"sync" "sync"
"sync/atomic" "sync/atomic"
@@ -20,7 +21,6 @@ import (
"github.com/zrepl/zrepl/rpc/versionhandshake" "github.com/zrepl/zrepl/rpc/versionhandshake"
"github.com/zrepl/zrepl/transport" "github.com/zrepl/zrepl/transport"
"github.com/zrepl/zrepl/util/envconst" "github.com/zrepl/zrepl/util/envconst"
"github.com/zrepl/zrepl/zfs"
) )
// Client implements the active side of a replication setup. // Client implements the active side of a replication setup.
@@ -82,22 +82,22 @@ func (c *Client) Close() {
// callers must ensure that the returned io.ReadCloser is closed // callers must ensure that the returned io.ReadCloser is closed
// TODO expose dataClient interface to the outside world // TODO expose dataClient interface to the outside world
func (c *Client) Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, zfs.StreamCopier, error) { func (c *Client) Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, io.ReadCloser, error) {
// TODO the returned sendStream may return a read error created by the remote side // TODO the returned sendStream may return a read error created by the remote side
res, streamCopier, err := c.dataClient.ReqSend(ctx, r) res, stream, err := c.dataClient.ReqSend(ctx, r)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
if streamCopier == nil { if stream == nil {
return res, nil, nil return res, nil, nil
} }
return res, streamCopier, nil return res, stream, nil
} }
func (c *Client) Receive(ctx context.Context, req *pdu.ReceiveReq, streamCopier zfs.StreamCopier) (*pdu.ReceiveRes, error) { func (c *Client) Receive(ctx context.Context, req *pdu.ReceiveReq, stream io.ReadCloser) (*pdu.ReceiveRes, error) {
return c.dataClient.ReqRecv(ctx, req, streamCopier) return c.dataClient.ReqRecv(ctx, req, stream)
} }
func (c *Client) ListFilesystems(ctx context.Context, in *pdu.ListFilesystemReq) (*pdu.ListFilesystemRes, error) { func (c *Client) ListFilesystems(ctx context.Context, in *pdu.ListFilesystemReq) (*pdu.ListFilesystemRes, error) {
+36 -6
View File
@@ -11,6 +11,39 @@ import (
"github.com/zrepl/zrepl/util/tcpsock" "github.com/zrepl/zrepl/util/tcpsock"
) )
type ipMapEntry struct {
ip net.IP
ident string
}
type ipMap struct {
entries []ipMapEntry
}
func ipMapFromConfig(clients map[string]string) (*ipMap, error) {
entries := make([]ipMapEntry, 0, len(clients))
for clientIPString, clientIdent := range clients {
clientIP := net.ParseIP(clientIPString)
if clientIP == nil {
return nil, errors.Errorf("cannot parse client IP %q", clientIPString)
}
if err := transport.ValidateClientIdentity(clientIdent); err != nil {
return nil, errors.Wrapf(err, "invalid client identity for IP %q", clientIPString)
}
entries = append(entries, ipMapEntry{clientIP, clientIdent})
}
return &ipMap{entries: entries}, nil
}
func (m *ipMap) Get(ip net.IP) (string, error) {
for _, e := range m.entries {
if e.ip.Equal(ip) {
return e.ident, nil
}
}
return "", errors.Errorf("no identity mapping for client IP %s", ip)
}
func TCPListenerFactoryFromConfig(c *config.Global, in *config.TCPServe) (transport.AuthenticatedListenerFactory, error) { func TCPListenerFactoryFromConfig(c *config.Global, in *config.TCPServe) (transport.AuthenticatedListenerFactory, error) {
clientMap, err := ipMapFromConfig(in.Clients) clientMap, err := ipMapFromConfig(in.Clients)
if err != nil { if err != nil {
@@ -36,13 +69,10 @@ func (f *TCPAuthListener) Accept(ctx context.Context) (*transport.AuthConn, erro
if err != nil { if err != nil {
return nil, err return nil, err
} }
clientAddr := &net.IPAddr{ clientIP := nc.RemoteAddr().(*net.TCPAddr).IP
IP: nc.RemoteAddr().(*net.TCPAddr).IP, clientIdent, err := f.clientMap.Get(clientIP)
Zone: nc.RemoteAddr().(*net.TCPAddr).Zone,
}
clientIdent, err := f.clientMap.Get(clientAddr)
if err != nil { if err != nil {
transport.GetLogger(ctx).WithField("ipaddr", clientAddr).Error("client IP not in client map") transport.GetLogger(ctx).WithField("ip", clientIP).Error("client IP not in client map")
nc.Close() nc.Close()
return nil, err return nil, err
} }
-166
View File
@@ -1,166 +0,0 @@
package tcp
import (
"bytes"
"fmt"
"net"
"sort"
"strings"
"github.com/pkg/errors"
"github.com/zrepl/zrepl/transport"
"golang.org/x/sys/unix"
)
type ipMapEntry struct {
subnet *net.IPNet
// ident is always not empty
ident string
// zone may be empty (e.g. for IPv4)
zone string
}
type ipMap struct {
entries []*ipMapEntry
}
func ipMapFromConfig(clients map[string]string) (*ipMap, error) {
entries := make([]*ipMapEntry, 0, len(clients))
for clientInput, clientIdent := range clients {
userIPMapEntry, err := newIPMapEntry(clientInput, clientIdent)
if err != nil {
return nil, errors.Wrapf(err, "cannot not parse %q:%q", clientInput, clientIdent)
}
entries = append(entries, userIPMapEntry)
}
sort.Sort(byPrefixlen(entries))
return &ipMap{entries: entries}, nil
}
func (m *ipMap) Get(ipAddr *net.IPAddr) (string, error) {
for _, e := range m.entries {
if e.zone != ipAddr.Zone {
continue
}
if e.subnet.Contains(ipAddr.IP) {
return zfsDatasetPathComponentCompatibleRepresentation(e.ident, ipAddr), nil
}
}
return "", errors.Errorf("no identity mapping for client IP: %s%%%s", ipAddr.IP, ipAddr.Zone)
}
var ipv6FullySpecifiedMask = bytes.Repeat([]byte{0xff}, net.IPv6len)
func newIPMapEntry(input string, ident string) (*ipMapEntry, error) {
ip := input
var zone string
ipZoneSplit := strings.SplitN(input, "%", 2)
if len(ipZoneSplit) > 1 {
ip = ipZoneSplit[0]
zone = ipZoneSplit[1]
}
_, subnet, err := net.ParseCIDR(ip)
if err != nil {
// expect full IP, no '*' placeholder expansion
if strings.Count(ident, "*") != 0 {
return nil, fmt.Errorf("non-CIDR matches must not contain '*' placeholder")
}
if err := transport.ValidateClientIdentity(ident); err != nil {
return nil, errors.Wrapf(err, "invalid client identity %q for IP %q", ident, ip)
}
parsedIP := net.ParseIP(ip)
if parsedIP == nil {
return nil, errors.Wrapf(err, "invalid client address %q", ip)
}
parsedIP = parsedIP.To16()
return &ipMapEntry{
subnet: &net.IPNet{
IP: parsedIP,
Mask: ipv6FullySpecifiedMask,
},
zone: zone,
ident: ident,
}, nil
}
// expect CIDR and '*' placeholder expansion
if strings.Count(ident, "*") != 1 {
err = fmt.Errorf("CIDRs require 1 IP placeholder")
return nil, errors.Wrapf(err, "invalid client identity %q for IP %q", ident, ip)
}
longestIPAddr := net.IPAddr{
IP: net.IP(bytes.Repeat([]byte{0xff}, net.IPv6len)),
Zone: strings.Repeat("i", unix.IFNAMSIZ),
}
longestIdent := zfsDatasetPathComponentCompatibleRepresentation(ident, &longestIPAddr)
if err := transport.ValidateClientIdentity(longestIdent); err != nil {
return nil, errors.Wrapf(err, "invalid client identity for IP %q", ip)
}
ones, _ := subnet.Mask.Size()
preExpansionAddrlen := len(subnet.IP) * 8
expanded := subnet.IP.To16()
postExpansionAddrlen := len(expanded) * 8
return &ipMapEntry{
subnet: &net.IPNet{
IP: expanded,
Mask: net.CIDRMask(postExpansionAddrlen-preExpansionAddrlen+ones, postExpansionAddrlen),
},
zone: zone,
ident: ident,
}, nil
}
func zfsDatasetPathComponentCompatibleRepresentation(identityWithWildcard string, addr *net.IPAddr) string {
// If a Zone exists we append it after the IP using a "-" because "%"
// is a zfs dataset forbidden char.
if addr.Zone != "" {
return strings.Replace(identityWithWildcard, "*", addr.IP.String()+"-"+addr.Zone, 1)
}
// newIPMapEntry validates that the line contains exactly one "*"
return strings.Replace(identityWithWildcard, "*", addr.IP.String(), 1)
}
func (e *ipMapEntry) String() string {
return fmt.Sprintf("&ipMapEntry{subnet=%q, ident=%q, zone=%q}", e.subnet.String(), e.ident, e.zone)
}
func (e *ipMapEntry) PrefixLen() int {
ones, bits := e.subnet.Mask.Size()
if bits != net.IPv6len*8 {
panic(fmt.Sprintf("impl error: we represent all addresses as 16byte internally ones=%v bits=%v: %s", ones, bits, e))
}
return ones
}
// newtype to support sorting by prefixlength
type byPrefixlen []*ipMapEntry
func (m byPrefixlen) Len() int { return len(m) }
func (m byPrefixlen) Less(i, j int) bool {
if m[i].PrefixLen() != m[j].PrefixLen() {
return m[i].PrefixLen() > m[j].PrefixLen()
}
addrCmp := bytes.Compare(m[i].subnet.IP.To16(), m[j].subnet.IP.To16())
if addrCmp != 0 {
return addrCmp < 0
}
return strings.Compare(m[i].zone, m[j].zone) < 0
}
func (m byPrefixlen) Swap(i, j int) { m[i], m[j] = m[j], m[i] }
-204
View File
@@ -1,204 +0,0 @@
package tcp
import (
"net"
"os"
"testing"
"github.com/kr/pretty"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestIPMap(t *testing.T) {
type testCaseExpect struct {
expectNoMapping bool
expectIdent string
}
type testCase struct {
name string
config map[string]string
expectInitErr bool
expect map[string]testCaseExpect
}
cases := []testCase{
{
name: "regular ips",
expectInitErr: false,
config: map[string]string{
"192.168.123.234": "ident1",
"192.168.20.23": "ident2",
},
expect: map[string]testCaseExpect{
"192.168.123.234": {expectIdent: "ident1"},
"192.168.20.23": {expectIdent: "ident2"},
"192.168.20.10": {expectNoMapping: true},
},
},
{
name: "full wildcard",
expectInitErr: false,
config: map[string]string{
"0.0.0.0/0": "*",
"0::/0": "*",
},
expect: map[string]testCaseExpect{
"10.123.234.24": {expectIdent: "10.123.234.24"},
// '[' and ']' are forbiddenin dataset names
"fe80::23:42": {expectIdent: "fe80::23:42"},
},
},
{
name: "longest prefix matching",
expectInitErr: false,
config: map[string]string{
"10.1.2.3": "specific-host",
"10.1.2.0/24": "subnet-one-two-*",
"10.1.1.0/24": "subnet-one-one-*",
"10.1.0.0/24": "subnet-one-zero-*",
"10.1.0.0/16": "subnet-one-*",
"fde4:8dba:82e1:1::1": "v6-48-1-specialhost",
"fde4:8dba:82e1::/48": "v6-48-*",
"fde4:8dba:82e1:1::/64": "v6-64-1-*",
"fde4:8dba:82e1:2::/64": "v6-64-2-*",
},
expect: map[string]testCaseExpect{
"10.1.2.3": {expectIdent: "specific-host"},
"10.1.2.1": {expectIdent: "subnet-one-two-10.1.2.1"},
"10.1.1.1": {expectIdent: "subnet-one-one-10.1.1.1"},
"10.1.0.23": {expectIdent: "subnet-one-zero-10.1.0.23"},
"10.1.3.1": {expectIdent: "subnet-one-10.1.3.1"},
"10.2.1.1": {expectNoMapping: true},
"fde4:8dba:82e1:1::1": {expectIdent: "v6-48-1-specialhost"},
"fde4:8dba:82e1:23::1": {expectIdent: "v6-48-fde4:8dba:82e1:23::1"},
"fde4:8dba:82e1:1::2": {expectIdent: "v6-64-1-fde4:8dba:82e1:1::2"},
"fde4:8dba:82e1:2::1": {expectIdent: "v6-64-2-fde4:8dba:82e1:2::1"},
"fde4:8dba:82e2::1": {expectNoMapping: true},
},
},
{
name: "different prefixes, mixed ipv4 ipv6, with interface ids",
expectInitErr: false,
config: map[string]string{
"192.168.23.0/24": "db-*",
"192.168.23.23": "db-twentythree",
"192.168.42.0/24": "web-*",
"10.1.4.0/24": "my-*-server",
"2001:0db8:85a3:0000:0000:8a2e:0370:7334": "aspecifichost",
"2001:0db8:85a3:0000:0000:8a2e:0370:7334%eth1": "aspecifichost",
"fe80::/16%eth1": "san-*",
"fde4:8dba:82e1::/64": "sub64-*",
},
expect: map[string]testCaseExpect{
"10.1.2.3": {expectNoMapping: true},
"192.168.23.1": {expectIdent: "db-192.168.23.1"},
"192.168.23.23": {expectIdent: "db-twentythree"},
"192.168.023.001": {expectIdent: "db-192.168.23.1"},
"10.1.4.5": {expectIdent: "my-10.1.4.5-server"},
// normalization
"192.168.42.1": {expectIdent: "web-192.168.42.1"},
"192.168.042.001": {expectIdent: "web-192.168.42.1"},
// v6 matching
"fe80::23:42%eth1": {expectIdent: "san-fe80::23:42-eth1"},
"fe80::23:42%eth2": {expectNoMapping: true},
// v6 subnet matching
"fde4:8dba:82e1::1": {expectIdent: "sub64-fde4:8dba:82e1::1"},
// v6 subnet matching with suffix that matches another allowed IPv4
"fde4:8dba:82e1::c0a8:1717": {expectIdent: "sub64-fde4:8dba:82e1::c0a8:1717"},
"2001:0db8:85a3:0000:0000:8a2e:0370:7334": {expectIdent: "aspecifichost"},
"2001:0db8:85a3:0000:0000:8a2e:0370:7334%eth1": {expectIdent: "aspecifichost"},
"2001:0db8:85a3:0000:0000:8a2e:0370:7334%eth2": {expectNoMapping: true},
},
},
{
name: "invalid user input: non ip or cidr",
expectInitErr: true,
config: map[string]string{
"jimmy": "db",
},
},
{
name: "invalid user input: v4 ip with an identity containing *",
expectInitErr: true,
config: map[string]string{
"192.168.1.2": "db-*",
},
},
{
name: "invalid user input: v4 subnet without an identity containing *",
expectInitErr: true,
config: map[string]string{
"192.168.1.0/24": "db-",
},
},
{
name: "invalid user input: v6 ip with an identity containing *",
expectInitErr: true,
config: map[string]string{
"2001:0db8:85a3:0000:0000:8a2e:0370:7334": "aspecifichost*",
},
},
{
name: "invalid user input: v6 subnet without identity containing *",
expectInitErr: true,
config: map[string]string{
"fe80::/16%eth1": "db-",
},
},
{
name: "invalid user input with subnet match: client identity with forbidden zfs dataset name char @",
expectInitErr: true,
config: map[string]string{
"fe80::/16": "db@-*",
},
},
{
name: "invalid user input with IP match: client identity with forbidden zfs dataset name char @",
expectInitErr: true,
config: map[string]string{
"fe80::1": "db@foo",
},
},
}
for i := range cases {
c := cases[i]
t.Run(c.name, func(t *testing.T) {
pretty.Fprintf(os.Stderr, "running %#v\n", c)
m, err := ipMapFromConfig(c.config)
if c.expectInitErr {
require.Error(t, err)
} else {
require.NoError(t, err)
require.NotNil(t, m)
}
for input, expect := range c.expect {
// reuse newIPMapEntry to parse test case input
// "test" is not used during testing but must not be empty.
ipMapEntry, _ := newIPMapEntry(input, "test")
ones, bits := ipMapEntry.subnet.Mask.Size()
require.Equal(t, bits, net.IPv6len*8, "and we know ipMapEntry always expands its IPs to 16bytes")
require.Equal(t, ones, net.IPv6len*8, "test case addresses must be fully specified")
require.NotNil(t, ipMapEntry)
ident, err := m.Get(&net.IPAddr{
IP: ipMapEntry.subnet.IP,
Zone: ipMapEntry.zone,
})
if expect.expectNoMapping {
assert.Empty(t, ident)
} else {
assert.NoError(t, err)
assert.Equal(t, expect.expectIdent, ident)
}
}
})
}
}
func TestPackageNetAssumptions(t *testing.T) {
}
+7 -4
View File
@@ -4,10 +4,10 @@ package transport
import ( import (
"context" "context"
"errors"
"net" "net"
"syscall" "syscall"
"github.com/pkg/errors"
"github.com/zrepl/zrepl/logger" "github.com/zrepl/zrepl/logger"
"github.com/zrepl/zrepl/rpc/dataconn/timeoutconn" "github.com/zrepl/zrepl/rpc/dataconn/timeoutconn"
"github.com/zrepl/zrepl/zfs" "github.com/zrepl/zrepl/zfs"
@@ -55,10 +55,13 @@ type Connecter interface {
} }
// A client identity must be a single component in a ZFS filesystem path // A client identity must be a single component in a ZFS filesystem path
func ValidateClientIdentity(in string) error { func ValidateClientIdentity(in string) (err error) {
err := zfs.ComponentNamecheck(in) path, err := zfs.NewDatasetPath(in)
if err != nil { if err != nil {
return errors.Wrap(err, "client identity must be usable as a single dataset path component") return err
}
if path.Length() != 1 {
return errors.New("client identity must be a single path component (not empty, no '/')")
} }
return nil return nil
} }
@@ -0,0 +1,39 @@
package bytecounter
import (
"io"
"sync/atomic"
)
// ReadCloser wraps an io.ReadCloser, reimplementing
// its interface and counting the bytes written to during copying.
type ReadCloser interface {
io.ReadCloser
Count() int64
}
// NewReadCloser wraps rc.
func NewReadCloser(rc io.ReadCloser) ReadCloser {
return &readCloser{rc, 0}
}
type readCloser struct {
rc io.ReadCloser
count int64
}
func (r *readCloser) Count() int64 {
return atomic.LoadInt64(&r.count)
}
var _ io.ReadCloser = &readCloser{}
func (r *readCloser) Close() error {
return r.rc.Close()
}
func (r *readCloser) Read(p []byte) (int, error) {
n, err := r.rc.Read(p)
atomic.AddInt64(&r.count, int64(n))
return n, err
}
-49
View File
@@ -1,49 +0,0 @@
package bytecounter
import (
"io"
"sync/atomic"
"time"
)
type ByteCounterReader struct {
reader io.ReadCloser
// called & accessed synchronously during Read, no external access
cb func(full int64)
cbEvery time.Duration
lastCbAt time.Time
// set atomically because it may be read by multiple threads
bytes int64
}
func NewByteCounterReader(reader io.ReadCloser) *ByteCounterReader {
return &ByteCounterReader{
reader: reader,
}
}
func (b *ByteCounterReader) SetCallback(every time.Duration, cb func(full int64)) {
b.cbEvery = every
b.cb = cb
}
func (b *ByteCounterReader) Close() error {
return b.reader.Close()
}
func (b *ByteCounterReader) Read(p []byte) (n int, err error) {
n, err = b.reader.Read(p)
full := atomic.AddInt64(&b.bytes, int64(n))
now := time.Now()
if b.cb != nil && now.Sub(b.lastCbAt) > b.cbEvery {
b.cb(full)
b.lastCbAt = now
}
return n, err
}
func (b *ByteCounterReader) Bytes() int64 {
return atomic.LoadInt64(&b.bytes)
}
@@ -1,71 +0,0 @@
package bytecounter
import (
"io"
"sync/atomic"
"github.com/zrepl/zrepl/zfs"
)
// StreamCopier wraps a zfs.StreamCopier, reimplementing
// its interface and counting the bytes written to during copying.
type StreamCopier interface {
zfs.StreamCopier
Count() int64
}
// NewStreamCopier wraps sc into a StreamCopier.
// If sc is io.Reader, it is guaranteed that the returned StreamCopier
// implements that interface, too.
func NewStreamCopier(sc zfs.StreamCopier) StreamCopier {
bsc := &streamCopier{sc, 0}
if scr, ok := sc.(io.Reader); ok {
return streamCopierAndReader{bsc, scr}
} else {
return bsc
}
}
type streamCopier struct {
sc zfs.StreamCopier
count int64
}
// proxy writer used by streamCopier
type streamCopierWriter struct {
parent *streamCopier
w io.Writer
}
func (w streamCopierWriter) Write(p []byte) (n int, err error) {
n, err = w.w.Write(p)
atomic.AddInt64(&w.parent.count, int64(n))
return
}
func (s *streamCopier) Count() int64 {
return atomic.LoadInt64(&s.count)
}
var _ zfs.StreamCopier = &streamCopier{}
func (s streamCopier) Close() error {
return s.sc.Close()
}
func (s *streamCopier) WriteStreamTo(w io.Writer) zfs.StreamCopierError {
ww := streamCopierWriter{s, w}
return s.sc.WriteStreamTo(ww)
}
// a streamCopier whose underlying sc is an io.Reader
type streamCopierAndReader struct {
*streamCopier
asReader io.Reader
}
func (scr streamCopierAndReader) Read(p []byte) (int, error) {
n, err := scr.asReader.Read(p)
atomic.AddInt64(&scr.streamCopier.count, int64(n))
return n, err
}
@@ -1,39 +0,0 @@
package bytecounter
import (
"io"
"testing"
"github.com/stretchr/testify/assert"
"github.com/zrepl/zrepl/zfs"
)
type mockStreamCopierAndReader struct {
zfs.StreamCopier // to satisfy interface
reads int
}
func (r *mockStreamCopierAndReader) Read(p []byte) (int, error) {
r.reads++
return len(p), nil
}
var _ io.Reader = &mockStreamCopierAndReader{}
func TestNewStreamCopierReexportsReader(t *testing.T) {
mock := &mockStreamCopierAndReader{}
x := NewStreamCopier(mock)
r, ok := x.(io.Reader)
if !ok {
t.Fatalf("%T does not implement io.Reader, hence reader cannout have been wrapped", x)
}
var buf [23]byte
n, err := r.Read(buf[:])
assert.True(t, mock.reads == 1)
assert.True(t, n == len(buf))
assert.NoError(t, err)
assert.True(t, x.Count() == 23)
}
+43
View File
@@ -0,0 +1,43 @@
package chainedio
import "io"
type ChainedReadCloser struct {
readers []io.Reader
curReader int
}
func NewChainedReader(reader ...io.Reader) *ChainedReadCloser {
return &ChainedReadCloser{
readers: reader,
curReader: 0,
}
}
func (c *ChainedReadCloser) Read(buf []byte) (n int, err error) {
n = 0
for c.curReader < len(c.readers) {
n, err = c.readers[c.curReader].Read(buf)
if err == io.EOF {
c.curReader++
continue
}
break
}
if c.curReader == len(c.readers) {
err = io.EOF // actually, there was no gap
}
return
}
func (c *ChainedReadCloser) Close() error {
for _, r := range c.readers {
if c, ok := r.(io.Closer); ok {
c.Close() // TODO debug log error?
}
}
return nil
}
-34
View File
@@ -1,34 +0,0 @@
package chainedio
import "io"
type ChainedReader struct {
Readers []io.Reader
curReader int
}
func NewChainedReader(reader ...io.Reader) *ChainedReader {
return &ChainedReader{
Readers: reader,
curReader: 0,
}
}
func (c *ChainedReader) Read(buf []byte) (n int, err error) {
n = 0
for c.curReader < len(c.Readers) {
n, err = c.Readers[c.curReader].Read(buf)
if err == io.EOF {
c.curReader++
continue
}
break
}
if c.curReader == len(c.Readers) {
err = io.EOF // actually, there was no gap
}
return
}
+5
View File
@@ -40,3 +40,8 @@ func (l *L) DropWhile(f func()) {
defer l.Unlock().Lock() defer l.Unlock().Lock()
f() f()
} }
func (l *L) HoldWhile(f func()) {
defer l.Lock().Unlock()
f()
}
+61 -91
View File
@@ -354,63 +354,6 @@ func (a ZFSSendArgsUnvalidated) buildCommonSendArgs() ([]string, error) {
return args, nil return args, nil
} }
type ReadCloserCopier struct {
recorder readErrRecorder
}
type readErrRecorder struct {
io.ReadCloser
readErr error
}
type sendStreamCopierError struct {
isReadErr bool // if false, it's a write error
err error
}
func (e sendStreamCopierError) Error() string {
if e.isReadErr {
return fmt.Sprintf("stream: read error: %s", e.err)
} else {
return fmt.Sprintf("stream: writer error: %s", e.err)
}
}
func (e sendStreamCopierError) IsReadError() bool { return e.isReadErr }
func (e sendStreamCopierError) IsWriteError() bool { return !e.isReadErr }
func (r *readErrRecorder) Read(p []byte) (n int, err error) {
n, err = r.ReadCloser.Read(p)
r.readErr = err
return n, err
}
func NewReadCloserCopier(stream io.ReadCloser) *ReadCloserCopier {
return &ReadCloserCopier{recorder: readErrRecorder{stream, nil}}
}
func (c *ReadCloserCopier) WriteStreamTo(w io.Writer) StreamCopierError {
debug("sendStreamCopier.WriteStreamTo: begin")
_, err := io.Copy(w, &c.recorder)
debug("sendStreamCopier.WriteStreamTo: copy done")
if err != nil {
if c.recorder.readErr != nil {
return sendStreamCopierError{isReadErr: true, err: c.recorder.readErr}
} else {
return sendStreamCopierError{isReadErr: false, err: err}
}
}
return nil
}
func (c *ReadCloserCopier) Read(p []byte) (n int, err error) {
return c.recorder.Read(p)
}
func (c *ReadCloserCopier) Close() error {
return c.recorder.ReadCloser.Close()
}
func pipeWithCapacityHint(capacity int) (r, w *os.File, err error) { func pipeWithCapacityHint(capacity int) (r, w *os.File, err error) {
if capacity <= 0 { if capacity <= 0 {
panic(fmt.Sprintf("capacity must be positive %v", capacity)) panic(fmt.Sprintf("capacity must be positive %v", capacity))
@@ -423,7 +366,7 @@ func pipeWithCapacityHint(capacity int) (r, w *os.File, err error) {
return stdoutReader, stdoutWriter, nil return stdoutReader, stdoutWriter, nil
} }
type sendStream struct { type SendStream struct {
cmd *zfscmd.Cmd cmd *zfscmd.Cmd
kill context.CancelFunc kill context.CancelFunc
@@ -433,7 +376,7 @@ type sendStream struct {
opErr error opErr error
} }
func (s *sendStream) Read(p []byte) (n int, err error) { func (s *SendStream) Read(p []byte) (n int, err error) {
s.closeMtx.Lock() s.closeMtx.Lock()
opErr := s.opErr opErr := s.opErr
s.closeMtx.Unlock() s.closeMtx.Unlock()
@@ -454,12 +397,12 @@ func (s *sendStream) Read(p []byte) (n int, err error) {
return n, err return n, err
} }
func (s *sendStream) Close() error { func (s *SendStream) Close() error {
debug("sendStream: close called") debug("sendStream: close called")
return s.killAndWait(nil) return s.killAndWait(nil)
} }
func (s *sendStream) killAndWait(precedingReadErr error) error { func (s *SendStream) killAndWait(precedingReadErr error) error {
debug("sendStream: killAndWait enter") debug("sendStream: killAndWait enter")
defer debug("sendStream: killAndWait leave") defer debug("sendStream: killAndWait leave")
@@ -830,7 +773,7 @@ var ErrEncryptedSendNotSupported = fmt.Errorf("raw sends which are required for
// (if from is "" a full ZFS send is done) // (if from is "" a full ZFS send is done)
// //
// Returns ErrEncryptedSendNotSupported if encrypted send is requested but not supported by CLI // Returns ErrEncryptedSendNotSupported if encrypted send is requested but not supported by CLI
func ZFSSend(ctx context.Context, sendArgs ZFSSendArgsValidated) (*ReadCloserCopier, error) { func ZFSSend(ctx context.Context, sendArgs ZFSSendArgsValidated) (*SendStream, error) {
args := make([]string, 0) args := make([]string, 0)
args = append(args, "send") args = append(args, "send")
@@ -879,14 +822,14 @@ func ZFSSend(ctx context.Context, sendArgs ZFSSendArgsValidated) (*ReadCloserCop
// close our writing-end of the pipe so that we don't wait for ourselves when reading from the reading end // close our writing-end of the pipe so that we don't wait for ourselves when reading from the reading end
stdoutWriter.Close() stdoutWriter.Close()
stream := &sendStream{ stream := &SendStream{
cmd: cmd, cmd: cmd,
kill: cancel, kill: cancel,
stdoutReader: stdoutReader, stdoutReader: stdoutReader,
stderrBuf: stderrBuf, stderrBuf: stderrBuf,
} }
return NewReadCloserCopier(stream), nil return stream, nil
} }
type DrySendType string type DrySendType string
@@ -1025,24 +968,6 @@ func ZFSSendDry(ctx context.Context, sendArgs ZFSSendArgsValidated) (_ *DrySendI
return &si, nil return &si, nil
} }
type StreamCopierError interface {
error
IsReadError() bool
IsWriteError() bool
}
type StreamCopier interface {
// WriteStreamTo writes the stream represented by this StreamCopier
// to the given io.Writer.
WriteStreamTo(w io.Writer) StreamCopierError
// Close must be called as soon as it is clear that no more data will
// be read from the StreamCopier.
// If StreamCopier gets its data from a connection, it might hold
// a lock on the connection until Close is called. Only closing ensures
// that the connection can be used afterwards.
Close() error
}
type RecvOptions struct { type RecvOptions struct {
// Rollback to the oldest snapshot, destroy it, then perform `recv -F`. // Rollback to the oldest snapshot, destroy it, then perform `recv -F`.
// Note that this doesn't change property values, i.e. an existing local property value will be kept. // Note that this doesn't change property values, i.e. an existing local property value will be kept.
@@ -1067,7 +992,9 @@ func (e *ErrRecvResumeNotSupported) Error() string {
return buf.String() return buf.String()
} }
func ZFSRecv(ctx context.Context, fs string, v *ZFSSendArgVersion, streamCopier StreamCopier, opts RecvOptions) (err error) { const RecvStderrBufSiz = 1 << 15
func ZFSRecv(ctx context.Context, fs string, v *ZFSSendArgVersion, stream io.ReadCloser, opts RecvOptions) (err error) {
if err := v.ValidateInMemory(fs); err != nil { if err := v.ValidateInMemory(fs); err != nil {
return errors.Wrap(err, "invalid version") return errors.Wrap(err, "invalid version")
@@ -1134,7 +1061,7 @@ func ZFSRecv(ctx context.Context, fs string, v *ZFSSendArgVersion, streamCopier
// cannot receive new filesystem stream: invalid backup stream // cannot receive new filesystem stream: invalid backup stream
stdout := bytes.NewBuffer(make([]byte, 0, 1024)) stdout := bytes.NewBuffer(make([]byte, 0, 1024))
stderr := bytes.NewBuffer(make([]byte, 0, 1024)) stderr := bytes.NewBuffer(make([]byte, 0, RecvStderrBufSiz))
stdin, stdinWriter, err := pipeWithCapacityHint(ZFSRecvPipeCapacityHint) stdin, stdinWriter, err := pipeWithCapacityHint(ZFSRecvPipeCapacityHint)
if err != nil { if err != nil {
@@ -1162,9 +1089,10 @@ func ZFSRecv(ctx context.Context, fs string, v *ZFSSendArgVersion, streamCopier
debug("started") debug("started")
copierErrChan := make(chan StreamCopierError) copierErrChan := make(chan error)
go func() { go func() {
copierErrChan <- streamCopier.WriteStreamTo(stdinWriter) _, err := io.Copy(stdinWriter, stream)
copierErrChan <- err
stdinWriter.Close() stdinWriter.Close()
}() }()
waitErrChan := make(chan error) waitErrChan := make(chan error)
@@ -1173,6 +1101,10 @@ func ZFSRecv(ctx context.Context, fs string, v *ZFSSendArgVersion, streamCopier
if err = cmd.Wait(); err != nil { if err = cmd.Wait(); err != nil {
if rtErr := tryRecvErrorWithResumeToken(ctx, stderr.String()); rtErr != nil { if rtErr := tryRecvErrorWithResumeToken(ctx, stderr.String()); rtErr != nil {
waitErrChan <- rtErr waitErrChan <- rtErr
} else if owErr := tryRecvDestroyOrOverwriteEncryptedErr(stderr.Bytes()); owErr != nil {
waitErrChan <- owErr
} else if readErr := tryRecvCannotReadFromStreamErr(stderr.Bytes()); readErr != nil {
waitErrChan <- readErr
} else { } else {
waitErrChan <- &ZFSError{ waitErrChan <- &ZFSError{
Stderr: stderr.Bytes(), Stderr: stderr.Bytes(),
@@ -1183,22 +1115,23 @@ func ZFSRecv(ctx context.Context, fs string, v *ZFSSendArgVersion, streamCopier
} }
}() }()
// streamCopier always fails before or simultaneously with Wait
// thus receive from it first
copierErr := <-copierErrChan copierErr := <-copierErrChan
debug("copierErr: %T %s", copierErr, copierErr) debug("copierErr: %T %s", copierErr, copierErr)
if copierErr != nil { if copierErr != nil {
debug("killing zfs recv command after copierErr")
cancelCmd() cancelCmd()
} }
waitErr := <-waitErrChan waitErr := <-waitErrChan
debug("waitErr: %T %s", waitErr, waitErr) debug("waitErr: %T %s", waitErr, waitErr)
if copierErr == nil && waitErr == nil { if copierErr == nil && waitErr == nil {
return nil return nil
} else if waitErr != nil && (copierErr == nil || copierErr.IsWriteError()) { } else if _, isReadErr := waitErr.(*RecvCannotReadFromStreamErr); isReadErr {
return waitErr // has more interesting info in that case return copierErr // likely network error reading from stream
} else {
return waitErr // almost always more interesting info. NOTE: do not wrap!
} }
return copierErr // if it's not a write error, the copier error is more interesting
} }
type RecvFailedWithResumeTokenErr struct { type RecvFailedWithResumeTokenErr struct {
@@ -1228,6 +1161,43 @@ func (e *RecvFailedWithResumeTokenErr) Error() string {
return fmt.Sprintf("receive failed, resume token available: %s\n%#v", e.ResumeTokenRaw, e.ResumeTokenParsed) return fmt.Sprintf("receive failed, resume token available: %s\n%#v", e.ResumeTokenRaw, e.ResumeTokenParsed)
} }
type RecvDestroyOrOverwriteEncryptedErr struct {
Msg string
}
func (e *RecvDestroyOrOverwriteEncryptedErr) Error() string {
return e.Msg
}
var recvDestroyOrOverwriteEncryptedErrRe = regexp.MustCompile(`^(cannot receive new filesystem stream: zfs receive -F cannot be used to destroy an encrypted filesystem or overwrite an unencrypted one with an encrypted one)`)
func tryRecvDestroyOrOverwriteEncryptedErr(stderr []byte) *RecvDestroyOrOverwriteEncryptedErr {
debug("tryRecvDestroyOrOverwriteEncryptedErr: %v", stderr)
m := recvDestroyOrOverwriteEncryptedErrRe.FindSubmatch(stderr)
if m == nil {
return nil
}
return &RecvDestroyOrOverwriteEncryptedErr{Msg: string(m[1])}
}
type RecvCannotReadFromStreamErr struct {
Msg string
}
func (e *RecvCannotReadFromStreamErr) Error() string {
return e.Msg
}
var reRecvCannotReadFromStreamErr = regexp.MustCompile(`^(cannot receive: failed to read from stream)$`)
func tryRecvCannotReadFromStreamErr(stderr []byte) *RecvCannotReadFromStreamErr {
m := reRecvCannotReadFromStreamErr.FindSubmatch(stderr)
if m == nil {
return nil
}
return &RecvCannotReadFromStreamErr{Msg: string(m[1])}
}
type ClearResumeTokenError struct { type ClearResumeTokenError struct {
ZFSOutput []byte ZFSOutput []byte
CmdError error CmdError error
+11
View File
@@ -2,9 +2,11 @@ package zfs
import ( import (
"context" "context"
"strings"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestZFSListHandlesProducesZFSErrorOnNonZeroExit(t *testing.T) { func TestZFSListHandlesProducesZFSErrorOnNonZeroExit(t *testing.T) {
@@ -259,3 +261,12 @@ size 10518512
}) })
} }
} }
func TestTryRecvDestroyOrOverwriteEncryptedErr(t *testing.T) {
msg := "cannot receive new filesystem stream: zfs receive -F cannot be used to destroy an encrypted filesystem or overwrite an unencrypted one with an encrypted one\n"
assert.GreaterOrEqual(t, RecvStderrBufSiz, len(msg))
err := tryRecvDestroyOrOverwriteEncryptedErr([]byte(msg))
require.NotNil(t, err)
assert.EqualError(t, err, strings.TrimSpace(msg))
}
+35
View File
@@ -5,11 +5,14 @@ import (
"bytes" "bytes"
"context" "context"
"io" "io"
"os"
"os/exec" "os/exec"
"strings"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/zrepl/zrepl/util/circlog"
) )
const testBin = "./zfscmd_platform_test.bash" const testBin = "./zfscmd_platform_test.bash"
@@ -85,5 +88,37 @@ func TestCmdProcessState(t *testing.T) {
require.True(t, ok) require.True(t, ok)
require.NotNil(t, ee.ProcessState) require.NotNil(t, ee.ProcessState)
require.Contains(t, ee.Error(), "killed") require.Contains(t, ee.Error(), "killed")
}
func TestSigpipe(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
cmd := CommandContext(ctx, "bash", "-c", "sleep 5; echo invalid input; exit 23")
r, w, err := os.Pipe()
require.NoError(t, err)
output := circlog.MustNewCircularLog(1 << 20)
cmd.SetStdio(Stdio{
Stdin: r,
Stdout: output,
Stderr: output,
})
err = cmd.Start()
require.NoError(t, err)
err = r.Close()
require.NoError(t, err)
// the script doesn't read stdin, but this input is almost certainly smaller than the pipe buffer
const LargerThanPipeBuffer = 1 << 21
_, err = io.Copy(w, bytes.NewBuffer(bytes.Repeat([]byte("i"), LargerThanPipeBuffer)))
// => io.Copy is going to block because the pipe buffer is full and the
// script is not reading from it
// => the script is going to exit after 5s
// => we should expect a broken pipe error from the copier's perspective
t.Logf("copy err = %T: %s", err, err)
require.NotNil(t, err)
require.True(t, strings.Contains(err.Error(), "broken pipe"))
err = cmd.Wait()
require.EqualError(t, err, "exit status 23")
} }