]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
librbd/migration/NBDStream: abstract out libnbd and add unit tests 63406/head
authorIlya Dryomov <idryomov@gmail.com>
Thu, 5 Sep 2024 13:43:07 +0000 (15:43 +0200)
committerIlya Dryomov <idryomov@gmail.com>
Wed, 21 May 2025 15:27:17 +0000 (17:27 +0200)
Signed-off-by: Ilya Dryomov <idryomov@gmail.com>
(cherry picked from commit 6fd11c0276dc3f0b2349e42d877d7da692161bc9)

src/librbd/migration/NBDStream.cc
src/librbd/migration/NBDStream.h
src/test/librbd/migration/test_mock_NBDStream.cc

index 57b9b72ba38886293710aa8f8e4aa3f35301f8fa..bf8c0c8519eab42126815f4d553b3011dce569b8 100644 (file)
@@ -47,6 +47,61 @@ int extent_cb(void* data, const char* metacontext, uint64_t offset,
 
 } // anonymous namespace
 
+template <typename>
+class NBDClient {
+public:
+  static NBDClient* create() {
+    return new NBDClient();
+  }
+
+  const char* get_error() {
+    return nbd_get_error();
+  }
+
+  int get_errno() {
+    return nbd_get_errno();
+  }
+
+  int init() {
+    m_handle.reset(nbd_create());
+    return m_handle != nullptr ? 0 : -1;
+  }
+
+  int add_meta_context(const char* name) {
+    return nbd_add_meta_context(m_handle.get(), name);
+  }
+
+  int connect_uri(const char* uri) {
+    return nbd_connect_uri(m_handle.get(), uri);
+  }
+
+  int64_t get_size() {
+    return nbd_get_size(m_handle.get());
+  }
+
+  int pread(void* buf, size_t count, uint64_t offset, uint32_t flags) {
+    return nbd_pread(m_handle.get(), buf, count, offset, flags);
+  }
+
+  int block_status(uint64_t count, uint64_t offset,
+                   nbd_extent_callback extent_callback, uint32_t flags) {
+    return nbd_block_status(m_handle.get(), count, offset, extent_callback,
+                            flags);
+  }
+
+  int shutdown(uint32_t flags) {
+    return nbd_shutdown(m_handle.get(), flags);
+  }
+
+private:
+  struct nbd_handle_deleter {
+    void operator()(nbd_handle* h) {
+      nbd_close(h);
+    }
+  };
+  std::unique_ptr<nbd_handle, nbd_handle_deleter> m_handle;
+};
+
 #define dout_subsys ceph_subsys_rbd
 #undef dout_prefix
 #define dout_prefix *_dout << "librbd::migration::NBDStream::ReadRequest: " \
