]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
rbd-mirror: state machine to apply any image-state deltas
authorJason Dillaman <dillaman@redhat.com>
Thu, 13 Feb 2020 21:41:03 +0000 (16:41 -0500)
committerJason Dillaman <dillaman@redhat.com>
Fri, 21 Feb 2020 15:00:56 +0000 (10:00 -0500)
When replaying mirror snapshots from a remote image, the image state
tracks the current state of the image at the image the snapshot was
created. Therefore, the new state machine will apply any discovered
deltas between the remote image state and the local image.

Signed-off-by: Jason Dillaman <dillaman@redhat.com>
src/test/rbd_mirror/CMakeLists.txt
src/test/rbd_mirror/image_replayer/snapshot/test_mock_ApplyImageStateRequest.cc [new file with mode: 0644]
src/tools/rbd_mirror/CMakeLists.txt
src/tools/rbd_mirror/image_replayer/snapshot/ApplyImageStateRequest.cc [new file with mode: 0644]
src/tools/rbd_mirror/image_replayer/snapshot/ApplyImageStateRequest.h [new file with mode: 0644]

index 0479a43c27fa1baaa1bb0693f7f168b52b94d347..36a1603d9c5e9ba8791fde7ee678bec0a1a2ed80 100644 (file)
@@ -42,6 +42,7 @@ add_executable(unittest_rbd_mirror
   image_replayer/journal/test_mock_PrepareReplayRequest.cc
   image_replayer/journal/test_mock_EventPreprocessor.cc
   image_replayer/journal/test_mock_Replayer.cc
+  image_replayer/snapshot/test_mock_ApplyImageStateRequest.cc
   image_replayer/snapshot/test_mock_CreateLocalImageRequest.cc
   image_replayer/snapshot/test_mock_Replayer.cc
   image_sync/test_mock_SyncPointCreateRequest.cc
diff --git a/src/test/rbd_mirror/image_replayer/snapshot/test_mock_ApplyImageStateRequest.cc b/src/test/rbd_mirror/image_replayer/snapshot/test_mock_ApplyImageStateRequest.cc
new file mode 100644 (file)
index 0000000..303aedb
--- /dev/null
@@ -0,0 +1,640 @@
+// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*-
+// vim: ts=8 sw=2 smarttab
+
+#include "test/rbd_mirror/test_mock_fixture.h"
+#include "librbd/internal.h"
+#include "librbd/Operations.h"
+#include "librbd/image/GetMetadataRequest.h"
+#include "tools/rbd_mirror/Threads.h"
+#include "tools/rbd_mirror/image_replayer/snapshot/ApplyImageStateRequest.h"
+#include "test/librados_test_stub/MockTestMemIoCtxImpl.h"
+#include "test/librbd/mock/MockImageCtx.h"
+
+namespace librbd {
+
+namespace {
+
+struct MockTestImageCtx : public librbd::MockImageCtx {
+  explicit MockTestImageCtx(librbd::ImageCtx &image_ctx)
+    : librbd::MockImageCtx(image_ctx) {
+  }
+};
+
+} // anonymous namespace
+
+namespace image {
+
+template <>
+struct GetMetadataRequest<MockTestImageCtx> {
+  std::map<std::string, bufferlist>* pairs = nullptr;
+  Context* on_finish = nullptr;
+
+  static GetMetadataRequest* s_instance;
+  static GetMetadataRequest* create(librados::IoCtx& io_ctx,
+                                    const std::string& oid,
+                                    const std::string& filter,
+                                    const std::string& last_key,
+                                    size_t max_results,
+                                    std::map<std::string, bufferlist>* pairs,
+                                    Context* on_finish) {
+    ceph_assert(s_instance != nullptr);
+    s_instance->pairs = pairs;
+    s_instance->on_finish = on_finish;
+    return s_instance;
+  }
+
+  GetMetadataRequest() {
+    s_instance = this;
+  }
+
+  MOCK_METHOD0(send, void());
+};
+
+GetMetadataRequest<MockTestImageCtx>* GetMetadataRequest<MockTestImageCtx>::s_instance = nullptr;
+
+} // namespace image
+} // namespace librbd
+
+#include "tools/rbd_mirror/image_replayer/snapshot/ApplyImageStateRequest.cc"
+
+using ::testing::_;
+using ::testing::InSequence;
+using ::testing::Invoke;
+using ::testing::Return;
+using ::testing::StrEq;
+using ::testing::WithArg;
+
+namespace rbd {
+namespace mirror {
+namespace image_replayer {
+namespace snapshot {
+
+class TestMockImageReplayerSnapshotApplyImageStateRequest : public TestMockFixture {
+public:
+  typedef ApplyImageStateRequest<librbd::MockTestImageCtx> MockApplyImageStateRequest;
+  typedef librbd::image::GetMetadataRequest<librbd::MockTestImageCtx> MockGetMetadataRequest;
+
+  void SetUp() override {
+    TestMockFixture::SetUp();
+
+    librbd::RBD rbd;
+    ASSERT_EQ(0, create_image(rbd, m_local_io_ctx, m_image_name, m_image_size));
+    ASSERT_EQ(0, open_image(m_local_io_ctx, m_image_name, &m_local_image_ctx));
+
+    ASSERT_EQ(0, create_image(rbd, m_remote_io_ctx, m_image_name, m_image_size));
+    ASSERT_EQ(0, open_image(m_remote_io_ctx, m_image_name, &m_remote_image_ctx));
+
+    m_mock_local_image_ctx = new librbd::MockTestImageCtx(*m_local_image_ctx);
+    m_mock_remote_image_ctx = new librbd::MockTestImageCtx(*m_remote_image_ctx);
+  }
+
+  void TearDown() override {
+    delete m_mock_remote_image_ctx;
+    delete m_mock_local_image_ctx;
+    TestMockFixture::TearDown();
+  }
+
+  void expect_rename_image(const std::string& name, int r) {
+    EXPECT_CALL(*m_mock_local_image_ctx->operations, execute_rename(name, _))
+      .WillOnce(WithArg<1>(Invoke([this, r](Context* ctx) {
+          m_threads->work_queue->queue(ctx, r);
+        })));
+  }
+
+  void expect_update_features(uint64_t features, bool enable, int r) {
+    EXPECT_CALL(*m_mock_local_image_ctx->operations,
+                execute_update_features(features, enable, _, 0U))
+      .WillOnce(WithArg<2>(Invoke([this, r](Context* ctx) {
+          m_threads->work_queue->queue(ctx, r);
+        })));
+  }
+
+  void expect_get_metadata(MockGetMetadataRequest& mock_get_metadata_request,
+                           const std::map<std::string, bufferlist>& pairs,
+                           int r) {
+    EXPECT_CALL(mock_get_metadata_request, send())
+      .WillOnce(Invoke([this, &mock_get_metadata_request, pairs, r]() {
+          *mock_get_metadata_request.pairs = pairs;
+          m_threads->work_queue->queue(mock_get_metadata_request.on_finish, r);
+        }));
+  }
+
+  void expect_update_metadata(const std::vector<std::string>& remove,
+                              const std::map<std::string, bufferlist>& pairs,
+                              int r) {
+    for (auto& key : remove) {
+      bufferlist bl;
+      ceph::encode(key, bl);
+      EXPECT_CALL(get_mock_io_ctx(m_mock_local_image_ctx->md_ctx),
+                  exec(m_mock_local_image_ctx->header_oid, _, StrEq("rbd"),
+                  StrEq("metadata_remove"), ContentsEqual(bl), _, _))
+        .WillOnce(Return(r));
+      if (r < 0) {
+        return;
+      }
+    }
+
+    if (!pairs.empty()) {
+      bufferlist bl;
+      ceph::encode(pairs, bl);
+      EXPECT_CALL(get_mock_io_ctx(m_mock_local_image_ctx->md_ctx),
+                  exec(m_mock_local_image_ctx->header_oid, _, StrEq("rbd"),
+                  StrEq("metadata_set"), ContentsEqual(bl), _, _))
+        .WillOnce(Return(r));
+    }
+  }
+
+  void expect_unprotect_snapshot(const std::string& name, int r) {
+    EXPECT_CALL(*m_mock_local_image_ctx->operations,
+                execute_snap_unprotect({cls::rbd::UserSnapshotNamespace{}},
+                                       name, _))
+      .WillOnce(WithArg<2>(Invoke([this, r](Context* ctx) {
+          m_threads->work_queue->queue(ctx, r);
+        })));
+  }
+
+  void expect_remove_snapshot(const std::string& name, int r) {
+    EXPECT_CALL(*m_mock_local_image_ctx->operations,
+                execute_snap_remove({cls::rbd::UserSnapshotNamespace{}},
+                                     name, _))
+      .WillOnce(WithArg<2>(Invoke([this, r](Context* ctx) {
+          m_threads->work_queue->queue(ctx, r);
+        })));
+  }
+
+  void expect_protect_snapshot(const std::string& name, int r) {
+    EXPECT_CALL(*m_mock_local_image_ctx->operations,
+                execute_snap_protect({cls::rbd::UserSnapshotNamespace{}},
+                                     name, _))
+      .WillOnce(WithArg<2>(Invoke([this, r](Context* ctx) {
+          m_threads->work_queue->queue(ctx, r);
+        })));
+  }
+
+  void expect_rename_snapshot(uint64_t snap_id, const std::string& name,
+                              int r) {
+    EXPECT_CALL(*m_mock_local_image_ctx->operations,
+                execute_snap_rename(snap_id, name, _))
+      .WillOnce(WithArg<2>(Invoke([this, r](Context* ctx) {
+          m_threads->work_queue->queue(ctx, r);
+        })));
+  }
+
+
+  void expect_set_snap_limit(uint64_t limit, int r) {
+    EXPECT_CALL(*m_mock_local_image_ctx->operations,
+                execute_snap_set_limit(limit, _))
+      .WillOnce(WithArg<1>(Invoke([this, r](Context* ctx) {
+          m_threads->work_queue->queue(ctx, r);
+        })));
+  }
+
+  librbd::ImageCtx *m_local_image_ctx;
+  librbd::ImageCtx *m_remote_image_ctx;
+
+  librbd::MockTestImageCtx *m_mock_local_image_ctx = nullptr;
+  librbd::MockTestImageCtx *m_mock_remote_image_ctx = nullptr;
+
+};
+
+TEST_F(TestMockImageReplayerSnapshotApplyImageStateRequest, NoChanges) {
+  InSequence seq;
+
+  MockGetMetadataRequest mock_get_metadata_request;
+  expect_get_metadata(mock_get_metadata_request, {}, 0);
+
+  expect_set_snap_limit(0, 0);
+
+  librbd::mirror::snapshot::ImageState image_state;
+  image_state.name = m_image_name;
+  image_state.features = m_remote_image_ctx->features;
+
+  C_SaferCond ctx;
+  auto req = MockApplyImageStateRequest::create(
+    "local mirror uuid", "remote mirror uuid", m_mock_local_image_ctx,
+    m_mock_remote_image_ctx, image_state, &ctx);
+  req->send();
+  ASSERT_EQ(0, ctx.wait());
+}
+
+TEST_F(TestMockImageReplayerSnapshotApplyImageStateRequest, RenameImage) {
+  InSequence seq;
+
+  expect_rename_image("new name", 0);
+
+  MockGetMetadataRequest mock_get_metadata_request;
+  expect_get_metadata(mock_get_metadata_request, {}, 0);
+
+  expect_set_snap_limit(0, 0);
+
+  librbd::mirror::snapshot::ImageState image_state;
+  image_state.name = "new name";
+  image_state.features = m_remote_image_ctx->features;
+
+  C_SaferCond ctx;
+  auto req = MockApplyImageStateRequest::create(
+    "local mirror uuid", "remote mirror uuid", m_mock_local_image_ctx,
+    m_mock_remote_image_ctx, image_state, &ctx);
+  req->send();
+  ASSERT_EQ(0, ctx.wait());
+}
+
+TEST_F(TestMockImageReplayerSnapshotApplyImageStateRequest, RenameImageError) {
+  InSequence seq;
+
+  expect_rename_image("new name", -EINVAL);
+
+  librbd::mirror::snapshot::ImageState image_state;
+  image_state.name = "new name";
+  image_state.features = m_remote_image_ctx->features;
+
+  C_SaferCond ctx;
+  auto req = MockApplyImageStateRequest::create(
+    "local mirror uuid", "remote mirror uuid", m_mock_local_image_ctx,
+    m_mock_remote_image_ctx, image_state, &ctx);
+  req->send();
+  ASSERT_EQ(-EINVAL, ctx.wait());
+}
+
+TEST_F(TestMockImageReplayerSnapshotApplyImageStateRequest, UpdateFeatures) {
+  InSequence seq;
+
+  expect_update_features(RBD_FEATURE_DEEP_FLATTEN, false, 0);
+  expect_update_features(RBD_FEATURE_OBJECT_MAP, true, 0);
+
+  MockGetMetadataRequest mock_get_metadata_request;
+  expect_get_metadata(mock_get_metadata_request, {}, 0);
+
+  expect_set_snap_limit(0, 0);
+
+  librbd::mirror::snapshot::ImageState image_state;
+  image_state.name = m_image_name;
+  image_state.features = RBD_FEATURE_EXCLUSIVE_LOCK |
+                         RBD_FEATURE_OBJECT_MAP;
+  m_mock_local_image_ctx->features = RBD_FEATURE_EXCLUSIVE_LOCK |
+                                     RBD_FEATURE_DEEP_FLATTEN;
+
+  C_SaferCond ctx;
+  auto req = MockApplyImageStateRequest::create(
+    "local mirror uuid", "remote mirror uuid", m_mock_local_image_ctx,
+    m_mock_remote_image_ctx, image_state, &ctx);
+  req->send();
+  ASSERT_EQ(0, ctx.wait());
+}
+
+TEST_F(TestMockImageReplayerSnapshotApplyImageStateRequest, UpdateFeaturesError) {
+  InSequence seq;
+
+  expect_update_features(RBD_FEATURE_DEEP_FLATTEN, false, -EINVAL);
+
+  librbd::mirror::snapshot::ImageState image_state;
+  image_state.name = m_image_name;
+  image_state.features = RBD_FEATURE_EXCLUSIVE_LOCK |
+                         RBD_FEATURE_OBJECT_MAP;
+  m_mock_local_image_ctx->features = RBD_FEATURE_EXCLUSIVE_LOCK |
+                                     RBD_FEATURE_DEEP_FLATTEN;
+
+  C_SaferCond ctx;
+  auto req = MockApplyImageStateRequest::create(
+    "local mirror uuid", "remote mirror uuid", m_mock_local_image_ctx,
+    m_mock_remote_image_ctx, image_state, &ctx);
+  req->send();
+  ASSERT_EQ(-EINVAL, ctx.wait());
+}
+
+TEST_F(TestMockImageReplayerSnapshotApplyImageStateRequest, UpdateImageMeta) {
+  InSequence seq;
+
+  bufferlist data_bl;
+  ceph::encode("data", data_bl);
+  MockGetMetadataRequest mock_get_metadata_request;
+  expect_get_metadata(mock_get_metadata_request,
+                      {{"key1", {}}, {"key2", {}}}, 0);
+  expect_update_metadata({"key2"}, {{"key1", data_bl}}, 0);
+
+  expect_set_snap_limit(0, 0);
+
+  librbd::mirror::snapshot::ImageState image_state;
+  image_state.name = m_image_name;
+  image_state.features = m_remote_image_ctx->features;
+  image_state.metadata = {{"key1", data_bl}};
+
+  C_SaferCond ctx;
+  auto req = MockApplyImageStateRequest::create(
+    "local mirror uuid", "remote mirror uuid", m_mock_local_image_ctx,
+    m_mock_remote_image_ctx, image_state, &ctx);
+  req->send();
+  ASSERT_EQ(0, ctx.wait());
+}
+
+TEST_F(TestMockImageReplayerSnapshotApplyImageStateRequest, GetImageMetaError) {
+  InSequence seq;
+
+  bufferlist data_bl;
+  ceph::encode("data", data_bl);
+  MockGetMetadataRequest mock_get_metadata_request;
+  expect_get_metadata(mock_get_metadata_request,
+                      {{"key1", {}}, {"key2", {}}}, -EINVAL);
+
+  librbd::mirror::snapshot::ImageState image_state;
+  image_state.name = m_image_name;
+  image_state.features = m_remote_image_ctx->features;
+  image_state.metadata = {{"key1", data_bl}};
+
+  C_SaferCond ctx;
+  auto req = MockApplyImageStateRequest::create(
+    "local mirror uuid", "remote mirror uuid", m_mock_local_image_ctx,
+    m_mock_remote_image_ctx, image_state, &ctx);
+  req->send();
+  ASSERT_EQ(-EINVAL, ctx.wait());
+}
+
+TEST_F(TestMockImageReplayerSnapshotApplyImageStateRequest, UpdateImageMetaError) {
+  InSequence seq;
+
+  bufferlist data_bl;
+  ceph::encode("data", data_bl);
+  MockGetMetadataRequest mock_get_metadata_request;
+  expect_get_metadata(mock_get_metadata_request,
+                      {{"key1", {}}, {"key2", {}}}, 0);
+  expect_update_metadata({"key2"}, {{"key1", data_bl}}, -EINVAL);
+
+  librbd::mirror::snapshot::ImageState image_state;
+  image_state.name = m_image_name;
+  image_state.features = m_remote_image_ctx->features;
+  image_state.metadata = {{"key1", data_bl}};
+
+  C_SaferCond ctx;
+  auto req = MockApplyImageStateRequest::create(
+    "local mirror uuid", "remote mirror uuid", m_mock_local_image_ctx,
+    m_mock_remote_image_ctx, image_state, &ctx);
+  req->send();
+  ASSERT_EQ(-EINVAL, ctx.wait());
+}
+
+TEST_F(TestMockImageReplayerSnapshotApplyImageStateRequest, UnprotectSnapshot) {
+  InSequence seq;
+
+  MockGetMetadataRequest mock_get_metadata_request;
+  expect_get_metadata(mock_get_metadata_request, {}, 0);
+
+  expect_unprotect_snapshot("snap1", 0);
+
+  expect_set_snap_limit(0, 0);
+
+  librbd::mirror::snapshot::ImageState image_state;
+  image_state.name = m_image_name;
+  image_state.features = m_remote_image_ctx->features;
+  image_state.snapshots = {
+    {1U, {cls::rbd::UserSnapshotNamespace{}, "snap1",
+          RBD_PROTECTION_STATUS_UNPROTECTED}}};
+  m_mock_local_image_ctx->snap_info = {
+    {11U, librbd::SnapInfo{"snap1", cls::rbd::UserSnapshotNamespace{},
+                           0U, {}, RBD_PROTECTION_STATUS_PROTECTED, 0, {}}},
+    {12U, librbd::SnapInfo{"snap2", cls::rbd::MirrorSnapshotNamespace{
+       cls::rbd::MIRROR_SNAPSHOT_STATE_NON_PRIMARY, {}, "remote mirror uuid",
+       1, true, 0, {{1, 11}, {2, CEPH_NOSNAP}}},
+     0, {}, 0, 0, {}}}};
+
+  C_SaferCond ctx;
+  auto req = MockApplyImageStateRequest::create(
+    "local mirror uuid", "remote mirror uuid", m_mock_local_image_ctx,
+    m_mock_remote_image_ctx, image_state, &ctx);
+  req->send();
+  ASSERT_EQ(0, ctx.wait());
+}
+
+TEST_F(TestMockImageReplayerSnapshotApplyImageStateRequest, UnprotectSnapshotError) {
+  InSequence seq;
+
+  MockGetMetadataRequest mock_get_metadata_request;
+  expect_get_metadata(mock_get_metadata_request, {}, 0);
+
+  expect_unprotect_snapshot("snap1", -EINVAL);
+
+  librbd::mirror::snapshot::ImageState image_state;
+  image_state.name = m_image_name;
+  image_state.features = m_remote_image_ctx->features;
+  image_state.snapshots = {
+    {1U, {cls::rbd::UserSnapshotNamespace{}, "snap1",
+          RBD_PROTECTION_STATUS_UNPROTECTED}}};
+  m_mock_local_image_ctx->snap_info = {
+    {11U, librbd::SnapInfo{"snap1", cls::rbd::UserSnapshotNamespace{},
+                           0U, {}, RBD_PROTECTION_STATUS_PROTECTED, 0, {}}},
+    {12U, librbd::SnapInfo{"snap2", cls::rbd::MirrorSnapshotNamespace{
+       cls::rbd::MIRROR_SNAPSHOT_STATE_NON_PRIMARY, {}, "remote mirror uuid",
+       1, true, 0, {{1, 11}, {2, CEPH_NOSNAP}}},
+     0, {}, 0, 0, {}}}};
+
+  C_SaferCond ctx;
+  auto req = MockApplyImageStateRequest::create(
+    "local mirror uuid", "remote mirror uuid", m_mock_local_image_ctx,
+    m_mock_remote_image_ctx, image_state, &ctx);
+  req->send();
+  ASSERT_EQ(-EINVAL, ctx.wait());
+}
+
+TEST_F(TestMockImageReplayerSnapshotApplyImageStateRequest, RemoveSnapshot) {
+  InSequence seq;
+
+  MockGetMetadataRequest mock_get_metadata_request;
+  expect_get_metadata(mock_get_metadata_request, {}, 0);
+
+  expect_remove_snapshot("snap1", 0);
+
+  expect_set_snap_limit(0, 0);
+
+  librbd::mirror::snapshot::ImageState image_state;
+  image_state.name = m_image_name;
+  image_state.features = m_remote_image_ctx->features;
+  m_mock_local_image_ctx->snap_info = {
+    {11U, librbd::SnapInfo{"snap1", cls::rbd::UserSnapshotNamespace{},
+                           0U, {}, RBD_PROTECTION_STATUS_UNPROTECTED, 0, {}}},
+    {12U, librbd::SnapInfo{"snap2", cls::rbd::MirrorSnapshotNamespace{
+       cls::rbd::MIRROR_SNAPSHOT_STATE_NON_PRIMARY, {}, "remote mirror uuid",
+       1, true, 0, {{1, 11}, {2, CEPH_NOSNAP}}},
+     0, {}, 0, 0, {}}}};
+
+  C_SaferCond ctx;
+  auto req = MockApplyImageStateRequest::create(
+    "local mirror uuid", "remote mirror uuid", m_mock_local_image_ctx,
+    m_mock_remote_image_ctx, image_state, &ctx);
+  req->send();
+  ASSERT_EQ(0, ctx.wait());
+}
+
+TEST_F(TestMockImageReplayerSnapshotApplyImageStateRequest, RemoveSnapshotError) {
+  InSequence seq;
+
+  MockGetMetadataRequest mock_get_metadata_request;
+  expect_get_metadata(mock_get_metadata_request, {}, 0);
+
+  expect_remove_snapshot("snap1", -EINVAL);
+
+  librbd::mirror::snapshot::ImageState image_state;
+  image_state.name = m_image_name;
+  image_state.features = m_remote_image_ctx->features;
+  m_mock_local_image_ctx->snap_info = {
+    {11U, librbd::SnapInfo{"snap1", cls::rbd::UserSnapshotNamespace{},
+                           0U, {}, RBD_PROTECTION_STATUS_UNPROTECTED, 0, {}}},
+    {12U, librbd::SnapInfo{"snap2", cls::rbd::MirrorSnapshotNamespace{
+       cls::rbd::MIRROR_SNAPSHOT_STATE_NON_PRIMARY, {}, "remote mirror uuid",
+       1, true, 0, {{1, 11}, {2, CEPH_NOSNAP}}},
+     0, {}, 0, 0, {}}}};
+
+  C_SaferCond ctx;
+  auto req = MockApplyImageStateRequest::create(
+    "local mirror uuid", "remote mirror uuid", m_mock_local_image_ctx,
+    m_mock_remote_image_ctx, image_state, &ctx);
+  req->send();
+  ASSERT_EQ(-EINVAL, ctx.wait());
+}
+
+TEST_F(TestMockImageReplayerSnapshotApplyImageStateRequest, ProtectSnapshot) {
+  InSequence seq;
+
+  MockGetMetadataRequest mock_get_metadata_request;
+  expect_get_metadata(mock_get_metadata_request, {}, 0);
+
+  expect_protect_snapshot("snap1", 0);
+
+  expect_set_snap_limit(0, 0);
+
+  librbd::mirror::snapshot::ImageState image_state;
+  image_state.name = m_image_name;
+  image_state.features = m_remote_image_ctx->features;
+  image_state.snapshots = {
+    {1U, {cls::rbd::UserSnapshotNamespace{}, "snap1",
+          RBD_PROTECTION_STATUS_PROTECTED}}};
+  m_mock_local_image_ctx->snap_info = {
+    {11U, librbd::SnapInfo{"snap1", cls::rbd::UserSnapshotNamespace{},
+                           0U, {}, RBD_PROTECTION_STATUS_UNPROTECTED, 0, {}}},
+    {12U, librbd::SnapInfo{"snap2", cls::rbd::MirrorSnapshotNamespace{
+       cls::rbd::MIRROR_SNAPSHOT_STATE_NON_PRIMARY, {}, "remote mirror uuid",
+       1, true, 0, {{1, 11}, {2, CEPH_NOSNAP}}},
+     0, {}, 0, 0, {}}}};
+
+  C_SaferCond ctx;
+  auto req = MockApplyImageStateRequest::create(
+    "local mirror uuid", "remote mirror uuid", m_mock_local_image_ctx,
+    m_mock_remote_image_ctx, image_state, &ctx);
+  req->send();
+  ASSERT_EQ(0, ctx.wait());
+}
+
+TEST_F(TestMockImageReplayerSnapshotApplyImageStateRequest, ProtectSnapshotError) {
+  InSequence seq;
+
+  MockGetMetadataRequest mock_get_metadata_request;
+  expect_get_metadata(mock_get_metadata_request, {}, 0);
+
+  expect_protect_snapshot("snap1", -EINVAL);
+
+  librbd::mirror::snapshot::ImageState image_state;
+  image_state.name = m_image_name;
+  image_state.features = m_remote_image_ctx->features;
+  image_state.snapshots = {
+    {1U, {cls::rbd::UserSnapshotNamespace{}, "snap1",
+          RBD_PROTECTION_STATUS_PROTECTED}}};
+  m_mock_local_image_ctx->snap_info = {
+    {11U, librbd::SnapInfo{"snap1", cls::rbd::UserSnapshotNamespace{},
+                           0U, {}, RBD_PROTECTION_STATUS_UNPROTECTED, 0, {}}},
+    {12U, librbd::SnapInfo{"snap2", cls::rbd::MirrorSnapshotNamespace{
+       cls::rbd::MIRROR_SNAPSHOT_STATE_NON_PRIMARY, {}, "remote mirror uuid",
+       1, true, 0, {{1, 11}, {2, CEPH_NOSNAP}}},
+     0, {}, 0, 0, {}}}};
+
+  C_SaferCond ctx;
+  auto req = MockApplyImageStateRequest::create(
+    "local mirror uuid", "remote mirror uuid", m_mock_local_image_ctx,
+    m_mock_remote_image_ctx, image_state, &ctx);
+  req->send();
+  ASSERT_EQ(-EINVAL, ctx.wait());
+}
+
+TEST_F(TestMockImageReplayerSnapshotApplyImageStateRequest, RenameSnapshot) {
+  InSequence seq;
+
+  MockGetMetadataRequest mock_get_metadata_request;
+  expect_get_metadata(mock_get_metadata_request, {}, 0);
+
+  expect_rename_snapshot(11, "snap1-renamed", 0);
+
+  expect_set_snap_limit(0, 0);
+
+  librbd::mirror::snapshot::ImageState image_state;
+  image_state.name = m_image_name;
+  image_state.features = m_remote_image_ctx->features;
+  image_state.snapshots = {
+    {1U, {cls::rbd::UserSnapshotNamespace{}, "snap1-renamed",
+          RBD_PROTECTION_STATUS_PROTECTED}}};
+  m_mock_local_image_ctx->snap_info = {
+    {11U, librbd::SnapInfo{"snap1", cls::rbd::UserSnapshotNamespace{},
+                           0U, {}, RBD_PROTECTION_STATUS_PROTECTED, 0, {}}},
+    {12U, librbd::SnapInfo{"snap2", cls::rbd::MirrorSnapshotNamespace{
+       cls::rbd::MIRROR_SNAPSHOT_STATE_NON_PRIMARY, {}, "remote mirror uuid",
+       1, true, 0, {{1, 11}, {2, CEPH_NOSNAP}}},
+     0, {}, 0, 0, {}}}};
+
+  C_SaferCond ctx;
+  auto req = MockApplyImageStateRequest::create(
+    "local mirror uuid", "remote mirror uuid", m_mock_local_image_ctx,
+    m_mock_remote_image_ctx, image_state, &ctx);
+  req->send();
+  ASSERT_EQ(0, ctx.wait());
+}
+
+TEST_F(TestMockImageReplayerSnapshotApplyImageStateRequest, RenameSnapshotError) {
+  InSequence seq;
+
+  MockGetMetadataRequest mock_get_metadata_request;
+  expect_get_metadata(mock_get_metadata_request, {}, 0);
+
+  expect_rename_snapshot(11, "snap1-renamed", -EINVAL);
+
+  librbd::mirror::snapshot::ImageState image_state;
+  image_state.name = m_image_name;
+  image_state.features = m_remote_image_ctx->features;
+  image_state.snapshots = {
+    {1U, {cls::rbd::UserSnapshotNamespace{}, "snap1-renamed",
+          RBD_PROTECTION_STATUS_PROTECTED}}};
+  m_mock_local_image_ctx->snap_info = {
+    {11U, librbd::SnapInfo{"snap1", cls::rbd::UserSnapshotNamespace{},
+                           0U, {}, RBD_PROTECTION_STATUS_PROTECTED, 0, {}}},
+    {12U, librbd::SnapInfo{"snap2", cls::rbd::MirrorSnapshotNamespace{
+       cls::rbd::MIRROR_SNAPSHOT_STATE_NON_PRIMARY, {}, "remote mirror uuid",
+       1, true, 0, {{1, 11}, {2, CEPH_NOSNAP}}},
+     0, {}, 0, 0, {}}}};
+
+  C_SaferCond ctx;
+  auto req = MockApplyImageStateRequest::create(
+    "local mirror uuid", "remote mirror uuid", m_mock_local_image_ctx,
+    m_mock_remote_image_ctx, image_state, &ctx);
+  req->send();
+  ASSERT_EQ(-EINVAL, ctx.wait());
+}
+
+TEST_F(TestMockImageReplayerSnapshotApplyImageStateRequest, SetSnapshotLimitError) {
+  InSequence seq;
+
+  MockGetMetadataRequest mock_get_metadata_request;
+  expect_get_metadata(mock_get_metadata_request, {}, 0);
+
+  expect_set_snap_limit(0, -EINVAL);
+
+  librbd::mirror::snapshot::ImageState image_state;
+  image_state.name = m_image_name;
+  image_state.features = m_remote_image_ctx->features;
+
+  C_SaferCond ctx;
+  auto req = MockApplyImageStateRequest::create(
+    "local mirror uuid", "remote mirror uuid", m_mock_local_image_ctx,
+    m_mock_remote_image_ctx, image_state, &ctx);
+  req->send();
+  ASSERT_EQ(-EINVAL, ctx.wait());
+}
+
+} // namespace journal
+} // namespace image_replayer
+} // namespace mirror
+} // namespace rbd
index 6140e3e62eed29ff6dd5aef7653d8cc4fdd7aa22..1c288c9cc5f8b75654b25da8a9d730c62e6019f5 100644 (file)
@@ -51,6 +51,7 @@ set(rbd_mirror_internal
   image_replayer/journal/ReplayStatusFormatter.cc
   image_replayer/journal/StateBuilder.cc
   image_replayer/journal/SyncPointHandler.cc
+  image_replayer/snapshot/ApplyImageStateRequest.cc
   image_replayer/snapshot/CreateLocalImageRequest.cc
   image_replayer/snapshot/PrepareReplayRequest.cc
   image_replayer/snapshot/Replayer.cc
diff --git a/src/tools/rbd_mirror/image_replayer/snapshot/ApplyImageStateRequest.cc b/src/tools/rbd_mirror/image_replayer/snapshot/ApplyImageStateRequest.cc
new file mode 100644 (file)
index 0000000..3d628ef
--- /dev/null
@@ -0,0 +1,677 @@
+// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*-
+// vim: ts=8 sw=2 smarttab
+
+#include "ApplyImageStateRequest.h"
+#include "common/debug.h"
+#include "common/errno.h"
+#include "cls/rbd/cls_rbd_client.h"
+#include "librbd/ImageCtx.h"
+#include "librbd/Operations.h"
+#include "librbd/Utils.h"
+#include "librbd/image/GetMetadataRequest.h"
+
+#define dout_context g_ceph_context
+#define dout_subsys ceph_subsys_rbd_mirror
+#undef dout_prefix
+#define dout_prefix *_dout << "rbd::mirror::image_replayer::snapshot::" \
+                           << "ApplyImageStateRequest: " << this << " " \
+                           << __func__ << ": "
+
+namespace rbd {
+namespace mirror {
+namespace image_replayer {
+namespace snapshot {
+
+using librbd::util::create_context_callback;
+using librbd::util::create_rados_callback;
+
+template <typename I>
+ApplyImageStateRequest<I>::ApplyImageStateRequest(
+    const std::string& local_mirror_uuid,
+    const std::string& remote_mirror_uuid,
+    I* local_image_ctx,
+    I* remote_image_ctx,
+    librbd::mirror::snapshot::ImageState image_state,
+    Context* on_finish)
+  : m_local_mirror_uuid(local_mirror_uuid),
+    m_remote_mirror_uuid(remote_mirror_uuid),
+    m_local_image_ctx(local_image_ctx),
+    m_remote_image_ctx(remote_image_ctx),
+    m_image_state(image_state),
+    m_on_finish(on_finish) {
+  dout(15) << "image_state=" << m_image_state << dendl;
+
+  std::shared_lock image_locker{m_local_image_ctx->image_lock};
+  m_features = m_local_image_ctx->features & ~RBD_FEATURES_IMPLICIT_ENABLE;
+  compute_local_to_remote_snap_ids();
+}
+
+template <typename I>
+void ApplyImageStateRequest<I>::send() {
+  rename_image();
+}
+
+template <typename I>
+void ApplyImageStateRequest<I>::rename_image() {
+  std::shared_lock owner_locker{m_local_image_ctx->owner_lock};
+  std::shared_lock image_locker{m_local_image_ctx->image_lock};
+  if (m_local_image_ctx->name == m_image_state.name) {
+    image_locker.unlock();
+    owner_locker.unlock();
+
+    update_features();
+    return;
+  }
+  image_locker.unlock();
+
+  dout(15) << "local_image_name=" << m_local_image_ctx->name << ", "
+           << "remote_image_name=" << m_image_state.name << dendl;
+
+  auto ctx = create_context_callback<
+    ApplyImageStateRequest<I>,
+    &ApplyImageStateRequest<I>::handle_rename_image>(this);
+  m_local_image_ctx->operations->execute_rename(m_image_state.name, ctx);
+}
+
+template <typename I>
+void ApplyImageStateRequest<I>::handle_rename_image(int r) {
+  dout(15) << "r=" << r << dendl;
+
+  if (r < 0) {
+    derr << "failed to rename image to '" << m_image_state.name << "': "
+         << cpp_strerror(r) << dendl;
+    finish(r);
+    return;
+  }
+
+  update_features();
+}
+
+template <typename I>
+void ApplyImageStateRequest<I>::update_features() {
+  uint64_t feature_updates = 0UL;
+  bool enabled = false;
+
+  auto image_state_features =
+    m_image_state.features & ~RBD_FEATURES_IMPLICIT_ENABLE;
+  feature_updates = (m_features & ~image_state_features);
+  if (feature_updates == 0UL) {
+    feature_updates = (image_state_features & ~m_features);
+    enabled = (feature_updates != 0UL);
+  }
+
+  if (feature_updates == 0UL) {
+    get_image_meta();
+    return;
+  }
+
+  dout(15) << "image_features=" << m_features << ", "
+           << "state_features=" << image_state_features << ", "
+           << "feature_updates=" << feature_updates << ", "
+           << "enabled=" << enabled << dendl;
+
+  if (enabled) {
+    m_features |= feature_updates;
+  } else {
+    m_features &= ~feature_updates;
+  }
+
+  std::shared_lock owner_lock{m_local_image_ctx->owner_lock};
+  auto ctx = create_context_callback<
+    ApplyImageStateRequest<I>,
+    &ApplyImageStateRequest<I>::handle_update_features>(this);
+  m_local_image_ctx->operations->execute_update_features(
+    feature_updates, enabled, ctx, 0U);
+}
+
+template <typename I>
+void ApplyImageStateRequest<I>::handle_update_features(int r) {
+  dout(15) << "r=" << r << dendl;
+
+  if (r < 0) {
+    derr << "failed to update image features: " << cpp_strerror(r) << dendl;
+    finish(r);
+    return;
+  }
+
+  update_features();
+}
+
+template <typename I>
+void ApplyImageStateRequest<I>::get_image_meta() {
+  dout(15) << dendl;
+
+  auto ctx = create_context_callback<
+    ApplyImageStateRequest<I>,
+    &ApplyImageStateRequest<I>::handle_get_image_meta>(this);
+  auto req = librbd::image::GetMetadataRequest<I>::create(
+    m_local_image_ctx->md_ctx, m_local_image_ctx->header_oid, "", "", 0U,
+    &m_metadata, ctx);
+  req->send();
+}
+
+template <typename I>
+void ApplyImageStateRequest<I>::handle_get_image_meta(int r) {
+  dout(15) << "r=" << r << dendl;
+
+  if (r < 0) {
+    derr << "failed to fetch local image metadata: " << cpp_strerror(r)
+         << dendl;
+    finish(r);
+    return;
+  }
+
+  update_image_meta();
+}
+
+template <typename I>
+void ApplyImageStateRequest<I>::update_image_meta() {
+  std::set<std::string> keys_to_remove;
+  for (const auto& [key, value] : m_metadata) {
+    if (m_image_state.metadata.count(key) == 0) {
+      dout(15) << "removing image-meta key '" << key << "'" << dendl;
+      keys_to_remove.insert(key);
+    }
+  }
+
+  std::map<std::string, bufferlist> metadata_to_update;
+  for (const auto& [key, value] : m_image_state.metadata) {
+    auto it = m_metadata.find(key);
+    if (it == m_metadata.end() || !it->second.contents_equal(value)) {
+      dout(15) << "updating image-meta key '" << key << "'" << dendl;
+      metadata_to_update.insert({key, value});
+    }
+  }
+
+  if (keys_to_remove.empty() && metadata_to_update.empty()) {
+    unprotect_snapshot();
+    return;
+  }
+
+  dout(15) << dendl;
+
+  librados::ObjectWriteOperation op;
+  for (const auto& key : keys_to_remove) {
+    librbd::cls_client::metadata_remove(&op, key);
+  }
+  if (!metadata_to_update.empty()) {
+    librbd::cls_client::metadata_set(&op, metadata_to_update);
+  }
+
+  auto aio_comp = create_rados_callback<
+    ApplyImageStateRequest<I>,
+    &ApplyImageStateRequest<I>::handle_update_image_meta>(this);
+  int r = m_local_image_ctx->md_ctx.aio_operate(m_local_image_ctx->header_oid, aio_comp,
+                                          &op);
+  ceph_assert(r == 0);
+  aio_comp->release();
+}
+
+template <typename I>
+void ApplyImageStateRequest<I>::handle_update_image_meta(int r) {
+  dout(15) << "r=" << r << dendl;
+
+  if (r < 0) {
+    derr << "failed to update image metadata: " << cpp_strerror(r) << dendl;
+    finish(r);
+    return;
+  }
+
+  m_metadata.clear();
+
+  m_prev_snap_id = CEPH_NOSNAP;
+  unprotect_snapshot();
+}
+
+template <typename I>
+void ApplyImageStateRequest<I>::unprotect_snapshot() {
+  std::shared_lock image_locker{m_local_image_ctx->image_lock};
+
+  auto snap_it = m_local_image_ctx->snap_info.begin();
+  if (m_prev_snap_id != CEPH_NOSNAP) {
+    snap_it = m_local_image_ctx->snap_info.upper_bound(m_prev_snap_id);
+  }
+
+  for (; snap_it != m_local_image_ctx->snap_info.end(); ++snap_it) {
+    auto snap_id = snap_it->first;
+    const auto& snap_info = snap_it->second;
+
+    auto user_ns = boost::get<cls::rbd::UserSnapshotNamespace>(
+      &snap_info.snap_namespace);
+    if (user_ns == nullptr) {
+      dout(20) << "snapshot " << snap_id << " is not a user snapshot" << dendl;
+      continue;
+    }
+
+    if (snap_info.protection_status == RBD_PROTECTION_STATUS_UNPROTECTED) {
+      dout(20) << "snapshot " << snap_id << " is already unprotected" << dendl;
+      continue;
+    }
+
+    auto snap_id_map_it = m_local_to_remote_snap_ids.find(snap_id);
+    if (snap_id_map_it == m_local_to_remote_snap_ids.end()) {
+      dout(15) << "snapshot " << snap_id << " does not exist in remote image"
+               << dendl;
+      break;
+    }
+
+    auto remote_snap_id = snap_id_map_it->second;
+    auto snap_state_it = m_image_state.snapshots.find(remote_snap_id);
+    if (snap_state_it == m_image_state.snapshots.end()) {
+      dout(15) << "snapshot " << snap_id << " does not exist in remote image "
+               << "state" << dendl;
+      break;
+    }
+
+    const auto& snap_state = snap_state_it->second;
+    if (snap_state.protection_status == RBD_PROTECTION_STATUS_UNPROTECTED) {
+      dout(15) << "snapshot " << snap_id << " is unprotected in remote image"
+               << dendl;
+      break;
+    }
+  }
+
+  if (snap_it == m_local_image_ctx->snap_info.end()) {
+    image_locker.unlock();
+
+    // no local snapshots to unprotect
+    m_prev_snap_id = CEPH_NOSNAP;
+    remove_snapshot();
+    return;
+  }
+
+  m_prev_snap_id = snap_it->first;
+  m_snap_name = snap_it->second.name;
+  image_locker.unlock();
+
+  dout(15) << "snap_name=" << m_snap_name << ", "
+           << "snap_id=" << m_prev_snap_id << dendl;
+
+  std::shared_lock owner_locker{m_local_image_ctx->owner_lock};
+  auto ctx = create_context_callback<
+    ApplyImageStateRequest<I>,
+    &ApplyImageStateRequest<I>::handle_unprotect_snapshot>(this);
+  m_local_image_ctx->operations->execute_snap_unprotect(
+    cls::rbd::UserSnapshotNamespace{}, m_snap_name.c_str(), ctx);
+}
+
+template <typename I>
+void ApplyImageStateRequest<I>::handle_unprotect_snapshot(int r) {
+  dout(15) << "r=" << r << dendl;
+
+  if (r < 0) {
+    derr << "failed to unprotect snapshot " << m_snap_name << ": "
+         << cpp_strerror(r) << dendl;
+    finish(r);
+    return;
+  }
+
+  unprotect_snapshot();
+}
+
+template <typename I>
+void ApplyImageStateRequest<I>::remove_snapshot() {
+  std::shared_lock image_locker{m_local_image_ctx->image_lock};
+
+  auto snap_it = m_local_image_ctx->snap_info.begin();
+  if (m_prev_snap_id != CEPH_NOSNAP) {
+    snap_it = m_local_image_ctx->snap_info.upper_bound(m_prev_snap_id);
+  }
+
+  for (; snap_it != m_local_image_ctx->snap_info.end(); ++snap_it) {
+    auto snap_id = snap_it->first;
+    const auto& snap_info = snap_it->second;
+
+    auto user_ns = boost::get<cls::rbd::UserSnapshotNamespace>(
+      &snap_info.snap_namespace);
+    if (user_ns == nullptr) {
+      dout(20) << "snapshot " << snap_id << " is not a user snapshot" << dendl;
+      continue;
+    }
+
+    auto snap_id_map_it = m_local_to_remote_snap_ids.find(snap_id);
+    if (snap_id_map_it == m_local_to_remote_snap_ids.end()) {
+      dout(15) << "snapshot " << snap_id << " does not exist in remote image"
+               << dendl;
+      break;
+    }
+
+    auto remote_snap_id = snap_id_map_it->second;
+    auto snap_state_it = m_image_state.snapshots.find(remote_snap_id);
+    if (snap_state_it == m_image_state.snapshots.end()) {
+      dout(15) << "snapshot " << snap_id << " does not exist in remote image "
+               << "state" << dendl;
+      break;
+    }
+  }
+
+  if (snap_it == m_local_image_ctx->snap_info.end()) {
+    image_locker.unlock();
+
+    // no local snapshots to remove
+    m_prev_snap_id = CEPH_NOSNAP;
+    protect_snapshot();
+    return;
+  }
+
+  m_prev_snap_id = snap_it->first;
+  m_snap_name = snap_it->second.name;
+  image_locker.unlock();
+
+  dout(15) << "snap_name=" << m_snap_name << ", "
+           << "snap_id=" << m_prev_snap_id << dendl;
+
+  std::shared_lock owner_locker{m_local_image_ctx->owner_lock};
+  auto ctx = create_context_callback<
+    ApplyImageStateRequest<I>,
+    &ApplyImageStateRequest<I>::handle_remove_snapshot>(this);
+  m_local_image_ctx->operations->execute_snap_remove(
+    cls::rbd::UserSnapshotNamespace{}, m_snap_name.c_str(), ctx);
+}
+
+template <typename I>
+void ApplyImageStateRequest<I>::handle_remove_snapshot(int r) {
+  dout(15) << "r=" << r << dendl;
+
+  if (r < 0) {
+    derr << "failed to remove snapshot " << m_snap_name << ": "
+         << cpp_strerror(r) << dendl;
+    finish(r);
+    return;
+  }
+
+  remove_snapshot();
+}
+
+template <typename I>
+void ApplyImageStateRequest<I>::protect_snapshot() {
+  std::shared_lock image_locker{m_local_image_ctx->image_lock};
+
+  auto snap_it = m_local_image_ctx->snap_info.begin();
+  if (m_prev_snap_id != CEPH_NOSNAP) {
+    snap_it = m_local_image_ctx->snap_info.upper_bound(m_prev_snap_id);
+  }
+
+  for (; snap_it != m_local_image_ctx->snap_info.end(); ++snap_it) {
+    auto snap_id = snap_it->first;
+    const auto& snap_info = snap_it->second;
+
+    auto user_ns = boost::get<cls::rbd::UserSnapshotNamespace>(
+      &snap_info.snap_namespace);
+    if (user_ns == nullptr) {
+      dout(20) << "snapshot " << snap_id << " is not a user snapshot" << dendl;
+      continue;
+    }
+
+    if (snap_info.protection_status == RBD_PROTECTION_STATUS_PROTECTED) {
+      dout(20) << "snapshot " << snap_id << " is already protected" << dendl;
+      continue;
+    }
+
+    auto snap_id_map_it = m_local_to_remote_snap_ids.find(snap_id);
+    if (snap_id_map_it == m_local_to_remote_snap_ids.end()) {
+      dout(15) << "snapshot " << snap_id << " does not exist in remote image"
+               << dendl;
+      continue;
+    }
+
+    auto remote_snap_id = snap_id_map_it->second;
+    auto snap_state_it = m_image_state.snapshots.find(remote_snap_id);
+    if (snap_state_it == m_image_state.snapshots.end()) {
+      dout(15) << "snapshot " << snap_id << " does not exist in remote image "
+               << "state" << dendl;
+      continue;
+    }
+
+    const auto& snap_state = snap_state_it->second;
+    if (snap_state.protection_status == RBD_PROTECTION_STATUS_PROTECTED) {
+      dout(15) << "snapshot " << snap_id << " is protected in remote image"
+               << dendl;
+      break;
+    }
+  }
+
+  if (snap_it == m_local_image_ctx->snap_info.end()) {
+    image_locker.unlock();
+
+    // no local snapshots to protect
+    m_prev_snap_id = CEPH_NOSNAP;
+    rename_snapshot();
+    return;
+  }
+
+  m_prev_snap_id = snap_it->first;
+  m_snap_name = snap_it->second.name;
+  image_locker.unlock();
+
+  dout(15) << "snap_name=" << m_snap_name << ", "
+           << "snap_id=" << m_prev_snap_id << dendl;
+
+  std::shared_lock owner_locker{m_local_image_ctx->owner_lock};
+  auto ctx = create_context_callback<
+    ApplyImageStateRequest<I>,
+    &ApplyImageStateRequest<I>::handle_protect_snapshot>(this);
+  m_local_image_ctx->operations->execute_snap_protect(
+    cls::rbd::UserSnapshotNamespace{}, m_snap_name.c_str(), ctx);
+}
+
+template <typename I>
+void ApplyImageStateRequest<I>::handle_protect_snapshot(int r) {
+  dout(15) << "r=" << r << dendl;
+
+  if (r < 0) {
+    derr << "failed to protect snapshot " << m_snap_name << ": "
+         << cpp_strerror(r) << dendl;
+    finish(r);
+    return;
+  }
+
+  protect_snapshot();
+}
+
+template <typename I>
+void ApplyImageStateRequest<I>::rename_snapshot() {
+  std::shared_lock image_locker{m_local_image_ctx->image_lock};
+
+  auto snap_it = m_local_image_ctx->snap_info.begin();
+  if (m_prev_snap_id != CEPH_NOSNAP) {
+    snap_it = m_local_image_ctx->snap_info.upper_bound(m_prev_snap_id);
+  }
+
+  for (; snap_it != m_local_image_ctx->snap_info.end(); ++snap_it) {
+    auto snap_id = snap_it->first;
+    const auto& snap_info = snap_it->second;
+
+    auto user_ns = boost::get<cls::rbd::UserSnapshotNamespace>(
+      &snap_info.snap_namespace);
+    if (user_ns == nullptr) {
+      dout(20) << "snapshot " << snap_id << " is not a user snapshot" << dendl;
+      continue;
+    }
+
+    auto snap_id_map_it = m_local_to_remote_snap_ids.find(snap_id);
+    if (snap_id_map_it == m_local_to_remote_snap_ids.end()) {
+      dout(15) << "snapshot " << snap_id << " does not exist in remote image"
+               << dendl;
+      continue;
+    }
+
+    auto remote_snap_id = snap_id_map_it->second;
+    auto snap_state_it = m_image_state.snapshots.find(remote_snap_id);
+    if (snap_state_it == m_image_state.snapshots.end()) {
+      dout(15) << "snapshot " << snap_id << " does not exist in remote image "
+               << "state" << dendl;
+      continue;
+    }
+
+    const auto& snap_state = snap_state_it->second;
+    if (snap_info.name != snap_state.name) {
+      dout(15) << "snapshot " << snap_id << " has been renamed from '"
+               << snap_info.name << "' to '" << snap_state.name << "'"
+               << dendl;
+      m_snap_name = snap_state.name;
+      break;
+    }
+  }
+
+  if (snap_it == m_local_image_ctx->snap_info.end()) {
+    image_locker.unlock();
+
+    // no local snapshots to protect
+    m_prev_snap_id = CEPH_NOSNAP;
+    set_snapshot_limit();
+    return;
+  }
+
+  m_prev_snap_id = snap_it->first;
+  image_locker.unlock();
+
+  dout(15) << "snap_name=" << m_snap_name << ", "
+           << "snap_id=" << m_prev_snap_id << dendl;
+
+  std::shared_lock owner_locker{m_local_image_ctx->owner_lock};
+  auto ctx = create_context_callback<
+    ApplyImageStateRequest<I>,
+    &ApplyImageStateRequest<I>::handle_rename_snapshot>(this);
+  m_local_image_ctx->operations->execute_snap_rename(
+    m_prev_snap_id, m_snap_name.c_str(), ctx);
+}
+
+template <typename I>
+void ApplyImageStateRequest<I>::handle_rename_snapshot(int r) {
+  dout(15) << "r=" << r << dendl;
+
+  if (r < 0) {
+    derr << "failed to protect snapshot " << m_snap_name << ": "
+         << cpp_strerror(r) << dendl;
+    finish(r);
+    return;
+  }
+
+  rename_snapshot();
+}
+
+template <typename I>
+void ApplyImageStateRequest<I>::set_snapshot_limit() {
+  dout(15) << dendl;
+
+  // no need to even check the current limit -- just set it
+  std::shared_lock owner_locker{m_local_image_ctx->owner_lock};
+  auto ctx = create_context_callback<
+    ApplyImageStateRequest<I>,
+    &ApplyImageStateRequest<I>::handle_set_snapshot_limit>(this);
+  m_local_image_ctx->operations->execute_snap_set_limit(
+    m_image_state.snap_limit, ctx);
+}
+
+template <typename I>
+void ApplyImageStateRequest<I>::handle_set_snapshot_limit(int r) {
+  dout(15) << "r=" << r << dendl;
+
+  if (r < 0) {
+    derr << "failed to update snapshot limit: " << cpp_strerror(r)
+         << dendl;
+  }
+
+  finish(r);
+}
+
+template <typename I>
+void ApplyImageStateRequest<I>::finish(int r) {
+  dout(15) << "r=" << r << dendl;
+
+  m_on_finish->complete(r);
+  delete this;
+}
+
+template <typename I>
+uint64_t ApplyImageStateRequest<I>::compute_remote_snap_id(
+    uint64_t local_snap_id) {
+  ceph_assert(ceph_mutex_is_locked(m_local_image_ctx->image_lock));
+  ceph_assert(ceph_mutex_is_locked(m_remote_image_ctx->image_lock));
+
+  // Search our local non-primary snapshots for a mapping to the remote
+  // snapshot. The non-primary mirror snapshot with the mappings will always
+  // come at or after the snapshot we are searching against
+  auto snap_it = m_local_image_ctx->snap_info.lower_bound(local_snap_id);
+  for (; snap_it != m_local_image_ctx->snap_info.end(); ++snap_it) {
+    auto mirror_ns = boost::get<cls::rbd::MirrorSnapshotNamespace>(
+      &snap_it->second.snap_namespace);
+    if (mirror_ns == nullptr || !mirror_ns->is_non_primary()) {
+      continue;
+    }
+
+    if (mirror_ns->primary_mirror_uuid != m_remote_mirror_uuid) {
+      dout(20) << "local snapshot " << snap_it->first << " not tied to remote"
+               << dendl;
+      continue;
+    } else if (local_snap_id == snap_it->first) {
+      dout(15) << "local snapshot " << local_snap_id << " maps to "
+               << "remote snapshot " << mirror_ns->primary_snap_id << dendl;
+      return mirror_ns->primary_snap_id;
+    }
+
+    const auto& snap_seqs = mirror_ns->snap_seqs;
+    for (auto [remote_snap_id_seq, local_snap_id_seq] : snap_seqs) {
+      if (local_snap_id_seq == local_snap_id) {
+        dout(15) << "local snapshot " << local_snap_id << " maps to "
+                 << "remote snapshot " << remote_snap_id_seq << dendl;
+        return remote_snap_id_seq;
+      }
+    }
+  }
+
+  // if we failed to find a match to a remote snapshot in our local non-primary
+  // snapshots, check the remote image for non-primary snapshot mappings back
+  // to our snapshot
+  for (snap_it = m_remote_image_ctx->snap_info.begin();
+       snap_it != m_remote_image_ctx->snap_info.end(); ++snap_it) {
+    auto snap_id = snap_it->first;
+    auto mirror_ns = boost::get<cls::rbd::MirrorSnapshotNamespace>(
+      &snap_it->second.snap_namespace);
+    if (mirror_ns == nullptr || !mirror_ns->is_non_primary()) {
+      continue;
+    }
+
+    if (mirror_ns->primary_mirror_uuid != m_local_mirror_uuid) {
+      dout(20) << "remote snapshot " << snap_id << " not tied to local"
+               << dendl;
+      continue;
+    } else if (mirror_ns->primary_snap_id == local_snap_id) {
+        dout(15) << "local snapshot " << local_snap_id << " maps to "
+                 << "remote snapshot " << snap_id << dendl;
+        return snap_id;
+    }
+
+    const auto& snap_seqs = mirror_ns->snap_seqs;
+    for (auto [local_snap_id_seq, remote_snap_id_seq] : snap_seqs) {
+      if (local_snap_id_seq == local_snap_id) {
+        dout(15) << "local snapshot " << local_snap_id << " maps to "
+                 << "remote snapshot " << remote_snap_id_seq << dendl;
+        return remote_snap_id_seq;
+      }
+    }
+  }
+
+  return CEPH_NOSNAP;
+}
+
+template <typename I>
+void ApplyImageStateRequest<I>::compute_local_to_remote_snap_ids() {
+  ceph_assert(ceph_mutex_is_locked(m_local_image_ctx->image_lock));
+  std::shared_lock remote_image_locker{m_remote_image_ctx->image_lock};
+
+  for (const auto& [snap_id, snap_info] : m_local_image_ctx->snap_info) {
+    m_local_to_remote_snap_ids[snap_id] = compute_remote_snap_id(snap_id);
+  }
+
+  dout(15) << "local_to_remote_snap_ids=" << m_local_to_remote_snap_ids
+           << dendl;
+}
+
+} // namespace snapshot
+} // namespace image_replayer
+} // namespace mirror
+} // namespace rbd
+
+template class rbd::mirror::image_replayer::snapshot::ApplyImageStateRequest<librbd::ImageCtx>;
diff --git a/src/tools/rbd_mirror/image_replayer/snapshot/ApplyImageStateRequest.h b/src/tools/rbd_mirror/image_replayer/snapshot/ApplyImageStateRequest.h
new file mode 100644 (file)
index 0000000..0e2d09d
--- /dev/null
@@ -0,0 +1,155 @@
+// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*-
+// vim: ts=8 sw=2 smarttab
+
+#ifndef RBD_MIRROR_IMAGE_REPLAYER_SNAPSHOT_APPLY_IMAGE_STATE_REQUEST_H
+#define RBD_MIRROR_IMAGE_REPLAYER_SNAPSHOT_APPLY_IMAGE_STATE_REQUEST_H
+
+#include "common/ceph_mutex.h"
+#include "librbd/mirror/snapshot/Types.h"
+#include <map>
+#include <string>
+
+struct Context;
+
+namespace librbd {
+
+struct ImageCtx;
+
+} // namespace librbd
+
+namespace rbd {
+namespace mirror {
+namespace image_replayer {
+namespace snapshot {
+
+template <typename> class EventPreprocessor;
+template <typename> class ReplayStatusFormatter;
+template <typename> class StateBuilder;
+
+template <typename ImageCtxT>
+class ApplyImageStateRequest {
+public:
+  static ApplyImageStateRequest* create(
+      const std::string& local_mirror_uuid,
+      const std::string& remote_mirror_uuid,
+      ImageCtxT* local_image_ctx,
+      ImageCtxT* remote_image_ctx,
+      librbd::mirror::snapshot::ImageState image_state,
+      Context* on_finish) {
+    return new ApplyImageStateRequest(local_mirror_uuid, remote_mirror_uuid,
+                                      local_image_ctx, remote_image_ctx,
+                                      image_state, on_finish);
+  }
+
+  ApplyImageStateRequest(
+      const std::string& local_mirror_uuid,
+      const std::string& remote_mirror_uuid,
+      ImageCtxT* local_image_ctx,
+      ImageCtxT* remote_image_ctx,
+      librbd::mirror::snapshot::ImageState image_state,
+      Context* on_finish);
+
+  void send();
+
+private:
+  /**
+   * @verbatim
+   *
+   * <start>
+   *    |
+   *    v
+   * RENAME_IMAGE
+   *    |
+   *    |       /---------\
+   *    |       |         |
+   *    v       v         |
+   * UPDATE_FEATURES -----/
+   *    |
+   *    v
+   * GET_IMAGE_META
+   *    |
+   *    |       /---------\
+   *    |       |         |
+   *    v       v         |
+   * UPDATE_IMAGE_META ---/
+   *    |
+   *    |       /---------\
+   *    |       |         |
+   *    v       v         |
+   * UNPROTECT_SNAPSHOT   |
+   *    |                 |
+   *    v                 |
+   * REMOVE_SNAPSHOT      |
+   *    |                 |
+   *    v                 |
+   * PROTECT_SNAPSHOT     |
+   *    |                 |
+   *    v                 |
+   * RENAME_SNAPSHOT -----/
+   *    |
+   *    v
+   * SET_SNAPSHOT_LIMIT
+   *    |
+   *    v
+   * <finish>
+   *
+   * @endverbatim
+   */
+
+  std::string m_local_mirror_uuid;
+  std::string m_remote_mirror_uuid;
+  ImageCtxT* m_local_image_ctx;
+  ImageCtxT* m_remote_image_ctx;
+  librbd::mirror::snapshot::ImageState m_image_state;
+  Context* m_on_finish;
+
+  std::map<uint64_t, uint64_t> m_local_to_remote_snap_ids;
+
+  uint64_t m_features = 0;
+
+  std::map<std::string, bufferlist> m_metadata;
+
+  uint64_t m_prev_snap_id = 0;
+  std::string m_snap_name;
+
+  void rename_image();
+  void handle_rename_image(int r);
+
+  void update_features();
+  void handle_update_features(int r);
+
+  void get_image_meta();
+  void handle_get_image_meta(int r);
+
+  void update_image_meta();
+  void handle_update_image_meta(int r);
+
+  void unprotect_snapshot();
+  void handle_unprotect_snapshot(int r);
+
+  void remove_snapshot();
+  void handle_remove_snapshot(int r);
+
+  void protect_snapshot();
+  void handle_protect_snapshot(int r);
+
+  void rename_snapshot();
+  void handle_rename_snapshot(int r);
+
+  void set_snapshot_limit();
+  void handle_set_snapshot_limit(int r);
+
+  void finish(int r);
+
+  uint64_t compute_remote_snap_id(uint64_t snap_id);
+  void compute_local_to_remote_snap_ids();
+};
+
+} // namespace snapshot
+} // namespace image_replayer
+} // namespace mirror
+} // namespace rbd
+
+extern template class rbd::mirror::image_replayer::snapshot::ApplyImageStateRequest<librbd::ImageCtx>;
+
+#endif // RBD_MIRROR_IMAGE_REPLAYER_SNAPSHOT_APPLY_IMAGE_STATE_REQUEST_H