]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
librbd: generalized deep copy function
authorMykola Golub <to.my.trociny@gmail.com>
Sat, 14 Oct 2017 15:58:11 +0000 (18:58 +0300)
committerMykola Golub <to.my.trociny@gmail.com>
Mon, 6 Nov 2017 08:29:44 +0000 (10:29 +0200)
(based on rbd-mirror image sync)

Signed-off-by: Mykola Golub <mgolub@mirantis.com>
35 files changed:
src/librbd/CMakeLists.txt
src/librbd/DeepCopyRequest.cc [new file with mode: 0644]
src/librbd/DeepCopyRequest.h [new file with mode: 0644]
src/librbd/Types.h
src/librbd/api/Image.cc
src/librbd/api/Image.h
src/librbd/deep_copy/ImageCopyRequest.cc [new file with mode: 0644]
src/librbd/deep_copy/ImageCopyRequest.h [new file with mode: 0644]
src/librbd/deep_copy/MetadataCopyRequest.cc [new file with mode: 0644]
src/librbd/deep_copy/MetadataCopyRequest.h [new file with mode: 0644]
src/librbd/deep_copy/ObjectCopyRequest.cc [new file with mode: 0644]
src/librbd/deep_copy/ObjectCopyRequest.h [new file with mode: 0644]
src/librbd/deep_copy/SetHeadRequest.cc [new file with mode: 0644]
src/librbd/deep_copy/SetHeadRequest.h [new file with mode: 0644]
src/librbd/deep_copy/SnapshotCopyRequest.cc [new file with mode: 0644]
src/librbd/deep_copy/SnapshotCopyRequest.h [new file with mode: 0644]
src/librbd/deep_copy/SnapshotCreateRequest.cc [new file with mode: 0644]
src/librbd/deep_copy/SnapshotCreateRequest.h [new file with mode: 0644]
src/librbd/deep_copy/Types.h [new file with mode: 0644]
src/librbd/deep_copy/Utils.cc [new file with mode: 0644]
src/librbd/deep_copy/Utils.h [new file with mode: 0644]
src/librbd/journal/Types.h
src/librbd/operation/SnapshotRemoveRequest.h
src/test/librbd/CMakeLists.txt
src/test/librbd/deep_copy/test_mock_ImageCopyRequest.cc [new file with mode: 0644]
src/test/librbd/deep_copy/test_mock_MetadataCopyRequest.cc [new file with mode: 0644]
src/test/librbd/deep_copy/test_mock_ObjectCopyRequest.cc [new file with mode: 0644]
src/test/librbd/deep_copy/test_mock_SetHeadRequest.cc [new file with mode: 0644]
src/test/librbd/deep_copy/test_mock_SnapshotCopyRequest.cc [new file with mode: 0644]
src/test/librbd/deep_copy/test_mock_SnapshotCreateRequest.cc [new file with mode: 0644]
src/test/librbd/mock/MockImageCtx.h
src/test/librbd/test_DeepCopy.cc [new file with mode: 0644]
src/test/librbd/test_main.cc
src/test/librbd/test_mock_DeepCopyRequest.cc [new file with mode: 0644]
src/test/rbd_mirror/image_replayer/test_mock_EventPreprocessor.cc