@@ -84,14 +139,14 @@ struct NBDStream<I>::ReadRequest {
     ldout(cct, 20) << "byte_offset=" << byte_offset << " byte_length="
                    << byte_length << dendl;
 
+    auto& nbd_client = nbd_stream->m_nbd_client;
     auto ptr = buffer::ptr_node::create(buffer::create_small_page_aligned(
       byte_length));
-    int rc = nbd_pread(nbd_stream->m_nbd, ptr->c_str(), byte_length,
-                       byte_offset, 0);
+    int rc = nbd_client->pread(ptr->c_str(), byte_length, byte_offset, 0);
     if (rc == -1) {
-      rc = nbd_get_errno();
+      rc = nbd_client->get_errno();
       lderr(cct) << "pread " << byte_offset << "~" << byte_length << ": "
-                 << nbd_get_error() << " (errno = " << rc << ")"
+                 << nbd_client->get_error() << " (errno = " << rc << ")"
                  << dendl;
       finish(from_nbd_errno(rc));
       return;
@@ -160,12 +215,13 @@ struct NBDStream<I>::ListSparseExtentsRequest {
     tmp_sparse_extents.insert(byte_offset, byte_length,
                               {io::SPARSE_EXTENT_STATE_DATA, byte_length});
 
-    int rc = nbd_block_status(nbd_stream->m_nbd, byte_length, byte_offset,
-                              {extent_cb, &tmp_sparse_extents}, 0);
+    auto& nbd_client = nbd_stream->m_nbd_client;
+    int rc = nbd_client->block_status(byte_length, byte_offset,
+                                      {extent_cb, &tmp_sparse_extents}, 0);
     if (rc == -1) {
-      rc = nbd_get_errno();
+      rc = nbd_client->get_errno();
       lderr(cct) << "block_status " << byte_offset << "~" << byte_length << ": "
-                 << nbd_get_error() << " (errno = " << rc << ")"
+                 << nbd_client->get_error() << " (errno = " << rc << ")"
                  << dendl;
       // don't propagate errors -- we are set up to list any missing
       // parts of the range as DATA if nbd_block_status() returns less
@@ -201,9 +257,6 @@ NBDStream<I>::NBDStream(I* image_ctx, const json_spirit::mObject& json_object)
 
 template <typename I>
 NBDStream<I>::~NBDStream() {
-  if (m_nbd != nullptr) {
-    nbd_close(m_nbd);
-  }
 }
 
 template <typename I>
@@ -228,28 +281,29 @@ void NBDStream<I>::open(Context* on_finish) {
 
   ldout(m_cct, 10) << "uri=" << uri << dendl;
 
-  m_nbd = nbd_create();
-  if (m_nbd == nullptr) {
-    rc = nbd_get_errno();
-    lderr(m_cct) << "create: " << nbd_get_error()
+  m_nbd_client.reset(NBDClient<I>::create());
+  rc = m_nbd_client->init();
+  if (rc == -1) {
+    rc = m_nbd_client->get_errno();
+    lderr(m_cct) << "init: " << m_nbd_client->get_error()
                  << " (errno = " << rc << ")" << dendl;
     on_finish->complete(from_nbd_errno(rc));
     return;
   }
 
-  rc = nbd_add_meta_context(m_nbd, LIBNBD_CONTEXT_BASE_ALLOCATION);
+  rc = m_nbd_client->add_meta_context(LIBNBD_CONTEXT_BASE_ALLOCATION);
   if (rc == -1) {
-    rc = nbd_get_errno();
-    lderr(m_cct) << "add_meta_context: " << nbd_get_error()
+    rc = m_nbd_client->get_errno();
+    lderr(m_cct) << "add_meta_context: " << m_nbd_client->get_error()
                  << " (errno = " << rc << ")" << dendl;
     on_finish->complete(from_nbd_errno(rc));
     return;
   }
 
-  rc = nbd_connect_uri(m_nbd, uri.c_str());
+  rc = m_nbd_client->connect_uri(uri.c_str());
   if (rc == -1) {
-    rc = nbd_get_errno();
-    lderr(m_cct) << "connect_uri: " << nbd_get_error()
+    rc = m_nbd_client->get_errno();
+    lderr(m_cct) << "connect_uri: " << m_nbd_client->get_error()
                  << " (errno = " << rc << ")" << dendl;
     on_finish->complete(from_nbd_errno(rc));
     return;
@@ -262,15 +316,13 @@ template <typename I>
 void NBDStream<I>::close(Context* on_finish) {
   ldout(m_cct, 20) << dendl;
 
-  if (m_nbd != nullptr) {
+  if (m_nbd_client != nullptr) {
     // send a graceful shutdown to the server
     // ignore errors -- we are read-only, also from the client's
     // POV there is no disadvantage to abruptly closing the socket
     // in nbd_close()
-    nbd_shutdown(m_nbd, 0);
-
-    nbd_close(m_nbd);
-    m_nbd = nullptr;
+    m_nbd_client->shutdown(0);
+    m_nbd_client.reset();
   }
 
   on_finish->complete(0);
@@ -280,10 +332,10 @@ template <typename I>
 void NBDStream<I>::get_size(uint64_t* size, Context* on_finish) {
   ldout(m_cct, 20) << dendl;
 
-  int64_t rc = nbd_get_size(m_nbd);
+  int64_t rc = m_nbd_client->get_size();
   if (rc == -1) {
-    rc = nbd_get_errno();
-    lderr(m_cct) << "get_size: " << nbd_get_error()
+    rc = m_nbd_client->get_errno();
+    lderr(m_cct) << "get_size: " << m_nbd_client->get_error()
                  << " (errno = " << rc << ")" << dendl;
     on_finish->complete(from_nbd_errno(rc));
     return;
index d0135f9a55ac40d3193a33112ce66074f1806526..aeced5d4f3dfcc599fffd4e3a2ef31d3c1acf2c8 100644 (file)
@@ -12,8 +12,6 @@
 
 struct Context;
 
-struct nbd_handle;
-
 namespace librbd {
 
 struct AsioEngine;
@@ -21,6 +19,8 @@ struct ImageCtx;
 
 namespace migration {
 
+template <typename> class NBDClient;
+
 template <typename ImageCtxT>
 class NBDStream : public StreamInterface {
 public:
@@ -53,7 +53,7 @@ private:
   json_spirit::mObject m_json_object;
   boost::asio::strand<boost::asio::io_context::executor_type> m_strand;
 
-  struct nbd_handle* m_nbd = nullptr;
+  std::unique_ptr<NBDClient<ImageCtxT>> m_nbd_client;
 
   struct ReadRequest;
   struct ListSparseExtentsRequest;
index 067749d76e352e4f64c9c7f9fd53a41800ab37fb..5977057b11f97bdadbf420753be4beb255d07402 100644 (file)
@@ -25,11 +25,42 @@ struct MockTestImageCtx : public MockImageCtx {
 namespace librbd {
 namespace migration {
 
+template <>
+struct NBDClient<MockTestImageCtx> {
+  static NBDClient* s_instance;
+  static NBDClient* create() {
+    ceph_assert(s_instance != nullptr);
+    return s_instance;
+  }
+
+  NBDClient() {
+    s_instance = this;
+  }
+
+  MOCK_METHOD0(get_error, const char*());
+  MOCK_METHOD0(get_errno, int());
+  MOCK_METHOD0(init, int());
+  MOCK_METHOD1(add_meta_context, int(const char*));
+  MOCK_METHOD1(connect_uri, int(const char*));
+  MOCK_METHOD0(get_size, int64_t());
+  MOCK_METHOD4(pread, int(void*, size_t, uint64_t, uint32_t));
+  MOCK_METHOD4(block_status, int(uint64_t, uint64_t, nbd_extent_callback,
+                                 uint32_t));
+  MOCK_METHOD1(shutdown, int(uint32_t));
+};
+
+NBDClient<MockTestImageCtx>* NBDClient<MockTestImageCtx>::s_instance = nullptr;
+
+using ::testing::_;
 using ::testing::Invoke;
+using ::testing::InSequence;
+using ::testing::Return;
+using ::testing::WithArg;
 
 class TestMockMigrationNBDStream : public TestMockFixture {
 public:
   typedef NBDStream<MockTestImageCtx> MockNBDStream;
+  typedef NBDClient<MockTestImageCtx> MockNBDClient;
 
   void SetUp() override {
     TestMockFixture::SetUp();
@@ -38,6 +69,65 @@ public:
     m_json_object["uri"] = "nbd://foo.example";
   }
 
+  void expect_get_errno(MockNBDClient& mock_nbd_client, int err) {
+    EXPECT_CALL(mock_nbd_client, get_errno()).WillOnce(Return(err));
+    EXPECT_CALL(mock_nbd_client, get_error()).WillOnce(Return("error message"));
+  }
+
+  void expect_init(MockNBDClient& mock_nbd_client, int rc) {
+    EXPECT_CALL(mock_nbd_client, init()).WillOnce(Return(rc));
+  }
+
+  void expect_add_meta_context(MockNBDClient& mock_nbd_client, int rc) {
+    EXPECT_CALL(mock_nbd_client, add_meta_context(_)).WillOnce(Return(rc));
+  }
+
+  void expect_connect_uri(MockNBDClient& mock_nbd_client, int rc) {
+    EXPECT_CALL(mock_nbd_client, connect_uri(_)).WillOnce(Return(rc));
+  }
+
+  void expect_get_size(MockNBDClient& mock_nbd_client, int64_t rc) {
+    EXPECT_CALL(mock_nbd_client, get_size()).WillOnce(Return(rc));
+  }
+
+  void expect_pread(MockNBDClient& mock_nbd_client, uint64_t byte_offset,
+                    uint64_t byte_length, const void* buf, int rc) {
+    EXPECT_CALL(mock_nbd_client, pread(_, byte_length, byte_offset, _))
+      .WillOnce(WithArg<0>(Invoke(
+        [byte_length, buf, rc](void* out_buf) {
+          memcpy(out_buf, buf, byte_length);
+          return rc;
+        })));
+  }
+
+  struct block_status_cb_args {
+    const char* metacontext;
+    uint64_t entries_offset;
+    std::vector<uint32_t> entries;
+  };
+
+  // cbs is taken by non-const reference only because of
+  // nbd_extent_callback::callback() signature
+  void expect_block_status(MockNBDClient& mock_nbd_client,
+                           uint64_t byte_offset, uint64_t byte_length,
+                           std::vector<block_status_cb_args>& cbs, int rc) {
+    EXPECT_CALL(mock_nbd_client, block_status(byte_length, byte_offset, _, _))
+      .WillOnce(WithArg<2>(Invoke(
+        [&cbs, rc](nbd_extent_callback extent_callback) {
+          int err = 0;
+          for (auto& cb : cbs) {
+            extent_callback.callback(extent_callback.user_data, cb.metacontext,
+                                     cb.entries_offset, cb.entries.data(),
+                                     cb.entries.size(), &err);
+          }
+          return rc;
+        })));
+  }
+
+  void expect_shutdown(MockNBDClient& mock_nbd_client, int rc) {
+    EXPECT_CALL(mock_nbd_client, shutdown(_)).WillOnce(Return(rc));
+  }
+
   librbd::ImageCtx *m_image_ctx;
   json_spirit::mObject m_json_object;
 };
@@ -72,5 +162,569 @@ TEST_F(TestMockMigrationNBDStream, OpenMissingURI) {
   ASSERT_EQ(0, ctx2.wait());
 }
 
+TEST_F(TestMockMigrationNBDStream, OpenInitError) {
+  MockTestImageCtx mock_image_ctx(*m_image_ctx);
+
+  InSequence seq;
+
+  auto mock_nbd_client = new MockNBDClient();
+  expect_init(*mock_nbd_client, -1);
+  expect_get_errno(*mock_nbd_client, ENOMEM);
+  expect_shutdown(*mock_nbd_client, 0);
+
+  MockNBDStream mock_nbd_stream(&mock_image_ctx, m_json_object);
+
+  C_SaferCond ctx1;
+  mock_nbd_stream.open(&ctx1);
+  ASSERT_EQ(-ENOMEM, ctx1.wait());
+
+  C_SaferCond ctx2;
+  mock_nbd_stream.close(&ctx2);
+  ASSERT_EQ(0, ctx2.wait());
+}
+
+TEST_F(TestMockMigrationNBDStream, OpenAddMetaContextError) {
+  MockTestImageCtx mock_image_ctx(*m_image_ctx);
+
+  InSequence seq;
+
+  auto mock_nbd_client = new MockNBDClient();
+  expect_init(*mock_nbd_client, 0);
+  expect_add_meta_context(*mock_nbd_client, -1);
+  expect_get_errno(*mock_nbd_client, EINVAL);
+  expect_shutdown(*mock_nbd_client, 0);
+
+  MockNBDStream mock_nbd_stream(&mock_image_ctx, m_json_object);
+
+  C_SaferCond ctx1;
+  mock_nbd_stream.open(&ctx1);
+  ASSERT_EQ(-EINVAL, ctx1.wait());
+
+  C_SaferCond ctx2;
+  mock_nbd_stream.close(&ctx2);
+  ASSERT_EQ(0, ctx2.wait());
+}
+
+TEST_F(TestMockMigrationNBDStream, OpenConnectURIError) {
+  MockTestImageCtx mock_image_ctx(*m_image_ctx);
+
+  InSequence seq;
+
+  auto mock_nbd_client = new MockNBDClient();
+  expect_init(*mock_nbd_client, 0);
+  expect_add_meta_context(*mock_nbd_client, 0);
+  expect_connect_uri(*mock_nbd_client, -1);
+  expect_get_errno(*mock_nbd_client, ECONNREFUSED);
+  expect_shutdown(*mock_nbd_client, 0);
+
+  MockNBDStream mock_nbd_stream(&mock_image_ctx, m_json_object);
+
+  C_SaferCond ctx1;
+  mock_nbd_stream.open(&ctx1);
+  ASSERT_EQ(-ECONNREFUSED, ctx1.wait());
+
+  C_SaferCond ctx2;
+  mock_nbd_stream.close(&ctx2);
+  ASSERT_EQ(0, ctx2.wait());
+}
+
+TEST_F(TestMockMigrationNBDStream, OpenConnectURIErrorNoErrno) {
+  MockTestImageCtx mock_image_ctx(*m_image_ctx);
+
+  InSequence seq;
+
+  auto mock_nbd_client = new MockNBDClient();
+  expect_init(*mock_nbd_client, 0);
+  expect_add_meta_context(*mock_nbd_client, 0);
+  expect_connect_uri(*mock_nbd_client, -1);
+  // libnbd actually does this for getaddrinfo() errors ("Name or
+  // service not known", etc)
+  expect_get_errno(*mock_nbd_client, 0);
+  expect_shutdown(*mock_nbd_client, 0);
+
+  MockNBDStream mock_nbd_stream(&mock_image_ctx, m_json_object);
+
+  C_SaferCond ctx1;
+  mock_nbd_stream.open(&ctx1);
+  ASSERT_EQ(-EIO, ctx1.wait());
+
+  C_SaferCond ctx2;
+  mock_nbd_stream.close(&ctx2);
+  ASSERT_EQ(0, ctx2.wait());
+}
+
+TEST_F(TestMockMigrationNBDStream, GetSize) {
+  MockTestImageCtx mock_image_ctx(*m_image_ctx);
+
+  InSequence seq;
+
+  auto mock_nbd_client = new MockNBDClient();
+  expect_init(*mock_nbd_client, 0);
+  expect_add_meta_context(*mock_nbd_client, 0);
+  expect_connect_uri(*mock_nbd_client, 0);
+  expect_get_size(*mock_nbd_client, 128);
+  expect_shutdown(*mock_nbd_client, 0);
+
+  MockNBDStream mock_nbd_stream(&mock_image_ctx, m_json_object);
+
+  C_SaferCond ctx1;
+  mock_nbd_stream.open(&ctx1);
+  ASSERT_EQ(0, ctx1.wait());
+
+  C_SaferCond ctx2;
+  uint64_t size;
+  mock_nbd_stream.get_size(&size, &ctx2);
+  ASSERT_EQ(0, ctx2.wait());
+  ASSERT_EQ(128, size);
+
+  C_SaferCond ctx3;
+  mock_nbd_stream.close(&ctx3);
+  ASSERT_EQ(0, ctx3.wait());
+}
+
+TEST_F(TestMockMigrationNBDStream, GetSizeError) {
+  MockTestImageCtx mock_image_ctx(*m_image_ctx);
+
+  InSequence seq;
+
+  auto mock_nbd_client = new MockNBDClient();
+  expect_init(*mock_nbd_client, 0);
+  expect_add_meta_context(*mock_nbd_client, 0);
+  expect_connect_uri(*mock_nbd_client, 0);
+  expect_get_size(*mock_nbd_client, -1);
+  expect_get_errno(*mock_nbd_client, EOVERFLOW);
+  expect_shutdown(*mock_nbd_client, 0);
+
+  MockNBDStream mock_nbd_stream(&mock_image_ctx, m_json_object);
+
+  C_SaferCond ctx1;
+  mock_nbd_stream.open(&ctx1);
+  ASSERT_EQ(0, ctx1.wait());
+
+  C_SaferCond ctx2;
+  uint64_t size;
+  mock_nbd_stream.get_size(&size, &ctx2);
+  ASSERT_EQ(-EOVERFLOW, ctx2.wait());
+
+  C_SaferCond ctx3;
+  mock_nbd_stream.close(&ctx3);
+  ASSERT_EQ(0, ctx3.wait());
+}
+
+TEST_F(TestMockMigrationNBDStream, Read) {
+  MockTestImageCtx mock_image_ctx(*m_image_ctx);
+
+  InSequence seq;
+
+  auto mock_nbd_client = new MockNBDClient();
+  expect_init(*mock_nbd_client, 0);
+  expect_add_meta_context(*mock_nbd_client, 0);
+  expect_connect_uri(*mock_nbd_client, 0);
+  std::string s1(128, '1');
+  expect_pread(*mock_nbd_client, 0, 128, s1.c_str(), 0);
+  std::string s2(64, '2');
+  expect_pread(*mock_nbd_client, 256, 64, s2.c_str(), 0);
+  expect_shutdown(*mock_nbd_client, 0);
+
+  MockNBDStream mock_nbd_stream(&mock_image_ctx, m_json_object);
+
+  C_SaferCond ctx1;
+  mock_nbd_stream.open(&ctx1);
+  ASSERT_EQ(0, ctx1.wait());
+
+  C_SaferCond ctx2;
+  bufferlist bl;
+  mock_nbd_stream.read({{0, 128}, {256, 64}}, &bl, &ctx2);
+  ASSERT_EQ(0, ctx2.wait());
+
+  bufferlist expected_bl;
+  expected_bl.append(s1);
+  expected_bl.append(s2);
+  ASSERT_EQ(expected_bl, bl);
+
+  C_SaferCond ctx3;
+  mock_nbd_stream.close(&ctx3);
+  ASSERT_EQ(0, ctx3.wait());
+}
+
+TEST_F(TestMockMigrationNBDStream, ReadError) {
+  MockTestImageCtx mock_image_ctx(*m_image_ctx);
+
+  InSequence seq;
+
+  auto mock_nbd_client = new MockNBDClient();
+  expect_init(*mock_nbd_client, 0);
+  expect_add_meta_context(*mock_nbd_client, 0);
+  expect_connect_uri(*mock_nbd_client, 0);
+  std::string s1(128, '1');
+  expect_pread(*mock_nbd_client, 0, 128, s1.c_str(), -1);
+  expect_get_errno(*mock_nbd_client, ERANGE);
+  expect_shutdown(*mock_nbd_client, 0);
+
+  MockNBDStream mock_nbd_stream(&mock_image_ctx, m_json_object);
+
+  C_SaferCond ctx1;
+  mock_nbd_stream.open(&ctx1);
+  ASSERT_EQ(0, ctx1.wait());
+
+  C_SaferCond ctx2;
+  bufferlist bl;
+  mock_nbd_stream.read({{0, 128}, {256, 64}}, &bl, &ctx2);
+  ASSERT_EQ(-ERANGE, ctx2.wait());
+
+  C_SaferCond ctx3;
+  mock_nbd_stream.close(&ctx3);
+  ASSERT_EQ(0, ctx3.wait());
+}
+
+TEST_F(TestMockMigrationNBDStream, ListSparseExtents) {
+  MockTestImageCtx mock_image_ctx(*m_image_ctx);
+
+  InSequence seq;
+
+  auto mock_nbd_client = new MockNBDClient();
+  expect_init(*mock_nbd_client, 0);
+  expect_add_meta_context(*mock_nbd_client, 0);
+  expect_connect_uri(*mock_nbd_client, 0);
+  // DATA
+  std::vector<block_status_cb_args> cbs1 = {
+    {LIBNBD_CONTEXT_BASE_ALLOCATION, 0, {128, 0}}
+  };
+  expect_block_status(*mock_nbd_client, 0, 128, cbs1, 0);
+  // ZEROED (zero)
+  std::vector<block_status_cb_args> cbs2 = {
+    {LIBNBD_CONTEXT_BASE_ALLOCATION, 256, {64, LIBNBD_STATE_ZERO}}
+  };
+  expect_block_status(*mock_nbd_client, 256, 64, cbs2, 0);
+  // ZEROED (hole)
+  std::vector<block_status_cb_args> cbs3 = {
+    {LIBNBD_CONTEXT_BASE_ALLOCATION, 352, {32, LIBNBD_STATE_HOLE}}
+  };
+  expect_block_status(*mock_nbd_client, 352, 32, cbs3, 0);
+  // ZEROED, DATA
+  std::vector<block_status_cb_args> cbs4 = {
+    {LIBNBD_CONTEXT_BASE_ALLOCATION, 384,
+     {56, LIBNBD_STATE_ZERO, 8, LIBNBD_STATE_HOLE, 16, 0}}
+  };
+  expect_block_status(*mock_nbd_client, 384, 80, cbs4, 0);
+  // DATA, ZEROED
+  std::vector<block_status_cb_args> cbs5 = {
+    {LIBNBD_CONTEXT_BASE_ALLOCATION, 464,
+     {40, 0, 16, LIBNBD_STATE_HOLE, 8, LIBNBD_STATE_ZERO}}
+  };
+  expect_block_status(*mock_nbd_client, 464, 64, cbs5, 0);
+  // ZEROED, DATA, ZEROED
+  std::vector<block_status_cb_args> cbs6 = {
+    {LIBNBD_CONTEXT_BASE_ALLOCATION, 528,
+     {80, LIBNBD_STATE_HOLE, 128, 0, 32, LIBNBD_STATE_HOLE}}
+  };
+  expect_block_status(*mock_nbd_client, 528, 240, cbs6, 0);
+  // DATA, ZEROED, DATA
+  std::vector<block_status_cb_args> cbs7 = {
+    {LIBNBD_CONTEXT_BASE_ALLOCATION, 1536,
+     {48, 0, 256, LIBNBD_STATE_ZERO, 16, 0}}
+  };
+  expect_block_status(*mock_nbd_client, 1536, 320, cbs7, 0);
+  expect_shutdown(*mock_nbd_client, 0);
+
+  MockNBDStream mock_nbd_stream(&mock_image_ctx, m_json_object);
+
+  C_SaferCond ctx1;
+  mock_nbd_stream.open(&ctx1);
+  ASSERT_EQ(0, ctx1.wait());
+
+  C_SaferCond ctx2;
+  io::SparseExtents sparse_extents;
+  mock_nbd_stream.list_sparse_extents({{0, 128}, {256, 64}, {352, 32},
+                                       {384, 80}, {464, 64}, {528, 240},
+                                       {1536, 320}}, &sparse_extents, &ctx2);
+  ASSERT_EQ(0, ctx2.wait());
+
+  io::SparseExtents expected_sparse_extents;
+  expected_sparse_extents.insert(0, 128, {io::SPARSE_EXTENT_STATE_DATA, 128});
+  expected_sparse_extents.insert(256, 64, {io::SPARSE_EXTENT_STATE_ZEROED, 64});
+  expected_sparse_extents.insert(352, 96, {io::SPARSE_EXTENT_STATE_ZEROED, 96});
+  expected_sparse_extents.insert(448, 56, {io::SPARSE_EXTENT_STATE_DATA, 56});
+  expected_sparse_extents.insert(504, 104, {io::SPARSE_EXTENT_STATE_ZEROED, 104});
+  expected_sparse_extents.insert(608, 128, {io::SPARSE_EXTENT_STATE_DATA, 128});
+  expected_sparse_extents.insert(736, 32, {io::SPARSE_EXTENT_STATE_ZEROED, 32});
+  expected_sparse_extents.insert(1536, 48, {io::SPARSE_EXTENT_STATE_DATA, 48});
+  expected_sparse_extents.insert(1584, 256, {io::SPARSE_EXTENT_STATE_ZEROED, 256});
+  expected_sparse_extents.insert(1840, 16, {io::SPARSE_EXTENT_STATE_DATA, 16});
+  ASSERT_EQ(expected_sparse_extents, sparse_extents);
+
+  C_SaferCond ctx3;
+  mock_nbd_stream.close(&ctx3);
+  ASSERT_EQ(0, ctx3.wait());
+}
+
+TEST_F(TestMockMigrationNBDStream, ListSparseExtentsMoreThanRequested) {
+  MockTestImageCtx mock_image_ctx(*m_image_ctx);
+
+  InSequence seq;
+
+  auto mock_nbd_client = new MockNBDClient();
+  expect_init(*mock_nbd_client, 0);
+  expect_add_meta_context(*mock_nbd_client, 0);
+  expect_connect_uri(*mock_nbd_client, 0);
+  // extra byte at the end
+  std::vector<block_status_cb_args> cbs1 = {
+    {LIBNBD_CONTEXT_BASE_ALLOCATION, 0, {129, LIBNBD_STATE_HOLE}}
+  };
+  expect_block_status(*mock_nbd_client, 0, 128, cbs1, 0);
+  // extra byte at the start
+  std::vector<block_status_cb_args> cbs2 = {
+    {LIBNBD_CONTEXT_BASE_ALLOCATION, 255, {65, LIBNBD_STATE_HOLE}}
+  };
+  expect_block_status(*mock_nbd_client, 256, 64, cbs2, 0);
+  // extra byte on both sides
+  std::vector<block_status_cb_args> cbs3 = {
+    {LIBNBD_CONTEXT_BASE_ALLOCATION, 351, {34, LIBNBD_STATE_HOLE}}
+  };
+  expect_block_status(*mock_nbd_client, 352, 32, cbs3, 0);
+  expect_shutdown(*mock_nbd_client, 0);
+
+  MockNBDStream mock_nbd_stream(&mock_image_ctx, m_json_object);
+
+  C_SaferCond ctx1;
+  mock_nbd_stream.open(&ctx1);
+  ASSERT_EQ(0, ctx1.wait());
+
+  C_SaferCond ctx2;
+  io::SparseExtents sparse_extents;
+  mock_nbd_stream.list_sparse_extents({{0, 128}, {256, 64}, {352, 32}},
+                                      &sparse_extents, &ctx2);
+  ASSERT_EQ(0, ctx2.wait());
+
+  io::SparseExtents expected_sparse_extents;
+  expected_sparse_extents.insert(0, 128, {io::SPARSE_EXTENT_STATE_ZEROED, 128});
+  expected_sparse_extents.insert(256, 64, {io::SPARSE_EXTENT_STATE_ZEROED, 64});
+  expected_sparse_extents.insert(352, 32, {io::SPARSE_EXTENT_STATE_ZEROED, 32});
+  ASSERT_EQ(expected_sparse_extents, sparse_extents);
+
+  C_SaferCond ctx3;
+  mock_nbd_stream.close(&ctx3);
+  ASSERT_EQ(0, ctx3.wait());
+}
+
+TEST_F(TestMockMigrationNBDStream, ListSparseExtentsLessThanRequested) {
+  MockTestImageCtx mock_image_ctx(*m_image_ctx);
+
+  InSequence seq;
+
+  auto mock_nbd_client = new MockNBDClient();
+  expect_init(*mock_nbd_client, 0);
+  expect_add_meta_context(*mock_nbd_client, 0);
+  expect_connect_uri(*mock_nbd_client, 0);
+  // missing byte at the end
+  std::vector<block_status_cb_args> cbs1 = {
+    {LIBNBD_CONTEXT_BASE_ALLOCATION, 0, {127, LIBNBD_STATE_HOLE}}
+  };
+  expect_block_status(*mock_nbd_client, 0, 128, cbs1, 0);
+  // missing byte at the start
+  std::vector<block_status_cb_args> cbs2 = {
+    {LIBNBD_CONTEXT_BASE_ALLOCATION, 257, {63, LIBNBD_STATE_HOLE}}
+  };
+  expect_block_status(*mock_nbd_client, 256, 64, cbs2, 0);
+  // missing byte on both sides
+  std::vector<block_status_cb_args> cbs3 = {
+    {LIBNBD_CONTEXT_BASE_ALLOCATION, 353, {30, LIBNBD_STATE_HOLE}}
+  };
+  expect_block_status(*mock_nbd_client, 352, 32, cbs3, 0);
+  // zero-sized entry
+  std::vector<block_status_cb_args> cbs4 = {
+    {LIBNBD_CONTEXT_BASE_ALLOCATION, 400, {0, LIBNBD_STATE_HOLE}}
+  };
+  expect_block_status(*mock_nbd_client, 400, 48, cbs4, 0);
+  // no entries
+  std::vector<block_status_cb_args> cbs5 = {
+    {LIBNBD_CONTEXT_BASE_ALLOCATION, 520, {}}
+  };
+  expect_block_status(*mock_nbd_client, 520, 16, cbs5, 0);
+  // no callback
+  std::vector<block_status_cb_args> cbs6;
+  expect_block_status(*mock_nbd_client, 608, 8, cbs6, 0);
+  expect_shutdown(*mock_nbd_client, 0);
+
+  MockNBDStream mock_nbd_stream(&mock_image_ctx, m_json_object);
+
+  C_SaferCond ctx1;
+  mock_nbd_stream.open(&ctx1);
+  ASSERT_EQ(0, ctx1.wait());
+
+  C_SaferCond ctx2;
+  io::SparseExtents sparse_extents;
+  mock_nbd_stream.list_sparse_extents({{0, 128}, {256, 64}, {352, 32},
+                                       {400, 48}, {520, 16}, {608, 8}},
+                                       &sparse_extents, &ctx2);
+  ASSERT_EQ(0, ctx2.wait());
+
+  io::SparseExtents expected_sparse_extents;
+  expected_sparse_extents.insert(0, 127, {io::SPARSE_EXTENT_STATE_ZEROED, 127});
+  expected_sparse_extents.insert(127, 1, {io::SPARSE_EXTENT_STATE_DATA, 1});
+  expected_sparse_extents.insert(256, 1, {io::SPARSE_EXTENT_STATE_DATA, 1});
+  expected_sparse_extents.insert(257, 63, {io::SPARSE_EXTENT_STATE_ZEROED, 63});
+  expected_sparse_extents.insert(352, 1, {io::SPARSE_EXTENT_STATE_DATA, 1});
+  expected_sparse_extents.insert(353, 30, {io::SPARSE_EXTENT_STATE_ZEROED, 30});
+  expected_sparse_extents.insert(383, 1, {io::SPARSE_EXTENT_STATE_DATA, 1});
+  expected_sparse_extents.insert(400, 48, {io::SPARSE_EXTENT_STATE_DATA, 48});
+  expected_sparse_extents.insert(520, 16, {io::SPARSE_EXTENT_STATE_DATA, 16});
+  expected_sparse_extents.insert(608, 8, {io::SPARSE_EXTENT_STATE_DATA, 8});
+  ASSERT_EQ(expected_sparse_extents, sparse_extents);
+
+  C_SaferCond ctx3;
+  mock_nbd_stream.close(&ctx3);
+  ASSERT_EQ(0, ctx3.wait());
+}
+
+TEST_F(TestMockMigrationNBDStream, ListSparseExtentsMultipleCallbacks) {
+  MockTestImageCtx mock_image_ctx(*m_image_ctx);
+
+  InSequence seq;
+
+  auto mock_nbd_client = new MockNBDClient();
+  expect_init(*mock_nbd_client, 0);
+  expect_add_meta_context(*mock_nbd_client, 0);
+  expect_connect_uri(*mock_nbd_client, 0);
+  std::vector<block_status_cb_args> cbs1 = {
+    {LIBNBD_CONTEXT_BASE_ALLOCATION, 96, {32, LIBNBD_STATE_HOLE}},
+    {LIBNBD_CONTEXT_BASE_ALLOCATION, 32, {32, LIBNBD_STATE_ZERO}},
+    {LIBNBD_CONTEXT_BASE_ALLOCATION, 0, {32, LIBNBD_STATE_ZERO}},
+    {LIBNBD_CONTEXT_BASE_ALLOCATION, 64, {32, LIBNBD_STATE_HOLE}}
+  };
+  expect_block_status(*mock_nbd_client, 0, 128, cbs1, 0);
+  std::vector<block_status_cb_args> cbs2 = {
+    {LIBNBD_CONTEXT_BASE_ALLOCATION, 192, {32, 0}},
+    {LIBNBD_CONTEXT_BASE_ALLOCATION, 128, {32, LIBNBD_STATE_ZERO, 32, 0}},
+    {LIBNBD_CONTEXT_BASE_ALLOCATION, 224, {32, LIBNBD_STATE_ZERO}}
+  };
+  expect_block_status(*mock_nbd_client, 128, 128, cbs2, 0);
+  expect_shutdown(*mock_nbd_client, 0);
+
+  MockNBDStream mock_nbd_stream(&mock_image_ctx, m_json_object);
+
+  C_SaferCond ctx1;
+  mock_nbd_stream.open(&ctx1);
+  ASSERT_EQ(0, ctx1.wait());
+
+  C_SaferCond ctx2;
+  io::SparseExtents sparse_extents;
+  mock_nbd_stream.list_sparse_extents({{0, 128}, {128, 128}}, &sparse_extents,
+                                      &ctx2);
+  ASSERT_EQ(0, ctx2.wait());
+
+  io::SparseExtents expected_sparse_extents;
+  expected_sparse_extents.insert(0, 160, {io::SPARSE_EXTENT_STATE_ZEROED, 160});
+  expected_sparse_extents.insert(160, 64, {io::SPARSE_EXTENT_STATE_DATA, 64});
+  expected_sparse_extents.insert(224, 32, {io::SPARSE_EXTENT_STATE_ZEROED, 32});
+  ASSERT_EQ(expected_sparse_extents, sparse_extents);
+
+  C_SaferCond ctx3;
+  mock_nbd_stream.close(&ctx3);
+  ASSERT_EQ(0, ctx3.wait());
+}
+
+TEST_F(TestMockMigrationNBDStream, ListSparseExtentsUnexpectedMetaContexts) {
+  MockTestImageCtx mock_image_ctx(*m_image_ctx);
+
+  InSequence seq;
+
+  auto mock_nbd_client = new MockNBDClient();
+  expect_init(*mock_nbd_client, 0);
+  expect_add_meta_context(*mock_nbd_client, 0);
+  expect_connect_uri(*mock_nbd_client, 0);
+  std::vector<block_status_cb_args> cbs = {
+    {"unexpected context 1", 0, {64, LIBNBD_STATE_ZERO, 64, 0}},
+    {LIBNBD_CONTEXT_BASE_ALLOCATION, 0, {32, LIBNBD_STATE_ZERO, 96, 0}},
+    {"unexpected context 2", 0, {128, LIBNBD_STATE_ZERO}}
+  };
+  expect_block_status(*mock_nbd_client, 0, 128, cbs, 0);
+  expect_shutdown(*mock_nbd_client, 0);
+
+  MockNBDStream mock_nbd_stream(&mock_image_ctx, m_json_object);
+
+  C_SaferCond ctx1;
+  mock_nbd_stream.open(&ctx1);
+  ASSERT_EQ(0, ctx1.wait());
+
+  C_SaferCond ctx2;
+  io::SparseExtents sparse_extents;
+  mock_nbd_stream.list_sparse_extents({{0, 128}}, &sparse_extents, &ctx2);
+  ASSERT_EQ(0, ctx2.wait());
+
+  io::SparseExtents expected_sparse_extents;
+  expected_sparse_extents.insert(0, 32, {io::SPARSE_EXTENT_STATE_ZEROED, 32});
+  expected_sparse_extents.insert(32, 96, {io::SPARSE_EXTENT_STATE_DATA, 96});
+  ASSERT_EQ(expected_sparse_extents, sparse_extents);
+
+  C_SaferCond ctx3;
+  mock_nbd_stream.close(&ctx3);
+  ASSERT_EQ(0, ctx3.wait());
+}
+
+TEST_F(TestMockMigrationNBDStream, ListSparseExtentsError) {
+  MockTestImageCtx mock_image_ctx(*m_image_ctx);
+
+  InSequence seq;
+
+  auto mock_nbd_client = new MockNBDClient();
+  expect_init(*mock_nbd_client, 0);
+  expect_add_meta_context(*mock_nbd_client, 0);
+  expect_connect_uri(*mock_nbd_client, 0);
+  // error isn't propagated -- DATA is assumed instead
+  std::vector<block_status_cb_args> cbs1;
+  expect_block_status(*mock_nbd_client, 0, 128, cbs1, -1);
+  expect_get_errno(*mock_nbd_client, ENOTSUP);
+  std::vector<block_status_cb_args> cbs2 = {
+    {LIBNBD_CONTEXT_BASE_ALLOCATION, 256, {64, LIBNBD_STATE_ZERO}}
+  };
+  expect_block_status(*mock_nbd_client, 256, 64, cbs2, 0);
+  expect_shutdown(*mock_nbd_client, 0);
+
+  MockNBDStream mock_nbd_stream(&mock_image_ctx, m_json_object);
+
+  C_SaferCond ctx1;
+  mock_nbd_stream.open(&ctx1);
+  ASSERT_EQ(0, ctx1.wait());
+
+  C_SaferCond ctx2;
+  io::SparseExtents sparse_extents;
+  mock_nbd_stream.list_sparse_extents({{0, 128}, {256, 64}}, &sparse_extents,
+                                      &ctx2);
+  ASSERT_EQ(0, ctx2.wait());
+
+  io::SparseExtents expected_sparse_extents;
+  expected_sparse_extents.insert(0, 128, {io::SPARSE_EXTENT_STATE_DATA, 128});
+  expected_sparse_extents.insert(256, 64, {io::SPARSE_EXTENT_STATE_ZEROED, 64});
+  ASSERT_EQ(expected_sparse_extents, sparse_extents);
+
+  C_SaferCond ctx3;
+  mock_nbd_stream.close(&ctx3);
+  ASSERT_EQ(0, ctx3.wait());
+}
+
+TEST_F(TestMockMigrationNBDStream, ShutdownError) {
+  MockTestImageCtx mock_image_ctx(*m_image_ctx);
+
+  InSequence seq;
+
+  auto mock_nbd_client = new MockNBDClient();
+  expect_init(*mock_nbd_client, 0);
+  expect_add_meta_context(*mock_nbd_client, 0);
+  expect_connect_uri(*mock_nbd_client, 0);
+  // error is ignored
+  expect_shutdown(*mock_nbd_client, -1);
+
+  MockNBDStream mock_nbd_stream(&mock_image_ctx, m_json_object);
+
+  C_SaferCond ctx1;
+  mock_nbd_stream.open(&ctx1);
+  ASSERT_EQ(0, ctx1.wait());
+
+  C_SaferCond ctx2;
+  mock_nbd_stream.close(&ctx2);
+  ASSERT_EQ(0, ctx2.wait());
+}
+
 } // namespace migration
 } // namespace librbd