From 5e0a3484098370e53f9b7ddef8aa92aa7482fdec Mon Sep 17 00:00:00 2001 From: Ilya Dryomov Date: Thu, 5 Sep 2024 15:43:07 +0200 Subject: [PATCH] librbd/migration/NBDStream: abstract out libnbd and add unit tests Signed-off-by: Ilya Dryomov (cherry picked from commit 6fd11c0276dc3f0b2349e42d877d7da692161bc9) --- src/librbd/migration/NBDStream.cc | 110 ++- src/librbd/migration/NBDStream.h | 6 +- .../librbd/migration/test_mock_NBDStream.cc | 654 ++++++++++++++++++ 3 files changed, 738 insertions(+), 32 deletions(-) diff --git a/src/librbd/migration/NBDStream.cc b/src/librbd/migration/NBDStream.cc index 57b9b72ba3888..bf8c0c8519eab 100644 --- a/src/librbd/migration/NBDStream.cc +++ b/src/librbd/migration/NBDStream.cc @@ -47,6 +47,61 @@ int extent_cb(void* data, const char* metacontext, uint64_t offset, } // anonymous namespace +template +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 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::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::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::NBDStream(I* image_ctx, const json_spirit::mObject& json_object) template NBDStream::~NBDStream() { - if (m_nbd != nullptr) { - nbd_close(m_nbd); - } } template @@ -228,28 +281,29 @@ void NBDStream::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::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 void NBDStream::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 void NBDStream::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; diff --git a/src/librbd/migration/NBDStream.h b/src/librbd/migration/NBDStream.h index d0135f9a55ac4..aeced5d4f3dfc 100644 --- a/src/librbd/migration/NBDStream.h +++ b/src/librbd/migration/NBDStream.h @@ -12,8 +12,6 @@ struct Context; -struct nbd_handle; - namespace librbd { struct AsioEngine; @@ -21,6 +19,8 @@ struct ImageCtx; namespace migration { +template class NBDClient; + template class NBDStream : public StreamInterface { public: @@ -53,7 +53,7 @@ private: json_spirit::mObject m_json_object; boost::asio::strand m_strand; - struct nbd_handle* m_nbd = nullptr; + std::unique_ptr> m_nbd_client; struct ReadRequest; struct ListSparseExtentsRequest; diff --git a/src/test/librbd/migration/test_mock_NBDStream.cc b/src/test/librbd/migration/test_mock_NBDStream.cc index 067749d76e352..5977057b11f97 100644 --- a/src/test/librbd/migration/test_mock_NBDStream.cc +++ b/src/test/librbd/migration/test_mock_NBDStream.cc @@ -25,11 +25,42 @@ struct MockTestImageCtx : public MockImageCtx { namespace librbd { namespace migration { +template <> +struct NBDClient { + 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* NBDClient::s_instance = nullptr; + +using ::testing::_; using ::testing::Invoke; +using ::testing::InSequence; +using ::testing::Return; +using ::testing::WithArg; class TestMockMigrationNBDStream : public TestMockFixture { public: typedef NBDStream MockNBDStream; + typedef NBDClient 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 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& 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 cbs1 = { + {LIBNBD_CONTEXT_BASE_ALLOCATION, 0, {128, 0}} + }; + expect_block_status(*mock_nbd_client, 0, 128, cbs1, 0); + // ZEROED (zero) + std::vector cbs2 = { + {LIBNBD_CONTEXT_BASE_ALLOCATION, 256, {64, LIBNBD_STATE_ZERO}} + }; + expect_block_status(*mock_nbd_client, 256, 64, cbs2, 0); + // ZEROED (hole) + std::vector cbs3 = { + {LIBNBD_CONTEXT_BASE_ALLOCATION, 352, {32, LIBNBD_STATE_HOLE}} + }; + expect_block_status(*mock_nbd_client, 352, 32, cbs3, 0); + // ZEROED, DATA + std::vector 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 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 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 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 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 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 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 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 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 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 cbs4 = { + {LIBNBD_CONTEXT_BASE_ALLOCATION, 400, {0, LIBNBD_STATE_HOLE}} + }; + expect_block_status(*mock_nbd_client, 400, 48, cbs4, 0); + // no entries + std::vector cbs5 = { + {LIBNBD_CONTEXT_BASE_ALLOCATION, 520, {}} + }; + expect_block_status(*mock_nbd_client, 520, 16, cbs5, 0); + // no callback + std::vector 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 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 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 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 cbs1; + expect_block_status(*mock_nbd_client, 0, 128, cbs1, -1); + expect_get_errno(*mock_nbd_client, ENOTSUP); + std::vector 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 -- 2.39.5