index 12d585021f77969f64950dfa02cad071e47fcd11..fc32f7faaa70a40c7da5ee6a5996f6a05fe135be 100644 (file)
@@ -7,6 +7,7 @@ add_library(rbd_types STATIC
 set(librbd_internal_srcs
   AsyncObjectThrottle.cc
   AsyncRequest.cc
+  DeepCopyRequest.cc
   ExclusiveLock.cc
   ImageCtx.cc
   ImageState.cc
@@ -27,6 +28,13 @@ set(librbd_internal_srcs
   api/Mirror.cc
   cache/ImageWriteback.cc
   cache/PassthroughImageCache.cc
+  deep_copy/ImageCopyRequest.cc
+  deep_copy/MetadataCopyRequest.cc
+  deep_copy/ObjectCopyRequest.cc
+  deep_copy/SetHeadRequest.cc
+  deep_copy/SnapshotCopyRequest.cc
+  deep_copy/SnapshotCreateRequest.cc
+  deep_copy/Utils.cc
   exclusive_lock/AutomaticPolicy.cc
   exclusive_lock/PreAcquireRequest.cc
   exclusive_lock/PostAcquireRequest.cc
diff --git a/src/librbd/DeepCopyRequest.cc b/src/librbd/DeepCopyRequest.cc
new file mode 100644 (file)
index 0000000..268a459
--- /dev/null
@@ -0,0 +1,374 @@
+// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*-
+// vim: ts=8 sw=2 smarttab
+
+#include "DeepCopyRequest.h"
+#include "common/errno.h"
+#include "librbd/ExclusiveLock.h"
+#include "librbd/ImageCtx.h"
+#include "librbd/ObjectMap.h"
+#include "librbd/Operations.h"
+#include "librbd/Utils.h"
+#include "librbd/deep_copy/ImageCopyRequest.h"
+#include "librbd/deep_copy/MetadataCopyRequest.h"
+#include "librbd/deep_copy/SetHeadRequest.h"
+#include "librbd/deep_copy/SnapshotCopyRequest.h"
+#include "librbd/internal.h"
+
+#define dout_subsys ceph_subsys_rbd
+#undef dout_prefix
+#define dout_prefix *_dout << "librbd::DeepCopyRequest: " \
+                           << this << " " << __func__ << ": "
+
+namespace librbd {
+
+using namespace librbd::deep_copy;
+
+using librbd::util::create_context_callback;
+using librbd::util::create_rados_callback;
+using librbd::util::unique_lock_name;
+
+template <typename I>
+DeepCopyRequest<I>::DeepCopyRequest(I *src_image_ctx, I *dst_image_ctx,
+                                    librados::snap_t snap_id_start,
+                                    librados::snap_t snap_id_end,
+                                    const ObjectNumber &object_number,
+                                    ContextWQ *work_queue, SnapSeqs *snap_seqs,
+                                    ProgressContext *prog_ctx,
+                                    Context *on_finish)
+  : RefCountedObject(dst_image_ctx->cct, 1), m_src_image_ctx(src_image_ctx),
+    m_dst_image_ctx(dst_image_ctx), m_snap_id_start(snap_id_start),
+    m_snap_id_end(snap_id_end), m_object_number(object_number),
+    m_work_queue(work_queue), m_snap_seqs(snap_seqs), m_prog_ctx(prog_ctx),
+    m_on_finish(on_finish), m_cct(dst_image_ctx->cct),
+    m_lock(unique_lock_name("DeepCopyRequest::m_lock", this)) {
+}
+
+template <typename I>
+DeepCopyRequest<I>::~DeepCopyRequest() {
+  assert(m_snapshot_copy_request == nullptr);
+  assert(m_image_copy_request == nullptr);
+}
+
+template <typename I>
+void DeepCopyRequest<I>::send() {
+  int r = validate_copy_points();
+  if (r < 0) {
+    finish(r);
+    return;
+  }
+
+  send_copy_snapshots();
+}
+
+template <typename I>
+void DeepCopyRequest<I>::cancel() {
+  Mutex::Locker locker(m_lock);
+
+  ldout(m_cct, 20) << dendl;
+
+  m_canceled = true;
+
+  if (m_snapshot_copy_request != nullptr) {
+    m_snapshot_copy_request->cancel();
+  }
+
+  if (m_image_copy_request != nullptr) {
+    m_image_copy_request->cancel();
+  }
+}
+
+template <typename I>
+void DeepCopyRequest<I>::send_copy_snapshots() {
+  m_lock.Lock();
+  if (m_canceled) {
+    m_lock.Unlock();
+    finish(-ECANCELED);
+    return;
+  }
+
+  ldout(m_cct, 20) << dendl;
+
+  Context *ctx = create_context_callback<
+    DeepCopyRequest<I>, &DeepCopyRequest<I>::handle_copy_snapshots>(this);
+  m_snapshot_copy_request = SnapshotCopyRequest<I>::create(
+    m_src_image_ctx, m_dst_image_ctx, m_work_queue, m_snap_seqs, ctx);
+  m_snapshot_copy_request->get();
+  m_lock.Unlock();
+
+  m_snapshot_copy_request->send();
+}
+
+template <typename I>
+void DeepCopyRequest<I>::handle_copy_snapshots(int r) {
+  ldout(m_cct, 20) << "r=" << r << dendl;
+
+  {
+    Mutex::Locker locker(m_lock);
+    m_snapshot_copy_request->put();
+    m_snapshot_copy_request = nullptr;
+    if (r == 0 && m_canceled) {
+      r = -ECANCELED;
+    }
+  }
+
+  if (r == -ECANCELED) {
+    ldout(m_cct, 10) << "snapshot copy canceled" << dendl;
+    finish(r);
+    return;
+  } else if (r < 0) {
+    lderr(m_cct) << "failed to copy snapshot metadata: " << cpp_strerror(r)
+                 << dendl;
+    finish(r);
+    return;
+  }
+
+  send_set_head();
+}
+
+template <typename I>
+void DeepCopyRequest<I>::send_set_head() {
+  if (m_snap_id_end != CEPH_NOSNAP) {
+    send_copy_image();
+    return;
+  }
+
+  ldout(m_cct, 20) << dendl;
+
+  uint64_t size;
+  ParentSpec parent_spec;
+  uint64_t parent_overlap;
+  {
+    RWLock::RLocker src_locker(m_src_image_ctx->snap_lock);
+    size = m_src_image_ctx->size;
+    parent_spec = m_src_image_ctx->parent_md.spec;
+    parent_overlap = m_src_image_ctx->parent_md.overlap;
+  }
+
+  auto ctx = create_context_callback<
+    DeepCopyRequest<I>, &DeepCopyRequest<I>::handle_set_head>(this);
+  auto req = SetHeadRequest<I>::create(m_dst_image_ctx, size, parent_spec,
+                                       parent_overlap, ctx);
+  req->send();
+}
+
+template <typename I>
+void DeepCopyRequest<I>::handle_set_head(int r) {
+  ldout(m_cct, 20) << "r=" << r << dendl;
+
+  {
+    Mutex::Locker locker(m_lock);
+    if (r == 0 && m_canceled) {
+      r = -ECANCELED;
+    }
+  }
+
+  if (r < 0) {
+    if (r != -ECANCELED) {
+      lderr(m_cct) << "failed to set head: " << cpp_strerror(r) << dendl;
+    }
+    finish(r);
+    return;
+  }
+
+  send_copy_image();
+}
+
+template <typename I>
+void DeepCopyRequest<I>::send_copy_image() {
+  m_lock.Lock();
+  if (m_canceled) {
+    m_lock.Unlock();
+    finish(-ECANCELED);
+    return;
+  }
+
+  ldout(m_cct, 20) << dendl;
+
+  Context *ctx = create_context_callback<
+    DeepCopyRequest<I>, &DeepCopyRequest<I>::handle_copy_image>(this);
+  m_image_copy_request = ImageCopyRequest<I>::create(
+      m_src_image_ctx, m_dst_image_ctx, m_snap_id_start, m_snap_id_end,
+      m_object_number, *m_snap_seqs, m_prog_ctx, ctx);
+  m_image_copy_request->get();
+  m_lock.Unlock();
+
+  m_image_copy_request->send();
+}
+
+template <typename I>
+void DeepCopyRequest<I>::handle_copy_image(int r) {
+  ldout(m_cct, 20) << "r=" << r << dendl;
+
+  {
+    Mutex::Locker locker(m_lock);
+    m_image_copy_request->put();
+    m_image_copy_request = nullptr;
+    if (r == 0 && m_canceled) {
+      r = -ECANCELED;
+    }
+  }
+
+  if (r == -ECANCELED) {
+    ldout(m_cct, 10) << "image copy canceled" << dendl;
+    finish(r);
+    return;
+  } else if (r < 0) {
+    lderr(m_cct) << "failed to copy image: " << cpp_strerror(r) << dendl;
+    finish(r);
+    return;
+  }
+
+  send_copy_object_map();
+}
+
+template <typename I>
+void DeepCopyRequest<I>::send_copy_object_map() {
+  m_dst_image_ctx->owner_lock.get_read();
+  m_dst_image_ctx->snap_lock.get_read();
+
+  if (!m_dst_image_ctx->test_features(RBD_FEATURE_OBJECT_MAP,
+                                      m_dst_image_ctx->snap_lock)) {
+    m_dst_image_ctx->snap_lock.put_read();
+    m_dst_image_ctx->owner_lock.put_read();
+    send_copy_metadata();
+    return;
+  }
+  if (m_snap_id_end == CEPH_NOSNAP) {
+    m_dst_image_ctx->snap_lock.put_read();
+    m_dst_image_ctx->owner_lock.put_read();
+    send_refresh_object_map();
+    return;
+  }
+
+  assert(m_dst_image_ctx->object_map != nullptr);
+
+  ldout(m_cct, 20) << dendl;
+
+  Context *finish_op_ctx = nullptr;
+  if (m_dst_image_ctx->exclusive_lock != nullptr) {
+    finish_op_ctx = m_dst_image_ctx->exclusive_lock->start_op();
+  }
+  if (finish_op_ctx == nullptr) {
+    lderr(m_cct) << "lost exclusive lock" << dendl;
+    m_dst_image_ctx->snap_lock.put_read();
+    m_dst_image_ctx->owner_lock.put_read();
+    finish(-EROFS);
+    return;
+  }
+
+  // rollback the object map (copy snapshot object map to HEAD)
+  RWLock::WLocker object_map_locker(m_dst_image_ctx->object_map_lock);
+  auto ctx = new FunctionContext([this, finish_op_ctx](int r) {
+      handle_copy_object_map(r);
+      finish_op_ctx->complete(0);
+    });
+  assert(m_snap_seqs->count(m_snap_id_end) > 0);
+  librados::snap_t copy_snap_id = (*m_snap_seqs)[m_snap_id_end];
+  m_dst_image_ctx->object_map->rollback(copy_snap_id, ctx);
+  m_dst_image_ctx->snap_lock.put_read();
+  m_dst_image_ctx->owner_lock.put_read();
+}
+
+template <typename I>
+void DeepCopyRequest<I>::handle_copy_object_map(int r) {
+  ldout(m_cct, 20) << dendl;
+
+  assert(r == 0);
+  send_refresh_object_map();
+}
+
+template <typename I>
+void DeepCopyRequest<I>::send_refresh_object_map() {
+  Context *finish_op_ctx = nullptr;
+  {
+    RWLock::RLocker owner_locker(m_dst_image_ctx->owner_lock);
+    if (m_dst_image_ctx->exclusive_lock != nullptr) {
+      finish_op_ctx = m_dst_image_ctx->exclusive_lock->start_op();
+    }
+  }
+  if (finish_op_ctx == nullptr) {
+    lderr(m_cct) << "lost exclusive lock" << dendl;
+    finish(-EROFS);
+    return;
+  }
+
+  ldout(m_cct, 20) << dendl;
+
+  auto ctx = new FunctionContext([this, finish_op_ctx](int r) {
+      handle_refresh_object_map(r);
+      finish_op_ctx->complete(0);
+    });
+  m_object_map = m_dst_image_ctx->create_object_map(CEPH_NOSNAP);
+  m_object_map->open(ctx);
+}
+
+template <typename I>
+void DeepCopyRequest<I>::handle_refresh_object_map(int r) {
+  ldout(m_cct, 20) << "r=" << r << dendl;
+
+  assert(r == 0);
+  {
+    RWLock::WLocker snap_locker(m_dst_image_ctx->snap_lock);
+    std::swap(m_dst_image_ctx->object_map, m_object_map);
+  }
+  delete m_object_map;
+
+  send_copy_metadata();
+}
+
+template <typename I>
+void DeepCopyRequest<I>::send_copy_metadata() {
+  ldout(m_cct, 20) << dendl;
+
+  Context *ctx = create_context_callback<
+    DeepCopyRequest<I>, &DeepCopyRequest<I>::handle_copy_metadata>(this);
+  auto request = MetadataCopyRequest<I>::create(m_src_image_ctx,
+                                                m_dst_image_ctx, ctx);
+  request->send();
+}
+
+template <typename I>
+void DeepCopyRequest<I>::handle_copy_metadata(int r) {
+  ldout(m_cct, 20) << "r=" << r << dendl;
+
+  if (r < 0) {
+    lderr(m_cct) << "failed to copy metadata: " << cpp_strerror(r) << dendl;
+    finish(r);
+    return;
+  }
+
+  finish(0);
+}
+
+template <typename I>
+int DeepCopyRequest<I>::validate_copy_points() {
+  RWLock::RLocker snap_locker(m_src_image_ctx->snap_lock);
+
+  if (m_snap_id_start != 0 &&
+      m_src_image_ctx->snap_info.find(m_snap_id_start) ==
+      m_src_image_ctx->snap_info.end()) {
+    lderr(m_cct) << "invalid start snap_id " << m_snap_id_start << dendl;
+    return -EINVAL;
+  }
+
+  if (m_snap_id_end != CEPH_NOSNAP &&
+      m_src_image_ctx->snap_info.find(m_snap_id_end) ==
+      m_src_image_ctx->snap_info.end()) {
+    lderr(m_cct) << "invalid end snap_id " << m_snap_id_end << dendl;
+    return -EINVAL;
+  }
+
+  return 0;
+}
+
+template <typename I>
+void DeepCopyRequest<I>::finish(int r) {
+  ldout(m_cct, 20) << "r=" << r << dendl;
+
+  m_on_finish->complete(r);
+  put();
+}
+
+} // namespace librbd
+
+template class librbd::DeepCopyRequest<librbd::ImageCtx>;
diff --git a/src/librbd/DeepCopyRequest.h b/src/librbd/DeepCopyRequest.h
new file mode 100644 (file)
index 0000000..e48527c
--- /dev/null
@@ -0,0 +1,135 @@
+// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*-
+// vim: ts=8 sw=2 smarttab
+
+#ifndef CEPH_LIBRBD_DEEP_COPY_REQUEST_H
+#define CEPH_LIBRBD_DEEP_COPY_REQUEST_H
+
+#include "common/Mutex.h"
+#include "common/RefCountedObj.h"
+#include "include/int_types.h"
+#include "librbd/ImageCtx.h"
+#include "librbd/Types.h"
+#include "librbd/deep_copy/Types.h"
+
+#include <map>
+#include <vector>
+
+class Context;
+
+namespace librbd {
+
+class ImageCtx;
+
+namespace deep_copy {
+
+template <typename> class ImageCopyRequest;
+template <typename> class SnapshotCopyRequest;
+
+}
+
+template <typename ImageCtxT = ImageCtx>
+class DeepCopyRequest : public RefCountedObject {
+public:
+  static DeepCopyRequest* create(ImageCtxT *src_image_ctx,
+                                 ImageCtxT *dst_image_ctx,
+                                 librados::snap_t snap_id_start,
+                                 librados::snap_t snap_id_end,
+                                 const deep_copy::ObjectNumber &object_number,
+                                 ContextWQ *work_queue,
+                                 SnapSeqs *snap_seqs,
+                                 ProgressContext *prog_ctx,
+                                 Context *on_finish) {
+    return new DeepCopyRequest(src_image_ctx, dst_image_ctx, snap_id_start,
+                               snap_id_end, object_number, work_queue,
+                               snap_seqs, prog_ctx, on_finish);
+  }
+
+  DeepCopyRequest(ImageCtxT *src_image_ctx, ImageCtxT *dst_image_ctx,
+                  librados::snap_t snap_id_start, librados::snap_t snap_id_end,
+                  const deep_copy::ObjectNumber &object_number,
+                  ContextWQ *work_queue, SnapSeqs *snap_seqs,
+                  ProgressContext *prog_ctx, Context *on_finish);
+  ~DeepCopyRequest();
+
+  void send();
+  void cancel();
+
+private:
+  /**
+   * @verbatim
+   *
+   * <start>
+   *    |
+   *    v
+   * COPY_SNAPSHOTS
+   *    |
+   *    v
+   * SET_HEAD (skip if not needed)
+   *    |
+   *    v
+   * COPY_IMAGE . . . . . . . . . . . . . .
+   *    |                                 .
+   *    v                                 .
+   * COPY_OBJECT_MAP (skip if object      .
+   *    |             map disabled)       .
+   *    v                                 .
+   * REFRESH_OBJECT_MAP (skip if object   . (image copy canceled)
+   *    |                map disabled)    .
+   *    v                                 .
+   * COPY_METADATA                        .
+   *    |                                 .
+   *    v                                 .
+   * <finish> < . . . . . . . . . . . . . .
+   *
+   * @endverbatim
+   */
+
+  typedef std::vector<librados::snap_t> SnapIds;
+  typedef std::map<librados::snap_t, SnapIds> SnapMap;
+
+  ImageCtxT *m_src_image_ctx;
+  ImageCtxT *m_dst_image_ctx;
+  librados::snap_t m_snap_id_start;
+  librados::snap_t m_snap_id_end;
+  deep_copy::ObjectNumber m_object_number;
+  ContextWQ *m_work_queue;
+  SnapSeqs *m_snap_seqs;
+  ProgressContext *m_prog_ctx;
+  Context *m_on_finish;
+
+  CephContext *m_cct;
+  Mutex m_lock;
+  bool m_canceled = false;
+
+  deep_copy::SnapshotCopyRequest<ImageCtxT> *m_snapshot_copy_request = nullptr;
+  deep_copy::ImageCopyRequest<ImageCtxT> *m_image_copy_request = nullptr;
+  decltype(ImageCtxT::object_map) m_object_map = nullptr;
+
+  void send_copy_snapshots();
+  void handle_copy_snapshots(int r);
+
+  void send_set_head();
+  void handle_set_head(int r);
+
+  void send_copy_image();
+  void handle_copy_image(int r);
+
+  void send_copy_object_map();
+  void handle_copy_object_map(int r);
+
+  void send_refresh_object_map();
+  void handle_refresh_object_map(int r);
+
+  void send_copy_metadata();
+  void handle_copy_metadata(int r);
+
+  int validate_copy_points();
+
+  void finish(int r);
+};
+
+} // namespace librbd
+
+extern template class librbd::DeepCopyRequest<librbd::ImageCtx>;
+
+#endif // CEPH_LIBRBD_DEEP_COPY_REQUEST_H
index 83f1e5689ba994e969ec771307a6bb0eb82468ca..809311adec26813e36af393438f1efb006b650b0 100644 (file)
@@ -6,6 +6,7 @@
 
 #include "include/types.h"
 #include "cls/rbd/cls_rbd_types.h"
+#include <map>
 #include <string>
 
 namespace librbd {
@@ -53,6 +54,8 @@ enum {
   l_librbd_last,
 };
 
+typedef std::map<uint64_t, uint64_t> SnapSeqs;
+
 /** @brief Unique identification of a parent in clone relationship.
  * Cloning an image creates a child image that keeps a reference
  * to its parent. This allows copy-on-write images. */
index 43eb592a7b632b811f9d15bea80d96c8d29174ce..d4983d5f299901b1342aef0a046ab6caadda1709 100644 (file)
@@ -6,8 +6,11 @@
 #include "common/dout.h"
 #include "common/errno.h"
 #include "cls/rbd/cls_rbd_client.h"
+#include "librbd/DeepCopyRequest.h"
+#include "librbd/ExclusiveLock.h"
 #include "librbd/ImageCtx.h"
 #include "librbd/ImageState.h"
+#include "librbd/internal.h"
 
 #define dout_subsys ceph_subsys_rbd
 #undef dout_prefix
@@ -111,6 +114,122 @@ int Image<I>::list_children(I *ictx, const ParentSpec &parent_spec,
   return 0;
 }
 
+template <typename I>
+int Image<I>::deep_copy(I *src, librados::IoCtx& dest_md_ctx,
+                        const char *destname, ImageOptions& opts,
+                        ProgressContext &prog_ctx) {
+  CephContext *cct = (CephContext *)dest_md_ctx.cct();
+  ldout(cct, 20) << src->name
+                 << (src->snap_name.length() ? "@" + src->snap_name : "")
+                 << " -> " << destname << " opts = " << opts << dendl;
+
+  uint64_t features;
+  uint64_t src_size;
+  {
+    RWLock::RLocker snap_locker(src->snap_lock);
+    features = src->features;
+    src_size = src->get_image_size(src->snap_id);
+  }
+  uint64_t format = src->old_format ? 1 : 2;
+  if (opts.get(RBD_IMAGE_OPTION_FORMAT, &format) != 0) {
+    opts.set(RBD_IMAGE_OPTION_FORMAT, format);
+  }
+  if (format == 1) {
+    lderr(cct) << "old format not supported for destination image" << dendl;
+    return -EINVAL;
+  }
+  uint64_t stripe_unit = src->stripe_unit;
+  if (opts.get(RBD_IMAGE_OPTION_STRIPE_UNIT, &stripe_unit) != 0) {
+    opts.set(RBD_IMAGE_OPTION_STRIPE_UNIT, stripe_unit);
+  }
+  uint64_t stripe_count = src->stripe_count;
+  if (opts.get(RBD_IMAGE_OPTION_STRIPE_COUNT, &stripe_count) != 0) {
+    opts.set(RBD_IMAGE_OPTION_STRIPE_COUNT, stripe_count);
+  }
+  uint64_t order = src->order;
+  if (opts.get(RBD_IMAGE_OPTION_ORDER, &order) != 0) {
+    opts.set(RBD_IMAGE_OPTION_ORDER, order);
+  }
+  if (opts.get(RBD_IMAGE_OPTION_FEATURES, &features) != 0) {
+    opts.set(RBD_IMAGE_OPTION_FEATURES, features);
+  }
+  if (features & ~RBD_FEATURES_ALL) {
+    lderr(cct) << "librbd does not support requested features" << dendl;
+    return -ENOSYS;
+  }
+
+  int r = create(dest_md_ctx, destname, "", src_size, opts, "", "", false);
+  if (r < 0) {
+    lderr(cct) << "header creation failed" << dendl;
+    return r;
+  }
+  opts.set(RBD_IMAGE_OPTION_ORDER, static_cast<uint64_t>(order));
+
+  ImageCtx *dest = new librbd::ImageCtx(destname, "", NULL,
+                                        dest_md_ctx, false);
+  r = dest->state->open(false);
+  if (r < 0) {
+    lderr(cct) << "failed to read newly created header" << dendl;
+    return r;
+  }
+
+  C_SaferCond lock_ctx;
+  {
+    RWLock::WLocker locker(dest->owner_lock);
+
+    if (dest->exclusive_lock == nullptr ||
+        dest->exclusive_lock->is_lock_owner()) {
+      lock_ctx.complete(0);
+    } else {
+      dest->exclusive_lock->acquire_lock(&lock_ctx);
+    }
+  }
+
+  r = lock_ctx.wait();
+  if (r < 0) {
+    lderr(cct) << "failed to request exclusive lock: " << cpp_strerror(r)
+               << dendl;
+    dest->state->close();
+    return r;
+  }
+
+  r = deep_copy(src, dest, prog_ctx);
+
+  int close_r = dest->state->close();
+  if (r == 0 && close_r < 0) {
+    r = close_r;
+  }
+  return r;
+}
+
+template <typename I>
+int Image<I>::deep_copy(I *src, I *dest, ProgressContext &prog_ctx) {
+  CephContext *cct = src->cct;
+  librados::snap_t snap_id_start = 0;
+  librados::snap_t snap_id_end;
+  {
+    RWLock::RLocker snap_locker(src->snap_lock);
+    snap_id_end = src->snap_id;
+  }
+
+  ThreadPool *thread_pool;
+  ContextWQ *op_work_queue;
+  ImageCtx::get_thread_pool_instance(cct, &thread_pool, &op_work_queue);
+
+  C_SaferCond cond;
+  SnapSeqs snap_seqs;
+  auto req = DeepCopyRequest<>::create(src, dest, snap_id_start, snap_id_end,
+                                       boost::none, op_work_queue, &snap_seqs,
+                                       &prog_ctx, &cond);
+  req->send();
+  int r = cond.wait();
+  if (r < 0) {
+    return r;
+  }
+
+  return 0;
+}
+
 } // namespace api
 } // namespace librbd
 
index 5f3dfff144357e92c4522c90353ab6a961147c42..d690d44cc270948f3e34f673ceade94cc85f7ae3 100644 (file)
@@ -13,6 +13,9 @@ namespace librados { struct IoCtx; }
 
 namespace librbd {
 
+class ImageOptions;
+class ProgressContext;
+
 struct ImageCtx;
 
 namespace api {
@@ -30,6 +33,11 @@ struct Image {
   static int list_children(ImageCtxT *ictx, const ParentSpec &parent_spec,
                            PoolImageIds *pool_image_ids);
 
+  static int deep_copy(ImageCtxT *ictx, librados::IoCtx& dest_md_ctx,
+                       const char *destname, ImageOptions& opts,
+                       ProgressContext &prog_ctx);
+  static int deep_copy(ImageCtxT *src, ImageCtxT *dest,
+                       ProgressContext &prog_ctx);
 };
 
 } // namespace api
diff --git a/src/librbd/deep_copy/ImageCopyRequest.cc b/src/librbd/deep_copy/ImageCopyRequest.cc
new file mode 100644 (file)
index 0000000..2006ea3
--- /dev/null
@@ -0,0 +1,170 @@
+// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*-
+// vim: ts=8 sw=2 smarttab
+
+#include "ImageCopyRequest.h"
+#include "ObjectCopyRequest.h"
+#include "common/errno.h"
+#include "librbd/Utils.h"
+#include "librbd/deep_copy/Utils.h"
+#include "osdc/Striper.h"
+
+#define dout_subsys ceph_subsys_rbd
+#undef dout_prefix
+#define dout_prefix *_dout << "librbd::deep_copy::ImageCopyRequest: " \
+                           << this << " " << __func__ << ": "
+
+namespace librbd {
+namespace deep_copy {
+
+using librbd::util::unique_lock_name;
+
+template <typename I>
+ImageCopyRequest<I>::ImageCopyRequest(I *src_image_ctx, I *dst_image_ctx,
+                                      librados::snap_t snap_id_start,
+                                      librados::snap_t snap_id_end,
+                                      const ObjectNumber &object_number,
+                                      const SnapSeqs &snap_seqs,
+                                      ProgressContext *prog_ctx,
+                                      Context *on_finish)
+  : RefCountedObject(dst_image_ctx->cct, 1), m_src_image_ctx(src_image_ctx),
+    m_dst_image_ctx(dst_image_ctx), m_snap_id_start(snap_id_start),
+    m_snap_id_end(snap_id_end), m_object_number(object_number),
+    m_snap_seqs(snap_seqs), m_prog_ctx(prog_ctx), m_on_finish(on_finish),
+    m_cct(dst_image_ctx->cct),
+    m_lock(unique_lock_name("ImageCopyRequest::m_lock", this)) {
+}
+
+template <typename I>
+void ImageCopyRequest<I>::send() {
+  util::compute_snap_map(m_snap_id_start, m_snap_id_end, m_snap_seqs,
+                         &m_snap_map);
+  if (m_snap_map.empty()) {
+    lderr(m_cct) << "failed to map snapshots within boundary" << dendl;
+    finish(-EINVAL);
+    return;
+  }
+
+  send_object_copies();
+}
+
+template <typename I>
+void ImageCopyRequest<I>::cancel() {
+  Mutex::Locker locker(m_lock);
+
+  ldout(m_cct, 20) << dendl;
+  m_canceled = true;
+}
+
+template <typename I>
+void ImageCopyRequest<I>::send_object_copies() {
+  m_object_no = 0;
+  if (m_object_number) {
+    m_object_no = *m_object_number + 1;
+  }
+
+  uint64_t size;
+  {
+    RWLock::RLocker snap_locker(m_src_image_ctx->snap_lock);
+    size =  m_src_image_ctx->get_image_size(CEPH_NOSNAP);
+    for (auto snap_id : m_src_image_ctx->snaps) {
+      size = std::max(size, m_src_image_ctx->get_image_size(snap_id));
+    }
+  }
+  m_end_object_no = Striper::get_num_objects(m_dst_image_ctx->layout, size);
+
+  ldout(m_cct, 20) << "start_object=" << m_object_no << ", "
+                   << "end_object=" << m_end_object_no << dendl;
+
+  bool complete;
+  {
+    Mutex::Locker locker(m_lock);
+    for (int i = 0;
+         i < m_cct->_conf->get_val<int64_t>("rbd_concurrent_management_ops");
+         ++i) {
+      send_next_object_copy();
+      if (m_ret_val < 0 && m_current_ops == 0) {
+        break;
+      }
+    }
+    complete = (m_current_ops == 0);
+  }
+
+  if (complete) {
+    finish(m_ret_val);
+  }
+}
+
+template <typename I>
+void ImageCopyRequest<I>::send_next_object_copy() {
+  assert(m_lock.is_locked());
+
+  if (m_canceled && m_ret_val == 0) {
+    ldout(m_cct, 10) << "image copy canceled" << dendl;
+    m_ret_val = -ECANCELED;
+  }
+
+  if (m_ret_val < 0 || m_object_no >= m_end_object_no) {
+    return;
+  }
+
+  uint64_t ono = m_object_no++;
+
+  ldout(m_cct, 20) << "object_num=" << ono << dendl;
+
+  ++m_current_ops;
+
+  Context *ctx = new FunctionContext(
+    [this, ono](int r) {
+      handle_object_copy(ono, r);
+    });
+  ObjectCopyRequest<I> *req = ObjectCopyRequest<I>::create(
+      m_src_image_ctx, m_dst_image_ctx, m_snap_map, ono, ctx);
+  req->send();
+}
+
+template <typename I>
+void ImageCopyRequest<I>::handle_object_copy(uint64_t object_no, int r) {
+  ldout(m_cct, 20) << "object_no=" << object_no << ", r=" << r << dendl;
+
+  bool complete;
+  {
+    Mutex::Locker locker(m_lock);
+    assert(m_current_ops > 0);
+    --m_current_ops;
+
+    if (r < 0) {
+      lderr(m_cct) << "object copy failed: " << cpp_strerror(r) << dendl;
+      if (m_ret_val == 0) {
+        m_ret_val = r;
+      }
+    } else {
+      m_copied_objects.push(object_no);
+      while (m_copied_objects.top() ==
+             (m_object_number ? *m_object_number + 1 : 0)) {
+        m_object_number = m_copied_objects.top();
+        m_copied_objects.pop();
+        m_prog_ctx->update_progress(*m_object_number + 1, m_end_object_no);
+      }
+    }
+
+    send_next_object_copy();
+    complete = (m_current_ops == 0);
+  }
+
+  if (complete) {
+    finish(m_ret_val);
+  }
+}
+
+template <typename I>
+void ImageCopyRequest<I>::finish(int r) {
+  ldout(m_cct, 20) << "r=" << r << dendl;
+
+  m_on_finish->complete(r);
+  put();
+}
+
+} // namespace deep_copy
+} // namespace librbd
+
+template class librbd::deep_copy::ImageCopyRequest<librbd::ImageCtx>;
diff --git a/src/librbd/deep_copy/ImageCopyRequest.h b/src/librbd/deep_copy/ImageCopyRequest.h
new file mode 100644 (file)
index 0000000..27636db
--- /dev/null
@@ -0,0 +1,100 @@
+// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*-
+// vim: ts=8 sw=2 smarttab
+
+#ifndef CEPH_LIBRBD_DEEP_COPY_IMAGE_DEEP_COPY_REQUEST_H
+#define CEPH_LIBRBD_DEEP_COPY_IMAGE_DEEP_COPY_REQUEST_H
+
+#include "include/int_types.h"
+#include "include/rados/librados.hpp"
+#include "common/Mutex.h"
+#include "common/RefCountedObj.h"
+#include "librbd/Types.h"
+#include "librbd/deep_copy/Types.h"
+#include <functional>
+#include <map>
+#include <queue>
+#include <vector>
+#include <boost/optional.hpp>
+
+class Context;
+
+namespace librbd {
+
+class ImageCtx;
+class ProgressContext;
+
+namespace deep_copy {
+
+template <typename ImageCtxT = ImageCtx>
+class ImageCopyRequest : public RefCountedObject {
+public:
+  static ImageCopyRequest* create(ImageCtxT *src_image_ctx,
+                                  ImageCtxT *dst_image_ctx,
+                                  librados::snap_t snap_id_start,
+                                  librados::snap_t snap_id_end,
+                                  const ObjectNumber &object_number,
+                                  const SnapSeqs &snap_seqs,
+                                  ProgressContext *prog_ctx,
+                                  Context *on_finish) {
+    return new ImageCopyRequest(src_image_ctx, dst_image_ctx, snap_id_start,
+                                snap_id_end, object_number, snap_seqs, prog_ctx,
+                                on_finish);
+  }
+
+  ImageCopyRequest(ImageCtxT *src_image_ctx, ImageCtxT *dst_image_ctx,
+                   librados::snap_t snap_id_start, librados::snap_t snap_id_end,
+                   const ObjectNumber &object_number, const SnapSeqs &snap_seqs,
+                   ProgressContext *prog_ctx, Context *on_finish);
+
+  void send();
+  void cancel();
+
+private:
+  /**
+   * @verbatim
+   *
+   * <start>   . . . . .
+   *    |      .       .  (parallel execution of
+   *    v      v       .   multiple objects at once)
+   * COPY_OBJECT . . . .
+   *    |
+   *    v
+   * <finish>
+   *
+   * @endverbatim
+   */
+
+  ImageCtxT *m_src_image_ctx;
+  ImageCtxT *m_dst_image_ctx;
+  librados::snap_t m_snap_id_start;
+  librados::snap_t m_snap_id_end;
+  ObjectNumber m_object_number;
+  SnapSeqs m_snap_seqs;
+  ProgressContext *m_prog_ctx;
+  Context *m_on_finish;
+
+  CephContext *m_cct;
+  Mutex m_lock;
+  bool m_canceled = false;
+
+  uint64_t m_object_no = 0;
+  uint64_t m_end_object_no = 0;
+  uint64_t m_current_ops = 0;
+  std::priority_queue<
+    uint64_t, std::vector<uint64_t>, std::greater<uint64_t>> m_copied_objects;
+  SnapMap m_snap_map;
+  int m_ret_val = 0;
+
+  void send_object_copies();
+  void send_next_object_copy();
+  void handle_object_copy(uint64_t object_no, int r);
+
+  void finish(int r);
+};
+
+} // namespace deep_copy
+} // namespace librbd
+
+extern template class librbd::deep_copy::ImageCopyRequest<librbd::ImageCtx>;
+
+#endif // CEPH_LIBRBD_DEEP_COPY_IMAGE_DEEP_COPY_REQUEST_H
diff --git a/src/librbd/deep_copy/MetadataCopyRequest.cc b/src/librbd/deep_copy/MetadataCopyRequest.cc
new file mode 100644 (file)
index 0000000..9f78608
--- /dev/null
@@ -0,0 +1,123 @@
+// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*-
+// vim: ts=8 sw=2 smarttab
+
+#include "MetadataCopyRequest.h"
+#include "common/dout.h"
+#include "common/errno.h"
+#include "cls/rbd/cls_rbd_client.h"
+#include "librbd/Utils.h"
+
+#define dout_subsys ceph_subsys_rbd
+#undef dout_prefix
+#define dout_prefix *_dout << "librbd::deep_copy::MetadataCopyRequest: " \
+                           << this << " " << __func__ << ": "
+
+namespace librbd {
+namespace deep_copy {
+
+namespace {
+
+const uint64_t MAX_METADATA_ITEMS = 128;
+
+} // anonymous namespace
+
+using librbd::util::create_rados_callback;
+
+template <typename I>
+MetadataCopyRequest<I>::MetadataCopyRequest(I *src_image_ctx, I *dst_image_ctx,
+                                            Context *on_finish)
+  : m_src_image_ctx(src_image_ctx), m_dst_image_ctx(dst_image_ctx),
+    m_on_finish(on_finish), m_cct(dst_image_ctx->cct) {
+}
+
+template <typename I>
+void MetadataCopyRequest<I>::send() {
+  list_src_metadata();
+}
+
+template <typename I>
+void MetadataCopyRequest<I>::list_src_metadata() {
+  ldout(m_cct, 20) << "start_key=" << m_last_metadata_key << dendl;
+
+  librados::ObjectReadOperation op;
+  librbd::cls_client::metadata_list_start(&op, m_last_metadata_key, MAX_METADATA_ITEMS);
+
+  librados::AioCompletion *aio_comp = create_rados_callback<
+    MetadataCopyRequest<I>,
+    &MetadataCopyRequest<I>::handle_list_src_metadata>(this);
+  m_out_bl.clear();
+  m_src_image_ctx->md_ctx.aio_operate(m_src_image_ctx->header_oid,
+                                      aio_comp, &op, &m_out_bl);
+  aio_comp->release();
+}
+
+template <typename I>
+void MetadataCopyRequest<I>::handle_list_src_metadata(int r) {
+  ldout(m_cct, 20) << "r=" << r << dendl;
+
+  Metadata metadata;
+  if (r == 0) {
+    bufferlist::iterator it = m_out_bl.begin();
+    r = librbd::cls_client::metadata_list_finish(&it, &metadata);
+  }
+
+  if (r < 0) {
+    lderr(m_cct) << "failed to retrieve metadata: " << cpp_strerror(r) << dendl;
+    finish(r);
+    return;
+  }
+
+  if (metadata.empty()) {
+    finish(0);
+    return;
+  }
+
+  m_last_metadata_key = metadata.rbegin()->first;
+  m_more_metadata = (metadata.size() >= MAX_METADATA_ITEMS);
+  set_dst_metadata(metadata);
+}
+
+template <typename I>
+void MetadataCopyRequest<I>::set_dst_metadata(const Metadata& metadata) {
+  ldout(m_cct, 20) << "count=" << metadata.size() << dendl;
+
+  librados::ObjectWriteOperation op;
+  librbd::cls_client::metadata_set(&op, metadata);
+
+  librados::AioCompletion *aio_comp = create_rados_callback<
+    MetadataCopyRequest<I>,
+    &MetadataCopyRequest<I>::handle_set_dst_metadata>(this);
+  m_dst_image_ctx->md_ctx.aio_operate(m_dst_image_ctx->header_oid, aio_comp,
+                                      &op);
+  aio_comp->release();
+}
+
+template <typename I>
+void MetadataCopyRequest<I>::handle_set_dst_metadata(int r) {
+  ldout(m_cct, 20) << "r=" << r << dendl;
+
+  if (r < 0) {
+    lderr(m_cct) << "failed to set metadata: " << cpp_strerror(r) << dendl;
+    finish(r);
+    return;
+  }
+
+  if (m_more_metadata) {
+    list_src_metadata();
+    return;
+  }
+
+  finish(0);
+}
+
+template <typename I>
+void MetadataCopyRequest<I>::finish(int r) {
+  ldout(m_cct, 20) << "r=" << r << dendl;
+  m_on_finish->complete(r);
+  delete this;
+}
+
+} // namespace deep_copy
+} // namespace librbd
+
+template class librbd::deep_copy::MetadataCopyRequest<librbd::ImageCtx>;
diff --git a/src/librbd/deep_copy/MetadataCopyRequest.h b/src/librbd/deep_copy/MetadataCopyRequest.h
new file mode 100644 (file)
index 0000000..5bd69d8
--- /dev/null
@@ -0,0 +1,77 @@
+// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*-
+// vim: ts=8 sw=2 smarttab
+
+#ifndef CEPH_LIBRBD_DEEP_COPY_METADATA_COPY_REQUEST_H
+#define CEPH_LIBRBD_DEEP_COPY_METADATA_COPY_REQUEST_H
+
+#include "include/int_types.h"
+#include "include/buffer.h"
+#include "include/rados/librados.hpp"
+#include "librbd/ImageCtx.h"
+#include <map>
+#include <string>
+
+class Context;
+
+namespace librbd {
+namespace deep_copy {
+
+template <typename ImageCtxT = librbd::ImageCtx>
+class MetadataCopyRequest {
+public:
+  static MetadataCopyRequest* create(ImageCtxT *src_image_ctx,
+                                     ImageCtxT *dst_image_ctx,
+                                     Context *on_finish) {
+    return new MetadataCopyRequest(src_image_ctx, dst_image_ctx, on_finish);
+  }
+
+  MetadataCopyRequest(ImageCtxT *src_image_ctx, ImageCtxT *dst_image_ctx,
+                      Context *on_finish);
+
+  void send();
+
+private:
+  /**
+   * @verbatim
+   *
+   * <start>
+   *    |
+   *    v
+   * LIST_SRC_METADATA <------\
+   *    |                     | (repeat if additional
+   *    v                     |  metadata)
+   * SET_DST_METADATA --------/
+   *    |
+   *    v
+   * <finish>
+   *
+   * @endverbatim
+   */
+  typedef std::map<std::string, bufferlist> Metadata;
+
+  ImageCtxT *m_src_image_ctx;
+  ImageCtxT *m_dst_image_ctx;
+  Context *m_on_finish;
+
+  CephContext *m_cct;
+  bufferlist m_out_bl;
+
+  std::string m_last_metadata_key;
+  bool m_more_metadata = false;
+
+  void list_src_metadata();
+  void handle_list_src_metadata(int r);
+
+  void set_dst_metadata(const Metadata& metadata);
+  void handle_set_dst_metadata(int r);
+
+  void finish(int r);
+
+};
+
+} // namespace deep_copy
+} // namespace librbd
+
+extern template class librbd::deep_copy::MetadataCopyRequest<librbd::ImageCtx>;
+
+#endif // CEPH_LIBRBD_DEEP_COPY_METADATA_COPY_REQUEST_H
diff --git a/src/librbd/deep_copy/ObjectCopyRequest.cc b/src/librbd/deep_copy/ObjectCopyRequest.cc
new file mode 100644 (file)
index 0000000..8bd9c57
--- /dev/null
@@ -0,0 +1,692 @@
+// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*-
+// vim: ts=8 sw=2 smarttab
+
+#include "ObjectCopyRequest.h"
+#include "common/errno.h"
+#include "librados/snap_set_diff.h"
+#include "librbd/ExclusiveLock.h"
+#include "librbd/ObjectMap.h"
+#include "librbd/Utils.h"
+#include "osdc/Striper.h"
+
+#define dout_subsys ceph_subsys_rbd
+#undef dout_prefix
+#define dout_prefix *_dout << "librbd::deep_copy::ObjectCopyRequest: " \
+                           << this << " " << __func__ << ": "
+
+namespace librados {
+
+inline bool operator==(const clone_info_t& rhs, const clone_info_t& lhs) {
+  return (rhs.cloneid == lhs.cloneid &&
+          rhs.snaps == lhs.snaps &&
+          rhs.overlap == lhs.overlap &&
+          rhs.size == lhs.size);
+}
+
+inline bool operator==(const snap_set_t& rhs, const snap_set_t& lhs) {
+  return (rhs.clones == lhs.clones &&
+          rhs.seq == lhs.seq);
+}
+
+} // namespace librados
+
+namespace librbd {
+namespace deep_copy {
+
+using librbd::util::create_context_callback;
+using librbd::util::create_rados_callback;
+
+template <typename I>
+ObjectCopyRequest<I>::ObjectCopyRequest(I *src_image_ctx, I *dst_image_ctx,
+                                        const SnapMap &snap_map,
+                                        uint64_t dst_object_number,
+                                        Context *on_finish)
+  : m_src_image_ctx(src_image_ctx), m_dst_image_ctx(dst_image_ctx),
+    m_cct(dst_image_ctx->cct), m_snap_map(snap_map),
+    m_dst_object_number(dst_object_number), m_on_finish(on_finish) {
+  assert(!m_snap_map.empty());
+
+  m_src_io_ctx.dup(m_src_image_ctx->data_ctx);
+  m_dst_io_ctx.dup(m_dst_image_ctx->data_ctx);
+
+  m_dst_oid = m_dst_image_ctx->get_object_name(dst_object_number);
+
+  ldout(m_cct, 20) << "dst_oid=" << m_dst_oid << dendl;
+
+  compute_src_object_extents();
+}
+
+template <typename I>
+void ObjectCopyRequest<I>::send() {
+  send_list_snaps();
+}
+
+template <typename I>
+void ObjectCopyRequest<I>::send_list_snaps() {
+  assert(!m_src_objects.empty());
+  m_src_ono = *m_src_objects.begin();
+  m_src_oid = m_src_image_ctx->get_object_name(m_src_ono);
+
+  ldout(m_cct, 20) << "src_oid=" << m_src_oid << dendl;
+
+  librados::AioCompletion *rados_completion = create_rados_callback<
+    ObjectCopyRequest<I>, &ObjectCopyRequest<I>::handle_list_snaps>(this);
+
+  librados::ObjectReadOperation op;
+  m_snap_set = {};
+  m_snap_ret = 0;
+  op.list_snaps(&m_snap_set, &m_snap_ret);
+
+  m_src_io_ctx.snap_set_read(CEPH_SNAPDIR);
+  int r = m_src_io_ctx.aio_operate(m_src_oid, rados_completion, &op,
+                                   nullptr);
+  assert(r == 0);
+  rados_completion->release();
+}
+
+template <typename I>
+void ObjectCopyRequest<I>::handle_list_snaps(int r) {
+  if (r == 0 && m_snap_ret < 0) {
+    r = m_snap_ret;
+  }
+
+  ldout(m_cct, 20) << "r=" << r << dendl;
+
+  if (r == -ENOENT) {
+    r = 0;
+  }
+
+  if (r < 0) {
+    lderr(m_cct) << "failed to list snaps: " << cpp_strerror(r) << dendl;
+    finish(r);
+    return;
+  }
+
+  if (m_retry_missing_read) {
+    if (m_snap_set == m_retry_snap_set) {
+      lderr(m_cct) << "read encountered missing object using up-to-date snap set"
+                   << dendl;
+      finish(-ENOENT);
+      return;
+    }
+
+    ldout(m_cct, 20) << "retrying using updated snap set" << dendl;
+    m_retry_missing_read = false;
+    m_retry_snap_set = {};
+  }
+
+  compute_read_ops();
+  send_read_object();
+}
+
+template <typename I>
+void ObjectCopyRequest<I>::send_read_object() {
+
+  if (m_read_snaps.empty()) {
+    // all snapshots have been read
+    merge_write_ops();
+
+    assert(!m_src_objects.empty());
+    m_src_objects.erase(m_src_objects.begin());
+
+    if (!m_src_objects.empty()) {
+      send_list_snaps();
+      return;
+    }
+
+    // all objects have been read
+
+    compute_zero_ops();
+
+    if (m_write_ops.empty()) {
+      // nothing to copy
+      finish(0);
+      return;
+    }
+
+    send_write_object();
+    return;
+  }
+
+  auto index = *m_read_snaps.begin();
+  auto src_snap_seq = index.second;
+
+  bool read_required = false;
+  librados::ObjectReadOperation op;
+
+  for (auto &copy_op : m_read_ops[index]) {
+    if (!read_required) {
+      // map the copy op start snap id back to the necessary read snap id
+      m_src_io_ctx.snap_set_read(src_snap_seq);
+
+      ldout(m_cct, 20) << "src_snap_seq=" << src_snap_seq << dendl;
+      read_required = true;
+    }
+    ldout(m_cct, 20) << "read op: " << copy_op.src_offset << "~"
+                     << copy_op.length << dendl;
+    op.sparse_read(copy_op.src_offset, copy_op.length, &copy_op.src_extent_map,
+                   &copy_op.out_bl, nullptr);
+    op.set_op_flags2(LIBRADOS_OP_FLAG_FADVISE_SEQUENTIAL |
+                     LIBRADOS_OP_FLAG_FADVISE_NOCACHE);
+  }
+
+  if (!read_required) {
+    // nothing written to this object for this snapshot (must be trunc/remove)
+    handle_read_object(0);
+  }
+
+  auto ctx = create_context_callback<
+    ObjectCopyRequest<I>, &ObjectCopyRequest<I>::handle_read_object>(this);
+  auto comp = create_rados_callback(ctx);
+
+  ldout(m_cct, 20) << "read " << m_src_oid << dendl;
+
+  int r = m_src_io_ctx.aio_operate(m_src_oid, comp, &op, nullptr);
+  assert(r == 0);
+  comp->release();
+}
+
+template <typename I>
+void ObjectCopyRequest<I>::handle_read_object(int r) {
+  ldout(m_cct, 20) << "r=" << r << dendl;
+
+  if (r == -ENOENT) {
+    m_retry_snap_set = m_snap_set;
+    m_retry_missing_read = true;
+
+    ldout(m_cct, 5) << "object missing potentially due to removed snapshot"
+                    << dendl;
+    send_list_snaps();
+    return;
+  }
+
+  if (r < 0) {
+    lderr(m_cct) << "failed to read from source object: " << cpp_strerror(r)
+                 << dendl;
+    finish(r);
+    return;
+  }
+
+  assert(!m_read_snaps.empty());
+  m_read_snaps.erase(m_read_snaps.begin());
+
+  send_read_object();
+}
+
+template <typename I>
+void ObjectCopyRequest<I>::send_write_object() {
+  assert(!m_write_ops.empty());
+
+  // retrieve the destination snap context for the op
+  SnapIds dst_snap_ids;
+  librados::snap_t dst_snap_seq = 0;
+  librados::snap_t src_snap_seq = m_write_ops.begin()->first;
+  if (src_snap_seq != 0) {
+    auto snap_map_it = m_snap_map.find(src_snap_seq);
+    assert(snap_map_it != m_snap_map.end());
+
+    // write snapshot context should be before actual snapshot
+    if (snap_map_it != m_snap_map.begin()) {
+      --snap_map_it;
+      assert(!snap_map_it->second.empty());
+      dst_snap_seq = snap_map_it->second.front();
+      dst_snap_ids = snap_map_it->second;
+    }
+  }
+
+  ldout(m_cct, 20) << "dst_snap_seq=" << dst_snap_seq << ", "
+                   << "dst_snaps=" << dst_snap_ids << dendl;
+
+  librados::ObjectWriteOperation op;
+  uint64_t buffer_offset;
+
+  for (auto &copy_op : m_write_ops.begin()->second) {
+    switch (copy_op.type) {
+    case COPY_OP_TYPE_WRITE:
+      buffer_offset = 0;
+      for (auto &e : copy_op.dst_extent_map) {
+        ldout(m_cct, 20) << ": write op: " << e.first << "~" << e.second
+                         << dendl;
+        bufferlist tmpbl;
+        tmpbl.substr_of(copy_op.out_bl, buffer_offset, e.second);
+        op.write(e.first, tmpbl);
+        op.set_op_flags2(LIBRADOS_OP_FLAG_FADVISE_SEQUENTIAL |
+                         LIBRADOS_OP_FLAG_FADVISE_NOCACHE);
+        buffer_offset += e.second;
+      }
+      break;
+    case COPY_OP_TYPE_ZERO:
+      ldout(m_cct, 20) << "zero op: " << copy_op.dst_offset << "~"
+                       << copy_op.length << dendl;
+      op.zero(copy_op.dst_offset, copy_op.length);
+      break;
+    case COPY_OP_TYPE_TRUNC:
+      ldout(m_cct, 20) << "trunc op: " << copy_op.dst_offset << dendl;
+      op.truncate(copy_op.dst_offset);
+      break;
+    case COPY_OP_TYPE_REMOVE:
+      ldout(m_cct, 20) << "remove op" << dendl;
+      op.remove();
+      break;
+    default:
+      ceph_abort();
+    }
+  }
+
+  if (op.size() == 0) {
+    handle_write_object(0);
+    return;
+  }
+
+  Context *finish_op_ctx;
+  {
+    RWLock::RLocker owner_locker(m_dst_image_ctx->owner_lock);
+    finish_op_ctx = start_lock_op(m_dst_image_ctx->owner_lock);
+  }
+  if (finish_op_ctx == nullptr) {
+    lderr(m_cct) << "lost exclusive lock" << dendl;
+    finish(-EROFS);
+    return;
+  }
+
+  auto ctx = new FunctionContext([this, finish_op_ctx](int r) {
+      handle_write_object(r);
+      finish_op_ctx->complete(0);
+    });
+  librados::AioCompletion *comp = create_rados_callback(ctx);
+  int r = m_dst_io_ctx.aio_operate(m_dst_oid, comp, &op, dst_snap_seq,
+                                   dst_snap_ids);
+  assert(r == 0);
+  comp->release();
+}
+
+template <typename I>
+void ObjectCopyRequest<I>::handle_write_object(int r) {
+  ldout(m_cct, 20) << "r=" << r << dendl;
+
+  if (r == -ENOENT) {
+    r = 0;
+  }
+  if (r < 0) {
+    lderr(m_cct) << "failed to write to destination object: " << cpp_strerror(r)
+                 << dendl;
+    finish(r);
+    return;
+  }
+
+  m_write_ops.erase(m_write_ops.begin());
+  if (!m_write_ops.empty()) {
+    send_write_object();
+    return;
+  }
+
+  send_update_object_map();
+}
+
+template <typename I>
+void ObjectCopyRequest<I>::send_update_object_map() {
+  if (!m_dst_image_ctx->test_features(RBD_FEATURE_OBJECT_MAP) ||
+      m_dst_object_state.empty()) {
+    finish(0);
+    return;
+  }
+
+  m_dst_image_ctx->owner_lock.get_read();
+  m_dst_image_ctx->snap_lock.get_read();
+  if (m_dst_image_ctx->object_map == nullptr) {
+    // possible that exclusive lock was lost in background
+    lderr(m_cct) << "object map is not initialized" << dendl;
+
+    m_dst_image_ctx->snap_lock.put_read();
+    m_dst_image_ctx->owner_lock.put_read();
+    finish(-EINVAL);
+    return;
+  }
+
+  auto &dst_object_state = *m_dst_object_state.begin();
+  auto it = m_snap_map.find(dst_object_state.first);
+  assert(it != m_snap_map.end());
+  auto dst_snap_id = it->second.front();
+  auto object_state = dst_object_state.second;
+  m_dst_object_state.erase(m_dst_object_state.begin());
+
+  ldout(m_cct, 20) << "dst_snap_id=" << dst_snap_id << ", object_state="
+                   << static_cast<uint32_t>(object_state) << dendl;
+
+  auto finish_op_ctx = start_lock_op(m_dst_image_ctx->owner_lock);
+  if (finish_op_ctx == nullptr) {
+    lderr(m_cct) << "lost exclusive lock" << dendl;
+    m_dst_image_ctx->snap_lock.put_read();
+    m_dst_image_ctx->owner_lock.put_read();
+    finish(-EROFS);
+    return;
+  }
+
+  auto ctx = new FunctionContext([this, finish_op_ctx](int r) {
+      handle_update_object_map(r);
+      finish_op_ctx->complete(0);
+    });
+
+  RWLock::WLocker object_map_locker(m_dst_image_ctx->object_map_lock);
+  bool sent = m_dst_image_ctx->object_map->template aio_update<
+    Context, &Context::complete>(dst_snap_id, m_dst_object_number, object_state,
+                                 {}, {}, ctx);
+  m_dst_image_ctx->snap_lock.put_read();
+  m_dst_image_ctx->owner_lock.put_read();
+  if (!sent) {
+    assert(dst_snap_id == CEPH_NOSNAP);
+    handle_update_object_map(0);
+  }
+}
+
+template <typename I>
+void ObjectCopyRequest<I>::handle_update_object_map(int r) {
+  ldout(m_cct, 20) << "r=" << r << dendl;
+
+  assert(r == 0);
+  if (!m_dst_object_state.empty()) {
+    send_update_object_map();
+    return;
+  }
+  finish(0);
+}
+
+template <typename I>
+Context *ObjectCopyRequest<I>::start_lock_op(RWLock &owner_lock) {
+  assert(m_dst_image_ctx->owner_lock.is_locked());
+  if (m_dst_image_ctx->exclusive_lock == nullptr) {
+    return new FunctionContext([](int r) {});
+  }
+  return m_dst_image_ctx->exclusive_lock->start_op();
+}
+
+template <typename I>
+uint64_t ObjectCopyRequest<I>::src_to_dst_object_offset(uint64_t objectno,
+                                                        uint64_t offset) {
+  std::vector<std::pair<uint64_t, uint64_t>> image_extents;
+  Striper::extent_to_file(m_cct, &m_src_image_ctx->layout, objectno, offset, 1,
+                          image_extents);
+  assert(image_extents.size() == 1);
+  auto dst_object_offset = image_extents.begin()->first;
+
+  std::map<object_t, std::vector<ObjectExtent>> dst_object_extents;
+  Striper::file_to_extents(m_cct, m_dst_image_ctx->format_string,
+                           &m_dst_image_ctx->layout, dst_object_offset, 1, 0,
+                           dst_object_extents);
+  assert(dst_object_extents.size() == 1);
+  assert(dst_object_extents.begin()->second.size() == 1);
+  auto &e = *dst_object_extents.begin()->second.begin();
+  assert(e.objectno == m_dst_object_number);
+
+  return e.offset;
+}
+
+template <typename I>
+void ObjectCopyRequest<I>::compute_src_object_extents() {
+  std::vector<std::pair<uint64_t, uint64_t>> image_extents;
+  Striper::extent_to_file(m_cct, &m_dst_image_ctx->layout, m_dst_object_number,
+                          0, m_dst_image_ctx->layout.object_size, image_extents);
+
+  size_t total = 0;
+  for (auto &e : image_extents) {
+    std::map<object_t, std::vector<ObjectExtent>> src_object_extents;
+    Striper::file_to_extents(m_cct, m_src_image_ctx->format_string,
+                             &m_src_image_ctx->layout, e.first, e.second, 0,
+                             src_object_extents);
+    auto stripe_unit = std::min(m_src_image_ctx->layout.stripe_unit,
+                                m_dst_image_ctx->layout.stripe_unit);
+    for (auto &p : src_object_extents) {
+      for (auto &s : p.second) {
+        m_src_objects.insert(s.objectno);
+        total += s.length;
+        while (s.length > 0) {
+          assert(s.length >= stripe_unit);
+          auto dst_object_offset = src_to_dst_object_offset(s.objectno, s.offset);
+          m_src_object_extents[dst_object_offset] = {s.objectno, s.offset,
+                                                     stripe_unit};
+          s.offset += stripe_unit;
+          s.length -= stripe_unit;
+        }
+      }
+    }
+  }
+
+  assert(total == m_dst_image_ctx->layout.object_size);
+
+  ldout(m_cct, 20) << m_src_object_extents.size() << " src extents" << dendl;
+}
+
+template <typename I>
+void ObjectCopyRequest<I>::compute_read_ops() {
+  m_read_ops = {};
+  m_read_snaps = {};
+  m_zero_interval = {};
+
+  librados::snap_t src_copy_point_snap_id = m_snap_map.rbegin()->first;
+  uint64_t prev_end_size = 0;
+  bool prev_exists = false;
+  librados::snap_t start_src_snap_id = 0;
+
+  for (auto &pair : m_snap_map) {
+    assert(!pair.second.empty());
+    librados::snap_t end_src_snap_id = pair.first;
+    librados::snap_t end_dst_snap_id = pair.second.front();
+
+    interval_set<uint64_t> diff;
+    uint64_t end_size;
+    bool exists;
+    librados::snap_t clone_end_snap_id;
+    calc_snap_set_diff(m_cct, m_snap_set, start_src_snap_id,
+                       end_src_snap_id, &diff, &end_size, &exists,
+                       &clone_end_snap_id);
+    if (!exists) {
+      end_size = 0;
+    }
+
+    ldout(m_cct, 20) << "start_src_snap_id=" << start_src_snap_id << ", "
+                     << "end_src_snap_id=" << end_src_snap_id << ", "
+                     << "clone_end_snap_id=" << clone_end_snap_id << ", "
+                     << "end_dst_snap_id=" << end_dst_snap_id << ", "
+                     << "diff=" << diff << ", "
+                     << "end_size=" << end_size << ", "
+                     << "exists=" << exists << dendl;
+
+    m_zero_interval[end_src_snap_id] = {};
+
+    if (exists || prev_exists) {
+      // clip diff to size of object (in case it was truncated)
+      if (end_size < prev_end_size) {
+        interval_set<uint64_t> trunc;
+        trunc.insert(end_size, prev_end_size);
+        trunc.intersection_of(diff);
+        diff.subtract(trunc);
+        ldout(m_cct, 20) << "clearing truncate diff: " << trunc << dendl;
+      }
+
+      if (exists) {
+        // reads should be issued against the newest (existing) snapshot within
+        // the associated snapshot object clone. writes should be issued
+        // against the oldest snapshot in the snap_map.
+        assert(clone_end_snap_id >= end_src_snap_id);
+        if (clone_end_snap_id > src_copy_point_snap_id) {
+          // do not read past the copy point snapshot
+          clone_end_snap_id = src_copy_point_snap_id;
+        }
+      }
+
+      for (auto &it : m_src_object_extents) {
+        auto dst_object_offset = it.first;
+        auto &e = it.second;
+
+        if (e.object_no != m_src_ono) {
+          continue;
+        }
+
+        interval_set<uint64_t> read_interval;
+        read_interval.insert(e.offset, e.length);
+
+        if (end_size < prev_end_size) {
+          interval_set<uint64_t> zero_interval;
+          zero_interval.insert(end_size, prev_end_size - end_size);
+          zero_interval.intersection_of(read_interval);
+          if (!zero_interval.empty()) {
+            auto it = zero_interval.begin();
+            auto offset = it.get_start() - e.offset;
+            m_zero_interval[end_src_snap_id].insert(dst_object_offset + offset,
+                                                    it.get_len());
+            ldout(m_cct, 20) << "extent " << e.offset << "~" << e.length
+                             << " intersects truncation " << end_size << "~"
+                             << prev_end_size - end_size << ", inserting zero "
+                             << dst_object_offset + offset << "~"
+                             << it.get_len() << dendl;
+          }
+        }
+
+        // limit read interval to diff
+        read_interval.intersection_of(diff);
+
+        ldout(m_cct, 20) << "src_object_extent: " << e.offset << "~" << e.length
+                         << ", dst_object_offset=" << dst_object_offset
+                         << ", read: " << read_interval << dendl;
+
+        assert(exists || read_interval.empty());
+
+        for (auto it = read_interval.begin(); it != read_interval.end();
+             it++) {
+            assert(it.get_start() >= e.offset);
+            auto offset = it.get_start() - e.offset;
+            ldout(m_cct, 20) << "read/write op: " << it.get_start() << "~"
+                             << it.get_len() << " dst: "
+                             << dst_object_offset + offset << dendl;
+            m_read_ops[{end_src_snap_id, clone_end_snap_id}]
+              .emplace_back(COPY_OP_TYPE_WRITE, it.get_start(),
+                            dst_object_offset + offset, it.get_len());
+        }
+      }
+    }
+
+    prev_end_size = end_size;
+    prev_exists = exists;
+    start_src_snap_id = end_src_snap_id;
+  }
+
+  for (auto &it : m_read_ops) {
+    m_read_snaps.push_back(it.first);
+  }
+}
+
+template <typename I>
+void ObjectCopyRequest<I>::merge_write_ops() {
+  ldout(m_cct, 20) << dendl;
+
+  for (auto &it : m_zero_interval) {
+    m_dst_zero_interval[it.first].insert(it.second);
+  }
+
+  for (auto &it : m_read_ops) {
+    auto src_snap_seq = it.first.first;
+    auto &copy_ops = it.second;
+    for (auto &copy_op : copy_ops) {
+      uint64_t src_offset = copy_op.src_offset;
+      uint64_t dst_offset = copy_op.dst_offset;
+      for (auto &e : copy_op.src_extent_map) {
+        uint64_t zero_len = e.first - src_offset;
+        if (zero_len > 0) {
+          ldout(m_cct, 20) << "src_snap_seq=" << src_snap_seq
+                           << ", inserting zero " << dst_offset << "~"
+                           << zero_len << dendl;
+          m_dst_zero_interval[src_snap_seq].insert(dst_offset, zero_len);
+          src_offset += zero_len;
+          dst_offset += zero_len;
+        }
+        copy_op.dst_extent_map[dst_offset] = e.second;
+        src_offset += e.second;
+        dst_offset += e.second;
+      }
+      if (dst_offset < copy_op.dst_offset + copy_op.length) {
+        uint64_t zero_len = copy_op.dst_offset + copy_op.length - dst_offset;
+        ldout(m_cct, 20) << "src_snap_seq=" << src_snap_seq
+                         << ", inserting zero " << dst_offset << "~"
+                         << zero_len << dendl;
+        m_dst_zero_interval[src_snap_seq].insert(dst_offset, zero_len);
+      } else {
+        assert(dst_offset == copy_op.dst_offset + copy_op.length);
+      }
+      m_write_ops[src_snap_seq].emplace_back(std::move(copy_op));
+    }
+  }
+}
+
+template <typename I>
+void ObjectCopyRequest<I>::compute_zero_ops() {
+  ldout(m_cct, 20) << dendl;
+
+  bool fast_diff = m_dst_image_ctx->test_features(RBD_FEATURE_FAST_DIFF);
+  uint64_t prev_end_size = 0;
+  for (auto &it : m_dst_zero_interval) {
+    auto src_snap_seq = it.first;
+    auto &zero_interval = it.second;
+
+    uint64_t end_size = prev_end_size;
+
+    // update end_size if there are writes into higher offsets
+    auto iter = m_write_ops.find(src_snap_seq);
+    if (iter != m_write_ops.end()) {
+      for (auto &copy_op : iter->second) {
+        for (auto &e : copy_op.dst_extent_map) {
+          end_size = std::max(end_size, e.first + e.second);
+        }
+      }
+    }
+
+    for (auto z = zero_interval.begin(); z != zero_interval.end(); z++) {
+      if (z.get_start() + z.get_len() >= end_size) {
+        // zero interval at the object end
+        if (z.get_start() < prev_end_size) {
+          if (z.get_start() == 0) {
+            m_write_ops[src_snap_seq]
+              .emplace_back(COPY_OP_TYPE_REMOVE, 0, 0, 0);
+            ldout(m_cct, 20) << "COPY_OP_TYPE_REMOVE" << dendl;
+          } else {
+            m_write_ops[src_snap_seq]
+              .emplace_back(COPY_OP_TYPE_TRUNC, 0, z.get_start(), 0);
+            ldout(m_cct, 20) << "COPY_OP_TYPE_TRUNC " << z.get_start() << dendl;
+          }
+        }
+        end_size = z.get_start();
+      } else {
+        // zero interval inside the object
+        m_write_ops[src_snap_seq]
+          .emplace_back(COPY_OP_TYPE_ZERO, 0, z.get_start(), z.get_len());
+        ldout(m_cct, 20) << "COPY_OP_TYPE_ZERO " << z.get_start() << "~"
+                         << z.get_len() << dendl;
+      }
+    }
+    ldout(m_cct, 20) << "src_snap_seq=" << src_snap_seq << ", end_size="
+                     << end_size << dendl;
+    if (end_size > 0) {
+      m_dst_object_state[src_snap_seq] = OBJECT_EXISTS;
+      if (fast_diff && end_size == prev_end_size &&
+          m_write_ops[src_snap_seq].empty()) {
+        m_dst_object_state[src_snap_seq] = OBJECT_EXISTS_CLEAN;
+      }
+    }
+    prev_end_size = end_size;
+  }
+}
+
+template <typename I>
+void ObjectCopyRequest<I>::finish(int r) {
+  ldout(m_cct, 20) << "r=" << r << dendl;
+
+  // ensure IoCtxs are closed prior to proceeding
+  auto on_finish = m_on_finish;
+  delete this;
+
+  on_finish->complete(r);
+}
+
+} // namespace deep_copy
+} // namespace librbd
+
+template class librbd::deep_copy::ObjectCopyRequest<librbd::ImageCtx>;
diff --git a/src/librbd/deep_copy/ObjectCopyRequest.h b/src/librbd/deep_copy/ObjectCopyRequest.h
new file mode 100644 (file)
index 0000000..e974b56
--- /dev/null
@@ -0,0 +1,182 @@
+// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*-
+// vim: ts=8 sw=2 smarttab
+
+#ifndef CEPH_LIBRBD_DEEP_COPY_OBJECT_COPY_REQUEST_H
+#define CEPH_LIBRBD_DEEP_COPY_OBJECT_COPY_REQUEST_H
+
+#include "include/int_types.h"
+#include "include/rados/librados.hpp"
+#include "common/snap_types.h"
+#include "librbd/ImageCtx.h"
+#include "librbd/deep_copy/Types.h"
+#include <list>
+#include <map>
+#include <string>
+
+class Context;
+class RWLock;
+
+namespace librbd {
+namespace deep_copy {
+
+template <typename ImageCtxT = librbd::ImageCtx>
+class ObjectCopyRequest {
+public:
+  static ObjectCopyRequest* create(ImageCtxT *src_image_ctx,
+                                   ImageCtxT *dst_image_ctx,
+                                   const SnapMap &snap_map,
+                                   uint64_t object_number, Context *on_finish) {
+    return new ObjectCopyRequest(src_image_ctx, dst_image_ctx, snap_map,
+                                 object_number, on_finish);
+  }
+
+  ObjectCopyRequest(ImageCtxT *src_image_ctx, ImageCtxT *dst_image_ctx,
+                    const SnapMap &snap_map, uint64_t object_number,
+                    Context *on_finish);
+
+  void send();
+
+  // testing support
+  inline librados::IoCtx &get_src_io_ctx() {
+    return m_src_io_ctx;
+  }
+  inline librados::IoCtx &get_dst_io_ctx() {
+    return m_dst_io_ctx;
+  }
+
+private:
+  /**
+   * @verbatim
+   *
+   * <start>
+   *    |     /----------------------\
+   *    |     |                      |
+   *    v     v                      | (repeat for each src object)
+   * LIST_SNAPS < * * *              |
+   *    |             * (-ENOENT and snap set stale)
+   *    |   * * * * * *              |
+   *    |   * /-----------\          |
+   *    |   * |           | (repeat for each snapshot)
+   *    v   * v           |          |
+   * READ_OBJECT ---------/          |
+   *    |     |                      |
+   *    |     \----------------------/
+   *    |
+   *    |     /-----------\
+   *    |     |           | (repeat for each snapshot)
+   *    v     v           |
+   * WRITE_OBJECT --------/
+   *    |
+   *    |     /-----------\
+   *    |     |           | (repeat for each snapshot)
+   *    v     v           |
+   * UPDATE_OBJECT_MAP ---/ (skip if object
+   *    |                    map disabled)
+   *    v
+   * <finish>
+   *
+   * @endverbatim
+   */
+
+  struct SrcObjectExtent {
+    uint64_t object_no = 0;
+    uint64_t offset = 0;
+    uint64_t length = 0;
+
+    SrcObjectExtent() {
+    }
+    SrcObjectExtent(uint64_t object_no, uint64_t offset, uint64_t length)
+      : object_no(object_no), offset(offset), length(length) {
+    }
+  };
+
+  typedef std::map<uint64_t, SrcObjectExtent> SrcObjectExtents;
+
+  enum CopyOpType {
+    COPY_OP_TYPE_WRITE,
+    COPY_OP_TYPE_ZERO,
+    COPY_OP_TYPE_TRUNC,
+    COPY_OP_TYPE_REMOVE,
+  };
+
+  typedef std::map<uint64_t, uint64_t> ExtentMap;
+
+  struct CopyOp {
+    CopyOp(CopyOpType type, uint64_t src_offset, uint64_t dst_offset,
+           uint64_t length)
+      : type(type), src_offset(src_offset), dst_offset(dst_offset),
+        length(length) {
+    }
+
+    CopyOpType type;
+    uint64_t src_offset;
+    uint64_t dst_offset;
+    uint64_t length;
+
+    ExtentMap src_extent_map;
+    ExtentMap dst_extent_map;
+    bufferlist out_bl;
+  };
+
+  typedef std::list<CopyOp> CopyOps;
+  typedef std::pair<librados::snap_t, librados::snap_t> WriteReadSnapIds;
+  typedef std::map<librados::snap_t, uint8_t> SnapObjectStates;
+  typedef std::map<librados::snap_t, std::map<uint64_t, uint64_t>> SnapObjectSizes;
+
+  ImageCtxT *m_src_image_ctx;
+  ImageCtxT *m_dst_image_ctx;
+  CephContext *m_cct;
+  const SnapMap &m_snap_map;
+  uint64_t m_dst_object_number;
+  Context *m_on_finish;
+
+  decltype(m_src_image_ctx->data_ctx) m_src_io_ctx;
+  decltype(m_dst_image_ctx->data_ctx) m_dst_io_ctx;
+  std::string m_dst_oid;
+
+  std::set<uint64_t> m_src_objects;
+  uint64_t m_src_ono;
+  std::string m_src_oid;
+  SrcObjectExtents m_src_object_extents;
+  librados::snap_set_t m_snap_set;
+  int m_snap_ret = 0;
+  bool m_retry_missing_read = false;
+  librados::snap_set_t m_retry_snap_set;
+
+  std::map<WriteReadSnapIds, CopyOps> m_read_ops;
+  std::list<WriteReadSnapIds> m_read_snaps;
+  std::map<librados::snap_t, CopyOps> m_write_ops;
+  std::map<librados::snap_t, interval_set<uint64_t>> m_zero_interval;
+  std::map<librados::snap_t, interval_set<uint64_t>> m_dst_zero_interval;
+  std::map<librados::snap_t, uint8_t> m_dst_object_state;
+
+  void send_list_snaps();
+  void handle_list_snaps(int r);
+
+  void send_read_object();
+  void handle_read_object(int r);
+
+  void send_write_object();
+  void handle_write_object(int r);
+
+  void send_update_object_map();
+  void handle_update_object_map(int r);
+
+  Context *start_lock_op(RWLock &owner_lock);
+
+  uint64_t src_to_dst_object_offset(uint64_t objectno, uint64_t offset);
+
+  void compute_src_object_extents();
+  void compute_read_ops();
+  void merge_write_ops();
+  void compute_zero_ops();
+
+  void finish(int r);
+};
+
+} // namespace deep_copy
+} // namespace librbd
+
+extern template class librbd::deep_copy::ObjectCopyRequest<librbd::ImageCtx>;
+
+#endif // CEPH_LIBRBD_DEEP_COPY_OBJECT_COPY_REQUEST_H
diff --git a/src/librbd/deep_copy/SetHeadRequest.cc b/src/librbd/deep_copy/SetHeadRequest.cc
new file mode 100644 (file)
index 0000000..067c297
--- /dev/null
@@ -0,0 +1,219 @@
+// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*-
+// vim: ts=8 sw=2 smarttab
+
+#include "SetHeadRequest.h"
+#include "common/errno.h"
+#include "cls/rbd/cls_rbd_client.h"
+#include "cls/rbd/cls_rbd_types.h"
+#include "librbd/ExclusiveLock.h"
+#include "librbd/Utils.h"
+
+#define dout_subsys ceph_subsys_rbd
+#undef dout_prefix
+#define dout_prefix *_dout << "librbd::deep_copy::SetHeadRequest: " \
+                           << this << " " << __func__ << ": "
+
+namespace librbd {
+namespace deep_copy {
+
+using librbd::util::create_context_callback;
+using librbd::util::create_rados_callback;
+
+template <typename I>
+SetHeadRequest<I>::SetHeadRequest(I *image_ctx, uint64_t size,
+                                  const librbd::ParentSpec &spec,
+                                  uint64_t parent_overlap,
+                                  Context *on_finish)
+  : m_image_ctx(image_ctx), m_size(size), m_parent_spec(spec),
+    m_parent_overlap(parent_overlap), m_on_finish(on_finish),
+    m_cct(image_ctx->cct) {
+}
+
+template <typename I>
+void SetHeadRequest<I>::send() {
+  send_set_size();
+}
+
+template <typename I>
+void SetHeadRequest<I>::send_set_size() {
+  m_image_ctx->snap_lock.get_read();
+  if (m_image_ctx->size == m_size) {
+    m_image_ctx->snap_lock.put_read();
+    send_remove_parent();
+    return;
+  }
+  m_image_ctx->snap_lock.put_read();
+
+  ldout(m_cct, 20) << dendl;
+
+  // Change the image size on disk so that the snapshot picks up
+  // the expected size.  We can do this because the last snapshot
+  // we process is the sync snapshot which was created to match the
+  // image size. We also don't need to worry about trimming because
+  // we track the highest possible object number within the sync record
+  librados::ObjectWriteOperation op;
+  librbd::cls_client::set_size(&op, m_size);
+
+  auto finish_op_ctx = start_lock_op();
+  if (finish_op_ctx == nullptr) {
+    lderr(m_cct) << "lost exclusive lock" << dendl;
+    finish(-EROFS);
+    return;
+  }
+
+  auto ctx = new FunctionContext([this, finish_op_ctx](int r) {
+      handle_set_size(r);
+      finish_op_ctx->complete(0);
+    });
+  librados::AioCompletion *comp = create_rados_callback(ctx);
+  int r = m_image_ctx->md_ctx.aio_operate(m_image_ctx->header_oid, comp, &op);
+  assert(r == 0);
+  comp->release();
+}
+
+template <typename I>
+void SetHeadRequest<I>::handle_set_size(int r) {
+  ldout(m_cct, 20) << "r=" << r << dendl;
+
+  if (r < 0) {
+    lderr(m_cct) << "failed to update image size: " << cpp_strerror(r) << dendl;
+    finish(r);
+    return;
+  }
+
+  {
+    // adjust in-memory image size now that it's updated on disk
+    RWLock::WLocker snap_locker(m_image_ctx->snap_lock);
+    m_image_ctx->size = m_size;
+  }
+
+  send_remove_parent();
+}
+
+template <typename I>
+void SetHeadRequest<I>::send_remove_parent() {
+  m_image_ctx->parent_lock.get_read();
+  if (m_image_ctx->parent_md.spec.pool_id == -1 ||
+      m_image_ctx->parent_md.spec == m_parent_spec) {
+    m_image_ctx->parent_lock.put_read();
+    send_set_parent();
+    return;
+  }
+  m_image_ctx->parent_lock.put_read();
+
+  ldout(m_cct, 20) << dendl;
+
+  librados::ObjectWriteOperation op;
+  librbd::cls_client::remove_parent(&op);
+
+  auto finish_op_ctx = start_lock_op();
+  if (finish_op_ctx == nullptr) {
+    lderr(m_cct) << "lost exclusive lock" << dendl;
+    finish(-EROFS);
+    return;
+  }
+
+  auto ctx = new FunctionContext([this, finish_op_ctx](int r) {
+      handle_remove_parent(r);
+      finish_op_ctx->complete(0);
+    });
+  librados::AioCompletion *comp = create_rados_callback(ctx);
+  int r = m_image_ctx->md_ctx.aio_operate(m_image_ctx->header_oid, comp, &op);
+  assert(r == 0);
+  comp->release();
+}
+
+template <typename I>
+void SetHeadRequest<I>::handle_remove_parent(int r) {
+  ldout(m_cct, 20) << "r=" << r << dendl;
+
+  if (r < 0) {
+    lderr(m_cct) << "failed to remove parent: " << cpp_strerror(r) << dendl;
+    finish(r);
+    return;
+  }
+
+  {
+    // adjust in-memory parent now that it's updated on disk
+    RWLock::WLocker parent_locker(m_image_ctx->parent_lock);
+    m_image_ctx->parent_md.spec = {};
+    m_image_ctx->parent_md.overlap = 0;
+  }
+
+  send_set_parent();
+}
+
+template <typename I>
+void SetHeadRequest<I>::send_set_parent() {
+  m_image_ctx->parent_lock.get_read();
+  if (m_image_ctx->parent_md.spec == m_parent_spec &&
+      m_image_ctx->parent_md.overlap == m_parent_overlap) {
+    m_image_ctx->parent_lock.put_read();
+    finish(0);
+    return;
+  }
+  m_image_ctx->parent_lock.put_read();
+
+  ldout(m_cct, 20) << dendl;
+
+  librados::ObjectWriteOperation op;
+  librbd::cls_client::set_parent(&op, m_parent_spec, m_parent_overlap);
+
+  auto finish_op_ctx = start_lock_op();
+  if (finish_op_ctx == nullptr) {
+    lderr(m_cct) << "lost exclusive lock" << dendl;
+    finish(-EROFS);
+    return;
+  }
+
+  auto ctx = new FunctionContext([this, finish_op_ctx](int r) {
+      handle_set_parent(r);
+      finish_op_ctx->complete(0);
+    });
+  librados::AioCompletion *comp = create_rados_callback(ctx);
+  int r = m_image_ctx->md_ctx.aio_operate(m_image_ctx->header_oid, comp, &op);
+  assert(r == 0);
+  comp->release();
+}
+
+template <typename I>
+void SetHeadRequest<I>::handle_set_parent(int r) {
+  ldout(m_cct, 20) << "r=" << r << dendl;
+
+  if (r < 0) {
+    lderr(m_cct) << "failed to set parent: " << cpp_strerror(r) << dendl;
+    finish(r);
+    return;
+  }
+
+  {
+    // adjust in-memory parent now that it's updated on disk
+    RWLock::WLocker parent_locker(m_image_ctx->parent_lock);
+    m_image_ctx->parent_md.spec = m_parent_spec;
+    m_image_ctx->parent_md.overlap = m_parent_overlap;
+  }
+
+  finish(0);
+}
+
+template <typename I>
+Context *SetHeadRequest<I>::start_lock_op() {
+  RWLock::RLocker owner_locker(m_image_ctx->owner_lock);
+  if (m_image_ctx->exclusive_lock == nullptr) {
+    return new FunctionContext([](int r) {});
+  }
+  return m_image_ctx->exclusive_lock->start_op();
+}
+
+template <typename I>
+void SetHeadRequest<I>::finish(int r) {
+  ldout(m_cct, 20) << "r=" << r << dendl;
+
+  m_on_finish->complete(r);
+  delete this;
+}
+
+} // namespace deep_copy
+} // namespace librbd
+
+template class librbd::deep_copy::SetHeadRequest<librbd::ImageCtx>;
diff --git a/src/librbd/deep_copy/SetHeadRequest.h b/src/librbd/deep_copy/SetHeadRequest.h
new file mode 100644 (file)
index 0000000..7754ff7
--- /dev/null
@@ -0,0 +1,87 @@
+// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*-
+// vim: ts=8 sw=2 smarttab
+
+#ifndef CEPH_LIBRBD_DEEP_COPY_SET_HEAD_REQUEST_H
+#define CEPH_LIBRBD_DEEP_COPY_SET_HEAD_REQUEST_H
+
+#include "include/int_types.h"
+#include "include/rados/librados.hpp"
+#include "common/snap_types.h"
+#include "librbd/ImageCtx.h"
+#include "librbd/Types.h"
+#include <map>
+#include <set>
+#include <string>
+#include <tuple>
+
+class Context;
+
+namespace librbd {
+namespace deep_copy {
+
+template <typename ImageCtxT = librbd::ImageCtx>
+class SetHeadRequest {
+public:
+  static SetHeadRequest* create(ImageCtxT *image_ctx, uint64_t size,
+                                const librbd::ParentSpec &parent_spec,
+                                uint64_t parent_overlap,
+                                Context *on_finish) {
+    return new SetHeadRequest(image_ctx, size, parent_spec, parent_overlap,
+                              on_finish);
+  }
+
+  SetHeadRequest(ImageCtxT *image_ctx, uint64_t size,
+                 const librbd::ParentSpec &parent_spec, uint64_t parent_overlap,
+                 Context *on_finish);
+
+  void send();
+
+private:
+  /**
+   * @verbatim
+   *
+   * <start>
+   *    |
+   *    v (skip if not needed)
+   * SET_SIZE
+   *    |
+   *    v (skip if not needed)
+   * REMOVE_PARENT
+   *    |
+   *    v (skip if not needed)
+   * SET_PARENT
+   *    |
+   *    v
+   * <finish>
+   *
+   * @endverbatim
+   */
+
+  ImageCtxT *m_image_ctx;
+  uint64_t m_size;
+  librbd::ParentSpec m_parent_spec;
+  uint64_t m_parent_overlap;
+  Context *m_on_finish;
+
+  CephContext *m_cct;
+
+  void send_set_size();
+  void handle_set_size(int r);
+
+  void send_remove_parent();
+  void handle_remove_parent(int r);
+
+  void send_set_parent();
+  void handle_set_parent(int r);
+
+  Context *start_lock_op();
+
+  void finish(int r);
+};
+
+} // namespace deep_copy
+} // namespace librbd
+
+extern template class librbd::deep_copy::SetHeadRequest<librbd::ImageCtx>;
+
+#endif // CEPH_LIBRBD_DEEP_COPY_SET_HEAD_REQUEST_H
diff --git a/src/librbd/deep_copy/SnapshotCopyRequest.cc b/src/librbd/deep_copy/SnapshotCopyRequest.cc
new file mode 100644 (file)
index 0000000..0032719
--- /dev/null
@@ -0,0 +1,559 @@
+// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*-
+// vim: ts=8 sw=2 smarttab
+
+#include "SnapshotCopyRequest.h"
+#include "SnapshotCreateRequest.h"
+#include "common/errno.h"
+#include "common/WorkQueue.h"
+#include "librbd/ExclusiveLock.h"
+#include "librbd/Operations.h"
+#include "librbd/Utils.h"
+
+#define dout_subsys ceph_subsys_rbd
+#undef dout_prefix
+#define dout_prefix *_dout << "librbd::deep_copy::SnapshotCopyRequest: " \
+                           << this << " " << __func__ << ": "
+
+namespace librbd {
+namespace deep_copy {
+
+namespace {
+
+template <typename I>
+const std::string &get_snapshot_name(I *image_ctx, librados::snap_t snap_id) {
+  auto snap_it = std::find_if(image_ctx->snap_ids.begin(),
+                              image_ctx->snap_ids.end(),
+                              [snap_id](
+                               const std::pair<
+                                         std::pair<cls::rbd::SnapshotNamespace,
+                                                   std::string>,
+                                         librados::snap_t> &pair) {
+    return pair.second == snap_id;
+  });
+  assert(snap_it != image_ctx->snap_ids.end());
+  return snap_it->first.second;
+}
+
+} // anonymous namespace
+
+using librbd::util::unique_lock_name;
+
+template <typename I>
+SnapshotCopyRequest<I>::SnapshotCopyRequest(I *src_image_ctx,
+                                            I *dst_image_ctx,
+                                            ContextWQ *work_queue,
+                                            SnapSeqs *snap_seqs,
+                                            Context *on_finish)
+  : RefCountedObject(dst_image_ctx->cct, 1), m_src_image_ctx(src_image_ctx),
+    m_dst_image_ctx(dst_image_ctx), m_work_queue(work_queue),
+    m_snap_seqs_result(snap_seqs), m_snap_seqs(*snap_seqs),
+    m_on_finish(on_finish), m_cct(dst_image_ctx->cct),
+    m_lock(unique_lock_name("SnapshotCopyRequest::m_lock", this)) {
+  // snap ids ordered from oldest to newest
+  m_src_snap_ids.insert(src_image_ctx->snaps.begin(),
+                        src_image_ctx->snaps.end());
+  m_dst_snap_ids.insert(dst_image_ctx->snaps.begin(),
+                        dst_image_ctx->snaps.end());
+}
+
+template <typename I>
+void SnapshotCopyRequest<I>::send() {
+  librbd::ParentSpec src_parent_spec;
+  int r = validate_parent(m_src_image_ctx, &src_parent_spec);
+  if (r < 0) {
+    lderr(m_cct) << "source image parent spec mismatch" << dendl;
+    error(r);
+    return;
+  }
+
+  r = validate_parent(m_dst_image_ctx, &m_dst_parent_spec);
+  if (r < 0) {
+    lderr(m_cct) << "destination image parent spec mismatch" << dendl;
+    error(r);
+    return;
+  }
+
+  send_snap_unprotect();
+}
+
+template <typename I>
+void SnapshotCopyRequest<I>::cancel() {
+  Mutex::Locker locker(m_lock);
+
+  ldout(m_cct, 20) << dendl;
+  m_canceled = true;
+}
+
+template <typename I>
+void SnapshotCopyRequest<I>::send_snap_unprotect() {
+
+  SnapIdSet::iterator snap_id_it = m_dst_snap_ids.begin();
+  if (m_prev_snap_id != CEPH_NOSNAP) {
+    snap_id_it = m_dst_snap_ids.upper_bound(m_prev_snap_id);
+  }
+
+  for (; snap_id_it != m_dst_snap_ids.end(); ++snap_id_it) {
+    librados::snap_t dst_snap_id = *snap_id_it;
+
+    m_dst_image_ctx->snap_lock.get_read();
+
+    bool dst_unprotected;
+    int r = m_dst_image_ctx->is_snap_unprotected(dst_snap_id, &dst_unprotected);
+    if (r < 0) {
+      lderr(m_cct) << "failed to retrieve destination snap unprotect status: "
+           << cpp_strerror(r) << dendl;
+      m_dst_image_ctx->snap_lock.put_read();
+      finish(r);
+      return;
+    }
+    m_dst_image_ctx->snap_lock.put_read();
+
+    if (dst_unprotected) {
+      // snap is already unprotected -- check next snap
+      continue;
+    }
+
+    // if destination snapshot is protected and (1) it isn't in our mapping
+    // table, or (2) the source snapshot isn't protected, unprotect it
+    auto snap_seq_it = std::find_if(
+      m_snap_seqs.begin(), m_snap_seqs.end(),
+      [dst_snap_id](const SnapSeqs::value_type& pair) {
+        return pair.second == dst_snap_id;
+      });
+
+    if (snap_seq_it != m_snap_seqs.end()) {
+      m_src_image_ctx->snap_lock.get_read();
+      bool src_unprotected;
+      r = m_src_image_ctx->is_snap_unprotected(snap_seq_it->first,
+                                               &src_unprotected);
+      ldout(m_cct, 20) << "m_src_image_ctx->is_snap_unprotected("<< snap_seq_it->first << "): r=" << r << ", src_unprotected=" << src_unprotected << dendl;
+      if (r == -ENOENT) {
+        src_unprotected = true;
+        r = 0;
+      }
+      if (r < 0) {
+        lderr(m_cct) << "failed to retrieve source snap unprotect status: "
+                     << cpp_strerror(r) << dendl;
+        m_src_image_ctx->snap_lock.put_read();
+        finish(r);
+        return;
+      }
+      m_src_image_ctx->snap_lock.put_read();
+
+      if (src_unprotected) {
+        // source is unprotected -- unprotect destination snap
+        break;
+      }
+    } else {
+      // source snapshot doesn't exist -- unprotect destination snap
+      break;
+    }
+  }
+
+  if (snap_id_it == m_dst_snap_ids.end()) {
+    // no destination snapshots to unprotect
+    m_prev_snap_id = CEPH_NOSNAP;
+    send_snap_remove();
+    return;
+  }
+
+  m_prev_snap_id = *snap_id_it;
+  m_snap_name = get_snapshot_name(m_dst_image_ctx, m_prev_snap_id);
+
+  ldout(m_cct, 20) << "snap_name=" << m_snap_name << ", "
+                   << "snap_id=" << m_prev_snap_id << dendl;
+
+  auto finish_op_ctx = start_lock_op();
+  if (finish_op_ctx == nullptr) {
+    lderr(m_cct) << "lost exclusive lock" << dendl;
+    finish(-EROFS);
+    return;
+  }
+
+  auto ctx = new FunctionContext([this, finish_op_ctx](int r) {
+      handle_snap_unprotect(r);
+      finish_op_ctx->complete(0);
+    });
+  RWLock::RLocker owner_locker(m_dst_image_ctx->owner_lock);
+  m_dst_image_ctx->operations->execute_snap_unprotect(
+    cls::rbd::UserSnapshotNamespace(), m_snap_name.c_str(), ctx);
+}
+
+template <typename I>
+void SnapshotCopyRequest<I>::handle_snap_unprotect(int r) {
+  ldout(m_cct, 20) << "r=" << r << dendl;
+
+  if (r < 0) {
+    lderr(m_cct) << "failed to unprotect snapshot '" << m_snap_name << "': "
+         << cpp_strerror(r) << dendl;
+    finish(r);
+    return;
+  }
+  if (handle_cancellation()) {
+    return;
+  }
+
+  send_snap_unprotect();
+}
+
+template <typename I>
+void SnapshotCopyRequest<I>::send_snap_remove() {
+  SnapIdSet::iterator snap_id_it = m_dst_snap_ids.begin();
+  if (m_prev_snap_id != CEPH_NOSNAP) {
+    snap_id_it = m_dst_snap_ids.upper_bound(m_prev_snap_id);
+  }
+
+  for (; snap_id_it != m_dst_snap_ids.end(); ++snap_id_it) {
+    librados::snap_t dst_snap_id = *snap_id_it;
+
+    cls::rbd::SnapshotNamespace snap_namespace;
+    m_dst_image_ctx->snap_lock.get_read();
+    int r = m_dst_image_ctx->get_snap_namespace(dst_snap_id, &snap_namespace);
+    m_dst_image_ctx->snap_lock.put_read();
+    if (r < 0) {
+      lderr(m_cct) << "failed to retrieve destination snap namespace: "
+                   << m_snap_name << dendl;
+      finish(r);
+      return;
+    }
+
+    if (boost::get<cls::rbd::UserSnapshotNamespace>(&snap_namespace) ==
+          nullptr) {
+      continue;
+    }
+
+    // if the destination snapshot isn't in our mapping table, remove it
+    auto snap_seq_it = std::find_if(
+      m_snap_seqs.begin(), m_snap_seqs.end(),
+      [dst_snap_id](const SnapSeqs::value_type& pair) {
+        return pair.second == dst_snap_id;
+      });
+
+    if (snap_seq_it == m_snap_seqs.end()) {
+      break;
+    }
+  }
+
+  if (snap_id_it == m_dst_snap_ids.end()) {
+    // no destination snapshots to delete
+    m_prev_snap_id = CEPH_NOSNAP;
+    send_snap_create();
+    return;
+  }
+
+  m_prev_snap_id = *snap_id_it;
+  m_snap_name = get_snapshot_name(m_dst_image_ctx, m_prev_snap_id);
+
+  ldout(m_cct, 20) << ""
+           << "snap_name=" << m_snap_name << ", "
+           << "snap_id=" << m_prev_snap_id << dendl;
+
+  auto finish_op_ctx = start_lock_op();
+  if (finish_op_ctx == nullptr) {
+    lderr(m_cct) << "lost exclusive lock" << dendl;
+    finish(-EROFS);
+    return;
+  }
+
+  auto ctx = new FunctionContext([this, finish_op_ctx](int r) {
+      handle_snap_remove(r);
+      finish_op_ctx->complete(0);
+    });
+  RWLock::RLocker owner_locker(m_dst_image_ctx->owner_lock);
+  m_dst_image_ctx->operations->execute_snap_remove(
+    cls::rbd::UserSnapshotNamespace(), m_snap_name.c_str(), ctx);
+}
+
+template <typename I>
+void SnapshotCopyRequest<I>::handle_snap_remove(int r) {
+  ldout(m_cct, 20) << "r=" << r << dendl;
+
+  if (r < 0) {
+    lderr(m_cct) << "failed to remove snapshot '" << m_snap_name << "': "
+                 << cpp_strerror(r) << dendl;
+    finish(r);
+    return;
+  }
+  if (handle_cancellation()) {
+    return;
+  }
+
+  send_snap_remove();
+}
+
+template <typename I>
+void SnapshotCopyRequest<I>::send_snap_create() {
+  SnapIdSet::iterator snap_id_it = m_src_snap_ids.begin();
+  if (m_prev_snap_id != CEPH_NOSNAP) {
+    snap_id_it = m_src_snap_ids.upper_bound(m_prev_snap_id);
+  }
+
+  for (; snap_id_it != m_src_snap_ids.end(); ++snap_id_it) {
+    librados::snap_t src_snap_id = *snap_id_it;
+
+    cls::rbd::SnapshotNamespace snap_namespace;
+    m_src_image_ctx->snap_lock.get_read();
+    int r = m_src_image_ctx->get_snap_namespace(src_snap_id, &snap_namespace);
+    m_src_image_ctx->snap_lock.put_read();
+    if (r < 0) {
+      lderr(m_cct) << "failed to retrieve source snap namespace: "
+                   << m_snap_name << dendl;
+      finish(r);
+      return;
+    }
+
+    // if the source snapshot isn't in our mapping table, create it
+    if (m_snap_seqs.find(src_snap_id) == m_snap_seqs.end() &&
+       boost::get<cls::rbd::UserSnapshotNamespace>(&snap_namespace) != nullptr) {
+      break;
+    }
+  }
+
+  if (snap_id_it == m_src_snap_ids.end()) {
+    // no source snapshots to create
+    m_prev_snap_id = CEPH_NOSNAP;
+    send_snap_protect();
+    return;
+  }
+
+  m_prev_snap_id = *snap_id_it;
+  m_snap_name = get_snapshot_name(m_src_image_ctx, m_prev_snap_id);
+
+  m_src_image_ctx->snap_lock.get_read();
+  auto snap_info_it = m_src_image_ctx->snap_info.find(m_prev_snap_id);
+  if (snap_info_it == m_src_image_ctx->snap_info.end()) {
+    m_src_image_ctx->snap_lock.put_read();
+    lderr(m_cct) << "failed to retrieve source snap info: " << m_snap_name
+                 << dendl;
+    finish(-ENOENT);
+    return;
+  }
+
+  uint64_t size = snap_info_it->second.size;
+  m_snap_namespace = snap_info_it->second.snap_namespace;
+  librbd::ParentSpec parent_spec;
+  uint64_t parent_overlap = 0;
+  if (snap_info_it->second.parent.spec.pool_id != -1) {
+    parent_spec = m_dst_parent_spec;
+    parent_overlap = snap_info_it->second.parent.overlap;
+  }
+  m_src_image_ctx->snap_lock.put_read();
+
+  ldout(m_cct, 20) << "snap_name=" << m_snap_name << ", "
+                   << "snap_id=" << m_prev_snap_id << ", "
+                   << "size=" << size << ", "
+                   << "parent_info=["
+                   << "pool_id=" << parent_spec.pool_id << ", "
+                   << "image_id=" << parent_spec.image_id << ", "
+                   << "snap_id=" << parent_spec.snap_id << ", "
+                   << "overlap=" << parent_overlap << "]" << dendl;
+
+  Context *finish_op_ctx = start_lock_op();
+  if (finish_op_ctx == nullptr) {
+    lderr(m_cct) << "lost exclusive lock" << dendl;
+    finish(-EROFS);
+    return;
+  }
+
+  auto ctx = new FunctionContext([this, finish_op_ctx](int r) {
+      handle_snap_create(r);
+      finish_op_ctx->complete(0);
+    });
+  SnapshotCreateRequest<I> *req = SnapshotCreateRequest<I>::create(
+    m_dst_image_ctx, m_snap_name, m_snap_namespace, size, parent_spec,
+    parent_overlap, ctx);
+  req->send();
+}
+
+template <typename I>
+void SnapshotCopyRequest<I>::handle_snap_create(int r) {
+  ldout(m_cct, 20) << "r=" << r << dendl;
+
+  if (r < 0) {
+    lderr(m_cct) << "failed to create snapshot '" << m_snap_name << "': "
+         << cpp_strerror(r) << dendl;
+    finish(r);
+    return;
+  }
+  if (handle_cancellation()) {
+    return;
+  }
+
+  assert(m_prev_snap_id != CEPH_NOSNAP);
+
+  auto snap_it = m_dst_image_ctx->snap_ids.find(
+      {cls::rbd::UserSnapshotNamespace(), m_snap_name});
+  assert(snap_it != m_dst_image_ctx->snap_ids.end());
+  librados::snap_t dst_snap_id = snap_it->second;
+
+  ldout(m_cct, 20) << "mapping source snap id " << m_prev_snap_id << " to "
+                   << dst_snap_id << dendl;
+  m_snap_seqs[m_prev_snap_id] = dst_snap_id;
+
+  send_snap_create();
+}
+
+template <typename I>
+void SnapshotCopyRequest<I>::send_snap_protect() {
+  SnapIdSet::iterator snap_id_it = m_src_snap_ids.begin();
+  if (m_prev_snap_id != CEPH_NOSNAP) {
+    snap_id_it = m_src_snap_ids.upper_bound(m_prev_snap_id);
+  }
+
+  for (; snap_id_it != m_src_snap_ids.end(); ++snap_id_it) {
+    librados::snap_t src_snap_id = *snap_id_it;
+
+    m_src_image_ctx->snap_lock.get_read();
+
+    bool src_protected;
+    int r = m_src_image_ctx->is_snap_protected(src_snap_id, &src_protected);
+    if (r < 0) {
+      lderr(m_cct) << "failed to retrieve source snap protect status: "
+                   << cpp_strerror(r) << dendl;
+      m_src_image_ctx->snap_lock.put_read();
+      finish(r);
+      return;
+    }
+    m_src_image_ctx->snap_lock.put_read();
+
+    if (!src_protected) {
+      // snap is not protected -- check next snap
+      continue;
+    }
+
+    // if destination snapshot is not protected, protect it
+    auto snap_seq_it = m_snap_seqs.find(src_snap_id);
+    assert(snap_seq_it != m_snap_seqs.end());
+
+    m_dst_image_ctx->snap_lock.get_read();
+    bool dst_protected;
+    r = m_dst_image_ctx->is_snap_protected(snap_seq_it->second, &dst_protected);
+    if (r < 0) {
+      lderr(m_cct) << "failed to retrieve destination snap protect status: "
+                   << cpp_strerror(r) << dendl;
+      m_dst_image_ctx->snap_lock.put_read();
+      finish(r);
+      return;
+    }
+    m_dst_image_ctx->snap_lock.put_read();
+
+    if (!dst_protected) {
+      break;
+    }
+  }
+
+  if (snap_id_it == m_src_snap_ids.end()) {
+    // no destination snapshots to protect
+    m_prev_snap_id = CEPH_NOSNAP;
+    finish(0);
+    return;
+  }
+
+  m_prev_snap_id = *snap_id_it;
+  m_snap_name = get_snapshot_name(m_src_image_ctx, m_prev_snap_id);
+
+  ldout(m_cct, 20) << "snap_name=" << m_snap_name << ", "
+                   << "snap_id=" << m_prev_snap_id << dendl;
+
+  auto finish_op_ctx = start_lock_op();
+  if (finish_op_ctx == nullptr) {
+    lderr(m_cct) << "lost exclusive lock" << dendl;
+    finish(-EROFS);
+    return;
+  }
+
+  auto ctx = new FunctionContext([this, finish_op_ctx](int r) {
+      handle_snap_protect(r);
+      finish_op_ctx->complete(0);
+    });
+  RWLock::RLocker owner_locker(m_dst_image_ctx->owner_lock);
+  m_dst_image_ctx->operations->execute_snap_protect(
+    cls::rbd::UserSnapshotNamespace(), m_snap_name.c_str(), ctx);
+}
+
+template <typename I>
+void SnapshotCopyRequest<I>::handle_snap_protect(int r) {
+  ldout(m_cct, 20) << "r=" << r << dendl;
+
+  if (r < 0) {
+    lderr(m_cct) << "failed to protect snapshot '" << m_snap_name << "': "
+                 << cpp_strerror(r) << dendl;
+    finish(r);
+    return;
+  }
+  if (handle_cancellation()) {
+    return;
+  }
+
+  send_snap_protect();
+}
+
+template <typename I>
+bool SnapshotCopyRequest<I>::handle_cancellation() {
+  {
+    Mutex::Locker locker(m_lock);
+    if (!m_canceled) {
+      return false;
+    }
+  }
+  ldout(m_cct, 10) << "snapshot copy canceled" << dendl;
+  finish(-ECANCELED);
+  return true;
+}
+
+template <typename I>
+void SnapshotCopyRequest<I>::error(int r) {
+  ldout(m_cct, 20) << "r=" << r << dendl;
+
+  m_work_queue->queue(new FunctionContext([this, r](int r1) { finish(r); }));
+}
+
+template <typename I>
+int SnapshotCopyRequest<I>::validate_parent(I *image_ctx,
+                                            librbd::ParentSpec *spec) {
+  RWLock::RLocker owner_locker(image_ctx->owner_lock);
+  RWLock::RLocker snap_locker(image_ctx->snap_lock);
+
+  // ensure source image's parent specs are still consistent
+  *spec = image_ctx->parent_md.spec;
+  for (auto &snap_info_pair : image_ctx->snap_info) {
+    auto &parent_spec = snap_info_pair.second.parent.spec;
+    if (parent_spec.pool_id == -1) {
+      continue;
+    } else if (spec->pool_id == -1) {
+      *spec = parent_spec;
+      continue;
+    }
+
+    if (*spec != parent_spec) {
+      return -EINVAL;
+    }
+  }
+  return 0;
+}
+
+template <typename I>
+Context *SnapshotCopyRequest<I>::start_lock_op() {
+  RWLock::RLocker owner_locker(m_dst_image_ctx->owner_lock);
+  if (m_dst_image_ctx->exclusive_lock == nullptr) {
+    return new FunctionContext([](int r) {});
+  }
+  return m_dst_image_ctx->exclusive_lock->start_op();
+}
+
+template <typename I>
+void SnapshotCopyRequest<I>::finish(int r) {
+  ldout(m_cct, 20) << "r=" << r << dendl;
+
+  if (r == 0) {
+    *m_snap_seqs_result = m_snap_seqs;
+  }
+
+  m_on_finish->complete(r);
+  put();
+}
+
+} // namespace deep_copy
+} // namespace librbd
+
+template class librbd::deep_copy::SnapshotCopyRequest<librbd::ImageCtx>;
diff --git a/src/librbd/deep_copy/SnapshotCopyRequest.h b/src/librbd/deep_copy/SnapshotCopyRequest.h
new file mode 100644 (file)
index 0000000..825e2d3
--- /dev/null
@@ -0,0 +1,124 @@
+// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*-
+// vim: ts=8 sw=2 smarttab
+
+#ifndef CEPH_LIBRBD_DEEP_COPY_SNAPSHOT_COPY_REQUEST_H
+#define CEPH_LIBRBD_DEEP_COPY_SNAPSHOT_COPY_REQUEST_H
+
+#include "include/int_types.h"
+#include "include/rados/librados.hpp"
+#include "common/RefCountedObj.h"
+#include "common/snap_types.h"
+#include "librbd/ImageCtx.h"
+#include "librbd/Types.h"
+#include <map>
+#include <set>
+#include <string>
+#include <tuple>
+
+class Context;
+
+namespace librbd {
+namespace deep_copy {
+
+template <typename ImageCtxT = librbd::ImageCtx>
+class SnapshotCopyRequest : public RefCountedObject {
+public:
+  static SnapshotCopyRequest* create(ImageCtxT *src_image_ctx,
+                                     ImageCtxT *dst_image_ctx,
+                                     ContextWQ *work_queue,
+                                     SnapSeqs *snap_seqs,
+                                     Context *on_finish) {
+    return new SnapshotCopyRequest(src_image_ctx, dst_image_ctx, work_queue,
+                                   snap_seqs, on_finish);
+  }
+
+  SnapshotCopyRequest(ImageCtxT *src_image_ctx, ImageCtxT *dst_image_ctx,
+                      ContextWQ *work_queue, SnapSeqs *snap_seqs,
+                      Context *on_finish);
+
+  void send();
+  void cancel();
+
+private:
+  /**
+   * @verbatim
+   *
+   * <start>
+   *    |
+   *    |   /-----------\
+   *    |   |           |
+   *    v   v           | (repeat as needed)
+   * UNPROTECT_SNAP ----/
+   *    |
+   *    |   /-----------\
+   *    |   |           |
+   *    v   v           | (repeat as needed)
+   * REMOVE_SNAP -------/
+   *    |
+   *    |   /-----------\
+   *    |   |           |
+   *    v   v           | (repeat as needed)
+   * CREATE_SNAP -------/
+   *    |
+   *    |   /-----------\
+   *    |   |           |
+   *    v   v           | (repeat as needed)
+   * PROTECT_SNAP ------/
+   *    |
+   *    v
+   * <finish>
+   *
+   * @endverbatim
+   */
+
+  typedef std::set<librados::snap_t> SnapIdSet;
+
+  ImageCtxT *m_src_image_ctx;
+  ImageCtxT *m_dst_image_ctx;
+  ContextWQ *m_work_queue;
+  SnapSeqs *m_snap_seqs_result;
+  SnapSeqs m_snap_seqs;
+  Context *m_on_finish;
+
+  CephContext *m_cct;
+  SnapIdSet m_src_snap_ids;
+  SnapIdSet m_dst_snap_ids;
+  librados::snap_t m_prev_snap_id = CEPH_NOSNAP;
+
+  std::string m_snap_name;
+  cls::rbd::SnapshotNamespace m_snap_namespace;
+
+  librbd::ParentSpec m_dst_parent_spec;
+
+  Mutex m_lock;
+  bool m_canceled = false;
+
+  void send_snap_unprotect();
+  void handle_snap_unprotect(int r);
+
+  void send_snap_remove();
+  void handle_snap_remove(int r);
+
+  void send_snap_create();
+  void handle_snap_create(int r);
+
+  void send_snap_protect();
+  void handle_snap_protect(int r);
+
+  bool handle_cancellation();
+
+  void error(int r);
+
+  int validate_parent(ImageCtxT *image_ctx, librbd::ParentSpec *spec);
+
+  Context *start_lock_op();
+
+  void finish(int r);
+};
+
+} // namespace deep_copy
+} // namespace librbd
+
+extern template class librbd::deep_copy::SnapshotCopyRequest<librbd::ImageCtx>;
+
+#endif // CEPH_LIBRBD_DEEP_COPY_SNAPSHOT_COPY_REQUEST_H
diff --git a/src/librbd/deep_copy/SnapshotCreateRequest.cc b/src/librbd/deep_copy/SnapshotCreateRequest.cc
new file mode 100644 (file)
index 0000000..a950376
--- /dev/null
@@ -0,0 +1,185 @@
+// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*-
+// vim: ts=8 sw=2 smarttab
+
+#include "SetHeadRequest.h"
+#include "SnapshotCreateRequest.h"
+#include "common/errno.h"
+#include "cls/rbd/cls_rbd_client.h"
+#include "cls/rbd/cls_rbd_types.h"
+#include "librbd/ExclusiveLock.h"
+#include "librbd/ObjectMap.h"
+#include "librbd/Operations.h"
+#include "librbd/Utils.h"
+#include "osdc/Striper.h"
+
+#define dout_subsys ceph_subsys_rbd
+#undef dout_prefix
+#define dout_prefix *_dout << "librbd::deep_copy::SnapshotCreateRequest: " \
+                           << this << " " << __func__ << ": "
+
+namespace librbd {
+namespace deep_copy {
+
+using librbd::util::create_context_callback;
+using librbd::util::create_rados_callback;
+
+template <typename I>
+SnapshotCreateRequest<I>::SnapshotCreateRequest(
+    I *dst_image_ctx, const std::string &snap_name,
+    const cls::rbd::SnapshotNamespace &snap_namespace,
+    uint64_t size, const librbd::ParentSpec &spec, uint64_t parent_overlap,
+    Context *on_finish)
+  : m_dst_image_ctx(dst_image_ctx), m_snap_name(snap_name),
+    m_snap_namespace(snap_namespace), m_size(size),
+    m_parent_spec(spec), m_parent_overlap(parent_overlap),
+    m_on_finish(on_finish), m_cct(dst_image_ctx->cct) {
+}
+
+template <typename I>
+void SnapshotCreateRequest<I>::send() {
+  send_set_head();
+}
+
+template <typename I>
+void SnapshotCreateRequest<I>::send_set_head() {
+  ldout(m_cct, 20) << dendl;
+
+  auto ctx = create_context_callback<
+    SnapshotCreateRequest<I>, &SnapshotCreateRequest<I>::handle_set_head>(this);
+  auto req = SetHeadRequest<I>::create(m_dst_image_ctx, m_size, m_parent_spec,
+                                       m_parent_overlap, ctx);
+  req->send();
+}
+
+template <typename I>
+void SnapshotCreateRequest<I>::handle_set_head(int r) {
+  ldout(m_cct, 20) << "r=" << r << dendl;
+
+  if (r < 0) {
+    lderr(m_cct) << "failed to set head: " << cpp_strerror(r) << dendl;
+    finish(r);
+    return;
+  }
+
+  send_create_snap();
+}
+
+template <typename I>
+void SnapshotCreateRequest<I>::send_create_snap() {
+  ldout(m_cct, 20) << "snap_name=" << m_snap_name << dendl;
+
+  auto finish_op_ctx = start_lock_op();
+  if (finish_op_ctx == nullptr) {
+    lderr(m_cct) << "lost exclusive lock" << dendl;
+    finish(-EROFS);
+    return;
+  }
+
+  auto ctx = new FunctionContext([this, finish_op_ctx](int r) {
+      handle_create_snap(r);
+      finish_op_ctx->complete(0);
+    });
+  RWLock::RLocker owner_locker(m_dst_image_ctx->owner_lock);
+  m_dst_image_ctx->operations->execute_snap_create(m_snap_namespace,
+                                                   m_snap_name.c_str(),
+                                                   ctx,
+                                                   0U, true);
+}
+
+template <typename I>
+void SnapshotCreateRequest<I>::handle_create_snap(int r) {
+  ldout(m_cct, 20) << "r=" << r << dendl;
+
+  if (r < 0) {
+    lderr(m_cct) << "failed to create snapshot '" << m_snap_name << "': "
+                 << cpp_strerror(r) << dendl;
+    finish(r);
+    return;
+  }
+
+  send_create_object_map();
+}
+template <typename I>
+void SnapshotCreateRequest<I>::send_create_object_map() {
+
+  if (!m_dst_image_ctx->test_features(RBD_FEATURE_OBJECT_MAP)) {
+    finish(0);
+    return;
+  }
+
+  m_dst_image_ctx->snap_lock.get_read();
+  auto snap_it = m_dst_image_ctx->snap_ids.find(
+    {cls::rbd::UserSnapshotNamespace(), m_snap_name});
+  if (snap_it == m_dst_image_ctx->snap_ids.end()) {
+    lderr(m_cct) << "failed to locate snap: " << m_snap_name << dendl;
+    m_dst_image_ctx->snap_lock.put_read();
+    finish(-ENOENT);
+    return;
+  }
+  librados::snap_t local_snap_id = snap_it->second;
+  m_dst_image_ctx->snap_lock.put_read();
+
+  std::string object_map_oid(librbd::ObjectMap<>::object_map_name(
+    m_dst_image_ctx->id, local_snap_id));
+  uint64_t object_count = Striper::get_num_objects(m_dst_image_ctx->layout,
+                                                   m_size);
+  ldout(m_cct, 20) << "object_map_oid=" << object_map_oid << ", "
+                   << "object_count=" << object_count << dendl;
+
+  // initialize an empty object map of the correct size (object sync
+  // will populate the object map)
+  librados::ObjectWriteOperation op;
+  librbd::cls_client::object_map_resize(&op, object_count, OBJECT_NONEXISTENT);
+
+  auto finish_op_ctx = start_lock_op();
+  if (finish_op_ctx == nullptr) {
+    lderr(m_cct) << "lost exclusive lock" << dendl;
+    finish(-EROFS);
+    return;
+  }
+
+  auto ctx = new FunctionContext([this, finish_op_ctx](int r) {
+      handle_create_object_map(r);
+      finish_op_ctx->complete(0);
+    });
+  librados::AioCompletion *comp = create_rados_callback(ctx);
+  int r = m_dst_image_ctx->md_ctx.aio_operate(object_map_oid, comp, &op);
+  assert(r == 0);
+  comp->release();
+}
+
+template <typename I>
+void SnapshotCreateRequest<I>::handle_create_object_map(int r) {
+  ldout(m_cct, 20) << "r=" << r << dendl;
+
+  if (r < 0) {
+    lderr(m_cct) << "failed to create object map: " << cpp_strerror(r)
+                 << dendl;
+    finish(r);
+    return;
+  }
+
+  finish(0);
+}
+
+template <typename I>
+Context *SnapshotCreateRequest<I>::start_lock_op() {
+  RWLock::RLocker owner_locker(m_dst_image_ctx->owner_lock);
+  if (m_dst_image_ctx->exclusive_lock == nullptr) {
+    return new FunctionContext([](int r) {});
+  }
+  return m_dst_image_ctx->exclusive_lock->start_op();
+}
+
+template <typename I>
+void SnapshotCreateRequest<I>::finish(int r) {
+  ldout(m_cct, 20) << "r=" << r << dendl;
+
+  m_on_finish->complete(r);
+  delete this;
+}
+
+} // namespace deep_copy
+} // namespace librbd
+
+template class librbd::deep_copy::SnapshotCreateRequest<librbd::ImageCtx>;
diff --git a/src/librbd/deep_copy/SnapshotCreateRequest.h b/src/librbd/deep_copy/SnapshotCreateRequest.h
new file mode 100644 (file)
index 0000000..4ddcefd
--- /dev/null
@@ -0,0 +1,95 @@
+// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*-
+// vim: ts=8 sw=2 smarttab
+
+#ifndef CEPH_LIBRBD_DEEP_COPY_SNAPSHOT_CREATE_REQUEST_H
+#define CEPH_LIBRBD_DEEP_COPY_SNAPSHOT_CREATE_REQUEST_H
+
+#include "include/int_types.h"
+#include "include/rados/librados.hpp"
+#include "common/snap_types.h"
+#include "librbd/ImageCtx.h"
+#include "librbd/Types.h"
+#include <map>
+#include <set>
+#include <string>
+#include <tuple>
+
+class Context;
+
+namespace librbd {
+namespace deep_copy {
+
+template <typename ImageCtxT = librbd::ImageCtx>
+class SnapshotCreateRequest {
+public:
+  static SnapshotCreateRequest* create(ImageCtxT *dst_image_ctx,
+                                       const std::string &snap_name,
+                                       const cls::rbd::SnapshotNamespace &snap_namespace,
+                                       uint64_t size,
+                                       const librbd::ParentSpec &parent_spec,
+                                       uint64_t parent_overlap,
+                                       Context *on_finish) {
+    return new SnapshotCreateRequest(dst_image_ctx, snap_name, snap_namespace, size,
+                                     parent_spec, parent_overlap, on_finish);
+  }
+
+  SnapshotCreateRequest(ImageCtxT *dst_image_ctx,
+                        const std::string &snap_name,
+                       const cls::rbd::SnapshotNamespace &snap_namespace,
+                       uint64_t size,
+                        const librbd::ParentSpec &parent_spec,
+                        uint64_t parent_overlap, Context *on_finish);
+
+  void send();
+
+private:
+  /**
+   * @verbatim
+   *
+   * <start>
+   *    |
+   *    v
+   * SET_HEAD
+   *    |
+   *    v
+   * CREATE_SNAP
+   *    |
+   *    v (skip if not needed)
+   * CREATE_OBJECT_MAP
+   *    |
+   *    v
+   * <finish>
+   *
+   * @endverbatim
+   */
+
+  ImageCtxT *m_dst_image_ctx;
+  std::string m_snap_name;
+  cls::rbd::SnapshotNamespace m_snap_namespace;
+  uint64_t m_size;
+  librbd::ParentSpec m_parent_spec;
+  uint64_t m_parent_overlap;
+  Context *m_on_finish;
+
+  CephContext *m_cct;
+
+  void send_set_head();
+  void handle_set_head(int r);
+
+  void send_create_snap();
+  void handle_create_snap(int r);
+
+  void send_create_object_map();
+  void handle_create_object_map(int r);
+
+  Context *start_lock_op();
+
+  void finish(int r);
+};
+
+} // namespace deep_copy
+} // namespace librbd
+
+extern template class librbd::deep_copy::SnapshotCreateRequest<librbd::ImageCtx>;
+
+#endif // CEPH_LIBRBD_DEEP_COPY_SNAPSHOT_CREATE_REQUEST_H
diff --git a/src/librbd/deep_copy/Types.h b/src/librbd/deep_copy/Types.h
new file mode 100644 (file)
index 0000000..1b513c3
--- /dev/null
@@ -0,0 +1,21 @@
+// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*-
+// vim: ts=8 sw=2 smarttab
+
+#ifndef CEPH_LIBRBD_DEEP_COPY_TYPES_H
+#define CEPH_LIBRBD_DEEP_COPY_TYPES_H
+
+#include "include/int_types.h"
+#include <boost/optional.hpp>
+
+namespace librbd {
+namespace deep_copy {
+
+typedef std::vector<librados::snap_t> SnapIds;
+typedef std::map<librados::snap_t, SnapIds> SnapMap;
+
+typedef boost::optional<uint64_t> ObjectNumber;
+
+} // namespace deep_copy
+} // namespace librbd
+
+#endif // CEPH_LIBRBD_DEEP_COPY_TYPES_H
diff --git a/src/librbd/deep_copy/Utils.cc b/src/librbd/deep_copy/Utils.cc
new file mode 100644 (file)
index 0000000..85a4b5b
--- /dev/null
@@ -0,0 +1,34 @@
+// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*-
+// vim: ts=8 sw=2 smarttab
+
+#include "Utils.h"
+
+namespace librbd {
+namespace deep_copy {
+namespace util {
+
+void compute_snap_map(librados::snap_t snap_id_start,
+                      librados::snap_t snap_id_end,
+                      const SnapSeqs &snap_seqs,
+                      SnapMap *snap_map) {
+  SnapIds snap_ids;
+  for (auto &it : snap_seqs) {
+    snap_ids.insert(snap_ids.begin(), it.second);
+    if (it.first < snap_id_start) {
+      continue;
+    } else if (snap_id_end != CEPH_NOSNAP && it.first > snap_id_end) {
+      break;
+    }
+
+    (*snap_map)[it.first] = snap_ids;
+  }
+
+  if (snap_id_end == CEPH_NOSNAP) {
+    snap_ids.insert(snap_ids.begin(), CEPH_NOSNAP);
+    (*snap_map)[CEPH_NOSNAP] = snap_ids;
+  }
+}
+
+} // util
+} // namespace deep_copy
+} // namespace librbd
diff --git a/src/librbd/deep_copy/Utils.h b/src/librbd/deep_copy/Utils.h
new file mode 100644 (file)
index 0000000..28e1d6d
--- /dev/null
@@ -0,0 +1,26 @@
+// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*-
+// vim: ts=8 sw=2 smarttab
+
+#ifndef CEPH_LIBRBD_DEEP_COPY_UTILS_H
+#define CEPH_LIBRBD_DEEP_COPY_UTILS_H
+
+#include "include/rados/librados.hpp"
+#include "librbd/Types.h"
+#include "librbd/deep_copy/Types.h"
+
+#include <boost/optional.hpp>
+
+namespace librbd {
+namespace deep_copy {
+namespace util {
+
+void compute_snap_map(librados::snap_t snap_id_start,
+                      librados::snap_t snap_id_end,
+                      const SnapSeqs &snap_seqs,
+                      SnapMap *snap_map);
+
+} // namespace util
+} // namespace deep_copy
+} // namespace librbd
+
+#endif // CEPH_LIBRBD_DEEP_COPY_UTILS_H
index 085ec023f7ef761b3ad36b7cab7aae1c0f045149..00fc2333ef591fe06472d6dff14d5940869b6665 100644 (file)
@@ -10,6 +10,7 @@
 #include "include/encoding.h"
 #include "include/types.h"
 #include "include/utime.h"
+#include "librbd/Types.h"
 #include <iosfwd>
 #include <list>
 #include <boost/none.hpp>
@@ -526,7 +527,6 @@ enum MirrorPeerState {
 
 struct MirrorPeerClientMeta {
   typedef std::list<MirrorPeerSyncPoint> SyncPoints;
-  typedef std::map<uint64_t, uint64_t> SnapSeqs;
 
   static const ClientMetaType TYPE = MIRROR_PEER_CLIENT_META_TYPE;
 
index 0f4331ee255501bfa0df44179dfb304848eedfa0..71fb8441228144e3b0551e870fc44626fb92811a 100644 (file)
@@ -56,6 +56,13 @@ public:
     STATE_ERROR
   };
 
+  static SnapshotRemoveRequest *create(
+      ImageCtxT &image_ctx, const cls::rbd::SnapshotNamespace &snap_namespace,
+      const std::string &snap_name, uint64_t snap_id, Context *on_finish) {
+    return new SnapshotRemoveRequest(image_ctx, on_finish, snap_namespace,
+                                     snap_name, snap_id);
+  }
+
   SnapshotRemoveRequest(ImageCtxT &image_ctx, Context *on_finish,
                        const cls::rbd::SnapshotNamespace &snap_namespace,
                        const std::string &snap_name,
index 8aa61a9456f048a63e79befe46c60ef3e7784563..b85a3592e71d7ad36e0c1e824394f32e7e33aa04 100644 (file)
@@ -6,6 +6,7 @@ set(librbd_test
   test_internal.cc
   test_mirroring.cc
   test_BlockGuard.cc
+  test_DeepCopy.cc
   test_Groups.cc
   test_MirroringWatcher.cc
   test_ObjectMap.cc
@@ -27,10 +28,17 @@ set_target_properties(rbd_test_mock PROPERTIES COMPILE_FLAGS
 set(unittest_librbd_srcs
   test_main.cc
   test_mock_fixture.cc
+  test_mock_DeepCopyRequest.cc
   test_mock_ExclusiveLock.cc
   test_mock_Journal.cc
   test_mock_ManagedLock.cc
   test_mock_ObjectMap.cc
+  deep_copy/test_mock_ImageCopyRequest.cc
+  deep_copy/test_mock_MetadataCopyRequest.cc
+  deep_copy/test_mock_ObjectCopyRequest.cc
+  deep_copy/test_mock_SetHeadRequest.cc
+  deep_copy/test_mock_SnapshotCopyRequest.cc
+  deep_copy/test_mock_SnapshotCreateRequest.cc
   exclusive_lock/test_mock_PreAcquireRequest.cc
   exclusive_lock/test_mock_PostAcquireRequest.cc
   exclusive_lock/test_mock_PreReleaseRequest.cc
diff --git a/src/test/librbd/deep_copy/test_mock_ImageCopyRequest.cc b/src/test/librbd/deep_copy/test_mock_ImageCopyRequest.cc
new file mode 100644 (file)
index 0000000..a839f9f
--- /dev/null
@@ -0,0 +1,508 @@
+// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*-
+// vim: ts=8 sw=2 smarttab
+
+#include "test/librbd/test_mock_fixture.h"
+#include "include/rbd/librbd.hpp"
+#include "librbd/ImageCtx.h"
+#include "librbd/ImageState.h"
+#include "librbd/Operations.h"
+#include "librbd/deep_copy/ImageCopyRequest.h"
+#include "librbd/deep_copy/ObjectCopyRequest.h"
+#include "librbd/internal.h"
+#include "test/librados_test_stub/MockTestMemIoCtxImpl.h"
+#include "test/librbd/mock/MockImageCtx.h"
+#include "test/librbd/test_support.h"
+#include <boost/scope_exit.hpp>
+
+namespace librbd {
+
+namespace {
+
+struct MockTestImageCtx : public librbd::MockImageCtx {
+  MockTestImageCtx(librbd::ImageCtx &image_ctx)
+    : librbd::MockImageCtx(image_ctx) {
+  }
+};
+
+} // anonymous namespace
+
+namespace deep_copy {
+
+template <>
+struct ObjectCopyRequest<librbd::MockTestImageCtx> {
+  static ObjectCopyRequest* s_instance;
+  static ObjectCopyRequest* create(
+      librbd::MockTestImageCtx *src_image_ctx,
+      librbd::MockTestImageCtx *dst_image_ctx, const SnapMap &snap_map,
+      uint64_t object_number, Context *on_finish) {
+    assert(s_instance != nullptr);
+    Mutex::Locker locker(s_instance->lock);
+    s_instance->snap_map = &snap_map;
+    s_instance->object_contexts[object_number] = on_finish;
+    s_instance->cond.Signal();
+    return s_instance;
+  }
+
+  MOCK_METHOD0(send, void());
+
+  Mutex lock;
+  Cond cond;
+
+  const SnapMap *snap_map = nullptr;
+  std::map<uint64_t, Context *> object_contexts;
+
+  ObjectCopyRequest() : lock("lock") {
+    s_instance = this;
+  }
+};
+
+ObjectCopyRequest<librbd::MockTestImageCtx>* ObjectCopyRequest<librbd::MockTestImageCtx>::s_instance = nullptr;
+
+} // namespace deep_copy
+} // namespace librbd
+
+// template definitions
+#include "librbd/deep_copy/ImageCopyRequest.cc"
+template class librbd::deep_copy::ImageCopyRequest<librbd::MockTestImageCtx>;
+
+namespace librbd {
+namespace deep_copy {
+
+using ::testing::_;
+using ::testing::InSequence;
+using ::testing::Return;
+
+class TestMockDeepCopyImageCopyRequest : public TestMockFixture {
+public:
+  typedef ImageCopyRequest<librbd::MockTestImageCtx> MockImageCopyRequest;
+  typedef ObjectCopyRequest<librbd::MockTestImageCtx> MockObjectCopyRequest;
+
+  librbd::ImageCtx *m_src_image_ctx;
+  librbd::ImageCtx *m_dst_image_ctx;
+  ThreadPool *m_thread_pool;
+  ContextWQ *m_work_queue;
+  librbd::SnapSeqs m_snap_seqs;
+  SnapMap m_snap_map;
+
+  void SetUp() override {
+    TestMockFixture::SetUp();
+
+    ASSERT_EQ(0, open_image(m_image_name, &m_src_image_ctx));
+
+    librbd::RBD rbd;
+    std::string dst_image_name = get_temp_image_name();
+    ASSERT_EQ(0, create_image_pp(rbd, m_ioctx, dst_image_name, m_image_size));
+    ASSERT_EQ(0, open_image(dst_image_name, &m_dst_image_ctx));
+
+    librbd::ImageCtx::get_thread_pool_instance(m_src_image_ctx->cct,
+                                               &m_thread_pool, &m_work_queue);
+  }
+
+  void expect_get_image_size(librbd::MockTestImageCtx &mock_image_ctx,
+                             uint64_t size) {
+    EXPECT_CALL(mock_image_ctx, get_image_size(_))
+      .WillOnce(Return(size)).RetiresOnSaturation();
+  }
+
+  void expect_object_copy_send(MockObjectCopyRequest &mock_object_copy_request) {
+    EXPECT_CALL(mock_object_copy_request, send());
+  }
+
+  bool complete_object_copy(MockObjectCopyRequest &mock_object_copy_request,
+                               uint64_t object_num, int r,
+                               std::function<void()> fn = []() {}) {
+    Mutex::Locker locker(mock_object_copy_request.lock);
+    while (mock_object_copy_request.object_contexts.count(object_num) == 0) {
+      if (mock_object_copy_request.cond.WaitInterval(mock_object_copy_request.lock,
+                                                     utime_t(10, 0)) != 0) {
+        return false;
+      }
+    }
+
+    FunctionContext *wrapper_ctx = new FunctionContext(
+      [&mock_object_copy_request, object_num, fn] (int r) {
+        fn();
+        mock_object_copy_request.object_contexts[object_num]->complete(r);
+      });
+    m_work_queue->queue(wrapper_ctx, r);
+    return true;
+  }
+
+  SnapMap wait_for_snap_map(MockObjectCopyRequest &mock_object_copy_request) {
+    Mutex::Locker locker(mock_object_copy_request.lock);
+    while (mock_object_copy_request.snap_map == nullptr) {
+      if (mock_object_copy_request.cond.WaitInterval(mock_object_copy_request.lock,
+                                                     utime_t(10, 0)) != 0) {
+        return SnapMap();
+      }
+    }
+    return *mock_object_copy_request.snap_map;
+  }
+
+  int create_snap(librbd::ImageCtx *image_ctx, const char* snap_name,
+                  librados::snap_t *snap_id) {
+    int r = image_ctx->operations->snap_create(
+        cls::rbd::UserSnapshotNamespace(), snap_name);
+    if (r < 0) {
+      return r;
+    }
+
+    r = image_ctx->state->refresh();
+    if (r < 0) {
+      return r;
+    }
+
+    if (image_ctx->snap_ids.count({cls::rbd::UserSnapshotNamespace(),
+                                   snap_name}) == 0) {
+      return -ENOENT;
+    }
+
+    if (snap_id != nullptr) {
+      *snap_id = image_ctx->snap_ids[{cls::rbd::UserSnapshotNamespace(),
+                                      snap_name}];
+    }
+    return 0;
+  }
+
+  int create_snap(const char* snap_name,
+                  librados::snap_t *src_snap_id_ = nullptr) {
+    librados::snap_t src_snap_id;
+    int r = create_snap(m_src_image_ctx, snap_name, &src_snap_id);
+    if (r < 0) {
+      return r;
+    }
+
+    if (src_snap_id_ != nullptr) {
+      *src_snap_id_ = src_snap_id;
+    }
+
+    librados::snap_t dst_snap_id;
+    r = create_snap(m_dst_image_ctx, snap_name, &dst_snap_id);
+    if (r < 0) {
+      return r;
+    }
+
+    // collection of all existing snaps in dst image
+    SnapIds dst_snap_ids({dst_snap_id});
+    if (!m_snap_map.empty()) {
+      dst_snap_ids.insert(dst_snap_ids.end(),
+                          m_snap_map.rbegin()->second.begin(),
+                          m_snap_map.rbegin()->second.end());
+    }
+    m_snap_map[src_snap_id] = dst_snap_ids;
+    m_snap_seqs[src_snap_id] = dst_snap_id;
+    return 0;
+  }
+};
+
+TEST_F(TestMockDeepCopyImageCopyRequest, SimpleImage) {
+  librados::snap_t snap_id_end;
+  ASSERT_EQ(0, create_snap("copy", &snap_id_end));
+
+  librbd::MockTestImageCtx mock_src_image_ctx(*m_src_image_ctx);
+  librbd::MockTestImageCtx mock_dst_image_ctx(*m_dst_image_ctx);
+  MockObjectCopyRequest mock_object_copy_request;
+
+  InSequence seq;
+  expect_get_image_size(mock_src_image_ctx, 1 << m_src_image_ctx->order);
+  expect_get_image_size(mock_src_image_ctx, 0);
+  expect_object_copy_send(mock_object_copy_request);
+
+  librbd::NoOpProgressContext no_op;
+  C_SaferCond ctx;
+  auto request = new MockImageCopyRequest(&mock_src_image_ctx,
+                                          &mock_dst_image_ctx,
+                                          0, snap_id_end, boost::none,
+                                          m_snap_seqs, &no_op, &ctx);
+  request->send();
+
+  ASSERT_EQ(m_snap_map, wait_for_snap_map(mock_object_copy_request));
+  ASSERT_TRUE(complete_object_copy(mock_object_copy_request, 0, 0));
+  ASSERT_EQ(0, ctx.wait());
+}
+
+TEST_F(TestMockDeepCopyImageCopyRequest, Throttled) {
+  librados::snap_t snap_id_end;
+  ASSERT_EQ(0, create_snap("copy", &snap_id_end));
+
+  uint64_t object_count = 55;
+
+  librbd::MockTestImageCtx mock_src_image_ctx(*m_src_image_ctx);
+  librbd::MockTestImageCtx mock_dst_image_ctx(*m_dst_image_ctx);
+  MockObjectCopyRequest mock_object_copy_request;
+
+  expect_get_image_size(mock_src_image_ctx,
+                        object_count * (1 << m_src_image_ctx->order));
+  expect_get_image_size(mock_src_image_ctx, 0);
+
+  EXPECT_CALL(mock_object_copy_request, send()).Times(object_count);
+
+  class ProgressContext : public librbd::ProgressContext {
+  public:
+    uint64_t object_count;
+    librbd::deep_copy::ObjectNumber expected_object_number;
+
+    ProgressContext(uint64_t object_count)
+      : object_count(object_count) {
+    }
+
+    int update_progress(uint64_t object_no, uint64_t end_object_no) override {
+      EXPECT_LE(object_no, object_count);
+      EXPECT_EQ(end_object_no, object_count);
+      if (!expected_object_number) {
+        expected_object_number = 0;
+      } else {
+        expected_object_number = *expected_object_number + 1;
+      }
+      EXPECT_EQ(*expected_object_number, object_no - 1);
+
+      return 0;
+    }
+  } prog_ctx(object_count);
+
+  C_SaferCond ctx;
+  auto request = new MockImageCopyRequest(&mock_src_image_ctx,
+                                          &mock_dst_image_ctx,
+                                          0, snap_id_end, boost::none,
+                                          m_snap_seqs, &prog_ctx, &ctx);
+  request->send();
+
+  std::function<void()> sleep_fn = [request]() {
+    sleep(1);
+  };
+
+  ASSERT_EQ(m_snap_map, wait_for_snap_map(mock_object_copy_request));
+  for (uint64_t i = 0; i < object_count; ++i) {
+    if (i % 10 == 0) {
+      ASSERT_TRUE(complete_object_copy(mock_object_copy_request, i, 0, sleep_fn));
+    } else {
+      ASSERT_TRUE(complete_object_copy(mock_object_copy_request, i, 0));
+    }
+  }
+  ASSERT_EQ(0, ctx.wait());
+}
+
+TEST_F(TestMockDeepCopyImageCopyRequest, SnapshotSubset) {
+  librados::snap_t snap_id_start;
+  librados::snap_t snap_id_end;
+  ASSERT_EQ(0, create_snap("snap1"));
+  ASSERT_EQ(0, create_snap("snap2", &snap_id_start));
+  ASSERT_EQ(0, create_snap("copy", &snap_id_end));
+
+  librbd::MockTestImageCtx mock_src_image_ctx(*m_src_image_ctx);
+  librbd::MockTestImageCtx mock_dst_image_ctx(*m_dst_image_ctx);
+  MockObjectCopyRequest mock_object_copy_request;
+
+  InSequence seq;
+  expect_get_image_size(mock_src_image_ctx, 1 << m_src_image_ctx->order);
+  expect_get_image_size(mock_src_image_ctx, 0);
+  expect_get_image_size(mock_src_image_ctx, 0);
+  expect_get_image_size(mock_src_image_ctx, 0);
+  expect_object_copy_send(mock_object_copy_request);
+
+  librbd::NoOpProgressContext no_op;
+  C_SaferCond ctx;
+  auto request = new MockImageCopyRequest(&mock_src_image_ctx,
+                                          &mock_dst_image_ctx,
+                                          snap_id_start, snap_id_end,
+                                          boost::none, m_snap_seqs, &no_op,
+                                          &ctx);
+  request->send();
+
+  SnapMap snap_map(m_snap_map);
+  snap_map.erase(snap_map.begin());
+  ASSERT_EQ(snap_map, wait_for_snap_map(mock_object_copy_request));
+
+  ASSERT_TRUE(complete_object_copy(mock_object_copy_request, 0, 0));
+  ASSERT_EQ(0, ctx.wait());
+}
+
+TEST_F(TestMockDeepCopyImageCopyRequest, RestartPartialSync) {
+  librados::snap_t snap_id_end;
+  ASSERT_EQ(0, create_snap("copy", &snap_id_end));
+
+  librbd::MockTestImageCtx mock_src_image_ctx(*m_src_image_ctx);
+  librbd::MockTestImageCtx mock_dst_image_ctx(*m_dst_image_ctx);
+  MockObjectCopyRequest mock_object_copy_request;
+
+  InSequence seq;
+  expect_get_image_size(mock_src_image_ctx, 2 * (1 << m_src_image_ctx->order));
+  expect_get_image_size(mock_src_image_ctx, 0);
+  expect_object_copy_send(mock_object_copy_request);
+
+  librbd::NoOpProgressContext no_op;
+  C_SaferCond ctx;
+  auto request = new MockImageCopyRequest(&mock_src_image_ctx,
+                                          &mock_dst_image_ctx,
+                                          0, snap_id_end,
+                                          librbd::deep_copy::ObjectNumber{0U},
+                                          m_snap_seqs, &no_op, &ctx);
+  request->send();
+
+  ASSERT_TRUE(complete_object_copy(mock_object_copy_request, 1, 0));
+  ASSERT_EQ(0, ctx.wait());
+}
+
+TEST_F(TestMockDeepCopyImageCopyRequest, Cancel) {
+  std::string max_ops_str;
+  ASSERT_EQ(0, _rados.conf_get("rbd_concurrent_management_ops", max_ops_str));
+  ASSERT_EQ(0, _rados.conf_set("rbd_concurrent_management_ops", "1"));
+  BOOST_SCOPE_EXIT( (max_ops_str) ) {
+    ASSERT_EQ(0, _rados.conf_set("rbd_concurrent_management_ops",
+                                 max_ops_str.c_str()));
+  } BOOST_SCOPE_EXIT_END;
+
+  librados::snap_t snap_id_end;
+  ASSERT_EQ(0, create_snap("copy", &snap_id_end));
+
+  librbd::MockTestImageCtx mock_src_image_ctx(*m_src_image_ctx);
+  librbd::MockTestImageCtx mock_dst_image_ctx(*m_dst_image_ctx);
+  MockObjectCopyRequest mock_object_copy_request;
+
+  InSequence seq;
+  expect_get_image_size(mock_src_image_ctx, 1 << m_src_image_ctx->order);
+  expect_get_image_size(mock_src_image_ctx, 0);
+  expect_object_copy_send(mock_object_copy_request);
+
+  librbd::NoOpProgressContext no_op;
+  C_SaferCond ctx;
+  auto request = new MockImageCopyRequest(&mock_src_image_ctx,
+                                          &mock_dst_image_ctx,
+                                          0, snap_id_end, boost::none,
+                                          m_snap_seqs, &no_op, &ctx);
+  request->send();
+
+  ASSERT_EQ(m_snap_map, wait_for_snap_map(mock_object_copy_request));
+  request->cancel();
+
+  ASSERT_TRUE(complete_object_copy(mock_object_copy_request, 0, 0));
+  ASSERT_EQ(-ECANCELED, ctx.wait());
+}
+
+TEST_F(TestMockDeepCopyImageCopyRequest, Cancel_Inflight_Sync) {
+  std::string max_ops_str;
+  ASSERT_EQ(0, _rados.conf_get("rbd_concurrent_management_ops", max_ops_str));
+  ASSERT_EQ(0, _rados.conf_set("rbd_concurrent_management_ops", "3"));
+  BOOST_SCOPE_EXIT( (max_ops_str) ) {
+    ASSERT_EQ(0, _rados.conf_set("rbd_concurrent_management_ops",
+                                 max_ops_str.c_str()));
+  } BOOST_SCOPE_EXIT_END;
+
+  librados::snap_t snap_id_end;
+  ASSERT_EQ(0, create_snap("copy", &snap_id_end));
+
+  librbd::MockTestImageCtx mock_src_image_ctx(*m_src_image_ctx);
+  librbd::MockTestImageCtx mock_dst_image_ctx(*m_dst_image_ctx);
+  MockObjectCopyRequest mock_object_copy_request;
+
+  InSequence seq;
+  expect_get_image_size(mock_src_image_ctx, 6 * (1 << m_src_image_ctx->order));
+  expect_get_image_size(mock_src_image_ctx, m_image_size);
+
+  EXPECT_CALL(mock_object_copy_request, send()).Times(6);
+
+  struct ProgressContext : public librbd::ProgressContext {
+    librbd::deep_copy::ObjectNumber object_number;
+
+    int update_progress(uint64_t object_no, uint64_t end_object_no) override {
+      object_number = object_number ? *object_number + 1 : 0;
+      return 0;
+    }
+  } prog_ctx;
+
+  C_SaferCond ctx;
+  auto request = new MockImageCopyRequest(&mock_src_image_ctx,
+                                          &mock_dst_image_ctx,
+                                          0, snap_id_end, boost::none,
+                                          m_snap_seqs, &prog_ctx, &ctx);
+  request->send();
+
+  ASSERT_EQ(m_snap_map, wait_for_snap_map(mock_object_copy_request));
+
+  std::function<void()> cancel_fn = [request]() {
+    sleep(2);
+    request->cancel();
+  };
+
+  ASSERT_TRUE(complete_object_copy(mock_object_copy_request, 0, 0));
+  ASSERT_TRUE(complete_object_copy(mock_object_copy_request, 1, 0));
+  ASSERT_TRUE(complete_object_copy(mock_object_copy_request, 2, 0));
+  ASSERT_TRUE(complete_object_copy(mock_object_copy_request, 3, 0, cancel_fn));
+  ASSERT_TRUE(complete_object_copy(mock_object_copy_request, 4, 0));
+  ASSERT_TRUE(complete_object_copy(mock_object_copy_request, 5, 0));
+
+  ASSERT_EQ(-ECANCELED, ctx.wait());
+  ASSERT_EQ(5u, prog_ctx.object_number.get());
+}
+
+TEST_F(TestMockDeepCopyImageCopyRequest, MissingSnap) {
+  librbd::MockTestImageCtx mock_src_image_ctx(*m_src_image_ctx);
+  librbd::MockTestImageCtx mock_dst_image_ctx(*m_dst_image_ctx);
+
+  librbd::NoOpProgressContext no_op;
+  C_SaferCond ctx;
+  auto request = new MockImageCopyRequest(&mock_src_image_ctx,
+                                          &mock_dst_image_ctx,
+                                          0, 123, boost::none,
+                                          m_snap_seqs, &no_op, &ctx);
+  request->send();
+  ASSERT_EQ(-EINVAL, ctx.wait());
+}
+
+TEST_F(TestMockDeepCopyImageCopyRequest, MissingFromSnap) {
+  librados::snap_t snap_id_end;
+  ASSERT_EQ(0, create_snap("copy", &snap_id_end));
+
+  librbd::MockTestImageCtx mock_src_image_ctx(*m_src_image_ctx);
+  librbd::MockTestImageCtx mock_dst_image_ctx(*m_dst_image_ctx);
+
+  librbd::NoOpProgressContext no_op;
+  C_SaferCond ctx;
+  auto request = new MockImageCopyRequest(&mock_src_image_ctx,
+                                          &mock_dst_image_ctx,
+                                          123, snap_id_end, boost::none,
+                                          m_snap_seqs, &no_op, &ctx);
+  request->send();
+  ASSERT_EQ(-EINVAL, ctx.wait());
+}
+
+TEST_F(TestMockDeepCopyImageCopyRequest, EmptySnapMap) {
+  librados::snap_t snap_id_start;
+  librados::snap_t snap_id_end;
+  ASSERT_EQ(0, create_snap("snap1", &snap_id_start));
+  ASSERT_EQ(0, create_snap("copy", &snap_id_end));
+
+  librbd::MockTestImageCtx mock_src_image_ctx(*m_src_image_ctx);
+  librbd::MockTestImageCtx mock_dst_image_ctx(*m_dst_image_ctx);
+
+  librbd::NoOpProgressContext no_op;
+  C_SaferCond ctx;
+  auto request = new MockImageCopyRequest(&mock_src_image_ctx,
+                                          &mock_dst_image_ctx,
+                                          snap_id_start, snap_id_end,
+                                          boost::none, {{0, 0}}, &no_op, &ctx);
+  request->send();
+  ASSERT_EQ(-EINVAL, ctx.wait());
+}
+
+TEST_F(TestMockDeepCopyImageCopyRequest, EmptySnapSeqs) {
+  librados::snap_t snap_id_start;
+  librados::snap_t snap_id_end;
+  ASSERT_EQ(0, create_snap("snap1", &snap_id_start));
+  ASSERT_EQ(0, create_snap("copy", &snap_id_end));
+
+  librbd::MockTestImageCtx mock_src_image_ctx(*m_src_image_ctx);
+  librbd::MockTestImageCtx mock_dst_image_ctx(*m_dst_image_ctx);
+
+  librbd::NoOpProgressContext no_op;
+  C_SaferCond ctx;
+  auto request = new MockImageCopyRequest(&mock_src_image_ctx,
+                                          &mock_dst_image_ctx,
+                                          snap_id_start, snap_id_end,
+                                          boost::none, {}, &no_op, &ctx);
+  request->send();
+  ASSERT_EQ(-EINVAL, ctx.wait());
+}
+
+} // namespace deep_copy
+} // namespace librbd
diff --git a/src/test/librbd/deep_copy/test_mock_MetadataCopyRequest.cc b/src/test/librbd/deep_copy/test_mock_MetadataCopyRequest.cc
new file mode 100644 (file)
index 0000000..09c266c
--- /dev/null
@@ -0,0 +1,180 @@
+// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*-
+// vim: ts=8 sw=2 smarttab
+
+#include "test/librbd/test_mock_fixture.h"
+#include "include/rbd/librbd.hpp"
+#include "include/stringify.h"
+#include "librbd/ImageCtx.h"
+#include "librbd/deep_copy/MetadataCopyRequest.h"
+#include "test/librados_test_stub/MockTestMemIoCtxImpl.h"
+#include "test/librbd/mock/MockImageCtx.h"
+#include "test/librbd/test_support.h"
+#include <map>
+
+namespace librbd {
+namespace {
+
+struct MockTestImageCtx : public librbd::MockImageCtx {
+  MockTestImageCtx(librbd::ImageCtx &image_ctx)
+    : librbd::MockImageCtx(image_ctx) {
+  }
+};
+
+} // anonymous namespace
+} // namespace librbd
+
+// template definitions
+#include "librbd/deep_copy/MetadataCopyRequest.cc"
+
+namespace librbd {
+namespace deep_copy {
+
+using ::testing::_;
+using ::testing::DoAll;
+using ::testing::InSequence;
+using ::testing::Return;
+using ::testing::StrEq;
+using ::testing::WithArg;
+
+class TestMockDeepCopyMetadataCopyRequest : public TestMockFixture {
+public:
+  typedef MetadataCopyRequest<librbd::MockTestImageCtx> MockMetadataCopyRequest;
+  typedef std::map<std::string, bufferlist> Metadata;
+
+  librbd::ImageCtx *m_src_image_ctx;
+  librbd::ImageCtx *m_dst_image_ctx;
+  ThreadPool *m_thread_pool;
+  ContextWQ *m_work_queue;
+
+  void SetUp() override {
+    TestMockFixture::SetUp();
+
+    ASSERT_EQ(0, open_image(m_image_name, &m_src_image_ctx));
+
+    librbd::RBD rbd;
+    std::string dst_image_name = get_temp_image_name();
+    ASSERT_EQ(0, create_image_pp(rbd, m_ioctx, dst_image_name, m_image_size));
+    ASSERT_EQ(0, open_image(dst_image_name, &m_dst_image_ctx));
+
+    librbd::ImageCtx::get_thread_pool_instance(m_src_image_ctx->cct,
+                                               &m_thread_pool, &m_work_queue);
+  }
+
+  void expect_metadata_list(librbd::MockTestImageCtx &mock_image_ctx,
+                            const Metadata& metadata, int r) {
+    bufferlist out_bl;
+    ::encode(metadata, out_bl);
+
+    EXPECT_CALL(get_mock_io_ctx(mock_image_ctx.md_ctx),
+                exec(mock_image_ctx.header_oid, _, StrEq("rbd"),
+                     StrEq("metadata_list"), _, _, _))
+                  .WillOnce(DoAll(WithArg<5>(CopyInBufferlist(out_bl)),
+                                  Return(r)));
+  }
+
+  void expect_metadata_set(librbd::MockTestImageCtx &mock_image_ctx,
+                           const Metadata& metadata, int r) {
+    bufferlist in_bl;
+    ::encode(metadata, in_bl);
+
+    EXPECT_CALL(get_mock_io_ctx(mock_image_ctx.md_ctx),
+                exec(mock_image_ctx.header_oid, _, StrEq("rbd"),
+                     StrEq("metadata_set"), ContentsEqual(in_bl), _, _))
+                  .WillOnce(Return(r));
+  }
+};
+
+TEST_F(TestMockDeepCopyMetadataCopyRequest, Success) {
+  librbd::MockTestImageCtx mock_src_image_ctx(*m_src_image_ctx);
+  librbd::MockTestImageCtx mock_dst_image_ctx(*m_dst_image_ctx);
+
+  size_t idx = 1;
+  Metadata key_values_1;
+  for (; idx <= 128; ++idx) {
+    bufferlist bl;
+    bl.append("value" + stringify(idx));
+    key_values_1.emplace("key" + stringify(idx), bl);
+  }
+
+  Metadata key_values_2;
+  for (; idx <= 255; ++idx) {
+    bufferlist bl;
+    bl.append("value" + stringify(idx));
+    key_values_2.emplace("key" + stringify(idx), bl);
+  }
+
+  InSequence seq;
+  expect_metadata_list(mock_src_image_ctx, key_values_1, 0);
+  expect_metadata_set(mock_dst_image_ctx, key_values_1, 0);
+  expect_metadata_list(mock_src_image_ctx, key_values_2, 0);
+  expect_metadata_set(mock_dst_image_ctx, key_values_2, 0);
+
+  C_SaferCond ctx;
+  auto request = MockMetadataCopyRequest::create(&mock_src_image_ctx,
+                                                 &mock_dst_image_ctx,
+                                                 &ctx);
+  request->send();
+
+  ASSERT_EQ(0, ctx.wait());
+}
+
+TEST_F(TestMockDeepCopyMetadataCopyRequest, Empty) {
+  librbd::MockTestImageCtx mock_src_image_ctx(*m_src_image_ctx);
+  librbd::MockTestImageCtx mock_dst_image_ctx(*m_dst_image_ctx);
+
+  Metadata key_values;
+
+  InSequence seq;
+  expect_metadata_list(mock_src_image_ctx, key_values, 0);
+
+  C_SaferCond ctx;
+  auto request = MockMetadataCopyRequest::create(&mock_src_image_ctx,
+                                                 &mock_dst_image_ctx,
+                                                 &ctx);
+  request->send();
+
+  ASSERT_EQ(0, ctx.wait());
+}
+
+TEST_F(TestMockDeepCopyMetadataCopyRequest, MetadataListError) {
+  librbd::MockTestImageCtx mock_src_image_ctx(*m_src_image_ctx);
+  librbd::MockTestImageCtx mock_dst_image_ctx(*m_dst_image_ctx);
+
+  Metadata key_values;
+
+  InSequence seq;
+  expect_metadata_list(mock_src_image_ctx, key_values, -EINVAL);
+
+  C_SaferCond ctx;
+  auto request = MockMetadataCopyRequest::create(&mock_src_image_ctx,
+                                                 &mock_dst_image_ctx,
+                                                 &ctx);
+  request->send();
+
+  ASSERT_EQ(-EINVAL, ctx.wait());
+}
+
+TEST_F(TestMockDeepCopyMetadataCopyRequest, MetadataSetError) {
+  librbd::MockTestImageCtx mock_src_image_ctx(*m_src_image_ctx);
+  librbd::MockTestImageCtx mock_dst_image_ctx(*m_dst_image_ctx);
+
+  Metadata key_values;
+  bufferlist bl;
+  bl.append("value");
+  key_values.emplace("key", bl);
+
+  InSequence seq;
+  expect_metadata_list(mock_src_image_ctx, key_values, 0);
+  expect_metadata_set(mock_dst_image_ctx, key_values, -EINVAL);
+
+  C_SaferCond ctx;
+  auto request = MockMetadataCopyRequest::create(&mock_src_image_ctx,
+                                                 &mock_dst_image_ctx,
+                                                 &ctx);
+  request->send();
+
+  ASSERT_EQ(-EINVAL, ctx.wait());
+}
+
+} // namespace deep_sync
+} // namespace librbd
diff --git a/src/test/librbd/deep_copy/test_mock_ObjectCopyRequest.cc b/src/test/librbd/deep_copy/test_mock_ObjectCopyRequest.cc
new file mode 100644 (file)
index 0000000..c46cc9e
--- /dev/null
@@ -0,0 +1,817 @@
+// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*-
+// vim: ts=8 sw=2 smarttab
+
+#include "test/librbd/test_mock_fixture.h"
+#include "include/interval_set.h"
+#include "include/rbd/librbd.hpp"
+#include "include/rbd/object_map_types.h"
+#include "librbd/ImageCtx.h"
+#include "librbd/ImageState.h"
+#include "librbd/internal.h"
+#include "librbd/Operations.h"
+#include "librbd/deep_copy/ObjectCopyRequest.h"
+#include "librbd/io/ImageRequestWQ.h"
+#include "librbd/io/ReadResult.h"
+#include "test/librados_test_stub/MockTestMemIoCtxImpl.h"
+#include "test/librbd/mock/MockImageCtx.h"
+#include "test/librbd/test_support.h"
+
+namespace librbd {
+namespace {
+
+struct MockTestImageCtx : public librbd::MockImageCtx {
+  MockTestImageCtx(librbd::ImageCtx &image_ctx)
+    : librbd::MockImageCtx(image_ctx) {
+  }
+};
+
+} // anonymous namespace
+} // namespace librbd
+
+// template definitions
+#include "librbd/deep_copy/ObjectCopyRequest.cc"
+template class librbd::deep_copy::ObjectCopyRequest<librbd::MockTestImageCtx>;
+
+bool operator==(const SnapContext& rhs, const SnapContext& lhs) {
+  return (rhs.seq == lhs.seq && rhs.snaps == lhs.snaps);
+}
+
+namespace librbd {
+namespace deep_copy {
+
+using ::testing::_;
+using ::testing::DoAll;
+using ::testing::DoDefault;
+using ::testing::InSequence;
+using ::testing::Invoke;
+using ::testing::Return;
+using ::testing::ReturnNew;
+using ::testing::WithArg;
+
+namespace {
+
+void scribble(librbd::ImageCtx *image_ctx, int num_ops, size_t max_size,
+              interval_set<uint64_t> *what)
+{
+  uint64_t object_size = 1 << image_ctx->order;
+  for (int i = 0; i < num_ops; i++) {
+    uint64_t off = rand() % (object_size - max_size + 1);
+    uint64_t len = 1 + rand() % max_size;
+    std::cout << __func__ << ": off=" << off << ", len=" << len << std::endl;
+
+    bufferlist bl;
+    bl.append(std::string(len, '1'));
+
+    int r = image_ctx->io_work_queue->write(off, len, std::move(bl), 0);
+    ASSERT_EQ(static_cast<int>(len), r);
+
+    interval_set<uint64_t> w;
+    w.insert(off, len);
+    what->union_of(w);
+  }
+  std::cout << " wrote " << *what << std::endl;
+}
+
+} // anonymous namespace
+
+class TestMockDeepCopyObjectCopyRequest : public TestMockFixture {
+public:
+  typedef ObjectCopyRequest<librbd::MockTestImageCtx> MockObjectCopyRequest;
+
+  librbd::ImageCtx *m_src_image_ctx;
+  librbd::ImageCtx *m_dst_image_ctx;
+  ThreadPool *m_thread_pool;
+  ContextWQ *m_work_queue;
+
+  SnapMap m_snap_map;
+  std::vector<librados::snap_t> m_src_snap_ids;
+  std::vector<librados::snap_t> m_dst_snap_ids;
+
+  void SetUp() override {
+    TestMockFixture::SetUp();
+
+    ASSERT_EQ(0, open_image(m_image_name, &m_src_image_ctx));
+
+    librbd::NoOpProgressContext no_op;
+    m_image_size = 1 << m_src_image_ctx->order;
+    ASSERT_EQ(0, m_src_image_ctx->operations->resize(m_image_size, true, no_op));
+
+    librbd::RBD rbd;
+    std::string dst_image_name = get_temp_image_name();
+    ASSERT_EQ(0, create_image_pp(rbd, m_ioctx, dst_image_name, m_image_size));
+    ASSERT_EQ(0, open_image(dst_image_name, &m_dst_image_ctx));
+
+    librbd::ImageCtx::get_thread_pool_instance(m_src_image_ctx->cct,
+                                               &m_thread_pool, &m_work_queue);
+  }
+
+  bool is_fast_diff(librbd::MockImageCtx &mock_image_ctx) {
+    return (mock_image_ctx.features & RBD_FEATURE_FAST_DIFF) != 0;
+  }
+
+  void prepare_exclusive_lock(librbd::MockImageCtx &mock_image_ctx,
+                              librbd::MockExclusiveLock &mock_exclusive_lock) {
+    if ((mock_image_ctx.features & RBD_FEATURE_EXCLUSIVE_LOCK) == 0) {
+      return;
+    }
+    mock_image_ctx.exclusive_lock = &mock_exclusive_lock;
+  }
+
+  void expect_test_features(librbd::MockImageCtx &mock_image_ctx) {
+    EXPECT_CALL(mock_image_ctx, test_features(_))
+      .WillRepeatedly(WithArg<0>(Invoke([&mock_image_ctx](uint64_t features) {
+              return (mock_image_ctx.features & features) != 0;
+            })));
+  }
+
+  void expect_start_op(librbd::MockExclusiveLock &mock_exclusive_lock) {
+    if ((m_src_image_ctx->features & RBD_FEATURE_EXCLUSIVE_LOCK) == 0) {
+      return;
+    }
+    EXPECT_CALL(mock_exclusive_lock, start_op()).WillOnce(
+      ReturnNew<FunctionContext>([](int) {}));
+  }
+
+  void expect_list_snaps(librbd::MockTestImageCtx &mock_image_ctx,
+                         librados::MockTestMemIoCtxImpl &mock_io_ctx,
+                         const librados::snap_set_t &snap_set) {
+    expect_get_object_name(mock_image_ctx);
+    expect_set_snap_read(mock_io_ctx, CEPH_SNAPDIR);
+    EXPECT_CALL(mock_io_ctx,
+                list_snaps(mock_image_ctx.image_ctx->get_object_name(0), _))
+      .WillOnce(DoAll(WithArg<1>(Invoke([&snap_set](librados::snap_set_t *out_snap_set) {
+                          *out_snap_set = snap_set;
+                        })),
+                      Return(0)));
+  }
+
+  void expect_list_snaps(librbd::MockTestImageCtx &mock_image_ctx,
+                         librados::MockTestMemIoCtxImpl &mock_io_ctx, int r) {
+    expect_get_object_name(mock_image_ctx);
+    expect_set_snap_read(mock_io_ctx, CEPH_SNAPDIR);
+    auto &expect = EXPECT_CALL(mock_io_ctx,
+                               list_snaps(mock_image_ctx.image_ctx->get_object_name(0),
+                                          _));
+    if (r < 0) {
+      expect.WillOnce(Return(r));
+    } else {
+      expect.WillOnce(DoDefault());
+    }
+  }
+
+  void expect_get_object_name(librbd::MockTestImageCtx &mock_image_ctx) {
+    EXPECT_CALL(mock_image_ctx, get_object_name(0))
+                  .WillOnce(Return(mock_image_ctx.image_ctx->get_object_name(0)));
+  }
+
+  MockObjectCopyRequest *create_request(
+      librbd::MockTestImageCtx &mock_src_image_ctx,
+      librbd::MockTestImageCtx &mock_dst_image_ctx, Context *on_finish) {
+    expect_get_object_name(mock_dst_image_ctx);
+    return new MockObjectCopyRequest(&mock_src_image_ctx, &mock_dst_image_ctx,
+                                     m_snap_map, 0, on_finish);
+  }
+
+  void expect_set_snap_read(librados::MockTestMemIoCtxImpl &mock_io_ctx,
+                            uint64_t snap_id) {
+    EXPECT_CALL(mock_io_ctx, set_snap_read(snap_id));
+  }
+
+  void expect_sparse_read(librados::MockTestMemIoCtxImpl &mock_io_ctx, uint64_t offset,
+                          uint64_t length, int r) {
+
+    auto &expect = EXPECT_CALL(mock_io_ctx, sparse_read(_, offset, length, _, _));
+    if (r < 0) {
+      expect.WillOnce(Return(r));
+    } else {
+      expect.WillOnce(DoDefault());
+    }
+  }
+
+  void expect_sparse_read(librados::MockTestMemIoCtxImpl &mock_io_ctx,
+                   const interval_set<uint64_t> &extents, int r) {
+    for (auto extent : extents) {
+      expect_sparse_read(mock_io_ctx, extent.first, extent.second, r);
+      if (r < 0) {
+        break;
+      }
+    }
+  }
+
+  void expect_write(librados::MockTestMemIoCtxImpl &mock_io_ctx,
+                    uint64_t offset, uint64_t length,
+                    const SnapContext &snapc, int r) {
+    auto &expect = EXPECT_CALL(mock_io_ctx, write(_, _, length, offset, snapc));
+    if (r < 0) {
+      expect.WillOnce(Return(r));
+    } else {
+      expect.WillOnce(DoDefault());
+    }
+  }
+
+  void expect_write(librados::MockTestMemIoCtxImpl &mock_io_ctx,
+                    const interval_set<uint64_t> &extents,
+                    const SnapContext &snapc, int r) {
+    for (auto extent : extents) {
+      expect_write(mock_io_ctx, extent.first, extent.second, snapc, r);
+      if (r < 0) {
+        break;
+      }
+    }
+  }
+
+  void expect_truncate(librados::MockTestMemIoCtxImpl &mock_io_ctx,
+                       uint64_t offset, int r) {
+    auto &expect = EXPECT_CALL(mock_io_ctx, truncate(_, offset, _));
+    if (r < 0) {
+      expect.WillOnce(Return(r));
+    } else {
+      expect.WillOnce(DoDefault());
+    }
+  }
+
+  void expect_remove(librados::MockTestMemIoCtxImpl &mock_io_ctx, int r) {
+    auto &expect = EXPECT_CALL(mock_io_ctx, remove(_, _));
+    if (r < 0) {
+      expect.WillOnce(Return(r));
+    } else {
+      expect.WillOnce(DoDefault());
+    }
+  }
+
+  void expect_update_object_map(librbd::MockTestImageCtx &mock_image_ctx,
+                                librbd::MockObjectMap &mock_object_map,
+                                librados::snap_t snap_id, uint8_t state,
+                                int r) {
+    if (mock_image_ctx.image_ctx->object_map != nullptr) {
+      auto &expect = EXPECT_CALL(mock_object_map, aio_update(snap_id, 0, 1, state, _, _, _));
+      if (r < 0) {
+        expect.WillOnce(DoAll(WithArg<6>(Invoke([this, r](Context *ctx) {
+                                  m_work_queue->queue(ctx, r);
+                                })),
+                              Return(true)));
+      } else {
+        expect.WillOnce(DoAll(WithArg<6>(Invoke([&mock_image_ctx, snap_id, state, r](Context *ctx) {
+                                  assert(mock_image_ctx.image_ctx->snap_lock.is_locked());
+                                  assert(mock_image_ctx.image_ctx->object_map_lock.is_wlocked());
+                                  mock_image_ctx.image_ctx->object_map->aio_update<Context>(
+                                    snap_id, 0, 1, state, boost::none, {}, ctx);
+                                })),
+                              Return(true)));
+      }
+    }
+  }
+
+  int create_snap(librbd::ImageCtx *image_ctx, const char* snap_name,
+                  librados::snap_t *snap_id) {
+    int r = image_ctx->operations->snap_create(
+        cls::rbd::UserSnapshotNamespace(), snap_name);
+    if (r < 0) {
+      return r;
+    }
+
+    r = image_ctx->state->refresh();
+    if (r < 0) {
+      return r;
+    }
+
+    if (image_ctx->snap_ids.count({cls::rbd::UserSnapshotNamespace(),
+                                   snap_name}) == 0) {
+      return -ENOENT;
+    }
+
+    if (snap_id != nullptr) {
+      *snap_id = image_ctx->snap_ids[{cls::rbd::UserSnapshotNamespace(),
+                                      snap_name}];
+    }
+    return 0;
+  }
+
+  int create_snap(const char* snap_name) {
+    librados::snap_t src_snap_id;
+    int r = create_snap(m_src_image_ctx, snap_name, &src_snap_id);
+    if (r < 0) {
+      return r;
+    }
+
+    librados::snap_t dst_snap_id;
+    r = create_snap(m_dst_image_ctx, snap_name, &dst_snap_id);
+    if (r < 0) {
+      return r;
+    }
+
+    // collection of all existing snaps in dst image
+    SnapIds dst_snap_ids({dst_snap_id});
+    if (!m_snap_map.empty()) {
+      dst_snap_ids.insert(dst_snap_ids.end(),
+                            m_snap_map.rbegin()->second.begin(),
+                            m_snap_map.rbegin()->second.end());
+    }
+    m_snap_map[src_snap_id] = dst_snap_ids;
+    m_src_snap_ids.push_back(src_snap_id);
+    m_dst_snap_ids.push_back(dst_snap_id);
+
+    return 0;
+  }
+
+  std::string get_snap_name(librbd::ImageCtx *image_ctx,
+                            librados::snap_t snap_id) {
+    auto it = std::find_if(image_ctx->snap_ids.begin(),
+                           image_ctx->snap_ids.end(),
+                           [snap_id](const std::pair<std::pair<cls::rbd::SnapshotNamespace,
+                                                              std::string>,
+                                                    librados::snap_t> &pair) {
+        return (pair.second == snap_id);
+      });
+    if (it == image_ctx->snap_ids.end()) {
+      return "";
+    }
+    return it->first.second;
+  }
+
+  int compare_objects() {
+    SnapMap snap_map(m_snap_map);
+    if (snap_map.empty()) {
+      return -ENOENT;
+    }
+
+    int r;
+    uint64_t object_size = 1 << m_src_image_ctx->order;
+    while (!snap_map.empty()) {
+      librados::snap_t src_snap_id = snap_map.begin()->first;
+      librados::snap_t dst_snap_id = *snap_map.begin()->second.begin();
+      snap_map.erase(snap_map.begin());
+
+      std::string snap_name = get_snap_name(m_src_image_ctx, src_snap_id);
+      if (snap_name.empty()) {
+        return -ENOENT;
+      }
+
+      std::cout << "comparing '" << snap_name << " (" << src_snap_id
+                << " to " << dst_snap_id << ")" << std::endl;
+
+      r = librbd::snap_set(m_src_image_ctx,
+                          cls::rbd::UserSnapshotNamespace(),
+                          snap_name.c_str());
+      if (r < 0) {
+        return r;
+      }
+
+      r = librbd::snap_set(m_dst_image_ctx,
+                          cls::rbd::UserSnapshotNamespace(),
+                          snap_name.c_str());
+      if (r < 0) {
+        return r;
+      }
+
+      bufferlist src_bl;
+      src_bl.append(std::string(object_size, '1'));
+      r = m_src_image_ctx->io_work_queue->read(
+        0, object_size, librbd::io::ReadResult{&src_bl}, 0);
+      if (r < 0) {
+        return r;
+      }
+
+      bufferlist dst_bl;
+      dst_bl.append(std::string(object_size, '1'));
+      r = m_dst_image_ctx->io_work_queue->read(
+        0, object_size, librbd::io::ReadResult{&dst_bl}, 0);
+      if (r < 0) {
+        return r;
+      }
+
+      if (!src_bl.contents_equal(dst_bl)) {
+        return -EBADMSG;
+      }
+    }
+
+    r = librbd::snap_set(m_src_image_ctx,
+                        cls::rbd::UserSnapshotNamespace(),
+                        nullptr);
+    if (r < 0) {
+      return r;
+    }
+    r = librbd::snap_set(m_dst_image_ctx,
+                        cls::rbd::UserSnapshotNamespace(),
+                        nullptr);
+    if (r < 0) {
+      return r;
+    }
+
+    return 0;
+  }
+};
+
+TEST_F(TestMockDeepCopyObjectCopyRequest, DNE) {
+  ASSERT_EQ(0, create_snap("copy"));
+  librbd::MockTestImageCtx mock_src_image_ctx(*m_src_image_ctx);
+  librbd::MockTestImageCtx mock_dst_image_ctx(*m_dst_image_ctx);
+
+  librbd::MockExclusiveLock mock_exclusive_lock;
+  prepare_exclusive_lock(mock_dst_image_ctx, mock_exclusive_lock);
+
+  librbd::MockObjectMap mock_object_map;
+  mock_dst_image_ctx.object_map = &mock_object_map;
+  expect_test_features(mock_dst_image_ctx);
+
+  C_SaferCond ctx;
+  MockObjectCopyRequest *request = create_request(mock_src_image_ctx,
+                                                  mock_dst_image_ctx, &ctx);
+
+  librados::MockTestMemIoCtxImpl &mock_src_io_ctx(get_mock_io_ctx(
+    request->get_src_io_ctx()));
+
+  InSequence seq;
+  expect_list_snaps(mock_src_image_ctx, mock_src_io_ctx, -ENOENT);
+
+  request->send();
+  ASSERT_EQ(0, ctx.wait());
+}
+
+TEST_F(TestMockDeepCopyObjectCopyRequest, Write) {
+  // scribble some data
+  interval_set<uint64_t> one;
+  scribble(m_src_image_ctx, 10, 102400, &one);
+
+  ASSERT_EQ(0, create_snap("copy"));
+  librbd::MockTestImageCtx mock_src_image_ctx(*m_src_image_ctx);
+  librbd::MockTestImageCtx mock_dst_image_ctx(*m_dst_image_ctx);
+
+  librbd::MockExclusiveLock mock_exclusive_lock;
+  prepare_exclusive_lock(mock_dst_image_ctx, mock_exclusive_lock);
+
+  librbd::MockObjectMap mock_object_map;
+  mock_dst_image_ctx.object_map = &mock_object_map;
+
+  expect_test_features(mock_dst_image_ctx);
+
+  C_SaferCond ctx;
+  MockObjectCopyRequest *request = create_request(mock_src_image_ctx,
+                                                  mock_dst_image_ctx, &ctx);
+
+  librados::MockTestMemIoCtxImpl &mock_src_io_ctx(get_mock_io_ctx(
+    request->get_src_io_ctx()));
+  librados::MockTestMemIoCtxImpl &mock_dst_io_ctx(get_mock_io_ctx(
+    request->get_dst_io_ctx()));
+
+  InSequence seq;
+  expect_list_snaps(mock_src_image_ctx, mock_src_io_ctx, 0);
+  expect_set_snap_read(mock_src_io_ctx, m_src_snap_ids[0]);
+  expect_sparse_read(mock_src_io_ctx, 0, one.range_end(), 0);
+  expect_start_op(mock_exclusive_lock);
+  expect_write(mock_dst_io_ctx, 0, one.range_end(), {0, {}}, 0);
+  expect_start_op(mock_exclusive_lock);
+  expect_update_object_map(mock_dst_image_ctx, mock_object_map,
+                           m_dst_snap_ids[0], OBJECT_EXISTS, 0);
+
+  request->send();
+  ASSERT_EQ(0, ctx.wait());
+  ASSERT_EQ(0, compare_objects());
+}
+
+TEST_F(TestMockDeepCopyObjectCopyRequest, ReadMissingStaleSnapSet) {
+  ASSERT_EQ(0, create_snap("one"));
+  ASSERT_EQ(0, create_snap("two"));
+
+  // scribble some data
+  interval_set<uint64_t> one;
+  scribble(m_src_image_ctx, 10, 102400, &one);
+  ASSERT_EQ(0, create_snap("three"));
+
+  ASSERT_EQ(0, create_snap("copy"));
+  librbd::MockTestImageCtx mock_src_image_ctx(*m_src_image_ctx);
+  librbd::MockTestImageCtx mock_dst_image_ctx(*m_dst_image_ctx);
+
+  librbd::MockExclusiveLock mock_exclusive_lock;
+  prepare_exclusive_lock(mock_dst_image_ctx, mock_exclusive_lock);
+
+  librbd::MockObjectMap mock_object_map;
+  mock_dst_image_ctx.object_map = &mock_object_map;
+
+  expect_test_features(mock_dst_image_ctx);
+
+  C_SaferCond ctx;
+  MockObjectCopyRequest *request = create_request(mock_src_image_ctx,
+                                                  mock_dst_image_ctx, &ctx);
+
+  librados::MockTestMemIoCtxImpl &mock_src_io_ctx(get_mock_io_ctx(
+    request->get_src_io_ctx()));
+  librados::MockTestMemIoCtxImpl &mock_dst_io_ctx(get_mock_io_ctx(
+    request->get_dst_io_ctx()));
+
+  librados::clone_info_t dummy_clone_info;
+  dummy_clone_info.cloneid = librados::SNAP_HEAD;
+  dummy_clone_info.size = 123;
+
+  librados::snap_set_t dummy_snap_set1;
+  dummy_snap_set1.clones.push_back(dummy_clone_info);
+
+  dummy_clone_info.size = 234;
+  librados::snap_set_t dummy_snap_set2;
+  dummy_snap_set2.clones.push_back(dummy_clone_info);
+
+  InSequence seq;
+  expect_list_snaps(mock_src_image_ctx, mock_src_io_ctx, dummy_snap_set1);
+  expect_set_snap_read(mock_src_io_ctx, m_src_snap_ids[3]);
+  expect_sparse_read(mock_src_io_ctx, 0, 123, -ENOENT);
+  expect_list_snaps(mock_src_image_ctx, mock_src_io_ctx, dummy_snap_set2);
+  expect_set_snap_read(mock_src_io_ctx, m_src_snap_ids[3]);
+  expect_sparse_read(mock_src_io_ctx, 0, 234, -ENOENT);
+  expect_list_snaps(mock_src_image_ctx, mock_src_io_ctx, 0);
+  expect_set_snap_read(mock_src_io_ctx, m_src_snap_ids[3]);
+  expect_sparse_read(mock_src_io_ctx, 0, one.range_end(), 0);
+  expect_start_op(mock_exclusive_lock);
+  expect_write(mock_dst_io_ctx, 0, one.range_end(),
+               {m_dst_snap_ids[1], {m_dst_snap_ids[1],
+                                    m_dst_snap_ids[0]}},
+               0);
+  expect_start_op(mock_exclusive_lock);
+  expect_update_object_map(mock_dst_image_ctx, mock_object_map,
+                           m_dst_snap_ids[2], OBJECT_EXISTS, 0);
+  expect_start_op(mock_exclusive_lock);
+  expect_update_object_map(mock_dst_image_ctx, mock_object_map,
+                           m_dst_snap_ids[3], is_fast_diff(mock_dst_image_ctx) ?
+                           OBJECT_EXISTS_CLEAN : OBJECT_EXISTS, 0);
+
+  request->send();
+  ASSERT_EQ(0, ctx.wait());
+  ASSERT_EQ(0, compare_objects());
+}
+
+TEST_F(TestMockDeepCopyObjectCopyRequest, ReadMissingUpToDateSnapMap) {
+  // scribble some data
+  interval_set<uint64_t> one;
+  scribble(m_src_image_ctx, 10, 102400, &one);
+
+  ASSERT_EQ(0, create_snap("copy"));
+  librbd::MockTestImageCtx mock_src_image_ctx(*m_src_image_ctx);
+  librbd::MockTestImageCtx mock_dst_image_ctx(*m_dst_image_ctx);
+
+  librbd::MockExclusiveLock mock_exclusive_lock;
+  prepare_exclusive_lock(mock_dst_image_ctx, mock_exclusive_lock);
+
+  librbd::MockObjectMap mock_object_map;
+  mock_dst_image_ctx.object_map = &mock_object_map;
+
+  expect_test_features(mock_dst_image_ctx);
+
+  C_SaferCond ctx;
+  MockObjectCopyRequest *request = create_request(mock_src_image_ctx,
+                                                  mock_dst_image_ctx, &ctx);
+
+  librados::MockTestMemIoCtxImpl &mock_src_io_ctx(get_mock_io_ctx(
+    request->get_src_io_ctx()));
+
+  InSequence seq;
+  expect_list_snaps(mock_src_image_ctx, mock_src_io_ctx, 0);
+  expect_set_snap_read(mock_src_io_ctx, m_src_snap_ids[0]);
+  expect_sparse_read(mock_src_io_ctx, 0, one.range_end(), -ENOENT);
+  expect_list_snaps(mock_src_image_ctx, mock_src_io_ctx, 0);
+
+  request->send();
+  ASSERT_EQ(-ENOENT, ctx.wait());
+}
+
+TEST_F(TestMockDeepCopyObjectCopyRequest, ReadError) {
+  // scribble some data
+  interval_set<uint64_t> one;
+  scribble(m_src_image_ctx, 10, 102400, &one);
+
+  ASSERT_EQ(0, create_snap("copy"));
+  librbd::MockTestImageCtx mock_src_image_ctx(*m_src_image_ctx);
+  librbd::MockTestImageCtx mock_dst_image_ctx(*m_dst_image_ctx);
+
+  librbd::MockExclusiveLock mock_exclusive_lock;
+  prepare_exclusive_lock(mock_dst_image_ctx, mock_exclusive_lock);
+
+  librbd::MockObjectMap mock_object_map;
+  mock_dst_image_ctx.object_map = &mock_object_map;
+
+  expect_test_features(mock_dst_image_ctx);
+
+  C_SaferCond ctx;
+  MockObjectCopyRequest *request = create_request(mock_src_image_ctx,
+                                                  mock_dst_image_ctx, &ctx);
+
+  librados::MockTestMemIoCtxImpl &mock_src_io_ctx(get_mock_io_ctx(
+    request->get_src_io_ctx()));
+
+  InSequence seq;
+  expect_list_snaps(mock_src_image_ctx, mock_src_io_ctx, 0);
+  expect_set_snap_read(mock_src_io_ctx, m_src_snap_ids[0]);
+  expect_sparse_read(mock_src_io_ctx, 0, one.range_end(),  -EINVAL);
+
+  request->send();
+  ASSERT_EQ(-EINVAL, ctx.wait());
+}
+
+TEST_F(TestMockDeepCopyObjectCopyRequest, WriteError) {
+  // scribble some data
+  interval_set<uint64_t> one;
+  scribble(m_src_image_ctx, 10, 102400, &one);
+
+  ASSERT_EQ(0, create_snap("copy"));
+  librbd::MockTestImageCtx mock_src_image_ctx(*m_src_image_ctx);
+  librbd::MockTestImageCtx mock_dst_image_ctx(*m_dst_image_ctx);
+
+  librbd::MockExclusiveLock mock_exclusive_lock;
+  prepare_exclusive_lock(mock_dst_image_ctx, mock_exclusive_lock);
+
+  librbd::MockObjectMap mock_object_map;
+  mock_dst_image_ctx.object_map = &mock_object_map;
+
+  expect_test_features(mock_dst_image_ctx);
+
+  C_SaferCond ctx;
+  MockObjectCopyRequest *request = create_request(mock_src_image_ctx,
+                                                  mock_dst_image_ctx, &ctx);
+
+  librados::MockTestMemIoCtxImpl &mock_src_io_ctx(get_mock_io_ctx(
+    request->get_src_io_ctx()));
+  librados::MockTestMemIoCtxImpl &mock_dst_io_ctx(get_mock_io_ctx(
+    request->get_dst_io_ctx()));
+
+  InSequence seq;
+  expect_list_snaps(mock_src_image_ctx, mock_src_io_ctx, 0);
+  expect_set_snap_read(mock_src_io_ctx, m_src_snap_ids[0]);
+  expect_sparse_read(mock_src_io_ctx, 0, one.range_end(), 0);
+  expect_start_op(mock_exclusive_lock);
+  expect_write(mock_dst_io_ctx, 0, one.range_end(), {0, {}}, -EINVAL);
+
+  request->send();
+  ASSERT_EQ(-EINVAL, ctx.wait());
+}
+
+TEST_F(TestMockDeepCopyObjectCopyRequest, WriteSnaps) {
+  // scribble some data
+  interval_set<uint64_t> one;
+  scribble(m_src_image_ctx, 10, 102400, &one);
+  ASSERT_EQ(0, create_snap("one"));
+
+  interval_set<uint64_t> two;
+  scribble(m_src_image_ctx, 10, 102400, &two);
+  ASSERT_EQ(0, create_snap("two"));
+
+  if (one.range_end() < two.range_end()) {
+    interval_set<uint64_t> resize_diff;
+    resize_diff.insert(one.range_end(), two.range_end() - one.range_end());
+    two.union_of(resize_diff);
+  }
+
+  ASSERT_EQ(0, create_snap("copy"));
+  librbd::MockTestImageCtx mock_src_image_ctx(*m_src_image_ctx);
+  librbd::MockTestImageCtx mock_dst_image_ctx(*m_dst_image_ctx);
+
+  librbd::MockExclusiveLock mock_exclusive_lock;
+  prepare_exclusive_lock(mock_dst_image_ctx, mock_exclusive_lock);
+
+  librbd::MockObjectMap mock_object_map;
+  mock_dst_image_ctx.object_map = &mock_object_map;
+
+  expect_test_features(mock_dst_image_ctx);
+
+  C_SaferCond ctx;
+  MockObjectCopyRequest *request = create_request(mock_src_image_ctx,
+                                                  mock_dst_image_ctx, &ctx);
+
+  librados::MockTestMemIoCtxImpl &mock_src_io_ctx(get_mock_io_ctx(
+    request->get_src_io_ctx()));
+  librados::MockTestMemIoCtxImpl &mock_dst_io_ctx(get_mock_io_ctx(
+    request->get_dst_io_ctx()));
+
+  InSequence seq;
+  expect_list_snaps(mock_src_image_ctx, mock_src_io_ctx, 0);
+  expect_set_snap_read(mock_src_io_ctx, m_src_snap_ids[0]);
+  expect_sparse_read(mock_src_io_ctx, 0, one.range_end(), 0);
+  expect_set_snap_read(mock_src_io_ctx, m_src_snap_ids[2]);
+  expect_sparse_read(mock_src_io_ctx, two, 0);
+  expect_start_op(mock_exclusive_lock);
+  expect_write(mock_dst_io_ctx, 0, one.range_end(), {0, {}}, 0);
+  expect_start_op(mock_exclusive_lock);
+  expect_write(mock_dst_io_ctx, two,
+               {m_dst_snap_ids[0], {m_dst_snap_ids[0]}}, 0);
+  expect_start_op(mock_exclusive_lock);
+  expect_update_object_map(mock_dst_image_ctx, mock_object_map,
+                           m_dst_snap_ids[0], OBJECT_EXISTS, 0);
+  expect_start_op(mock_exclusive_lock);
+  expect_update_object_map(mock_dst_image_ctx, mock_object_map,
+                           m_dst_snap_ids[1], OBJECT_EXISTS, 0);
+  expect_start_op(mock_exclusive_lock);
+  expect_update_object_map(mock_dst_image_ctx, mock_object_map,
+                           m_dst_snap_ids[2], is_fast_diff(mock_dst_image_ctx) ?
+                           OBJECT_EXISTS_CLEAN : OBJECT_EXISTS, 0);
+
+  request->send();
+  ASSERT_EQ(0, ctx.wait());
+  ASSERT_EQ(0, compare_objects());
+}
+
+TEST_F(TestMockDeepCopyObjectCopyRequest, Trim) {
+  ASSERT_EQ(0, m_src_image_ctx->operations->metadata_set(
+              "conf_rbd_skip_partial_discard", "false"));
+  // scribble some data
+  interval_set<uint64_t> one;
+  scribble(m_src_image_ctx, 10, 102400, &one);
+  ASSERT_EQ(0, create_snap("one"));
+
+  // trim the object
+  uint64_t trim_offset = rand() % one.range_end();
+  ASSERT_LE(0, m_src_image_ctx->io_work_queue->discard(
+    trim_offset, one.range_end() - trim_offset, m_src_image_ctx->skip_partial_discard));
+  ASSERT_EQ(0, create_snap("copy"));
+
+  librbd::MockTestImageCtx mock_src_image_ctx(*m_src_image_ctx);
+  librbd::MockTestImageCtx mock_dst_image_ctx(*m_dst_image_ctx);
+
+  librbd::MockExclusiveLock mock_exclusive_lock;
+  prepare_exclusive_lock(mock_dst_image_ctx, mock_exclusive_lock);
+
+  librbd::MockObjectMap mock_object_map;
+  mock_dst_image_ctx.object_map = &mock_object_map;
+
+  expect_test_features(mock_dst_image_ctx);
+
+  C_SaferCond ctx;
+  MockObjectCopyRequest *request = create_request(mock_src_image_ctx,
+                                                  mock_dst_image_ctx, &ctx);
+
+  librados::MockTestMemIoCtxImpl &mock_src_io_ctx(get_mock_io_ctx(
+    request->get_src_io_ctx()));
+  librados::MockTestMemIoCtxImpl &mock_dst_io_ctx(get_mock_io_ctx(
+    request->get_dst_io_ctx()));
+
+  InSequence seq;
+  expect_list_snaps(mock_src_image_ctx, mock_src_io_ctx, 0);
+  expect_set_snap_read(mock_src_io_ctx, m_src_snap_ids[0]);
+  expect_sparse_read(mock_src_io_ctx, 0, one.range_end(), 0);
+  expect_start_op(mock_exclusive_lock);
+  expect_write(mock_dst_io_ctx, 0, one.range_end(), {0, {}}, 0);
+  expect_start_op(mock_exclusive_lock);
+  expect_truncate(mock_dst_io_ctx, trim_offset, 0);
+  expect_start_op(mock_exclusive_lock);
+  expect_update_object_map(mock_dst_image_ctx, mock_object_map,
+                           m_dst_snap_ids[0], OBJECT_EXISTS, 0);
+  expect_start_op(mock_exclusive_lock);
+  expect_update_object_map(mock_dst_image_ctx, mock_object_map,
+                           m_dst_snap_ids[1], OBJECT_EXISTS, 0);
+
+  request->send();
+  ASSERT_EQ(0, ctx.wait());
+  ASSERT_EQ(0, compare_objects());
+}
+
+TEST_F(TestMockDeepCopyObjectCopyRequest, Remove) {
+  // scribble some data
+  interval_set<uint64_t> one;
+  scribble(m_src_image_ctx, 10, 102400, &one);
+  ASSERT_EQ(0, create_snap("one"));
+  ASSERT_EQ(0, create_snap("two"));
+
+  // remove the object
+  uint64_t object_size = 1 << m_src_image_ctx->order;
+  ASSERT_LE(0, m_src_image_ctx->io_work_queue->discard(0, object_size, m_src_image_ctx->skip_partial_discard));
+  ASSERT_EQ(0, create_snap("copy"));
+  librbd::MockTestImageCtx mock_src_image_ctx(*m_src_image_ctx);
+  librbd::MockTestImageCtx mock_dst_image_ctx(*m_dst_image_ctx);
+
+  librbd::MockExclusiveLock mock_exclusive_lock;
+  prepare_exclusive_lock(mock_dst_image_ctx, mock_exclusive_lock);
+
+  librbd::MockObjectMap mock_object_map;
+  mock_dst_image_ctx.object_map = &mock_object_map;
+
+  expect_test_features(mock_dst_image_ctx);
+
+  C_SaferCond ctx;
+  MockObjectCopyRequest *request = create_request(mock_src_image_ctx,
+                                                  mock_dst_image_ctx, &ctx);
+
+  librados::MockTestMemIoCtxImpl &mock_src_io_ctx(get_mock_io_ctx(
+    request->get_src_io_ctx()));
+  librados::MockTestMemIoCtxImpl &mock_dst_io_ctx(get_mock_io_ctx(
+    request->get_dst_io_ctx()));
+
+  InSequence seq;
+  expect_list_snaps(mock_src_image_ctx, mock_src_io_ctx, 0);
+  expect_set_snap_read(mock_src_io_ctx, m_src_snap_ids[1]);
+  expect_sparse_read(mock_src_io_ctx, 0, one.range_end(), 0);
+  expect_start_op(mock_exclusive_lock);
+  expect_write(mock_dst_io_ctx, 0, one.range_end(), {0, {}}, 0);
+  expect_start_op(mock_exclusive_lock);
+  expect_remove(mock_dst_io_ctx, 0);
+  expect_start_op(mock_exclusive_lock);
+  uint8_t state = OBJECT_EXISTS;
+  expect_update_object_map(mock_dst_image_ctx, mock_object_map,
+                           m_dst_snap_ids[0], state, 0);
+  expect_start_op(mock_exclusive_lock);
+  expect_update_object_map(mock_dst_image_ctx, mock_object_map,
+                           m_dst_snap_ids[1], is_fast_diff(mock_dst_image_ctx) ?
+                           OBJECT_EXISTS_CLEAN : OBJECT_EXISTS, 0);
+
+  request->send();
+  ASSERT_EQ(0, ctx.wait());
+  ASSERT_EQ(0, compare_objects());
+}
+
+} // namespace deep_copy
+} // namespace librbd
diff --git a/src/test/librbd/deep_copy/test_mock_SetHeadRequest.cc b/src/test/librbd/deep_copy/test_mock_SetHeadRequest.cc
new file mode 100644 (file)
index 0000000..b29b875
--- /dev/null
@@ -0,0 +1,233 @@
+// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*-
+// vim: ts=8 sw=2 smarttab
+
+#include "test/librbd/test_mock_fixture.h"
+#include "test/librados_test_stub/LibradosTestStub.h"
+#include "include/rbd/librbd.hpp"
+#include "librbd/ImageCtx.h"
+#include "librbd/ImageState.h"
+#include "osdc/Striper.h"
+#include "test/librados_test_stub/MockTestMemIoCtxImpl.h"
+#include "test/librbd/mock/MockImageCtx.h"
+#include "librbd/deep_copy/SetHeadRequest.h"
+
+namespace librbd {
+namespace {
+
+struct MockTestImageCtx : public librbd::MockImageCtx {
+  MockTestImageCtx(librbd::ImageCtx &image_ctx)
+    : librbd::MockImageCtx(image_ctx) {
+  }
+};
+
+} // anonymous namespace
+} // namespace librbd
+
+// template definitions
+#include "librbd/deep_copy/SetHeadRequest.cc"
+template class librbd::deep_copy::SetHeadRequest<librbd::MockTestImageCtx>;
+
+namespace librbd {
+namespace deep_copy {
+
+using ::testing::_;
+using ::testing::DoAll;
+using ::testing::InSequence;
+using ::testing::Invoke;
+using ::testing::InvokeWithoutArgs;
+using ::testing::Return;
+using ::testing::ReturnNew;
+using ::testing::StrEq;
+using ::testing::WithArg;
+
+class TestMockDeepCopySetHeadRequest : public TestMockFixture {
+public:
+  typedef SetHeadRequest<librbd::MockTestImageCtx> MockSetHeadRequest;
+
+  librbd::ImageCtx *m_image_ctx;
+  ThreadPool *m_thread_pool;
+  ContextWQ *m_work_queue;
+
+  void SetUp() override {
+    TestMockFixture::SetUp();
+
+    ASSERT_EQ(0, open_image(m_image_name, &m_image_ctx));
+
+    librbd::ImageCtx::get_thread_pool_instance(m_image_ctx->cct, &m_thread_pool,
+                                               &m_work_queue);
+  }
+
+  void expect_start_op(librbd::MockExclusiveLock &mock_exclusive_lock) {
+    EXPECT_CALL(mock_exclusive_lock, start_op()).WillOnce(
+      ReturnNew<FunctionContext>([](int) {}));
+  }
+
+  void expect_test_features(librbd::MockTestImageCtx &mock_image_ctx,
+                            uint64_t features, bool enabled) {
+    EXPECT_CALL(mock_image_ctx, test_features(features))
+                  .WillOnce(Return(enabled));
+  }
+
+  void expect_set_size(librbd::MockTestImageCtx &mock_image_ctx, int r) {
+    EXPECT_CALL(get_mock_io_ctx(mock_image_ctx.md_ctx),
+                exec(mock_image_ctx.header_oid, _, StrEq("rbd"), StrEq("set_size"), _, _, _))
+                  .WillOnce(Return(r));
+  }
+
+  void expect_remove_parent(librbd::MockTestImageCtx &mock_image_ctx, int r) {
+    EXPECT_CALL(get_mock_io_ctx(mock_image_ctx.md_ctx),
+                exec(mock_image_ctx.header_oid, _, StrEq("rbd"), StrEq("remove_parent"), _, _, _))
+                  .WillOnce(Return(r));
+  }
+
+  void expect_set_parent(librbd::MockTestImageCtx &mock_image_ctx, int r) {
+    EXPECT_CALL(get_mock_io_ctx(mock_image_ctx.md_ctx),
+                exec(mock_image_ctx.header_oid, _, StrEq("rbd"), StrEq("set_parent"), _, _, _))
+                  .WillOnce(Return(r));
+  }
+
+  MockSetHeadRequest *create_request(
+      librbd::MockTestImageCtx &mock_local_image_ctx, uint64_t size,
+      const librbd::ParentSpec &parent_spec, uint64_t parent_overlap,
+      Context *on_finish) {
+    return new MockSetHeadRequest(&mock_local_image_ctx, size, parent_spec,
+                                  parent_overlap, on_finish);
+  }
+};
+
+TEST_F(TestMockDeepCopySetHeadRequest, Resize) {
+  librbd::MockTestImageCtx mock_image_ctx(*m_image_ctx);
+  librbd::MockExclusiveLock mock_exclusive_lock;
+  mock_image_ctx.exclusive_lock = &mock_exclusive_lock;
+
+  InSequence seq;
+  expect_start_op(mock_exclusive_lock);
+  expect_set_size(mock_image_ctx, 0);
+
+  C_SaferCond ctx;
+  auto request = create_request(mock_image_ctx, 123, {}, 0, &ctx);
+  request->send();
+  ASSERT_EQ(0, ctx.wait());
+}
+
+TEST_F(TestMockDeepCopySetHeadRequest, ResizeError) {
+  librbd::MockTestImageCtx mock_image_ctx(*m_image_ctx);
+  librbd::MockExclusiveLock mock_exclusive_lock;
+  mock_image_ctx.exclusive_lock = &mock_exclusive_lock;
+
+  InSequence seq;
+  expect_start_op(mock_exclusive_lock);
+  expect_set_size(mock_image_ctx, -EINVAL);
+
+  C_SaferCond ctx;
+  auto request = create_request(mock_image_ctx, 123, {}, 0, &ctx);
+  request->send();
+  ASSERT_EQ(-EINVAL, ctx.wait());
+}
+
+TEST_F(TestMockDeepCopySetHeadRequest, RemoveParent) {
+  librbd::MockTestImageCtx mock_image_ctx(*m_image_ctx);
+  librbd::MockExclusiveLock mock_exclusive_lock;
+  mock_image_ctx.exclusive_lock = &mock_exclusive_lock;
+
+  mock_image_ctx.parent_md.spec.pool_id = 213;
+
+  InSequence seq;
+  expect_start_op(mock_exclusive_lock);
+  expect_remove_parent(mock_image_ctx, 0);
+
+  C_SaferCond ctx;
+  auto request = create_request(mock_image_ctx, m_image_ctx->size, {}, 0, &ctx);
+  request->send();
+  ASSERT_EQ(0, ctx.wait());
+}
+
+TEST_F(TestMockDeepCopySetHeadRequest, RemoveParentError) {
+  librbd::MockTestImageCtx mock_image_ctx(*m_image_ctx);
+  librbd::MockExclusiveLock mock_exclusive_lock;
+  mock_image_ctx.exclusive_lock = &mock_exclusive_lock;
+
+  mock_image_ctx.parent_md.spec.pool_id = 213;
+
+  InSequence seq;
+  expect_start_op(mock_exclusive_lock);
+  expect_remove_parent(mock_image_ctx, -EINVAL);
+
+  C_SaferCond ctx;
+  auto request = create_request(mock_image_ctx, m_image_ctx->size, {}, 0, &ctx);
+  request->send();
+  ASSERT_EQ(-EINVAL, ctx.wait());
+}
+
+TEST_F(TestMockDeepCopySetHeadRequest, RemoveSetParent) {
+  librbd::MockTestImageCtx mock_image_ctx(*m_image_ctx);
+  librbd::MockExclusiveLock mock_exclusive_lock;
+  mock_image_ctx.exclusive_lock = &mock_exclusive_lock;
+
+  mock_image_ctx.parent_md.spec.pool_id = 213;
+
+  InSequence seq;
+  expect_start_op(mock_exclusive_lock);
+  expect_remove_parent(mock_image_ctx, 0);
+  expect_start_op(mock_exclusive_lock);
+  expect_set_parent(mock_image_ctx, 0);
+
+  C_SaferCond ctx;
+  auto request = create_request(mock_image_ctx, m_image_ctx->size,
+                                {123, "test", 0}, 0, &ctx);
+  request->send();
+  ASSERT_EQ(0, ctx.wait());
+}
+
+TEST_F(TestMockDeepCopySetHeadRequest, SetParentSpec) {
+  librbd::MockTestImageCtx mock_image_ctx(*m_image_ctx);
+  librbd::MockExclusiveLock mock_exclusive_lock;
+  mock_image_ctx.exclusive_lock = &mock_exclusive_lock;
+
+  InSequence seq;
+  expect_start_op(mock_exclusive_lock);
+  expect_set_parent(mock_image_ctx, 0);
+
+  C_SaferCond ctx;
+  auto request = create_request(mock_image_ctx, m_image_ctx->size,
+                                {123, "test", 0}, 0, &ctx);
+  request->send();
+  ASSERT_EQ(0, ctx.wait());
+}
+
+TEST_F(TestMockDeepCopySetHeadRequest, SetParentOverlap) {
+  librbd::MockTestImageCtx mock_image_ctx(*m_image_ctx);
+  librbd::MockExclusiveLock mock_exclusive_lock;
+  mock_image_ctx.exclusive_lock = &mock_exclusive_lock;
+
+  mock_image_ctx.parent_md.spec = {123, "test", 0};
+
+  InSequence seq;
+  expect_start_op(mock_exclusive_lock);
+  expect_set_parent(mock_image_ctx, 0);
+
+  C_SaferCond ctx;
+  auto request = create_request(mock_image_ctx, m_image_ctx->size,
+                                mock_image_ctx.parent_md.spec, 123, &ctx);
+  request->send();
+  ASSERT_EQ(0, ctx.wait());
+}
+
+TEST_F(TestMockDeepCopySetHeadRequest, SetParentError) {
+  librbd::MockTestImageCtx mock_image_ctx(*m_image_ctx);
+  librbd::MockExclusiveLock mock_exclusive_lock;
+  mock_image_ctx.exclusive_lock = &mock_exclusive_lock;
+
+  InSequence seq;
+  expect_start_op(mock_exclusive_lock);
+  expect_set_parent(mock_image_ctx, -ESTALE);
+
+  C_SaferCond ctx;
+  auto request = create_request(mock_image_ctx, m_image_ctx->size,
+                                {123, "test", 0}, 0, &ctx);
+  request->send();
+  ASSERT_EQ(-ESTALE, ctx.wait());
+}
+
+} // namespace deep_copy
+} // namespace librbd
diff --git a/src/test/librbd/deep_copy/test_mock_SnapshotCopyRequest.cc b/src/test/librbd/deep_copy/test_mock_SnapshotCopyRequest.cc
new file mode 100644 (file)
index 0000000..a8e90ef
--- /dev/null
@@ -0,0 +1,722 @@
+// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*-
+// vim: ts=8 sw=2 smarttab
+
+#include "test/librbd/test_mock_fixture.h"
+#include "include/rbd/librbd.hpp"
+#include "librbd/ImageCtx.h"
+#include "librbd/ImageState.h"
+#include "librbd/Operations.h"
+#include "librbd/deep_copy/SnapshotCopyRequest.h"
+#include "librbd/deep_copy/SnapshotCreateRequest.h"
+#include "test/librados_test_stub/MockTestMemIoCtxImpl.h"
+#include "test/librbd/mock/MockImageCtx.h"
+#include "test/librbd/test_support.h"
+
+namespace librbd {
+
+namespace {
+
+struct MockTestImageCtx : public librbd::MockImageCtx {
+  MockTestImageCtx(librbd::ImageCtx &image_ctx)
+    : librbd::MockImageCtx(image_ctx) {
+  }
+};
+
+} // anonymous namespace
+
+namespace deep_copy {
+
+template <>
+struct SnapshotCreateRequest<librbd::MockTestImageCtx> {
+  static SnapshotCreateRequest* s_instance;
+  static SnapshotCreateRequest* create(librbd::MockTestImageCtx* image_ctx,
+                                       const std::string &snap_name,
+                                       const cls::rbd::SnapshotNamespace &snap_namespace,
+                                       uint64_t size,
+                                       const librbd::ParentSpec &parent_spec,
+                                       uint64_t parent_overlap,
+                                       Context *on_finish) {
+    assert(s_instance != nullptr);
+    s_instance->on_finish = on_finish;
+    return s_instance;
+  }
+
+  Context *on_finish = nullptr;
+
+  SnapshotCreateRequest() {
+    s_instance = this;
+  }
+
+  MOCK_METHOD0(send, void());
+};
+
+SnapshotCreateRequest<librbd::MockTestImageCtx>* SnapshotCreateRequest<librbd::MockTestImageCtx>::s_instance = nullptr;
+
+} // namespace deep_copy
+} // namespace librbd
+
+// template definitions
+#include "librbd/deep_copy/SnapshotCopyRequest.cc"
+template class librbd::deep_copy::SnapshotCopyRequest<librbd::MockTestImageCtx>;
+
+namespace librbd {
+namespace deep_copy {
+
+using ::testing::_;
+using ::testing::DoAll;
+using ::testing::DoDefault;
+using ::testing::InSequence;
+using ::testing::Invoke;
+using ::testing::InvokeWithoutArgs;
+using ::testing::Return;
+using ::testing::ReturnNew;
+using ::testing::SetArgPointee;
+using ::testing::StrEq;
+using ::testing::WithArg;
+
+class TestMockDeepCopySnapshotCopyRequest : public TestMockFixture {
+public:
+  typedef SnapshotCopyRequest<librbd::MockTestImageCtx> MockSnapshotCopyRequest;
+  typedef SnapshotCreateRequest<librbd::MockTestImageCtx> MockSnapshotCreateRequest;
+
+  librbd::ImageCtx *m_src_image_ctx;
+  librbd::ImageCtx *m_dst_image_ctx;
+  ThreadPool *m_thread_pool;
+  ContextWQ *m_work_queue;
+
+  librbd::SnapSeqs m_snap_seqs;
+
+  void SetUp() override {
+    TestMockFixture::SetUp();
+
+    ASSERT_EQ(0, open_image(m_image_name, &m_src_image_ctx));
+
+    librbd::RBD rbd;
+    std::string dst_image_name = get_temp_image_name();
+    ASSERT_EQ(0, create_image_pp(rbd, m_ioctx, dst_image_name, m_image_size));
+    ASSERT_EQ(0, open_image(dst_image_name, &m_dst_image_ctx));
+
+    librbd::ImageCtx::get_thread_pool_instance(m_src_image_ctx->cct,
+                                               &m_thread_pool, &m_work_queue);
+  }
+
+  void prepare_exclusive_lock(librbd::MockImageCtx &mock_image_ctx,
+                              librbd::MockExclusiveLock &mock_exclusive_lock) {
+    if ((mock_image_ctx.features & RBD_FEATURE_EXCLUSIVE_LOCK) == 0) {
+      return;
+    }
+    mock_image_ctx.exclusive_lock = &mock_exclusive_lock;
+  }
+
+  void expect_test_features(librbd::MockImageCtx &mock_image_ctx) {
+    EXPECT_CALL(mock_image_ctx, test_features(_, _))
+      .WillRepeatedly(WithArg<0>(Invoke([&mock_image_ctx](uint64_t features) {
+              return (mock_image_ctx.features & features) != 0;
+            })));
+  }
+
+  void expect_start_op(librbd::MockExclusiveLock &mock_exclusive_lock) {
+    if ((m_src_image_ctx->features & RBD_FEATURE_EXCLUSIVE_LOCK) == 0) {
+      return;
+    }
+    EXPECT_CALL(mock_exclusive_lock, start_op()).WillOnce(
+      ReturnNew<FunctionContext>([](int) {}));
+  }
+
+  void expect_get_snap_namespace(librbd::MockTestImageCtx &mock_image_ctx,
+                                 uint64_t snap_id) {
+    EXPECT_CALL(mock_image_ctx, get_snap_namespace(snap_id, _))
+      .WillOnce(DoAll(SetArgPointee<1>(cls::rbd::UserSnapshotNamespace()),
+                      Return(0)));
+  }
+
+  void expect_snap_create(librbd::MockTestImageCtx &mock_image_ctx,
+                          MockSnapshotCreateRequest &mock_snapshot_create_request,
+                          const std::string &snap_name, uint64_t snap_id, int r) {
+    EXPECT_CALL(mock_snapshot_create_request, send())
+      .WillOnce(DoAll(Invoke([&mock_image_ctx, snap_id, snap_name]() {
+                        inject_snap(mock_image_ctx, snap_id, snap_name);
+                      }),
+                      Invoke([this, &mock_snapshot_create_request, r]() {
+                        m_work_queue->queue(mock_snapshot_create_request.on_finish, r);
+                      })));
+  }
+
+  void expect_snap_remove(librbd::MockTestImageCtx &mock_image_ctx,
+                          const std::string &snap_name, int r) {
+    EXPECT_CALL(*mock_image_ctx.operations, execute_snap_remove(_, StrEq(snap_name), _))
+                  .WillOnce(WithArg<2>(Invoke([this, r](Context *ctx) {
+                              m_work_queue->queue(ctx, r);
+                            })));
+  }
+
+  void expect_snap_protect(librbd::MockTestImageCtx &mock_image_ctx,
+                           const std::string &snap_name, int r) {
+    EXPECT_CALL(*mock_image_ctx.operations, execute_snap_protect(_, StrEq(snap_name), _))
+                  .WillOnce(WithArg<2>(Invoke([this, r](Context *ctx) {
+                              m_work_queue->queue(ctx, r);
+                            })));
+  }
+
+  void expect_snap_unprotect(librbd::MockTestImageCtx &mock_image_ctx,
+                             const std::string &snap_name, int r) {
+    EXPECT_CALL(*mock_image_ctx.operations, execute_snap_unprotect(_, StrEq(snap_name), _))
+                  .WillOnce(WithArg<2>(Invoke([this, r](Context *ctx) {
+                              m_work_queue->queue(ctx, r);
+                            })));
+  }
+
+  void expect_snap_is_protected(librbd::MockTestImageCtx &mock_image_ctx,
+                                uint64_t snap_id, bool is_protected, int r) {
+    EXPECT_CALL(mock_image_ctx, is_snap_protected(snap_id, _))
+                  .WillOnce(DoAll(SetArgPointee<1>(is_protected),
+                                  Return(r)));
+  }
+
+  void expect_snap_is_unprotected(librbd::MockTestImageCtx &mock_image_ctx,
+                                  uint64_t snap_id, bool is_unprotected, int r) {
+    EXPECT_CALL(mock_image_ctx, is_snap_unprotected(snap_id, _))
+                  .WillOnce(DoAll(SetArgPointee<1>(is_unprotected),
+                                  Return(r)));
+  }
+
+  static void inject_snap(librbd::MockTestImageCtx &mock_image_ctx,
+                          uint64_t snap_id, const std::string &snap_name) {
+    mock_image_ctx.snap_ids[{cls::rbd::UserSnapshotNamespace(),
+                            snap_name}] = snap_id;
+  }
+
+  MockSnapshotCopyRequest *create_request(
+      librbd::MockTestImageCtx &mock_src_image_ctx,
+      librbd::MockTestImageCtx &mock_dst_image_ctx, Context *on_finish) {
+    return new MockSnapshotCopyRequest(&mock_src_image_ctx, &mock_dst_image_ctx,
+                                       m_work_queue, &m_snap_seqs, on_finish);
+  }
+
+  int create_snap(librbd::ImageCtx *image_ctx, const std::string &snap_name,
+                  bool protect = false) {
+    int r = image_ctx->operations->snap_create(cls::rbd::UserSnapshotNamespace(),
+                                              snap_name.c_str());
+    if (r < 0) {
+      return r;
+    }
+
+    if (protect) {
+      r = image_ctx->operations->snap_protect(cls::rbd::UserSnapshotNamespace(),
+                                             snap_name.c_str());
+      if (r < 0) {
+        return r;
+      }
+    }
+
+    r = image_ctx->state->refresh();
+    if (r < 0) {
+      return r;
+    }
+    return 0;
+  }
+
+  void validate_snap_seqs(const librbd::SnapSeqs &snap_seqs) {
+    ASSERT_EQ(snap_seqs, m_snap_seqs);
+  }
+};
+
+TEST_F(TestMockDeepCopySnapshotCopyRequest, Empty) {
+  librbd::MockTestImageCtx mock_src_image_ctx(*m_src_image_ctx);
+  librbd::MockTestImageCtx mock_dst_image_ctx(*m_dst_image_ctx);
+
+  librbd::MockExclusiveLock mock_exclusive_lock;
+  prepare_exclusive_lock(mock_dst_image_ctx, mock_exclusive_lock);
+
+  expect_test_features(mock_dst_image_ctx);
+
+  InSequence seq;
+
+  C_SaferCond ctx;
+  MockSnapshotCopyRequest *request = create_request(mock_src_image_ctx,
+                                                    mock_dst_image_ctx, &ctx);
+  request->send();
+  ASSERT_EQ(0, ctx.wait());
+
+  validate_snap_seqs({});
+}
+
+TEST_F(TestMockDeepCopySnapshotCopyRequest, SnapCreate) {
+  ASSERT_EQ(0, create_snap(m_src_image_ctx, "snap1"));
+  ASSERT_EQ(0, create_snap(m_src_image_ctx, "snap2"));
+
+  uint64_t src_snap_id1 = m_src_image_ctx->snap_ids[
+    {cls::rbd::UserSnapshotNamespace(), "snap1"}];
+  uint64_t src_snap_id2 = m_src_image_ctx->snap_ids[
+    {cls::rbd::UserSnapshotNamespace(), "snap2"}];
+
+  librbd::MockTestImageCtx mock_src_image_ctx(*m_src_image_ctx);
+  librbd::MockTestImageCtx mock_dst_image_ctx(*m_dst_image_ctx);
+  MockSnapshotCreateRequest mock_snapshot_create_request;
+
+  librbd::MockExclusiveLock mock_exclusive_lock;
+  prepare_exclusive_lock(mock_dst_image_ctx, mock_exclusive_lock);
+
+  expect_test_features(mock_dst_image_ctx);
+
+  InSequence seq;
+  expect_get_snap_namespace(mock_src_image_ctx, src_snap_id1);
+  expect_start_op(mock_exclusive_lock);
+  expect_snap_create(mock_dst_image_ctx, mock_snapshot_create_request, "snap1", 12, 0);
+  expect_get_snap_namespace(mock_src_image_ctx, src_snap_id2);
+  expect_start_op(mock_exclusive_lock);
+  expect_snap_create(mock_dst_image_ctx, mock_snapshot_create_request, "snap2", 14, 0);
+  expect_snap_is_protected(mock_src_image_ctx, src_snap_id1, false, 0);
+  expect_snap_is_protected(mock_src_image_ctx, src_snap_id2, false, 0);
+
+  C_SaferCond ctx;
+  MockSnapshotCopyRequest *request = create_request(mock_src_image_ctx,
+                                                    mock_dst_image_ctx, &ctx);
+  request->send();
+  ASSERT_EQ(0, ctx.wait());
+
+  validate_snap_seqs({{src_snap_id1, 12}, {src_snap_id2, 14}});
+}
+
+TEST_F(TestMockDeepCopySnapshotCopyRequest, SnapCreateError) {
+  ASSERT_EQ(0, create_snap(m_src_image_ctx, "snap1"));
+
+  librbd::MockTestImageCtx mock_src_image_ctx(*m_src_image_ctx);
+  librbd::MockTestImageCtx mock_dst_image_ctx(*m_dst_image_ctx);
+  MockSnapshotCreateRequest mock_snapshot_create_request;
+
+  librbd::MockExclusiveLock mock_exclusive_lock;
+  prepare_exclusive_lock(mock_dst_image_ctx, mock_exclusive_lock);
+
+  uint64_t src_snap_id1 = mock_src_image_ctx.snap_ids[
+    {cls::rbd::UserSnapshotNamespace(), "snap1"}];
+
+  expect_test_features(mock_dst_image_ctx);
+
+  InSequence seq;
+  expect_get_snap_namespace(mock_src_image_ctx, src_snap_id1);
+  expect_start_op(mock_exclusive_lock);
+  expect_snap_create(mock_dst_image_ctx, mock_snapshot_create_request, "snap1",
+                     12, -EINVAL);
+
+  C_SaferCond ctx;
+  MockSnapshotCopyRequest *request = create_request(mock_src_image_ctx,
+                                                    mock_dst_image_ctx, &ctx);
+  request->send();
+  ASSERT_EQ(-EINVAL, ctx.wait());
+}
+
+TEST_F(TestMockDeepCopySnapshotCopyRequest, SnapCreateCancel) {
+  ASSERT_EQ(0, create_snap(m_src_image_ctx, "snap1"));
+
+  librbd::MockTestImageCtx mock_src_image_ctx(*m_src_image_ctx);
+  librbd::MockTestImageCtx mock_dst_image_ctx(*m_dst_image_ctx);
+  MockSnapshotCreateRequest mock_snapshot_create_request;
+
+  librbd::MockExclusiveLock mock_exclusive_lock;
+  prepare_exclusive_lock(mock_dst_image_ctx, mock_exclusive_lock);
+
+  uint64_t src_snap_id1 = m_src_image_ctx->snap_ids[
+    {cls::rbd::UserSnapshotNamespace(), "snap1"}];
+  expect_get_snap_namespace(mock_src_image_ctx, src_snap_id1);
+
+  C_SaferCond ctx;
+  MockSnapshotCopyRequest *request = create_request(mock_src_image_ctx,
+                                                    mock_dst_image_ctx, &ctx);
+  expect_test_features(mock_dst_image_ctx);
+
+  InSequence seq;
+  expect_start_op(mock_exclusive_lock);
+  EXPECT_CALL(mock_snapshot_create_request, send())
+    .WillOnce(DoAll(InvokeWithoutArgs([request]() {
+           request->cancel();
+         }),
+       Invoke([this, &mock_snapshot_create_request]() {
+           m_work_queue->queue(mock_snapshot_create_request.on_finish, 0);
+         })));
+
+  request->send();
+  ASSERT_EQ(-ECANCELED, ctx.wait());
+}
+
+TEST_F(TestMockDeepCopySnapshotCopyRequest, SnapRemoveAndCreate) {
+  ASSERT_EQ(0, create_snap(m_src_image_ctx, "snap1"));
+  ASSERT_EQ(0, create_snap(m_dst_image_ctx, "snap1"));
+
+  uint64_t src_snap_id1 = m_src_image_ctx->snap_ids[
+    {cls::rbd::UserSnapshotNamespace(), "snap1"}];
+  uint64_t dst_snap_id1 = m_dst_image_ctx->snap_ids[
+    {cls::rbd::UserSnapshotNamespace(), "snap1"}];
+
+  librbd::MockTestImageCtx mock_src_image_ctx(*m_src_image_ctx);
+  librbd::MockTestImageCtx mock_dst_image_ctx(*m_dst_image_ctx);
+  MockSnapshotCreateRequest mock_snapshot_create_request;
+
+  librbd::MockExclusiveLock mock_exclusive_lock;
+  prepare_exclusive_lock(mock_dst_image_ctx, mock_exclusive_lock);
+
+  expect_test_features(mock_dst_image_ctx);
+
+  InSequence seq;
+  expect_snap_is_unprotected(mock_dst_image_ctx,
+                             m_dst_image_ctx->snap_ids[
+                               {cls::rbd::UserSnapshotNamespace(), "snap1"}],
+                             true, 0);
+  expect_get_snap_namespace(mock_dst_image_ctx, dst_snap_id1);
+  expect_start_op(mock_exclusive_lock);
+  expect_snap_remove(mock_dst_image_ctx, "snap1", 0);
+  expect_get_snap_namespace(mock_src_image_ctx, src_snap_id1);
+  expect_start_op(mock_exclusive_lock);
+  expect_snap_create(mock_dst_image_ctx, mock_snapshot_create_request, "snap1", 12, 0);
+  expect_snap_is_protected(mock_src_image_ctx, src_snap_id1, false, 0);
+
+  C_SaferCond ctx;
+  MockSnapshotCopyRequest *request = create_request(mock_src_image_ctx,
+                                                    mock_dst_image_ctx, &ctx);
+  request->send();
+  ASSERT_EQ(0, ctx.wait());
+
+  validate_snap_seqs({{src_snap_id1, 12}});
+}
+
+TEST_F(TestMockDeepCopySnapshotCopyRequest, SnapRemoveError) {
+  ASSERT_EQ(0, create_snap(m_dst_image_ctx, "snap1"));
+
+  librbd::MockTestImageCtx mock_src_image_ctx(*m_src_image_ctx);
+  librbd::MockTestImageCtx mock_dst_image_ctx(*m_dst_image_ctx);
+
+  librbd::MockExclusiveLock mock_exclusive_lock;
+  prepare_exclusive_lock(mock_dst_image_ctx, mock_exclusive_lock);
+
+  uint64_t dst_snap_id1 = m_dst_image_ctx->snap_ids[
+    {cls::rbd::UserSnapshotNamespace(), "snap1"}];
+
+  expect_test_features(mock_dst_image_ctx);
+
+  InSequence seq;
+  expect_snap_is_unprotected(mock_dst_image_ctx,
+                             m_dst_image_ctx->snap_ids[
+                               {cls::rbd::UserSnapshotNamespace(), "snap1"}],
+                             true, 0);
+  expect_get_snap_namespace(mock_dst_image_ctx, dst_snap_id1);
+  expect_start_op(mock_exclusive_lock);
+  expect_snap_remove(mock_dst_image_ctx, "snap1", -EINVAL);
+
+  C_SaferCond ctx;
+  MockSnapshotCopyRequest *request = create_request(mock_src_image_ctx,
+                                                    mock_dst_image_ctx, &ctx);
+  request->send();
+  ASSERT_EQ(-EINVAL, ctx.wait());
+}
+
+TEST_F(TestMockDeepCopySnapshotCopyRequest, SnapUnprotect) {
+  REQUIRE_FEATURE(RBD_FEATURE_LAYERING);
+
+  ASSERT_EQ(0, create_snap(m_src_image_ctx, "snap1", true));
+  ASSERT_EQ(0, create_snap(m_dst_image_ctx, "snap1", true));
+
+  uint64_t src_snap_id1 = m_src_image_ctx->snap_ids[
+    {cls::rbd::UserSnapshotNamespace(), "snap1"}];
+  uint64_t dst_snap_id1 = m_dst_image_ctx->snap_ids[
+    {cls::rbd::UserSnapshotNamespace(), "snap1"}];
+  m_snap_seqs[src_snap_id1] = dst_snap_id1;
+
+  librbd::MockTestImageCtx mock_src_image_ctx(*m_src_image_ctx);
+  librbd::MockTestImageCtx mock_dst_image_ctx(*m_dst_image_ctx);
+
+  librbd::MockExclusiveLock mock_exclusive_lock;
+  prepare_exclusive_lock(mock_dst_image_ctx, mock_exclusive_lock);
+
+  expect_test_features(mock_dst_image_ctx);
+
+  InSequence seq;
+  expect_snap_is_unprotected(mock_dst_image_ctx, dst_snap_id1, false, 0);
+  expect_snap_is_unprotected(mock_src_image_ctx, src_snap_id1, true, 0);
+  expect_start_op(mock_exclusive_lock);
+  expect_snap_unprotect(mock_dst_image_ctx, "snap1", 0);
+  expect_get_snap_namespace(mock_dst_image_ctx, dst_snap_id1);
+  expect_get_snap_namespace(mock_src_image_ctx, src_snap_id1);
+  expect_snap_is_protected(mock_src_image_ctx, src_snap_id1, false, 0);
+
+  C_SaferCond ctx;
+  MockSnapshotCopyRequest *request = create_request(mock_src_image_ctx,
+                                                    mock_dst_image_ctx, &ctx);
+  request->send();
+  ASSERT_EQ(0, ctx.wait());
+
+  validate_snap_seqs({{src_snap_id1, dst_snap_id1}});
+}
+
+TEST_F(TestMockDeepCopySnapshotCopyRequest, SnapUnprotectError) {
+  REQUIRE_FEATURE(RBD_FEATURE_LAYERING);
+
+  ASSERT_EQ(0, create_snap(m_src_image_ctx, "snap1", true));
+  ASSERT_EQ(0, create_snap(m_dst_image_ctx, "snap1", true));
+
+  uint64_t src_snap_id1 = m_src_image_ctx->snap_ids[
+    {cls::rbd::UserSnapshotNamespace(), "snap1"}];
+  uint64_t dst_snap_id1 = m_dst_image_ctx->snap_ids[
+    {cls::rbd::UserSnapshotNamespace(), "snap1"}];
+  m_snap_seqs[src_snap_id1] = dst_snap_id1;
+
+  librbd::MockTestImageCtx mock_src_image_ctx(*m_src_image_ctx);
+  librbd::MockTestImageCtx mock_dst_image_ctx(*m_dst_image_ctx);
+
+  librbd::MockExclusiveLock mock_exclusive_lock;
+  prepare_exclusive_lock(mock_dst_image_ctx, mock_exclusive_lock);
+
+  expect_test_features(mock_dst_image_ctx);
+
+  InSequence seq;
+  expect_snap_is_unprotected(mock_dst_image_ctx, dst_snap_id1, false, 0);
+  expect_snap_is_unprotected(mock_src_image_ctx, src_snap_id1, true, 0);
+  expect_start_op(mock_exclusive_lock);
+  expect_snap_unprotect(mock_dst_image_ctx, "snap1", -EBUSY);
+
+  C_SaferCond ctx;
+  MockSnapshotCopyRequest *request = create_request(mock_src_image_ctx,
+                                                    mock_dst_image_ctx, &ctx);
+  request->send();
+  ASSERT_EQ(-EBUSY, ctx.wait());
+}
+
+TEST_F(TestMockDeepCopySnapshotCopyRequest, SnapUnprotectCancel) {
+  REQUIRE_FEATURE(RBD_FEATURE_LAYERING);
+
+  ASSERT_EQ(0, create_snap(m_src_image_ctx, "snap1", true));
+  ASSERT_EQ(0, create_snap(m_dst_image_ctx, "snap1", true));
+
+  uint64_t src_snap_id1 = m_src_image_ctx->snap_ids[
+    {cls::rbd::UserSnapshotNamespace(), "snap1"}];
+  uint64_t dst_snap_id1 = m_dst_image_ctx->snap_ids[
+    {cls::rbd::UserSnapshotNamespace(), "snap1"}];
+  m_snap_seqs[src_snap_id1] = dst_snap_id1;
+
+  librbd::MockTestImageCtx mock_src_image_ctx(*m_src_image_ctx);
+  librbd::MockTestImageCtx mock_dst_image_ctx(*m_dst_image_ctx);
+
+  librbd::MockExclusiveLock mock_exclusive_lock;
+  prepare_exclusive_lock(mock_dst_image_ctx, mock_exclusive_lock);
+
+  C_SaferCond ctx;
+  MockSnapshotCopyRequest *request = create_request(mock_src_image_ctx,
+                                                    mock_dst_image_ctx, &ctx);
+  expect_test_features(mock_dst_image_ctx);
+
+  InSequence seq;
+  expect_snap_is_unprotected(mock_dst_image_ctx, dst_snap_id1, false, 0);
+  expect_snap_is_unprotected(mock_src_image_ctx, src_snap_id1, true, 0);
+  expect_start_op(mock_exclusive_lock);
+  EXPECT_CALL(*mock_dst_image_ctx.operations,
+             execute_snap_unprotect(_, StrEq("snap1"), _))
+    .WillOnce(DoAll(InvokeWithoutArgs([request]() {
+           request->cancel();
+         }),
+       WithArg<2>(Invoke([this](Context *ctx) {
+           m_work_queue->queue(ctx, 0);
+           }))));
+
+  request->send();
+  ASSERT_EQ(-ECANCELED, ctx.wait());
+}
+
+TEST_F(TestMockDeepCopySnapshotCopyRequest, SnapUnprotectRemove) {
+  REQUIRE_FEATURE(RBD_FEATURE_LAYERING);
+
+  ASSERT_EQ(0, create_snap(m_src_image_ctx, "snap1", true));
+  ASSERT_EQ(0, create_snap(m_dst_image_ctx, "snap1", true));
+
+  uint64_t src_snap_id1 = m_src_image_ctx->snap_ids[
+    {cls::rbd::UserSnapshotNamespace(), "snap1"}];
+  uint64_t dst_snap_id1 = m_dst_image_ctx->snap_ids[
+    {cls::rbd::UserSnapshotNamespace(), "snap1"}];
+
+  librbd::MockTestImageCtx mock_src_image_ctx(*m_src_image_ctx);
+  librbd::MockTestImageCtx mock_dst_image_ctx(*m_dst_image_ctx);
+  MockSnapshotCreateRequest mock_snapshot_create_request;
+
+  librbd::MockExclusiveLock mock_exclusive_lock;
+  prepare_exclusive_lock(mock_dst_image_ctx, mock_exclusive_lock);
+
+  expect_test_features(mock_dst_image_ctx);
+
+  InSequence seq;
+  expect_snap_is_unprotected(mock_dst_image_ctx,
+                             m_dst_image_ctx->snap_ids[
+                               {cls::rbd::UserSnapshotNamespace(), "snap1"}],
+                             false, 0);
+  expect_start_op(mock_exclusive_lock);
+  expect_snap_unprotect(mock_dst_image_ctx, "snap1", 0);
+  expect_get_snap_namespace(mock_dst_image_ctx, dst_snap_id1);
+  expect_start_op(mock_exclusive_lock);
+  expect_snap_remove(mock_dst_image_ctx, "snap1", 0);
+  expect_get_snap_namespace(mock_src_image_ctx, src_snap_id1);
+  expect_start_op(mock_exclusive_lock);
+  expect_snap_create(mock_dst_image_ctx, mock_snapshot_create_request, "snap1",
+                     12, 0);
+  expect_snap_is_protected(mock_src_image_ctx, src_snap_id1, false, 0);
+
+  C_SaferCond ctx;
+  MockSnapshotCopyRequest *request = create_request(mock_src_image_ctx,
+                                                    mock_dst_image_ctx, &ctx);
+  request->send();
+  ASSERT_EQ(0, ctx.wait());
+
+  validate_snap_seqs({{src_snap_id1, 12}});
+}
+
+TEST_F(TestMockDeepCopySnapshotCopyRequest, SnapCreateProtect) {
+  REQUIRE_FEATURE(RBD_FEATURE_LAYERING);
+
+  ASSERT_EQ(0, create_snap(m_src_image_ctx, "snap1", true));
+
+  uint64_t src_snap_id1 = m_src_image_ctx->snap_ids[
+    {cls::rbd::UserSnapshotNamespace(), "snap1"}];
+
+  librbd::MockTestImageCtx mock_src_image_ctx(*m_src_image_ctx);
+  librbd::MockTestImageCtx mock_dst_image_ctx(*m_dst_image_ctx);
+  MockSnapshotCreateRequest mock_snapshot_create_request;
+
+  librbd::MockExclusiveLock mock_exclusive_lock;
+  prepare_exclusive_lock(mock_dst_image_ctx, mock_exclusive_lock);
+
+  expect_test_features(mock_dst_image_ctx);
+
+  InSequence seq;
+  expect_get_snap_namespace(mock_src_image_ctx, src_snap_id1);
+  expect_start_op(mock_exclusive_lock);
+  expect_snap_create(mock_dst_image_ctx, mock_snapshot_create_request, "snap1",
+                     12, 0);
+  expect_snap_is_protected(mock_src_image_ctx, src_snap_id1, true, 0);
+  expect_snap_is_protected(mock_dst_image_ctx, 12, false, 0);
+  expect_start_op(mock_exclusive_lock);
+  expect_snap_protect(mock_dst_image_ctx, "snap1", 0);
+
+  C_SaferCond ctx;
+  MockSnapshotCopyRequest *request = create_request(mock_src_image_ctx,
+                                                    mock_dst_image_ctx, &ctx);
+  request->send();
+  ASSERT_EQ(0, ctx.wait());
+
+  validate_snap_seqs({{src_snap_id1, 12}});
+}
+
+TEST_F(TestMockDeepCopySnapshotCopyRequest, SnapProtect) {
+  REQUIRE_FEATURE(RBD_FEATURE_LAYERING);
+
+  ASSERT_EQ(0, create_snap(m_src_image_ctx, "snap1", true));
+  ASSERT_EQ(0, create_snap(m_dst_image_ctx, "snap1", true));
+
+  uint64_t src_snap_id1 = m_src_image_ctx->snap_ids[
+    {cls::rbd::UserSnapshotNamespace(), "snap1"}];
+  uint64_t dst_snap_id1 = m_dst_image_ctx->snap_ids[
+    {cls::rbd::UserSnapshotNamespace(), "snap1"}];
+  m_snap_seqs[src_snap_id1] = dst_snap_id1;
+
+  librbd::MockTestImageCtx mock_src_image_ctx(*m_src_image_ctx);
+  librbd::MockTestImageCtx mock_dst_image_ctx(*m_dst_image_ctx);
+
+  librbd::MockExclusiveLock mock_exclusive_lock;
+  prepare_exclusive_lock(mock_dst_image_ctx, mock_exclusive_lock);
+
+  expect_test_features(mock_dst_image_ctx);
+
+  InSequence seq;
+  expect_snap_is_unprotected(mock_dst_image_ctx, dst_snap_id1, true, 0);
+  expect_get_snap_namespace(mock_dst_image_ctx, dst_snap_id1);
+  expect_get_snap_namespace(mock_src_image_ctx, src_snap_id1);
+  expect_snap_is_protected(mock_src_image_ctx, src_snap_id1, true, 0);
+  expect_snap_is_protected(mock_dst_image_ctx, dst_snap_id1, false, 0);
+  expect_start_op(mock_exclusive_lock);
+  expect_snap_protect(mock_dst_image_ctx, "snap1", 0);
+
+  C_SaferCond ctx;
+  MockSnapshotCopyRequest *request = create_request(mock_src_image_ctx,
+                                                    mock_dst_image_ctx, &ctx);
+  request->send();
+  ASSERT_EQ(0, ctx.wait());
+
+  validate_snap_seqs({{src_snap_id1, dst_snap_id1}});
+}
+
+TEST_F(TestMockDeepCopySnapshotCopyRequest, SnapProtectError) {
+  REQUIRE_FEATURE(RBD_FEATURE_LAYERING);
+
+  ASSERT_EQ(0, create_snap(m_src_image_ctx, "snap1", true));
+  ASSERT_EQ(0, create_snap(m_dst_image_ctx, "snap1", true));
+
+  uint64_t src_snap_id1 = m_src_image_ctx->snap_ids[
+    {cls::rbd::UserSnapshotNamespace(), "snap1"}];
+  uint64_t dst_snap_id1 = m_dst_image_ctx->snap_ids[
+    {cls::rbd::UserSnapshotNamespace(), "snap1"}];
+  m_snap_seqs[src_snap_id1] = dst_snap_id1;
+
+  librbd::MockTestImageCtx mock_src_image_ctx(*m_src_image_ctx);
+  librbd::MockTestImageCtx mock_dst_image_ctx(*m_dst_image_ctx);
+
+  librbd::MockExclusiveLock mock_exclusive_lock;
+  prepare_exclusive_lock(mock_dst_image_ctx, mock_exclusive_lock);
+
+  expect_test_features(mock_dst_image_ctx);
+
+  InSequence seq;
+  expect_snap_is_unprotected(mock_dst_image_ctx, dst_snap_id1, true, 0);
+  expect_get_snap_namespace(mock_dst_image_ctx, dst_snap_id1);
+  expect_get_snap_namespace(mock_src_image_ctx, src_snap_id1);
+  expect_snap_is_protected(mock_src_image_ctx, src_snap_id1, true, 0);
+  expect_snap_is_protected(mock_dst_image_ctx, dst_snap_id1, false, 0);
+  expect_start_op(mock_exclusive_lock);
+  expect_snap_protect(mock_dst_image_ctx, "snap1", -EINVAL);
+
+  C_SaferCond ctx;
+  MockSnapshotCopyRequest *request = create_request(mock_src_image_ctx,
+                                                    mock_dst_image_ctx, &ctx);
+  request->send();
+  ASSERT_EQ(-EINVAL, ctx.wait());
+}
+
+TEST_F(TestMockDeepCopySnapshotCopyRequest, SnapProtectCancel) {
+  REQUIRE_FEATURE(RBD_FEATURE_LAYERING);
+
+  ASSERT_EQ(0, create_snap(m_src_image_ctx, "snap1", true));
+  ASSERT_EQ(0, create_snap(m_dst_image_ctx, "snap1", true));
+
+  uint64_t src_snap_id1 = m_src_image_ctx->snap_ids[
+    {cls::rbd::UserSnapshotNamespace(), "snap1"}];
+  uint64_t dst_snap_id1 = m_dst_image_ctx->snap_ids[
+    {cls::rbd::UserSnapshotNamespace(), "snap1"}];
+  m_snap_seqs[src_snap_id1] = dst_snap_id1;
+
+  librbd::MockTestImageCtx mock_src_image_ctx(*m_src_image_ctx);
+  librbd::MockTestImageCtx mock_dst_image_ctx(*m_dst_image_ctx);
+
+  librbd::MockExclusiveLock mock_exclusive_lock;
+  prepare_exclusive_lock(mock_dst_image_ctx, mock_exclusive_lock);
+
+  C_SaferCond ctx;
+  MockSnapshotCopyRequest *request = create_request(mock_src_image_ctx,
+                                                    mock_dst_image_ctx, &ctx);
+  expect_test_features(mock_dst_image_ctx);
+
+  InSequence seq;
+  expect_snap_is_unprotected(mock_dst_image_ctx, dst_snap_id1, true, 0);
+  expect_get_snap_namespace(mock_dst_image_ctx, dst_snap_id1);
+  expect_get_snap_namespace(mock_src_image_ctx, src_snap_id1);
+  expect_snap_is_protected(mock_src_image_ctx, src_snap_id1, true, 0);
+  expect_snap_is_protected(mock_dst_image_ctx, dst_snap_id1, false, 0);
+  expect_start_op(mock_exclusive_lock);
+  EXPECT_CALL(*mock_dst_image_ctx.operations,
+             execute_snap_protect(_, StrEq("snap1"), _))
+    .WillOnce(DoAll(InvokeWithoutArgs([request]() {
+           request->cancel();
+         }),
+       WithArg<2>(Invoke([this](Context *ctx) {
+             m_work_queue->queue(ctx, 0);
+           }))));
+
+  request->send();
+  ASSERT_EQ(-ECANCELED, ctx.wait());
+}
+
+} // namespace deep_copy
+} // namespace librbd
diff --git a/src/test/librbd/deep_copy/test_mock_SnapshotCreateRequest.cc b/src/test/librbd/deep_copy/test_mock_SnapshotCreateRequest.cc
new file mode 100644 (file)
index 0000000..f12a48c
--- /dev/null
@@ -0,0 +1,256 @@
+// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*-
+// vim: ts=8 sw=2 smarttab
+
+#include "test/librbd/test_mock_fixture.h"
+#include "test/librados_test_stub/LibradosTestStub.h"
+#include "include/rbd/librbd.hpp"
+#include "librbd/ImageCtx.h"
+#include "librbd/ImageState.h"
+#include "librbd/Operations.h"
+#include "osdc/Striper.h"
+#include "test/librados_test_stub/MockTestMemIoCtxImpl.h"
+#include "test/librbd/mock/MockImageCtx.h"
+#include "librbd/deep_copy/SetHeadRequest.h"
+#include "librbd/deep_copy/SnapshotCreateRequest.h"
+
+namespace librbd {
+namespace {
+
+struct MockTestImageCtx : public librbd::MockImageCtx {
+  MockTestImageCtx(librbd::ImageCtx &image_ctx)
+    : librbd::MockImageCtx(image_ctx) {
+  }
+};
+
+} // anonymous namespace
+
+namespace deep_copy {
+
+template <>
+class SetHeadRequest<librbd::MockTestImageCtx> {
+public:
+  static SetHeadRequest* s_instance;
+  Context *on_finish;
+
+  static SetHeadRequest* create(librbd::MockTestImageCtx *image_ctx,
+                                uint64_t size,
+                                const librbd::ParentSpec &parent_spec,
+                                uint64_t parent_overlap, Context *on_finish) {
+    assert(s_instance != nullptr);
+    s_instance->on_finish = on_finish;
+    return s_instance;
+  }
+
+  SetHeadRequest() {
+    s_instance = this;
+  }
+
+  MOCK_METHOD0(send, void());
+};
+
+SetHeadRequest<librbd::MockTestImageCtx>* SetHeadRequest<librbd::MockTestImageCtx>::s_instance = nullptr;
+
+} // namespace deep_copy
+} // namespace librbd
+
+// template definitions
+#include "librbd/deep_copy/SnapshotCreateRequest.cc"
+template class librbd::deep_copy::SnapshotCreateRequest<librbd::MockTestImageCtx>;
+
+namespace librbd {
+namespace deep_copy {
+
+using ::testing::_;
+using ::testing::DoAll;
+using ::testing::InSequence;
+using ::testing::Invoke;
+using ::testing::InvokeWithoutArgs;
+using ::testing::Return;
+using ::testing::ReturnNew;
+using ::testing::StrEq;
+using ::testing::WithArg;
+
+class TestMockDeepCopySnapshotCreateRequest : public TestMockFixture {
+public:
+  typedef SetHeadRequest<librbd::MockTestImageCtx> MockSetHeadRequest;
+  typedef SnapshotCreateRequest<librbd::MockTestImageCtx> MockSnapshotCreateRequest;
+
+  librbd::ImageCtx *m_image_ctx;
+  ThreadPool *m_thread_pool;
+  ContextWQ *m_work_queue;
+
+  void SetUp() override {
+    TestMockFixture::SetUp();
+
+    ASSERT_EQ(0, open_image(m_image_name, &m_image_ctx));
+
+    librbd::ImageCtx::get_thread_pool_instance(m_image_ctx->cct, &m_thread_pool,
+                                               &m_work_queue);
+  }
+
+  void expect_start_op(librbd::MockExclusiveLock &mock_exclusive_lock) {
+    EXPECT_CALL(mock_exclusive_lock, start_op()).WillOnce(
+      ReturnNew<FunctionContext>([](int) {}));
+  }
+
+  void expect_test_features(librbd::MockTestImageCtx &mock_image_ctx,
+                            uint64_t features, bool enabled) {
+    EXPECT_CALL(mock_image_ctx, test_features(features))
+                  .WillOnce(Return(enabled));
+  }
+
+  void expect_set_head(MockSetHeadRequest &mock_set_head_request, int r) {
+    EXPECT_CALL(mock_set_head_request, send())
+      .WillOnce(Invoke([this, &mock_set_head_request, r]() {
+            mock_set_head_request.on_finish->complete(r);
+          }));
+  }
+
+  void expect_snap_create(librbd::MockTestImageCtx &mock_image_ctx,
+                          const std::string &snap_name, uint64_t snap_id, int r) {
+    EXPECT_CALL(*mock_image_ctx.operations, execute_snap_create(_, StrEq(snap_name), _, 0, true))
+                  .WillOnce(DoAll(InvokeWithoutArgs([&mock_image_ctx, snap_id, snap_name]() {
+                                    inject_snap(mock_image_ctx, snap_id, snap_name);
+                                  }),
+                                  WithArg<2>(Invoke([this, r](Context *ctx) {
+                                    m_work_queue->queue(ctx, r);
+                                  }))));
+  }
+
+  void expect_object_map_resize(librbd::MockTestImageCtx &mock_image_ctx,
+                                librados::snap_t snap_id, int r) {
+    std::string oid(librbd::ObjectMap<>::object_map_name(mock_image_ctx.id,
+                                                         snap_id));
+    EXPECT_CALL(get_mock_io_ctx(mock_image_ctx.md_ctx),
+                exec(oid, _, StrEq("rbd"), StrEq("object_map_resize"), _, _, _))
+                  .WillOnce(Return(r));
+  }
+
+  static void inject_snap(librbd::MockTestImageCtx &mock_image_ctx,
+                   uint64_t snap_id, const std::string &snap_name) {
+    mock_image_ctx.snap_ids[{cls::rbd::UserSnapshotNamespace(),
+                            snap_name}] = snap_id;
+  }
+
+  MockSnapshotCreateRequest *create_request(librbd::MockTestImageCtx &mock_local_image_ctx,
+                                            const std::string &snap_name,
+                                           const cls::rbd::SnapshotNamespace &snap_namespace,
+                                            uint64_t size,
+                                            const librbd::ParentSpec &spec,
+                                            uint64_t parent_overlap,
+                                            Context *on_finish) {
+    return new MockSnapshotCreateRequest(&mock_local_image_ctx, snap_name, snap_namespace, size,
+                                         spec, parent_overlap, on_finish);
+  }
+};
+
+TEST_F(TestMockDeepCopySnapshotCreateRequest, SnapCreate) {
+  librbd::MockTestImageCtx mock_image_ctx(*m_image_ctx);
+  librbd::MockExclusiveLock mock_exclusive_lock;
+  mock_image_ctx.exclusive_lock = &mock_exclusive_lock;
+  MockSetHeadRequest mock_set_head_request;
+
+  InSequence seq;
+  expect_set_head(mock_set_head_request, 0);
+  expect_start_op(mock_exclusive_lock);
+  expect_snap_create(mock_image_ctx, "snap1", 10, 0);
+  expect_test_features(mock_image_ctx, RBD_FEATURE_OBJECT_MAP, false);
+
+  C_SaferCond ctx;
+  MockSnapshotCreateRequest *request = create_request(mock_image_ctx,
+                                                      "snap1",
+                                                     cls::rbd::UserSnapshotNamespace(),
+                                                      m_image_ctx->size,
+                                                      {}, 0, &ctx);
+  request->send();
+  ASSERT_EQ(0, ctx.wait());
+}
+
+TEST_F(TestMockDeepCopySnapshotCreateRequest, SetHeadError) {
+  librbd::MockTestImageCtx mock_image_ctx(*m_image_ctx);
+  MockSetHeadRequest mock_set_head_request;
+
+  InSequence seq;
+  expect_set_head(mock_set_head_request, -EINVAL);
+
+  C_SaferCond ctx;
+  MockSnapshotCreateRequest *request = create_request(mock_image_ctx,
+                                                      "snap1",
+                                                     cls::rbd::UserSnapshotNamespace(),
+                                                     123, {}, 0,
+                                                      &ctx);
+  request->send();
+  ASSERT_EQ(-EINVAL, ctx.wait());
+}
+
+TEST_F(TestMockDeepCopySnapshotCreateRequest, SnapCreateError) {
+  librbd::MockTestImageCtx mock_image_ctx(*m_image_ctx);
+  librbd::MockExclusiveLock mock_exclusive_lock;
+  mock_image_ctx.exclusive_lock = &mock_exclusive_lock;
+  MockSetHeadRequest mock_set_head_request;
+
+  InSequence seq;
+  expect_set_head(mock_set_head_request, 0);
+  expect_start_op(mock_exclusive_lock);
+  expect_snap_create(mock_image_ctx, "snap1", 10, -EINVAL);
+
+  C_SaferCond ctx;
+  MockSnapshotCreateRequest *request = create_request(mock_image_ctx,
+                                                      "snap1",
+                                                     cls::rbd::UserSnapshotNamespace(),
+                                                      m_image_ctx->size,
+                                                      {}, 0, &ctx);
+  request->send();
+  ASSERT_EQ(-EINVAL, ctx.wait());
+}
+
+TEST_F(TestMockDeepCopySnapshotCreateRequest, ResizeObjectMap) {
+  librbd::MockTestImageCtx mock_image_ctx(*m_image_ctx);
+  librbd::MockExclusiveLock mock_exclusive_lock;
+  mock_image_ctx.exclusive_lock = &mock_exclusive_lock;
+  MockSetHeadRequest mock_set_head_request;
+
+  InSequence seq;
+  expect_set_head(mock_set_head_request, 0);
+  expect_start_op(mock_exclusive_lock);
+  expect_snap_create(mock_image_ctx, "snap1", 10, 0);
+  expect_test_features(mock_image_ctx, RBD_FEATURE_OBJECT_MAP, true);
+  expect_start_op(mock_exclusive_lock);
+  expect_object_map_resize(mock_image_ctx, 10, 0);
+
+  C_SaferCond ctx;
+  MockSnapshotCreateRequest *request = create_request(mock_image_ctx,
+                                                      "snap1",
+                                                     cls::rbd::UserSnapshotNamespace(),
+                                                      m_image_ctx->size,
+                                                      {}, 0, &ctx);
+  request->send();
+  ASSERT_EQ(0, ctx.wait());
+}
+
+TEST_F(TestMockDeepCopySnapshotCreateRequest, ResizeObjectMapError) {
+  librbd::MockTestImageCtx mock_image_ctx(*m_image_ctx);
+  librbd::MockExclusiveLock mock_exclusive_lock;
+  mock_image_ctx.exclusive_lock = &mock_exclusive_lock;
+  MockSetHeadRequest mock_set_head_request;
+
+  InSequence seq;
+  expect_set_head(mock_set_head_request, 0);
+  expect_start_op(mock_exclusive_lock);
+  expect_snap_create(mock_image_ctx, "snap1", 10, 0);
+  expect_test_features(mock_image_ctx, RBD_FEATURE_OBJECT_MAP, true);
+  expect_start_op(mock_exclusive_lock);
+  expect_object_map_resize(mock_image_ctx, 10, -EINVAL);
+
+  C_SaferCond ctx;
+  MockSnapshotCreateRequest *request = create_request(mock_image_ctx,
+                                                      "snap1",
+                                                     cls::rbd::UserSnapshotNamespace(),
+                                                      m_image_ctx->size,
+                                                      {}, 0, &ctx);
+  request->send();
+  ASSERT_EQ(-EINVAL, ctx.wait());
+}
+
+} // namespace deep_copy
+} // namespace librbd
index 3b9228194594d37fcfc9ec5bc60fd7733d0a5f34..ee020c1350982cdbc06720d6fa50059a1711e497 100644 (file)
@@ -155,6 +155,7 @@ struct MockImageCtx {
                     librados::snap_t(cls::rbd::SnapshotNamespace snap_namespace,
                                      std::string in_snap_name));
   MOCK_CONST_METHOD1(get_snap_info, const SnapInfo*(librados::snap_t));
+  MOCK_CONST_METHOD2(get_snap_name, int(librados::snap_t, std::string *));
   MOCK_CONST_METHOD2(get_snap_namespace, int(librados::snap_t,
                                             cls::rbd::SnapshotNamespace *out_snap_namespace));
   MOCK_CONST_METHOD2(get_parent_spec, int(librados::snap_t in_snap_id,
diff --git a/src/test/librbd/test_DeepCopy.cc b/src/test/librbd/test_DeepCopy.cc
new file mode 100644 (file)
index 0000000..ec78686
--- /dev/null
@@ -0,0 +1,405 @@
+// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*-
+// vim: ts=8 sw=2 smarttab
+
+#include "test/librbd/test_fixture.h"
+#include "test/librbd/test_support.h"
+#include "librbd/Operations.h"
+#include "librbd/api/Image.h"
+#include "librbd/internal.h"
+#include "librbd/io/ImageRequestWQ.h"
+#include "librbd/io/ReadResult.h"
+
+void register_test_deep_copy() {
+}
+
+struct TestDeepCopy : public TestFixture {
+  void SetUp() override {
+    TestFixture::SetUp();
+
+    std::string image_name = get_temp_image_name();
+    int order = 22;
+    uint64_t size = (1 << order) * 20;
+    uint64_t features = 0;
+    bool old_format = get_features(&features);
+    EXPECT_EQ(0, create_image_full_pp(m_rbd, m_ioctx, image_name, size,
+                                      features, old_format, &order));
+    ASSERT_EQ(0, open_image(image_name, &m_src_ictx));
+
+    if (old_format) {
+      // The destination should always be in the new format.
+      uint64_t format = 2;
+      ASSERT_EQ(0, m_opts.set(RBD_IMAGE_OPTION_FORMAT, format));
+    }
+  }
+
+  void TearDown() override {
+    if (m_src_ictx != nullptr) {
+      deep_copy();
+      if (m_dst_ictx != nullptr) {
+        compare();
+        close_image(m_dst_ictx);
+      }
+      close_image(m_src_ictx);
+    }
+
+    TestFixture::TearDown();
+  }
+
+  void deep_copy() {
+    std::string dst_name = get_temp_image_name();
+    librbd::NoOpProgressContext no_op;
+    EXPECT_EQ(0, m_src_ictx->io_work_queue->flush());
+    EXPECT_EQ(0, librbd::api::Image<>::deep_copy(m_src_ictx, m_src_ictx->md_ctx,
+                                                 dst_name.c_str(), m_opts,
+                                                 no_op));
+    EXPECT_EQ(0, open_image(dst_name, &m_dst_ictx));
+  }
+
+  void compare() {
+    vector<librbd::snap_info_t> src_snaps, dst_snaps;
+
+    EXPECT_EQ(m_src_ictx->size, m_dst_ictx->size);
+    EXPECT_EQ(0, librbd::snap_list(m_src_ictx, src_snaps));
+    EXPECT_EQ(0, librbd::snap_list(m_dst_ictx, dst_snaps));
+    EXPECT_EQ(src_snaps.size(), dst_snaps.size());
+    for (size_t i = 0; i <= src_snaps.size(); i++) {
+      const char *src_snap_name = nullptr;
+      const char *dst_snap_name = nullptr;
+      if (i < src_snaps.size()) {
+        EXPECT_EQ(src_snaps[i].name, dst_snaps[i].name);
+        src_snap_name = src_snaps[i].name.c_str();
+        dst_snap_name = dst_snaps[i].name.c_str();
+      }
+      EXPECT_EQ(0, librbd::snap_set(m_src_ictx, cls::rbd::UserSnapshotNamespace(),
+                                    src_snap_name));
+      EXPECT_EQ(0, librbd::snap_set(m_dst_ictx, cls::rbd::UserSnapshotNamespace(),
+                                    dst_snap_name));
+      uint64_t src_size, dst_size;
+      {
+        RWLock::RLocker src_locker(m_src_ictx->snap_lock);
+        RWLock::RLocker dst_locker(m_dst_ictx->snap_lock);
+        src_size = m_src_ictx->get_image_size(m_src_ictx->snap_id);
+        dst_size = m_dst_ictx->get_image_size(m_dst_ictx->snap_id);
+      }
+      EXPECT_EQ(src_size, dst_size);
+
+      ssize_t read_size = 1 << m_src_ictx->order;
+      uint64_t offset = 0;
+      while (offset < src_size) {
+        read_size = std::min(read_size, static_cast<ssize_t>(src_size - offset));
+
+        bufferptr src_ptr(read_size);
+        bufferlist src_bl;
+        src_bl.push_back(src_ptr);
+        librbd::io::ReadResult src_result{&src_bl};
+        EXPECT_EQ(read_size, m_src_ictx->io_work_queue->read(
+                    offset, read_size, librbd::io::ReadResult{src_result}, 0));
+
+        bufferptr dst_ptr(read_size);
+        bufferlist dst_bl;
+        dst_bl.push_back(dst_ptr);
+        librbd::io::ReadResult dst_result{&dst_bl};
+        EXPECT_EQ(read_size, m_dst_ictx->io_work_queue->read(
+                    offset, read_size, librbd::io::ReadResult{dst_result}, 0));
+
+        if (!src_bl.contents_equal(dst_bl)) {
+          std::cout << "snap: " << (src_snap_name ? src_snap_name : "null")
+                    << ", block " << offset << "~" << read_size << " differs"
+                    << std::endl;
+          // std::cout << "src block: " << std::endl; src_bl.hexdump(std::cout);
+          // std::cout << "dst block: " << std::endl; dst_bl.hexdump(std::cout);
+        }
+        EXPECT_TRUE(src_bl.contents_equal(dst_bl));
+        offset += read_size;
+      }
+    }
+  }
+
+  void test_no_snaps() {
+    bufferlist bl;
+    bl.append(std::string(((1 << m_src_ictx->order) * 2) + 1, '1'));
+    ASSERT_EQ(static_cast<ssize_t>(bl.length()),
+              m_src_ictx->io_work_queue->write(0 * bl.length(), bl.length(),
+                                               bufferlist{bl}, 0));
+    ASSERT_EQ(static_cast<ssize_t>(bl.length()),
+              m_src_ictx->io_work_queue->write(2 * bl.length(), bl.length(),
+                                               bufferlist{bl}, 0));
+  }
+
+  void test_snaps() {
+    bufferlist bl;
+    bl.append(std::string(((1 << m_src_ictx->order) * 2) + 1, '1'));
+    ASSERT_EQ(static_cast<ssize_t>(bl.length()),
+              m_src_ictx->io_work_queue->write(0 * bl.length(), bl.length(),
+                                               bufferlist{bl}, 0));
+    ASSERT_EQ(0, m_src_ictx->io_work_queue->flush());
+
+    ASSERT_EQ(0, snap_create(*m_src_ictx, "snap1"));
+
+    ASSERT_EQ(static_cast<ssize_t>(bl.length()),
+              m_src_ictx->io_work_queue->write(1 * bl.length(), bl.length(),
+                                               bufferlist{bl}, 0));
+    bufferlist bl1;
+    bl1.append(std::string(1000, 'X'));
+    ASSERT_EQ(static_cast<ssize_t>(bl1.length()),
+              m_src_ictx->io_work_queue->write(0 * bl.length(), bl1.length(),
+                                               bufferlist{bl1}, 0));
+    ASSERT_EQ(static_cast<ssize_t>(bl1.length()),
+              m_src_ictx->io_work_queue->discard(bl1.length() + 10,
+                                                 bl1.length(), false));
+
+    ASSERT_EQ(0, m_src_ictx->io_work_queue->flush());
+
+    ASSERT_EQ(0, snap_create(*m_src_ictx, "snap2"));
+    ASSERT_EQ(static_cast<ssize_t>(bl1.length()),
+              m_src_ictx->io_work_queue->write(1 * bl.length(), bl1.length(),
+                                               bufferlist{bl1}, 0));
+    ASSERT_EQ(static_cast<ssize_t>(bl1.length()),
+              m_src_ictx->io_work_queue->discard(2 * bl1.length() + 10,
+                                                 bl1.length(), false));
+  }
+
+  void test_snap_discard() {
+    bufferlist bl;
+    bl.append(std::string(100, '1'));
+    ASSERT_EQ(static_cast<ssize_t>(bl.length()),
+              m_src_ictx->io_work_queue->write(0, bl.length(), bufferlist{bl},
+                                               0));
+    ASSERT_EQ(0, m_src_ictx->io_work_queue->flush());
+
+    ASSERT_EQ(0, snap_create(*m_src_ictx, "snap"));
+
+    size_t len = (1 << m_src_ictx->order) * 2;
+    ASSERT_EQ(static_cast<ssize_t>(len),
+              m_src_ictx->io_work_queue->discard(0, len, false));
+  }
+
+  void test_stress() {
+    uint64_t initial_size, size;
+    {
+      RWLock::RLocker src_locker(m_src_ictx->snap_lock);
+      size = initial_size = m_src_ictx->get_image_size(CEPH_NOSNAP);
+    }
+
+    int nsnaps = 4;
+    const char *c = getenv("TEST_RBD_DEEPCOPY_STRESS_NSNAPS");
+    if (c != NULL) {
+      std::stringstream ss(c);
+      ASSERT_TRUE(ss >> nsnaps);
+    }
+
+    int nwrites = 4;
+    c = getenv("TEST_RBD_DEEPCOPY_STRESS_NWRITES");
+    if (c != NULL) {
+      std::stringstream ss(c);
+      ASSERT_TRUE(ss >> nwrites);
+    }
+
+    for (int i = 0; i < nsnaps; i++) {
+      for (int j = 0; j < nwrites; j++) {
+        size_t len = rand() % ((1 << m_src_ictx->order) * 2);
+        ASSERT_GT(size, len);
+        bufferlist bl;
+        bl.append(std::string(len, static_cast<char>('A' + i)));
+        uint64_t off = std::min(static_cast<uint64_t>(rand() % size),
+                                static_cast<uint64_t>(size - len));
+        std::cout << "write: " << static_cast<char>('A' + i) << " " << off
+                  << "~" << len << std::endl;
+        ASSERT_EQ(static_cast<ssize_t>(bl.length()),
+                  m_src_ictx->io_work_queue->write(off, bl.length(),
+                                                   bufferlist{bl}, 0));
+        len = rand() % ((1 << m_src_ictx->order) * 2);
+        ASSERT_GT(size, len);
+        off = std::min(static_cast<uint64_t>(rand() % size),
+                       static_cast<uint64_t>(size - len));
+        std::cout << "discard: " << off << "~" << len << std::endl;
+        ASSERT_EQ(static_cast<ssize_t>(len),
+                  m_src_ictx->io_work_queue->discard(off, len, false));
+      }
+
+      ASSERT_EQ(0, m_src_ictx->io_work_queue->flush());
+
+      std::string snap_name = "snap" + stringify(i);
+      std::cout << "snap: " << snap_name << std::endl;
+      ASSERT_EQ(0, snap_create(*m_src_ictx, snap_name.c_str()));
+
+      if (rand() % 2) {
+        librbd::NoOpProgressContext no_op;
+        uint64_t new_size =  initial_size + rand() % size;
+        std::cout << "resize: " << new_size << std::endl;
+        ASSERT_EQ(0, m_src_ictx->operations->resize(new_size, true, no_op));
+        {
+          RWLock::RLocker src_locker(m_src_ictx->snap_lock);
+          size = m_src_ictx->get_image_size(CEPH_NOSNAP);
+        }
+        ASSERT_EQ(new_size, size);
+      }
+    }
+  }
+
+  librbd::ImageCtx *m_src_ictx = nullptr;
+  librbd::ImageCtx *m_dst_ictx = nullptr;
+  librbd::ImageOptions m_opts;
+};
+
+TEST_F(TestDeepCopy, Empty)
+{
+}
+
+TEST_F(TestDeepCopy, NoSnaps)
+{
+  test_no_snaps();
+}
+
+TEST_F(TestDeepCopy, Snaps)
+{
+  test_snaps();
+}
+
+TEST_F(TestDeepCopy, SnapDiscard)
+{
+  test_snap_discard();
+}
+
+TEST_F(TestDeepCopy, Stress)
+{
+  test_stress();
+}
+
+TEST_F(TestDeepCopy, NoSnaps_LargerDstObjSize)
+{
+  uint64_t order = m_src_ictx->order + 1;
+  ASSERT_EQ(0, m_opts.set(RBD_IMAGE_OPTION_ORDER, order));
+
+  test_no_snaps();
+}
+
+TEST_F(TestDeepCopy, Snaps_LargerDstObjSize)
+{
+  uint64_t order = m_src_ictx->order + 1;
+  ASSERT_EQ(0, m_opts.set(RBD_IMAGE_OPTION_ORDER, order));
+
+  test_snaps();
+}
+
+TEST_F(TestDeepCopy, Stress_LargerDstObjSize)
+{
+  uint64_t order = m_src_ictx->order + 1 + rand() % 2;
+  ASSERT_EQ(0, m_opts.set(RBD_IMAGE_OPTION_ORDER, order));
+
+  test_stress();
+}
+
+TEST_F(TestDeepCopy, NoSnaps_SmallerDstObjSize)
+{
+  uint64_t order = m_src_ictx->order - 1;
+  ASSERT_EQ(0, m_opts.set(RBD_IMAGE_OPTION_ORDER, order));
+  uint64_t stripe_unit = m_src_ictx->stripe_unit >> 1;
+  ASSERT_EQ(0, m_opts.set(RBD_IMAGE_OPTION_STRIPE_UNIT, stripe_unit));
+
+  test_no_snaps();
+}
+
+TEST_F(TestDeepCopy, Snaps_SmallerDstObjSize)
+{
+  uint64_t order = m_src_ictx->order - 1;
+  ASSERT_EQ(0, m_opts.set(RBD_IMAGE_OPTION_ORDER, order));
+  uint64_t stripe_unit = m_src_ictx->stripe_unit >> 1;
+  ASSERT_EQ(0, m_opts.set(RBD_IMAGE_OPTION_STRIPE_UNIT, stripe_unit));
+
+  test_snaps();
+}
+
+TEST_F(TestDeepCopy, Stress_SmallerDstObjSize)
+{
+  uint64_t order = m_src_ictx->order - 1 - rand() % 2;
+  ASSERT_EQ(0, m_opts.set(RBD_IMAGE_OPTION_ORDER, order));
+  uint64_t stripe_unit = m_src_ictx->stripe_unit >> 2;
+  ASSERT_EQ(0, m_opts.set(RBD_IMAGE_OPTION_STRIPE_UNIT, stripe_unit));
+
+  test_stress();
+}
+
+TEST_F(TestDeepCopy, NoSnaps_StrippingLargerDstObjSize)
+{
+  REQUIRE_FEATURE(RBD_FEATURE_STRIPINGV2);
+
+  uint64_t order = m_src_ictx->order + 1;
+  uint64_t stripe_unit = 1 << (order - 2);
+  uint64_t stripe_count = 4;
+  ASSERT_EQ(0, m_opts.set(RBD_IMAGE_OPTION_ORDER, order));
+  ASSERT_EQ(0, m_opts.set(RBD_IMAGE_OPTION_STRIPE_UNIT, stripe_unit));
+  ASSERT_EQ(0, m_opts.set(RBD_IMAGE_OPTION_STRIPE_COUNT, stripe_count));
+
+  test_no_snaps();
+}
+
+TEST_F(TestDeepCopy, Snaps_StrippingLargerDstObjSize)
+{
+  REQUIRE_FEATURE(RBD_FEATURE_STRIPINGV2);
+
+  uint64_t order = m_src_ictx->order + 1;
+  uint64_t stripe_unit = 1 << (order - 2);
+  uint64_t stripe_count = 4;
+  ASSERT_EQ(0, m_opts.set(RBD_IMAGE_OPTION_ORDER, order));
+  ASSERT_EQ(0, m_opts.set(RBD_IMAGE_OPTION_STRIPE_UNIT, stripe_unit));
+  ASSERT_EQ(0, m_opts.set(RBD_IMAGE_OPTION_STRIPE_COUNT, stripe_count));
+
+  test_snaps();
+}
+
+TEST_F(TestDeepCopy, Stress_StrippingLargerDstObjSize)
+{
+  REQUIRE_FEATURE(RBD_FEATURE_STRIPINGV2);
+
+  uint64_t order = m_src_ictx->order + 1 + rand() % 2;
+  uint64_t stripe_unit = 1 << (order - rand() % 4);
+  uint64_t stripe_count = 2 + rand() % 14;
+  ASSERT_EQ(0, m_opts.set(RBD_IMAGE_OPTION_ORDER, order));
+  ASSERT_EQ(0, m_opts.set(RBD_IMAGE_OPTION_STRIPE_UNIT, stripe_unit));
+  ASSERT_EQ(0, m_opts.set(RBD_IMAGE_OPTION_STRIPE_COUNT, stripe_count));
+
+  test_stress();
+}
+
+TEST_F(TestDeepCopy, NoSnaps_StrippingSmallerDstObjSize)
+{
+  REQUIRE_FEATURE(RBD_FEATURE_STRIPINGV2);
+
+  uint64_t order = m_src_ictx->order - 1;
+  uint64_t stripe_unit = 1 << (order - 2);
+  uint64_t stripe_count = 4;
+  ASSERT_EQ(0, m_opts.set(RBD_IMAGE_OPTION_ORDER, order));
+  ASSERT_EQ(0, m_opts.set(RBD_IMAGE_OPTION_STRIPE_UNIT, stripe_unit));
+  ASSERT_EQ(0, m_opts.set(RBD_IMAGE_OPTION_STRIPE_COUNT, stripe_count));
+
+  test_no_snaps();
+}
+
+TEST_F(TestDeepCopy, Snaps_StrippingSmallerDstObjSize)
+{
+  REQUIRE_FEATURE(RBD_FEATURE_STRIPINGV2);
+
+  uint64_t order = m_src_ictx->order - 1;
+  uint64_t stripe_unit = 1 << (order - 2);
+  uint64_t stripe_count = 4;
+  ASSERT_EQ(0, m_opts.set(RBD_IMAGE_OPTION_ORDER, order));
+  ASSERT_EQ(0, m_opts.set(RBD_IMAGE_OPTION_STRIPE_UNIT, stripe_unit));
+  ASSERT_EQ(0, m_opts.set(RBD_IMAGE_OPTION_STRIPE_COUNT, stripe_count));
+
+  test_snaps();
+}
+
+TEST_F(TestDeepCopy, Stress_StrippingSmallerDstObjSize)
+{
+  REQUIRE_FEATURE(RBD_FEATURE_STRIPINGV2);
+
+  uint64_t order = m_src_ictx->order - 1 - rand() % 2;
+  uint64_t stripe_unit = 1 << (order - rand() % 4);
+  uint64_t stripe_count = 2 + rand() % 14;
+  ASSERT_EQ(0, m_opts.set(RBD_IMAGE_OPTION_ORDER, order));
+  ASSERT_EQ(0, m_opts.set(RBD_IMAGE_OPTION_STRIPE_UNIT, stripe_unit));
+  ASSERT_EQ(0, m_opts.set(RBD_IMAGE_OPTION_STRIPE_COUNT, stripe_count));
+
+  test_stress();
+}
index 281ccabfb78640e474f2941cf22939c191b352ca..654c8912d6f7ec7e6fd7c3482fba1d6b65d7d007 100644 (file)
 
 extern void register_test_librbd();
 #ifdef TEST_LIBRBD_INTERNALS
+extern void register_test_deep_copy();
 extern void register_test_groups();
 extern void register_test_image_watcher();
 extern void register_test_internal();
 extern void register_test_journal_entries();
 extern void register_test_journal_replay();
-extern void register_test_object_map();
-extern void register_test_operations();
 extern void register_test_mirroring();
 extern void register_test_mirroring_watcher();
+extern void register_test_object_map();
+extern void register_test_operations();
 #endif // TEST_LIBRBD_INTERNALS
 
 int main(int argc, char **argv)
 {
   register_test_librbd();
 #ifdef TEST_LIBRBD_INTERNALS
+  register_test_deep_copy();
   register_test_groups();
   register_test_image_watcher();
   register_test_internal();
   register_test_journal_entries();
   register_test_journal_replay();
-  register_test_object_map();
-  register_test_operations();
   register_test_mirroring();
   register_test_mirroring_watcher();
+  register_test_object_map();
+  register_test_operations();
 #endif // TEST_LIBRBD_INTERNALS
 
   ::testing::InitGoogleTest(&argc, argv);
diff --git a/src/test/librbd/test_mock_DeepCopyRequest.cc b/src/test/librbd/test_mock_DeepCopyRequest.cc
new file mode 100644 (file)
index 0000000..dbc63b5
--- /dev/null
@@ -0,0 +1,436 @@
+// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*-
+// vim: ts=8 sw=2 smarttab
+
+#include "test/librbd/test_mock_fixture.h"
+#include "include/rbd/librbd.hpp"
+#include "librbd/DeepCopyRequest.h"
+#include "librbd/ImageState.h"
+#include "librbd/Operations.h"
+#include "librbd/internal.h"
+#include "librbd/deep_copy/ImageCopyRequest.h"
+#include "librbd/deep_copy/MetadataCopyRequest.h"
+#include "librbd/deep_copy/SetHeadRequest.h"
+#include "librbd/deep_copy/SnapshotCopyRequest.h"
+#include "test/librbd/mock/MockImageCtx.h"
+#include "test/librbd/mock/MockObjectMap.h"
+#include "test/librbd/test_support.h"
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+
+namespace librbd {
+
+namespace {
+
+struct MockTestImageCtx : public librbd::MockImageCtx {
+  MockTestImageCtx(librbd::ImageCtx &image_ctx)
+    : librbd::MockImageCtx(image_ctx) {
+  }
+};
+
+} // anonymous namespace
+
+namespace deep_copy {
+
+template <>
+class ImageCopyRequest<librbd::MockTestImageCtx> {
+public:
+  static ImageCopyRequest* s_instance;
+  Context *on_finish;
+
+  static ImageCopyRequest* create(
+      librbd::MockTestImageCtx *src_image_ctx,
+      librbd::MockTestImageCtx *dst_image_ctx, librados::snap_t snap_id_start,
+      librados::snap_t snap_id_end, const ObjectNumber &object_number,
+      const SnapSeqs &snap_seqs, ProgressContext *prog_ctx,
+      Context *on_finish) {
+    assert(s_instance != nullptr);
+    s_instance->on_finish = on_finish;
+    return s_instance;
+  }
+
+  ImageCopyRequest() {
+    s_instance = this;
+  }
+
+  void put() {
+  }
+
+  void get() {
+  }
+
+  MOCK_METHOD0(cancel, void());
+  MOCK_METHOD0(send, void());
+};
+
+template <>
+class MetadataCopyRequest<librbd::MockTestImageCtx> {
+public:
+  static MetadataCopyRequest* s_instance;
+  Context *on_finish;
+
+  static MetadataCopyRequest* create(librbd::MockTestImageCtx *src_image_ctx,
+                                     librbd::MockTestImageCtx *dst_image_ctx,
+                                     Context *on_finish) {
+    assert(s_instance != nullptr);
+    s_instance->on_finish = on_finish;
+    return s_instance;
+  }
+
+  MetadataCopyRequest() {
+    s_instance = this;
+  }
+
+  MOCK_METHOD0(send, void());
+};
+
+template <>
+class SetHeadRequest<librbd::MockTestImageCtx> {
+public:
+  static SetHeadRequest* s_instance;
+  Context *on_finish;
+
+  static SetHeadRequest* create(librbd::MockTestImageCtx *image_ctx,
+                                uint64_t size,
+                                const librbd::ParentSpec &parent_spec,
+                                uint64_t parent_overlap, Context *on_finish) {
+    assert(s_instance != nullptr);
+    s_instance->on_finish = on_finish;
+    return s_instance;
+  }
+
+  SetHeadRequest() {
+    s_instance = this;
+  }
+
+  MOCK_METHOD0(send, void());
+};
+
+template <>
+class SnapshotCopyRequest<librbd::MockTestImageCtx> {
+public:
+  static SnapshotCopyRequest* s_instance;
+  Context *on_finish;
+
+  static SnapshotCopyRequest* create(librbd::MockTestImageCtx *src_image_ctx,
+                                     librbd::MockTestImageCtx *dst_image_ctx,
+                                     ContextWQ *work_queue, SnapSeqs *snap_seqs,
+                                     Context *on_finish) {
+    assert(s_instance != nullptr);
+    s_instance->on_finish = on_finish;
+    return s_instance;
+  }
+
+  SnapshotCopyRequest() {
+    s_instance = this;
+  }
+
+  void put() {
+  }
+
+  void get() {
+  }
+
+  MOCK_METHOD0(cancel, void());
+  MOCK_METHOD0(send, void());
+};
+
+ImageCopyRequest<librbd::MockTestImageCtx>* ImageCopyRequest<librbd::MockTestImageCtx>::s_instance = nullptr;
+MetadataCopyRequest<librbd::MockTestImageCtx>* MetadataCopyRequest<librbd::MockTestImageCtx>::s_instance = nullptr;
+SetHeadRequest<librbd::MockTestImageCtx>* SetHeadRequest<librbd::MockTestImageCtx>::s_instance = nullptr;
+SnapshotCopyRequest<librbd::MockTestImageCtx>* SnapshotCopyRequest<librbd::MockTestImageCtx>::s_instance = nullptr;
+
+} // namespace deep_copy
+} // namespace librbd
+
+// template definitions
+#include "librbd/DeepCopyRequest.cc"
+
+using ::testing::_;
+using ::testing::DoAll;
+using ::testing::InSequence;
+using ::testing::Invoke;
+using ::testing::Return;
+using ::testing::ReturnNew;
+using ::testing::SetArgPointee;
+using ::testing::WithArg;
+
+class TestMockDeepCopyRequest : public TestMockFixture {
+public:
+  typedef librbd::DeepCopyRequest<librbd::MockTestImageCtx> MockDeepCopyRequest;
+  typedef librbd::deep_copy::ImageCopyRequest<librbd::MockTestImageCtx> MockImageCopyRequest;
+  typedef librbd::deep_copy::MetadataCopyRequest<librbd::MockTestImageCtx> MockMetadataCopyRequest;
+  typedef librbd::deep_copy::SetHeadRequest<librbd::MockTestImageCtx> MockSetHeadRequest;
+  typedef librbd::deep_copy::SnapshotCopyRequest<librbd::MockTestImageCtx> MockSnapshotCopyRequest;
+
+  librbd::ImageCtx *m_src_image_ctx;
+  librbd::ImageCtx *m_dst_image_ctx;
+  ThreadPool *m_thread_pool;
+  ContextWQ *m_work_queue;
+
+  void SetUp() override {
+    TestMockFixture::SetUp();
+
+    librbd::RBD rbd;
+    ASSERT_EQ(0, open_image(m_image_name, &m_src_image_ctx));
+
+    ASSERT_EQ(0, open_image(m_image_name, &m_dst_image_ctx));
+
+    librbd::ImageCtx::get_thread_pool_instance(m_src_image_ctx->cct,
+                                               &m_thread_pool, &m_work_queue);
+  }
+
+  void TearDown() override {
+    TestMockFixture::TearDown();
+  }
+
+  void expect_test_features(librbd::MockImageCtx &mock_image_ctx) {
+    EXPECT_CALL(mock_image_ctx, test_features(_, _))
+      .WillRepeatedly(WithArg<0>(Invoke([&mock_image_ctx](uint64_t features) {
+              return (mock_image_ctx.features & features) != 0;
+            })));
+  }
+
+  void expect_start_op(librbd::MockExclusiveLock &mock_exclusive_lock) {
+    EXPECT_CALL(mock_exclusive_lock, start_op()).WillOnce(
+      ReturnNew<FunctionContext>([](int) {}));
+  }
+
+  void expect_rollback_object_map(librbd::MockObjectMap &mock_object_map, int r) {
+    EXPECT_CALL(mock_object_map, rollback(_, _))
+      .WillOnce(WithArg<1>(Invoke([this, r](Context *ctx) {
+              m_work_queue->queue(ctx, r);
+            })));
+  }
+
+  void expect_create_object_map(librbd::MockTestImageCtx &mock_image_ctx,
+                                librbd::MockObjectMap *mock_object_map) {
+    EXPECT_CALL(mock_image_ctx, create_object_map(CEPH_NOSNAP))
+      .WillOnce(Return(mock_object_map));
+  }
+
+  void expect_open_object_map(librbd::MockTestImageCtx &mock_image_ctx,
+                              librbd::MockObjectMap &mock_object_map) {
+    EXPECT_CALL(mock_object_map, open(_))
+      .WillOnce(Invoke([this](Context *ctx) {
+          m_work_queue->queue(ctx, 0);
+        }));
+  }
+
+  void expect_copy_snapshots(
+      MockSnapshotCopyRequest &mock_snapshot_copy_request, int r) {
+    EXPECT_CALL(mock_snapshot_copy_request, send())
+      .WillOnce(Invoke([this, &mock_snapshot_copy_request, r]() {
+            m_work_queue->queue(mock_snapshot_copy_request.on_finish, r);
+          }));
+  }
+
+  void expect_set_head(MockSetHeadRequest &mock_set_head_request, int r) {
+    EXPECT_CALL(mock_set_head_request, send())
+      .WillOnce(Invoke([this, &mock_set_head_request, r]() {
+            mock_set_head_request.on_finish->complete(r);
+          }));
+  }
+
+  void expect_copy_image(MockImageCopyRequest &mock_image_copy_request, int r) {
+    EXPECT_CALL(mock_image_copy_request, send())
+      .WillOnce(Invoke([this, &mock_image_copy_request, r]() {
+            m_work_queue->queue(mock_image_copy_request.on_finish, r);
+          }));
+  }
+
+  void expect_copy_object_map(librbd::MockExclusiveLock &mock_exclusive_lock,
+                              librbd::MockObjectMap *mock_object_map) {
+    expect_start_op(mock_exclusive_lock);
+    expect_rollback_object_map(*mock_object_map, 0);
+  }
+
+  void expect_refresh_object_map(librbd::MockTestImageCtx &mock_image_ctx,
+                                 librbd::MockObjectMap *mock_object_map) {
+    expect_start_op(*mock_image_ctx.exclusive_lock);
+    expect_create_object_map(mock_image_ctx, mock_object_map);
+    expect_open_object_map(mock_image_ctx, *mock_object_map);
+  }
+
+  void expect_copy_metadata(MockMetadataCopyRequest &mock_metadata_copy_request,
+                            int r) {
+    EXPECT_CALL(mock_metadata_copy_request, send())
+      .WillOnce(Invoke([this, &mock_metadata_copy_request, r]() {
+            m_work_queue->queue(mock_metadata_copy_request.on_finish, r);
+          }));
+  }
+};
+
+TEST_F(TestMockDeepCopyRequest, SimpleCopy) {
+  librbd::MockTestImageCtx mock_src_image_ctx(*m_src_image_ctx);
+  librbd::MockTestImageCtx mock_dst_image_ctx(*m_dst_image_ctx);
+  MockImageCopyRequest mock_image_copy_request;
+  MockMetadataCopyRequest mock_metadata_copy_request;
+  MockSetHeadRequest mock_set_head_request;
+  MockSnapshotCopyRequest mock_snapshot_copy_request;
+
+  librbd::MockExclusiveLock mock_exclusive_lock;
+  mock_dst_image_ctx.exclusive_lock = &mock_exclusive_lock;
+
+  librbd::MockObjectMap *mock_object_map =
+    is_feature_enabled(RBD_FEATURE_OBJECT_MAP) ?
+    new librbd::MockObjectMap() : nullptr;
+  mock_dst_image_ctx.object_map = mock_object_map;
+
+  expect_test_features(mock_dst_image_ctx);
+
+  InSequence seq;
+  expect_copy_snapshots(mock_snapshot_copy_request, 0);
+  expect_set_head(mock_set_head_request, 0);
+  expect_copy_image(mock_image_copy_request, 0);
+  if (mock_object_map != nullptr) {
+    expect_refresh_object_map(mock_dst_image_ctx, mock_object_map);
+  }
+  expect_copy_metadata(mock_metadata_copy_request, 0);
+
+  C_SaferCond ctx;
+  librbd::SnapSeqs snap_seqs;
+  librbd::NoOpProgressContext no_op;
+  auto request = librbd::DeepCopyRequest<librbd::MockTestImageCtx>::create(
+      &mock_src_image_ctx, &mock_dst_image_ctx, 0, CEPH_NOSNAP, boost::none,
+      m_work_queue, &snap_seqs, &no_op, &ctx);
+  request->send();
+  ASSERT_EQ(0, ctx.wait());
+}
+
+TEST_F(TestMockDeepCopyRequest, ErrorOnCopySnapshots) {
+  librbd::MockTestImageCtx mock_src_image_ctx(*m_src_image_ctx);
+  librbd::MockTestImageCtx mock_dst_image_ctx(*m_dst_image_ctx);
+  MockSnapshotCopyRequest mock_snapshot_copy_request;
+
+  InSequence seq;
+  expect_copy_snapshots(mock_snapshot_copy_request, -EINVAL);
+
+  C_SaferCond ctx;
+  librbd::SnapSeqs snap_seqs;
+  librbd::NoOpProgressContext no_op;
+  auto request = librbd::DeepCopyRequest<librbd::MockTestImageCtx>::create(
+      &mock_src_image_ctx, &mock_dst_image_ctx, 0, CEPH_NOSNAP, boost::none,
+      m_work_queue, &snap_seqs, &no_op, &ctx);
+  request->send();
+  ASSERT_EQ(-EINVAL, ctx.wait());
+}
+
+TEST_F(TestMockDeepCopyRequest, ErrorOnSetHead) {
+  librbd::MockTestImageCtx mock_src_image_ctx(*m_src_image_ctx);
+  librbd::MockTestImageCtx mock_dst_image_ctx(*m_dst_image_ctx);
+  MockSetHeadRequest mock_set_head_request;
+  MockSnapshotCopyRequest mock_snapshot_copy_request;
+
+  InSequence seq;
+  expect_copy_snapshots(mock_snapshot_copy_request, 0);
+  expect_set_head(mock_set_head_request, -EINVAL);
+
+  C_SaferCond ctx;
+  librbd::SnapSeqs snap_seqs;
+  librbd::NoOpProgressContext no_op;
+  auto request = librbd::DeepCopyRequest<librbd::MockTestImageCtx>::create(
+      &mock_src_image_ctx, &mock_dst_image_ctx, 0, CEPH_NOSNAP, boost::none,
+      m_work_queue, &snap_seqs, &no_op, &ctx);
+  request->send();
+  ASSERT_EQ(-EINVAL, ctx.wait());
+}
+
+TEST_F(TestMockDeepCopyRequest, ErrorOnCopyImage) {
+  librbd::MockTestImageCtx mock_src_image_ctx(*m_src_image_ctx);
+  librbd::MockTestImageCtx mock_dst_image_ctx(*m_dst_image_ctx);
+  MockImageCopyRequest mock_image_copy_request;
+  MockSetHeadRequest mock_set_head_request;
+  MockSnapshotCopyRequest mock_snapshot_copy_request;
+
+  InSequence seq;
+  expect_copy_snapshots(mock_snapshot_copy_request, 0);
+  expect_set_head(mock_set_head_request, 0);
+  expect_copy_image(mock_image_copy_request, -EINVAL);
+
+  C_SaferCond ctx;
+  librbd::SnapSeqs snap_seqs;
+  librbd::NoOpProgressContext no_op;
+  auto request = librbd::DeepCopyRequest<librbd::MockTestImageCtx>::create(
+      &mock_src_image_ctx, &mock_dst_image_ctx, 0, CEPH_NOSNAP, boost::none,
+      m_work_queue, &snap_seqs, &no_op, &ctx);
+  request->send();
+  ASSERT_EQ(-EINVAL, ctx.wait());
+}
+
+TEST_F(TestMockDeepCopyRequest, ErrorOnCopyMetadata) {
+  librbd::MockTestImageCtx mock_src_image_ctx(*m_src_image_ctx);
+  librbd::MockTestImageCtx mock_dst_image_ctx(*m_dst_image_ctx);
+  MockImageCopyRequest mock_image_copy_request;
+  MockMetadataCopyRequest mock_metadata_copy_request;
+  MockSetHeadRequest mock_set_head_request;
+  MockSnapshotCopyRequest mock_snapshot_copy_request;
+
+  librbd::MockExclusiveLock mock_exclusive_lock;
+  mock_dst_image_ctx.exclusive_lock = &mock_exclusive_lock;
+
+  librbd::MockObjectMap *mock_object_map =
+    is_feature_enabled(RBD_FEATURE_OBJECT_MAP) ?
+    new librbd::MockObjectMap() : nullptr;
+  mock_dst_image_ctx.object_map = mock_object_map;
+
+  expect_test_features(mock_dst_image_ctx);
+
+  InSequence seq;
+  expect_copy_snapshots(mock_snapshot_copy_request, 0);
+  expect_set_head(mock_set_head_request, 0);
+  expect_copy_image(mock_image_copy_request, 0);
+  if (mock_object_map != nullptr) {
+    expect_refresh_object_map(mock_dst_image_ctx, mock_object_map);
+  }
+  expect_copy_metadata(mock_metadata_copy_request, -EINVAL);
+
+  C_SaferCond ctx;
+  librbd::SnapSeqs snap_seqs;
+  librbd::NoOpProgressContext no_op;
+  auto request = librbd::DeepCopyRequest<librbd::MockTestImageCtx>::create(
+      &mock_src_image_ctx, &mock_dst_image_ctx, 0, CEPH_NOSNAP, boost::none,
+      m_work_queue, &snap_seqs, &no_op, &ctx);
+  request->send();
+  ASSERT_EQ(-EINVAL, ctx.wait());
+}
+
+TEST_F(TestMockDeepCopyRequest, Snap) {
+  EXPECT_EQ(0, snap_create(*m_src_image_ctx, "copy"));
+  EXPECT_EQ(0, librbd::snap_set(m_src_image_ctx,
+                                cls::rbd::UserSnapshotNamespace(), "copy"));
+
+  librbd::MockTestImageCtx mock_src_image_ctx(*m_src_image_ctx);
+  librbd::MockTestImageCtx mock_dst_image_ctx(*m_dst_image_ctx);
+  MockImageCopyRequest mock_image_copy_request;
+  MockMetadataCopyRequest mock_metadata_copy_request;
+  MockSnapshotCopyRequest mock_snapshot_copy_request;
+
+  librbd::MockExclusiveLock mock_exclusive_lock;
+  mock_dst_image_ctx.exclusive_lock = &mock_exclusive_lock;
+
+  librbd::MockObjectMap *mock_object_map =
+    is_feature_enabled(RBD_FEATURE_OBJECT_MAP) ?
+    new librbd::MockObjectMap() : nullptr;
+  mock_dst_image_ctx.object_map = mock_object_map;
+
+  expect_test_features(mock_dst_image_ctx);
+
+  InSequence seq;
+  expect_copy_snapshots(mock_snapshot_copy_request, 0);
+  expect_copy_image(mock_image_copy_request, 0);
+  if (mock_object_map != nullptr) {
+    expect_copy_object_map(mock_exclusive_lock, mock_object_map);
+    expect_refresh_object_map(mock_dst_image_ctx, mock_object_map);
+  }
+  expect_copy_metadata(mock_metadata_copy_request, 0);
+
+  C_SaferCond ctx;
+  librbd::SnapSeqs snap_seqs = {{m_src_image_ctx->snap_id, 123}};
+  librbd::NoOpProgressContext no_op;
+  auto request = librbd::DeepCopyRequest<librbd::MockTestImageCtx>::create(
+      &mock_src_image_ctx, &mock_dst_image_ctx, 0, m_src_image_ctx->snap_id,
+      boost::none, m_work_queue, &snap_seqs, &no_op, &ctx);
+  request->send();
+  ASSERT_EQ(0, ctx.wait());
+}
index d7bbce2d5a5b55c0779bc7bbc261be86a530e57d..84511411f74b9d98db80c8b3c1c5b587e51336b4 100644 (file)
@@ -133,7 +133,7 @@ TEST_F(TestMockImageReplayerEventPreprocessor, PreprocessSnapMapPrune) {
   event_preprocessor.preprocess(&event_entry, &ctx);
   ASSERT_EQ(0, ctx.wait());
 
-  librbd::journal::MirrorPeerClientMeta::SnapSeqs expected_snap_seqs = {{5, 6}};
+  librbd::SnapSeqs expected_snap_seqs = {{5, 6}};
   ASSERT_EQ(expected_snap_seqs, m_client_meta.snap_seqs);
 }
 
@@ -159,7 +159,7 @@ TEST_F(TestMockImageReplayerEventPreprocessor, PreprocessSnapRename) {
   event_preprocessor.preprocess(&event_entry, &ctx);
   ASSERT_EQ(0, ctx.wait());
 
-  librbd::journal::MirrorPeerClientMeta::SnapSeqs expected_snap_seqs = {{5, 6}};
+  librbd::SnapSeqs expected_snap_seqs = {{5, 6}};
   ASSERT_EQ(expected_snap_seqs, m_client_meta.snap_seqs);
 
   librbd::journal::SnapRenameEvent *event =
@@ -211,7 +211,7 @@ TEST_F(TestMockImageReplayerEventPreprocessor, PreprocessSnapRenameKnown) {
   event_preprocessor.preprocess(&event_entry, &ctx);
   ASSERT_EQ(0, ctx.wait());
 
-  librbd::journal::MirrorPeerClientMeta::SnapSeqs expected_snap_seqs = {{5, 6}};
+  librbd::SnapSeqs expected_snap_seqs = {{5, 6}};
   ASSERT_EQ(expected_snap_seqs, m_client_meta.snap_seqs);
 
   librbd::journal::SnapRenameEvent *event =