libzfs_sendrecv.c revision 54a91118eee5bfd63eb614a44e1b68f1571a99ea
/*
* CDDL HEADER START
*
* The contents of this file are subject to the terms of the
* Common Development and Distribution License (the "License").
* You may not use this file except in compliance with the License.
*
* You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
* See the License for the specific language governing permissions
* and limitations under the License.
*
* When distributing Covered Code, include this CDDL HEADER in each
* file and include the License file at usr/src/OPENSOLARIS.LICENSE.
* If applicable, add the following below this CDDL HEADER, with the
* fields enclosed by brackets "[]" replaced with your own identifying
* information: Portions Copyright [yyyy] [name of copyright owner]
*
* CDDL HEADER END
*/
/*
* Copyright 2010 Sun Microsystems, Inc. All rights reserved.
* Use is subject to license terms.
*/
#include <assert.h>
#include <ctype.h>
#include <errno.h>
#include <libintl.h>
#include <stdio.h>
#include <stdlib.h>
#include <strings.h>
#include <unistd.h>
#include <stddef.h>
#include <fcntl.h>
#include <pthread.h>
#include <umem.h>
#include <libzfs.h>
#include "zfs_namecheck.h"
#include "zfs_prop.h"
#include "zfs_fletcher.h"
#include "libzfs_impl.h"
#include <sha2.h>
#include <sys/zio_checksum.h>
/* in libzfs_dataset.c */
int, avl_tree_t *, char **);
static const zio_cksum_t zero_cksum = { 0 };
typedef struct dedup_arg {
int inputfd;
int outputfd;
} dedup_arg_t;
typedef struct dataref {
} dataref_t;
typedef struct dedup_entry {
struct dedup_entry *dde_next;
#define MAX_DDT_PHYSMEM_PERCENT 20
#define SMALLEST_POSSIBLE_MAX_DDT_MB 128
typedef struct dedup_table {
int numhashbits;
static int
{
int count;
n >>= 1;
return (count);
}
static size_t
{
return (0);
return (outlen);
}
static void
{
"Dedup table full. Deduplication will continue "
"with existing table entries"));
}
return;
}
!= NULL) {
}
}
/*
* Using the specified dedup table, do a lookup for an entry with
* the checksum cs. If found, return the block's reference info
* in *dr. Otherwise, insert a new entry in the dedup table, using
* the reference information specified by *dr.
*
* return value: true - entry was found
* false - entry was not found
*/
static boolean_t
{
return (B_TRUE);
}
}
return (B_FALSE);
}
static int
{
}
/*
* This function is started in a separate thread when the dedup option
* has been requested. The main send thread determines the list of
* snapshots to be included in the send stream and makes the ioctl calls
* for each one. But instead of having the ioctl send the output to the
* the output fd specified by the caller of zfs_send()), the
* ioctl is told to direct the output to a pipe, which is read by the
* alternate thread running THIS function. This function does the
* dedup'ing by:
* 1. building a dedup table (the DDT)
* 2. doing checksums on each data block and inserting a record in the DDT
* 3. looking for matching checksums, and
* 4. sending a DRR_WRITE_BYREF record instead of a write record whenever
* a duplicate block is found.
* The output of this function then goes to the output fd requested
* by the caller of zfs_send().
*/
static void *
{
int outfd;
dmu_replay_record_t wbr_drr = {0};
/*
* numbuckets must be a power of 2. Increase number to
* a power of 2 if necessary.
*/
if (!ISP2(numbuckets))
/* Initialize the write-by-reference block. */
wbr_drr.drr_payloadlen = 0;
case DRR_BEGIN:
{
int fflags;
ZIO_SET_CHECKSUM(&stream_cksum, 0, 0, 0, 0);
/* set the DEDUP feature flag for this stream */
goto out;
}
perror("fread");
outfd) == -1)
goto out;
}
break;
}
case DRR_END:
{
/* use the recalculated checksum */
sizeof (dmu_replay_record_t))) == -1)
goto out;
break;
}
case DRR_OBJECT:
{
goto out;
if (drro->drr_bonuslen > 0) {
ofp);
if (cksum_and_write(buf,
goto out;
}
break;
}
case DRR_FREEOBJECTS:
{
goto out;
break;
}
case DRR_WRITE:
{
/*
* Use the existing checksum if it's dedup-capable,
* else calculate a SHA256 checksum for it.
*/
zero_cksum) ||
SHA256Init(&ctx);
}
&dataref)) {
/* block already present in stream */
if (cksum_and_write(&wbr_drr,
sizeof (dmu_replay_record_t), &stream_cksum,
outfd) == -1)
goto out;
} else {
/* block not previously seen */
if (cksum_and_write(drr,
sizeof (dmu_replay_record_t), &stream_cksum,
outfd) == -1)
goto out;
if (cksum_and_write(buf,
goto out;
}
break;
}
case DRR_FREE:
{
goto out;
break;
}
default:
(void) printf("INVALID record type 0x%x\n",
/* should never happen, so assert */
}
}
out:
return (NULL);
}
/*
* Routines for dealing with the AVL tree of fs-nvlists
*/
typedef struct fsavl_node {
char *fn_snapname;
} fsavl_node_t;
static int
{
return (+1);
return (-1);
else
return (0);
}
/*
* Given the GUID of a snapshot, find its containing filesystem and
* (optionally) name.
*/
static nvlist_t *
{
if (fn) {
if (snapname)
}
return (NULL);
}
static void
{
void *cookie;
return;
}
/*
* Given an nvlist, produce an avl tree of snapshots, ordered by guid
*/
static avl_tree_t *
{
return (NULL);
while ((snapelem =
return (NULL);
}
/*
* Note: if there are multiple snaps with the
* same GUID, we ignore all but one.
*/
else
}
}
return (fsavl);
}
/*
* Routines for dealing with the giant nvlist of fs-nvlists, etc.
*/
typedef struct send_data {
const char *fromsnap;
const char *tosnap;
/*
* The header nvlist is of the following format:
* {
* "tosnap" -> string
* "fromsnap" -> string (if incremental)
* "fss" -> {
* id -> {
*
* "name" -> string (full name; for debugging)
* "parentfromsnap" -> number (guid of fromsnap in parent)
*
* "props" -> { name -> value (only if set here) }
* "snaps" -> { name (lastname) -> number (guid) }
* "snapprops" -> { name (lastname) -> { name -> value } }
*
* "origin" -> number (guid) (if clone)
* "sent" -> boolean (not on-disk)
* }
* }
* }
*
*/
} send_data_t;
static int
{
char *snapname;
/*
* NB: if there is no fromsnap here (it's a newly created fs in
* an incremental replication), we will substitute the tosnap.
*/
}
return (0);
}
static void
{
if (!zfs_prop_user(propname)) {
/*
* Realistically, this should never happen. However,
* we want the ability to add DSL properties without
* needing to make incompatible version changes. We
* need to ignore unknown properties to allow older
* software to still send datasets containing these
* properties, with the unknown properties elided.
*/
if (prop == ZPROP_INVAL)
continue;
if (zfs_prop_readonly(prop))
continue;
}
prop == ZFS_PROP_REFQUOTA ||
prop == ZFS_PROP_REFRESERVATION) {
char *source;
ZPROP_VALUE, &value) == 0);
continue;
/*
* May have no source before SPA_VERSION_RECVD_PROPS,
* but is still modifiable.
*/
ZPROP_SOURCE, &source) == 0) {
ZPROP_SOURCE_VAL_RECVD) != 0))
continue;
}
} else {
char *source;
ZPROP_SOURCE, &source) != 0)
continue;
continue;
}
if (zfs_prop_user(propname) ||
char *value;
ZPROP_VALUE, &value) == 0);
} else {
ZPROP_VALUE, &value) == 0);
}
}
}
/*
* recursively generate nvlists describing datasets. See comment
* for the data structure send_data_t above for description of contents
* of the nvlist.
*/
static int
{
int rv = 0;
char guidstring[64];
return (-1);
}
/* iterate over props */
/* iterate over snaps, and set sd->parent_fromsnap_guid */
sd->parent_fromsnap_guid = 0;
/* add this fs to nvlist */
/* iterate over children */
return (rv);
}
static int
{
send_data_t sd = { 0 };
int error;
return (EZFS_BADTYPE);
return (error);
}
return (EZFS_NOMEM);
}
return (0);
}
/*
* Routines for dealing with the sorted snapshot functionality
*/
typedef struct zfs_node {
} zfs_node_t;
static int
{
return (0);
}
/* ARGSUSED */
static int
{
/*
* Sort them according to creation time. We use the hidden
* CREATETXG property to get an absolute ordering of snapshots.
*/
return (-1);
return (+1);
else
return (0);
}
int
{
int ret = 0;
avl_destroy(&avl);
return (ret);
}
/*
* Routines specific to "zfs send"
*/
typedef struct send_dump_data {
/* these are all just the short snapname (the part after the @) */
const char *fromsnap;
const char *tosnap;
char prevsnap[ZFS_MAXNAMELEN];
int outfd;
void *filter_cb_arg;
/*
* Dumps a backup of the given snapshot (incremental from fromsnap if it's not
* NULL) to the file descriptor specified by outfd.
*/
static int
{
if (fromsnap)
*got_enoent = B_FALSE;
char errbuf[1024];
switch (errno) {
case EXDEV:
"not an earlier snapshot from the same fs"));
case ENOENT:
if (enoent_ok) {
*got_enoent = B_TRUE;
return (0);
}
"incremental source (@%s) does not exist"),
}
case EDQUOT:
case EFBIG:
case EIO:
case ENOLINK:
case ENOSPC:
case ENOSTR:
case ENXIO:
case EPIPE:
case ERANGE:
case EFAULT:
case EROFS:
default:
}
}
return (0);
}
static int
{
const char *thissnap;
int err;
return (0);
}
return (0);
}
/*
* If a filter function exists, call it to determine whether
* this snapshot will be sent.
*/
/*
* This snapshot is filtered out. Don't send it, and don't
* set prevsnap, so it will be as if this snapshot didn't
* exist, and the next accepted snapshot will be sent as
* an incremental from the last accepted one, or as the
* first (and full) snapshot in the case of a replication,
* non-incremental send.
*/
return (0);
}
/* send it */
}
if (got_enoent)
err = 0;
else
return (err);
}
static int
{
int rv = 0;
"could not send %s@%s: does not exist\n",
return (0);
}
/*
* If this fs does not have fromsnap, and we're doing
* recursive, we need to send a full stream from the
* beginning (or an incremental from the origin if this
* is a clone). If we're doing non-recursive, then let
* them get the error.
*/
ZFS_IOC_OBJSET_STATS, &zc) != 0) {
}
}
"WARNING: could not send %s@%s:\n"
"incremental source (%s@%s) does not exist\n",
"WARNING: could not send %s@%s:\n"
"incremental source (%s@%s) "
"is not earlier than it\n",
} else {
"could not send %s@%s: does not exist\n",
}
}
} else {
char snapname[ZFS_MAXNAMELEN];
rv = -1;
} else {
B_TRUE) {
}
}
}
return (rv);
}
static int
{
char *fsname;
int err;
uint64_t origin_guid = 0;
continue;
if (origin_nv &&
/*
* origin has not been sent yet;
* skip this clone.
*/
continue;
}
return (-1);
if (err)
return (err);
}
if (needagain) {
goto again;
}
return (0);
}
/*
* Generate a send stream for the dataset identified by the argument zhp.
*
* The content of the send stream is the snapshot identified by
* 'tosnap'. Incremental streams are requested in two ways:
* - from the snapshot identified by "fromsnap" (if non-null) or
* - from the origin of the dataset identified by zhp, which must
* be a clone. In this case, "fromsnap" is null and "fromorigin"
* is TRUE.
*
* The send stream is recursive (i.e. dumps a hierarchy of snapshots) and
* uses a special header (with a hdrtype field of DMU_COMPOUNDSTREAM)
* if "replicate" is set. If "doall" is set, dump all the intermediate
* snapshots. The DMU_COMPOUNDSTREAM header is used in the "doall"
* case too. If "props" is set, send properties.
*/
int
void *cb_arg)
{
char errbuf[1024];
send_dump_data_t sdd = { 0 };
int err;
char holdtag[128];
int spa_version;
int pipefd[2];
dedup_arg_t dda = { 0 };
int featureflags = 0;
"zero-length incremental source"));
}
errbuf));
}
}
}
dmu_replay_record_t drr = { 0 };
zio_cksum_t zc = { 0 };
if (holdsnaps) {
++holdseq;
cb_arg);
if (err)
goto err_out;
}
if (fromsnap) {
"fromsnap", fromsnap));
}
"not_recursive"));
}
if (err) {
if (holdsnaps) {
}
goto err_out;
}
NV_ENCODE_XDR, 0);
if (err) {
if (holdsnaps) {
}
goto stderr_out;
}
}
/* write first begin record */
/* write header nvlist */
}
if (err == -1) {
if (holdsnaps) {
}
goto stderr_out;
}
/* write end record */
if (err != -1) {
if (err == -1) {
if (holdsnaps) {
}
goto stderr_out;
}
}
}
/* dump each stream */
else
}
/*
* write final end record. NB: want to do this even if
* there was some error, because it might not be totally
* failed.
*/
dmu_replay_record_t drr = { 0 };
if (holdsnaps) {
}
}
}
(void) pthread_cancel(tid);
}
return (err);
}
/*
* Routines specific to "zfs recv"
*/
static int
{
int rv;
do {
} while (rv > 0);
"failed to read from stream"));
"cannot receive")));
}
if (zc) {
if (byteswap)
else
}
return (0);
}
static int
{
char *buf;
int err;
return (ENOMEM);
if (err != 0) {
return (err);
}
if (err != 0) {
"stream (malformed nvlist)"));
return (EINVAL);
}
return (0);
}
static int
{
static int seq;
int err;
return (-1);
return (-1);
if (err)
return (err);
if (tryname) {
(void) printf("attempting rename %s to %s\n",
}
if (err == 0)
} else {
}
seq++;
(void) printf("failed - trying rename %s to %s\n",
}
if (err == 0)
(void) printf("failed (%u) - "
"will try again on next pass\n", errno);
}
if (err == 0)
(void) printf("success\n");
else
}
(void) changelist_postfix(clp);
return (err);
}
static int
{
int err = 0;
int spa_version;
return (-1);
return (-1);
if (err)
return (err);
if (err == 0) {
(void) printf("success\n");
}
(void) changelist_postfix(clp);
/*
* Deferred destroy might destroy the snapshot or only mark it to be
* destroyed later, and it returns success in either case.
*/
ZFS_TYPE_SNAPSHOT))) {
}
return (err);
}
typedef struct guid_to_name_data {
char *name;
static int
{
int err;
return (EEXIST);
}
return (err);
}
static int
char *name)
{
/* exhaustive search all local snapshots */
int err = 0;
char *cp;
return (0);
}
}
if (cp)
*cp = '\0';
if (cp)
*cp = '/';
if (zhp) {
}
}
/*
* Return true if dataset guid1 is created before guid2.
*/
static int
{
char buf[ZFS_MAXNAMELEN];
int rv;
if (guid2 == 0)
return (0);
if (guid1 == 0)
return (1);
return (-1);
return (-1);
}
return (rv);
}
static int
{
char newname[ZFS_MAXNAMELEN];
int error;
ENOENT);
return (0);
return (error);
/*
* Process deletes and renames
*/
uint64_t originguid = 0;
char *fsname, *stream_fsname;
/*
* First find the stream's fs, so we can check for
* a different origin (due to "zfs promote")
*/
if (stream_nvfs != NULL)
break;
}
/* check for promote */
case 1: {
/* promote it! */
char *origin_fsname;
NULL);
"name", &origin_fsname));
if (error == 0)
break;
}
default:
break;
case -1:
return (-1);
}
/*
* list of snapshots is wrong. Need to handle
* them on the next pass.
*/
continue;
}
char *stream_snapname;
/* check for delete */
char name[ZFS_MAXNAMELEN];
continue;
if (error)
else
continue;
}
stream_nvfs = found;
stream_snapname, &props)) {
props) == 0) {
ZFS_IOC_SET_PROP, &zc);
}
}
/* check for different snapname */
stream_snapname) != 0) {
char name[ZFS_MAXNAMELEN];
char tryname[ZFS_MAXNAMELEN];
if (error)
else
}
}
/* check for delete */
if (stream_nvfs == NULL) {
continue;
if (error)
else
continue;
}
(void) printf("local fs %s does not have fromsnap "
"(%s in stream); must have been deleted locally; "
continue;
}
"name", &stream_fsname));
"parentfromsnap", &stream_parent_fromsnap_guid));
/* check for rename */
if ((stream_parent_fromsnap_guid != 0 &&
char tryname[ZFS_MAXNAMELEN];
/*
* NB: parent might not be found if we used the
* tosnap for stream_parent_fromsnap_guid,
* because the parent is a newly-created fs;
* we'll be able to rename it after we recv the
* new fs.
*/
char *pname;
&pname));
} else {
tryname[0] = '\0';
(void) printf("local fs %s new parent "
"not found\n", fsname);
}
}
if (error)
else
}
}
/* do another pass to fix up temporary names */
(void) printf("another pass:\n");
goto again;
}
return (needagain);
}
static int
char **top_zfs)
{
char tofs[ZFS_MAXNAMELEN];
char errbuf[1024];
int error;
"cannot receive"));
"can not specify snapshot name for multi-snapshot stream"));
}
/*
* Read in the nvlist from the stream.
*/
if (drr->drr_payloadlen != 0) {
if (error) {
goto out;
}
}
/*
* Read in the end record and verify checksum.
*/
goto out;
}
goto out;
}
"incorrect header checksum"));
goto out;
}
if (drr->drr_payloadlen != 0) {
&stream_fss));
"couldn't allocate avl tree"));
goto out;
}
"/@");
/* zfs_receive_one() will create_parents() */
}
}
}
/* Finally, receive each contained stream */
do {
/*
* we should figure out if it has a recoverable
* error, in which case do a recv_skip() and drive on.
* Note, if we fail due to already having this guid,
* zfs_receive_one() will take care of it (ie,
* recv_skip() and return 0).
*/
error = 0;
break;
}
} while (error == 0);
/*
* Now that we have the fs's they sent us, try the
* renames again.
*/
}
out:
if (stream_nv)
if (softerr)
error = -2;
if (anyerr)
error = -1;
return (error);
}
static void
trunc_prop_errs(int truncated)
{
if (truncated == 1)
"1 more property could not be set\n"));
else
"%d more properties could not be set\n"), truncated);
}
static int
{
char errbuf[1024];
"cannot receive:"));
/* XXX would be great to use lseek if possible... */
if (byteswap)
case DRR_BEGIN:
/* NB: not to be used on v2 stream packages */
if (drr->drr_payloadlen != 0) {
"invalid substream header"));
}
break;
case DRR_END:
return (0);
case DRR_OBJECT:
if (byteswap) {
}
break;
case DRR_WRITE:
if (byteswap) {
}
break;
case DRR_WRITE_BYREF:
case DRR_FREEOBJECTS:
case DRR_FREE:
break;
default:
"invalid record type"));
}
}
return (-1);
}
/*
* Restores a backup of tosnap from the file descriptor specified by infd.
*/
static int
char **top_zfs)
{
char *cp;
char errbuf[1024];
char prop_errbuf[1024];
char chopprefix[ZFS_MAXNAMELEN];
uint64_t parent_snapguid = 0;
"cannot receive"));
if (stream_avl != NULL) {
char *snapname;
&snapname);
int ret;
if (err)
if (flags.canmountoff) {
}
if (err)
snapname, &snapprops_nvlist));
}
if (ret != 0)
return (-1);
}
/*
* Determine how much of the snapshot name stored in the stream
* we are going to tack on to the name they specified on the
* command line, and how much we are going to chop off.
*
* If they specified a snapshot, chop the entire name stored in
* the stream.
*/
/*
* They specified a fs with -d or -e. We want to tack on
* everything but the first element of the sent snapshot path
* (all but the pool name) in the case of -d, or only the tail
* of the sent snapshot path in the case of -e.
*/
"argument - snapshot not allowed with %s"),
}
*cp = '\0';
/*
* If they specified a filesystem without -d or -e, we want to
* tack on everything after the fs specified in the first name
* from the stream.
*/
*cp = '\0';
}
/*
* Determine name of destination snapshot, store in zc_value.
*/
}
/*
* Determine the name of the origin snapshot, store in zc_string.
*/
"local origin for clone %s does not exist"),
}
}
if (stream_wantsnewfs) {
/*
* if the parent fs does not exist, look for it based on
* the parent snap GUID
*/
"cannot receive new filesystem stream"));
if (cp)
*cp = '\0';
if (cp &&
char suffix[ZFS_MAXNAMELEN];
}
}
} else {
/*
* if the fs does not exist, look for it based on the
* fromsnap GUID
*/
"cannot receive incremental stream"));
char snap[ZFS_MAXNAMELEN];
}
}
}
/*
* Destination fs exists. Therefore this should either
* be an incremental, or the stream specifies a new fs
* (full stream or clone) and they want us to blow it
* away (and have therefore specified -F and removed any
* snapshots).
*/
if (stream_wantsnewfs) {
"destination '%s' exists\n"
"must specify -F to overwrite it"),
}
&zc) == 0) {
"destination has snapshots (eg. %s)\n"
"must destroy them to overwrite it"),
}
}
return (-1);
}
if (stream_wantsnewfs &&
"destination '%s' is a clone\n"
"must destroy it to overwrite it"),
}
/* We can't do online recv in this case */
return (-1);
}
if (changelist_prefix(clp) != 0) {
return (-1);
}
}
} else {
/*
* Destination filesystem does not exist. Therefore we better
* be creating a new filesystem (either from a full backup, or
* a clone). It would therefore be invalid if the user
* specified only the pool name (i.e. if the destination name
* contained no slash character).
*/
if (!stream_wantsnewfs ||
}
/*
* Trim off the final dataset component so we perform the
* recvbackup ioctl to the filesystems's parent.
*/
*cp = '\0';
}
}
(void) printf("%s %s stream of %s into %s\n",
}
}
ioctl_errno = errno;
if (err == 0) {
char tbuf[1024];
int intval;
ZPROP_N_MORE_ERRORS) == 0) {
break;
} else {
"cannot receive %s property on %s"),
}
}
}
zc.zc_nvlist_dst = 0;
zc.zc_nvlist_dst_size = 0;
if (err == 0 && snapprops_nvlist) {
}
}
/*
* It may be that this snapshot already exists,
* in which case we want to consume & ignore it
* rather than failing.
*/
/*
* XXX Do this faster by just iterating over snaps in
* this fs. Also if zc_value does not exist, we will
* get a strange "does not exist" error message.
*/
*cp = '\0';
*cp = '@';
(void) printf("snap %s already exists; "
}
}
}
*cp = '@';
}
if (ioctl_err != 0) {
switch (ioctl_errno) {
case ENODEV:
*cp = '\0';
"most recent snapshot of %s does not\n"
*cp = '@';
break;
case ETXTBSY:
"destination %s has been modified\n"
break;
case EEXIST:
if (newfs) {
/* it's the containing fs that exists */
*cp = '\0';
}
"destination already exists"));
*cp = '@';
break;
case EINVAL:
break;
case ECKSUM:
"invalid stream (checksum mismatch)"));
break;
default:
}
}
/*
* Mount the target filesystem (if created). Also mount any
* children of the target filesystem if we did a replication
* receive (indicated by stream_avl being non-NULL).
*/
zfs_handle_t *h;
*cp = '\0';
if (h != NULL) {
if (h->zfs_type == ZFS_TYPE_VOLUME) {
*cp = '@';
} else if (newfs || stream_avl) {
/*
* for mounting and sharing later.
*/
}
zfs_close(h);
}
*cp = '@';
}
if (clp) {
}
if (prop_errflags & ZPROP_ERR_NOCLEAR) {
"failed to clear unreceived properties on %s"),
}
if (prop_errflags & ZPROP_ERR_NORESTORE) {
"failed to restore original properties on %s"),
}
return (-1);
char buf1[64];
char buf2[64];
if (delta == 0)
delta = 1;
}
return (0);
}
static int
{
int err;
char errbuf[1024];
zio_cksum_t zcksum = { 0 };
int hdrtype;
"cannot receive"));
"(%s) does not exist"), tosnap);
}
/* read in the BEGIN record */
&zcksum)))
return (err);
/* It's the double end record at the end of a package */
return (ENODATA);
}
/* the kernel needs the non-byteswapped begin record */
drr_noswap = drr;
/*
* We computed the checksum in the wrong byteorder in
* recv_read() above; do it again correctly.
*/
}
"stream (bad magic number)"));
}
if (!DMU_STREAM_SUPPORTED(featureflags) ||
"stream has unsupported feature, feature flags = %lx"),
}
"stream (bad snapshot name)"));
}
} else { /* must be DMU_COMPOUNDSTREAM */
}
}
/*
* Restores a backup of tosnap from the file descriptor specified by infd.
* Return 0 on total success, -2 if some things couldn't be
* (-1 will override -2).
*/
int
{
int err;
/* mount and share received datasets */
}
}
err = -1;
}
if (top_zfs)
return (err);
}