]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
librbd: auto-remove trash snapshots when image is deleted 20376/head
authorJason Dillaman <dillaman@redhat.com>
Thu, 8 Feb 2018 22:12:00 +0000 (17:12 -0500)
committerJason Dillaman <dillaman@redhat.com>
Fri, 9 Feb 2018 18:34:55 +0000 (13:34 -0500)
Fixes: http://tracker.ceph.com/issues/22873
Signed-off-by: Jason Dillaman <dillaman@redhat.com>
qa/workunits/rbd/cli_generic.sh
src/librbd/image/RemoveRequest.cc
src/librbd/image/RemoveRequest.h
src/test/librbd/image/test_mock_RemoveRequest.cc
src/tools/rbd/action/Remove.cc

index 27e313d955def2ea532eda7d93507e2646f2b318..3a4bdb39ebbd769476f85ba33d4de9e578e50b81 100755 (executable)
@@ -3,7 +3,7 @@
 # make sure rbd pool is EMPTY.. this is a test script!!
 rbd ls | wc -l | grep -v '^0$' && echo "nonempty rbd pool, aborting!  run this script on an empty test cluster only." && exit 1
 
-IMGS="testimg1 testimg2 testimg3 testimg4 testimg5 testimg6 testimg-diff1 testimg-diff2 testimg-diff3 foo foo2 bar bar2 test1 test2 test3 clone2"
+IMGS="testimg1 testimg2 testimg3 testimg4 testimg5 testimg6 testimg-diff1 testimg-diff2 testimg-diff3 foo foo2 bar bar2 test1 test2 test3 test4 clone2"
 
 tiered=0
 if ceph osd dump | grep ^pool | grep "'rbd'" | grep tier; then
@@ -505,6 +505,41 @@ test_deep_copy_clone() {
     remove_images
 }
 
+test_clone_v2() {
+    echo "testing clone v2..."
+    remove_images
+
+    rbd create $RBD_CREATE_ARGS -s 1 test1
+    rbd snap create test1@1
+    rbd clone --rbd-default-clone-format=1 test1@1 test2 && exit 1 || true
+    rbd clone --rbd-default-clone-format=2 test1@1 test2
+    rbd clone --rbd-default-clone-format=2 test1@1 test3
+
+    rbd snap protect test1@1
+    rbd clone --rbd-default-clone-format=1 test1@1 test4
+
+    rbd children test1@1 | sort | tr '\n' ' ' | grep -E "test2.*test3.*test4"
+
+    rbd remove test4
+    rbd snap unprotect test1@1
+
+    rbd snap remove test1@1
+    rbd snap list --all test1 | grep -E "trash[ ]*$"
+
+    rbd snap create test1@2
+    rbd rm test1 2>&1 | grep 'image has snapshots'
+
+    rbd snap rm test1@2
+    rbd rm test1 2>&1 | grep 'linked clones'
+
+    rbd rm test3
+    rbd rm test1 2>&1 | grep 'linked clones'
+
+    rbd flatten test2
+    rbd rm test1
+    rbd rm test2
+}
+
 test_pool_image_args
 test_rename
 test_ls
@@ -519,5 +554,6 @@ test_clone
 test_trash
 test_purge
 test_deep_copy_clone
+test_clone_v2
 
 echo OK
index 0013bee5c0fe23b82c561b8cad439b493404f6a6..91243f55d5281f9450b91b6e1897a430f83adebf 100644 (file)
@@ -13,6 +13,7 @@
 #include "librbd/image/DetachChildRequest.h"
 #include "librbd/journal/RemoveRequest.h"
 #include "librbd/mirror/DisableRequest.h"
+#include "librbd/operation/SnapshotRemoveRequest.h"
 #include "librbd/operation/TrimRequest.h"
 
 #define dout_subsys ceph_subsys_rbd
 namespace librbd {
 namespace image {
 
+namespace {
+
+bool auto_delete_snapshot(const SnapInfo& snap_info) {
+  auto snap_namespace_type = cls::rbd::get_snap_namespace_type(
+    snap_info.snap_namespace);
+  switch (snap_namespace_type) {
+  case cls::rbd::SNAPSHOT_NAMESPACE_TYPE_TRASH:
+    return true;
+  default:
+    return false;
+  }
+}
+
+} // anonymous namespace
+
 using librados::IoCtx;
 using util::create_context_callback;
 using util::create_async_context_callback;
@@ -166,11 +182,19 @@ template<typename I>
 void RemoveRequest<I>::check_image_snaps() {
   ldout(m_cct, 20) << dendl;
 
-  if (m_image_ctx->snaps.size()) {
-    lderr(m_cct) << "image has snapshots - not removing" << dendl;
-    send_close_image(-ENOTEMPTY);
-    return;
+  m_image_ctx->snap_lock.get_read();
+  for (auto& snap_info : m_image_ctx->snap_info) {
+    if (auto_delete_snapshot(snap_info.second)) {
+      m_snap_infos.insert(snap_info);
+    } else {
+      m_image_ctx->snap_lock.put_read();
+
+      lderr(m_cct) << "image has snapshots - not removing" << dendl;
+      send_close_image(-ENOTEMPTY);
+      return;
+    }
   }
+  m_image_ctx->snap_lock.put_read();
 
   list_image_watchers();
 }
@@ -296,6 +320,11 @@ void RemoveRequest<I>::check_image_watchers() {
 
 template<typename I>
 void RemoveRequest<I>::check_group() {
+  if (m_old_format) {
+    trim_image();
+    return;
+  }
+
   ldout(m_cct, 20) << dendl;
 
   librados::ObjectReadOperation op;
@@ -333,7 +362,50 @@ void RemoveRequest<I>::handle_check_group(int r) {
     return;
   }
 
-  trim_image();
+  remove_snapshot();
+}
+
+template<typename I>
+void RemoveRequest<I>::remove_snapshot() {
+  if (m_snap_infos.empty()) {
+    trim_image();
+    return;
+  }
+
+  auto snap_id = m_snap_infos.begin()->first;
+  auto& snap_info = m_snap_infos.begin()->second;
+  ldout(m_cct, 20) << "snap_id=" << snap_id << ", "
+                   << "snap_name=" << snap_info.name << dendl;
+
+  RWLock::RLocker owner_lock(m_image_ctx->owner_lock);
+  auto ctx = create_context_callback<
+    RemoveRequest<I>, &RemoveRequest<I>::handle_remove_snapshot>(this);
+  auto req = librbd::operation::SnapshotRemoveRequest<I>::create(
+    *m_image_ctx, snap_info.snap_namespace, snap_info.name,
+    snap_id, ctx);
+  req->send();
+}
+
+template<typename I>
+void RemoveRequest<I>::handle_remove_snapshot(int r) {
+  ldout(m_cct, 20) << "r=" << r << dendl;
+
+  if (r < 0 && r != -ENOENT) {
+    auto snap_id = m_snap_infos.begin()->first;
+    lderr(m_cct) << "failed to auto-prune snapshot " << snap_id << ": "
+                 << cpp_strerror(r) << dendl;
+
+    if (r == -EBUSY) {
+      r = -ENOTEMPTY;
+    }
+    send_close_image(r);
+    return;
+  }
+
+  assert(!m_snap_infos.empty());
+  m_snap_infos.erase(m_snap_infos.begin());
+
+  remove_snapshot();
 }
 
 template<typename I>
index ab16ed72d15338f4509da7cf5721168d5ed58ade..d85e301cfd979418d2a357d782364f6c0ad2db4c 100644 (file)
@@ -59,7 +59,10 @@ private:
    * |               |                   v                |     |
    * |               |            VALIDATE IMAGE REMOVAL<-/     |
    * |               |                /  |                      v
-   * |               \------<--------/   |                             |
+   * |               \------<--------/   |   /------\          |
+   * |                                   v   v      |           |
+   * |                             REMOVE SNAPS ----/           |
+   * |                                   |                      |
    * |                                   v                      |
    * |                              TRIM IMAGE                  |
    * |                                   |                      |
@@ -128,6 +131,8 @@ private:
   std::list<obj_watch_t> m_watchers;
   std::list<obj_watch_t> m_mirror_watchers;
 
+  std::map<uint64_t, SnapInfo> m_snap_infos;
+
   void open_image();
   void handle_open_image(int r);
 
@@ -163,6 +168,9 @@ private:
   void check_group();
   void handle_check_group(int r);
 
+  void remove_snapshot();
+  void handle_remove_snapshot(int r);
+
   void trim_image();
   void handle_trim_image(int r);
 
index 3b565d594b903bff9d82328f2bf6d609d31176d7..a35d53c3c0934ea24c59cf9eb37581fe0dc16c37 100644 (file)
@@ -9,14 +9,15 @@
 #include "test/librados_test_stub/MockTestMemRadosClient.h"
 #include "librbd/ImageState.h"
 #include "librbd/internal.h"
-#include "librbd/journal/RemoveRequest.h"
 #include "librbd/Operations.h"
-#include "librbd/operation/TrimRequest.h"
 #include "librbd/image/TypeTraits.h"
 #include "librbd/image/DetachChildRequest.h"
 #include "librbd/image/RemoveRequest.h"
 #include "librbd/image/RefreshParentRequest.h"
+#include "librbd/journal/RemoveRequest.h"
 #include "librbd/mirror/DisableRequest.h"
+#include "librbd/operation/SnapshotRemoveRequest.h"
+#include "librbd/operation/TrimRequest.h"
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
 #include <arpa/inet.h>
@@ -77,6 +78,30 @@ DetachChildRequest<MockTestImageCtx> *DetachChildRequest<MockTestImageCtx>::s_in
 
 namespace operation {
 
+template <>
+class SnapshotRemoveRequest<MockTestImageCtx> {
+public:
+  static SnapshotRemoveRequest *s_instance;
+  static SnapshotRemoveRequest *create(MockTestImageCtx &image_ctx,
+                                       cls::rbd::SnapshotNamespace sn,
+                                       std::string name,
+                                       uint64_t id, Context *on_finish) {
+    assert(s_instance != nullptr);
+    s_instance->on_finish = on_finish;
+    return s_instance;
+  }
+
+  Context *on_finish = nullptr;
+
+  SnapshotRemoveRequest() {
+    s_instance = this;
+  }
+
+  MOCK_METHOD0(send, void());
+};
+
+SnapshotRemoveRequest<MockTestImageCtx> *SnapshotRemoveRequest<MockTestImageCtx>::s_instance;
+
 template <>
 class TrimRequest<MockTestImageCtx> {
 public:
@@ -181,6 +206,7 @@ public:
   typedef typename TypeTraits::ContextWQ ContextWQ;
   typedef RemoveRequest<MockTestImageCtx> MockRemoveRequest;
   typedef DetachChildRequest<MockTestImageCtx> MockDetachChildRequest;
+  typedef librbd::operation::SnapshotRemoveRequest<MockTestImageCtx> MockSnapshotRemoveRequest;
   typedef librbd::operation::TrimRequest<MockTestImageCtx> MockTrimRequest;
   typedef librbd::journal::RemoveRequest<MockTestImageCtx> MockJournalRemoveRequest;
   typedef librbd::mirror::DisableRequest<MockTestImageCtx> MockMirrorDisableRequest;
@@ -189,14 +215,17 @@ public:
   MockTestImageCtx *m_mock_imctx = NULL;
 
 
-  void TestImageRemoveSetUp() {
+  void SetUp() override {
+    TestMockFixture::SetUp();
+
     ASSERT_EQ(0, open_image(m_image_name, &m_test_imctx));
     m_mock_imctx = new MockTestImageCtx(*m_test_imctx);
     librbd::MockTestImageCtx::s_instance = m_mock_imctx;
   }
-  void TestImageRemoveTearDown() {
+  void TearDown() override {
     librbd::MockTestImageCtx::s_instance = NULL;
     delete m_mock_imctx;
+    TestMockFixture::TearDown();
   }
 
   void expect_state_open(MockTestImageCtx &mock_image_ctx, int r) {
@@ -235,6 +264,13 @@ public:
     }
   }
 
+  void expect_remove_snap(MockTestImageCtx &mock_image_ctx,
+                          MockSnapshotRemoveRequest& mock_snap_remove_request,
+                          int r) {
+    EXPECT_CALL(mock_snap_remove_request, send())
+      .WillOnce(FinishRequest(&mock_snap_remove_request, r, &mock_image_ctx));
+  }
+
   void expect_trim(MockTestImageCtx &mock_image_ctx,
                    MockTrimRequest &mock_trim_request, int r) {
     EXPECT_CALL(mock_trim_request, send())
@@ -244,13 +280,13 @@ public:
   void expect_journal_remove(MockTestImageCtx &mock_image_ctx,
                    MockJournalRemoveRequest &mock_journal_remove_request, int r) {
     EXPECT_CALL(mock_journal_remove_request, send())
-                  .WillOnce(FinishRequest(&mock_journal_remove_request, r, &mock_image_ctx));
+      .WillOnce(FinishRequest(&mock_journal_remove_request, r, &mock_image_ctx));
   }
 
   void expect_mirror_disable(MockTestImageCtx &mock_image_ctx,
                    MockMirrorDisableRequest &mock_mirror_disable_request, int r) {
     EXPECT_CALL(mock_mirror_disable_request, send())
-                  .WillOnce(FinishRequest(&mock_mirror_disable_request, r, &mock_image_ctx));
+      .WillOnce(FinishRequest(&mock_mirror_disable_request, r, &mock_image_ctx));
   }
 
   void expect_remove_mirror_image(librados::IoCtx &ioctx, int r) {
@@ -261,10 +297,12 @@ public:
   }
 
   void expect_mirror_image_get(MockTestImageCtx &mock_image_ctx, int r) {
-    EXPECT_CALL(get_mock_io_ctx(mock_image_ctx.md_ctx),
-                exec(RBD_MIRRORING, _, StrEq("rbd"), StrEq("mirror_image_get"),
-                     _, _, _))
-      .WillOnce(Return(r));
+    if ((mock_image_ctx.features & RBD_FEATURE_JOURNALING) != 0ULL) {
+      EXPECT_CALL(get_mock_io_ctx(mock_image_ctx.md_ctx),
+                  exec(RBD_MIRRORING, _, StrEq("rbd"),
+                       StrEq("mirror_image_get"), _, _, _))
+        .WillOnce(Return(r));
+    }
   }
 
   void expect_dir_remove_image(librados::IoCtx &ioctx, int r) {
@@ -283,63 +321,49 @@ public:
 
 TEST_F(TestMockImageRemoveRequest, SuccessV1) {
   REQUIRE_FORMAT_V1();
-  TestImageRemoveSetUp();
-
-  C_SaferCond ctx;
-  librbd::NoOpProgressContext no_op;
-  ContextWQ op_work_queue;
-  MockTrimRequest mock_trim_request;
-  MockJournalRemoveRequest mock_journal_remove_request;
+  expect_op_work_queue(*m_mock_imctx);
 
   InSequence seq;
   expect_state_open(*m_mock_imctx, 0);
-  expect_get_group(*m_mock_imctx, 0);
+
+  MockTrimRequest mock_trim_request;
   expect_trim(*m_mock_imctx, mock_trim_request, 0);
-  expect_op_work_queue(*m_mock_imctx);
+
   expect_state_close(*m_mock_imctx);
+
+  ContextWQ op_work_queue;
   expect_wq_queue(op_work_queue, 0);
 
+  C_SaferCond ctx;
+  librbd::NoOpProgressContext no_op;
   MockRemoveRequest *req = MockRemoveRequest::create(m_ioctx, m_image_name, "",
                                              true, false, no_op, &op_work_queue, &ctx);
   req->send();
 
   ASSERT_EQ(0, ctx.wait());
-
-  TestImageRemoveTearDown();
 }
 
 TEST_F(TestMockImageRemoveRequest, OpenFailV1) {
   REQUIRE_FORMAT_V1();
-  TestImageRemoveSetUp();
-
-  C_SaferCond ctx;
-  librbd::NoOpProgressContext no_op;
-  ContextWQ op_work_queue;
-  MockTrimRequest mock_trim_request;
 
   InSequence seq;
   expect_state_open(*m_mock_imctx, -ENOENT);
+
+  ContextWQ op_work_queue;
   expect_wq_queue(op_work_queue, 0);
 
+  C_SaferCond ctx;
+  librbd::NoOpProgressContext no_op;
   MockRemoveRequest *req = MockRemoveRequest::create(m_ioctx, m_image_name, "",
                                              true, false, no_op, &op_work_queue, &ctx);
   req->send();
 
   ASSERT_EQ(0, ctx.wait());
-
-  TestImageRemoveTearDown();
 }
 
 TEST_F(TestMockImageRemoveRequest, SuccessV2CloneV1) {
-  REQUIRE_FEATURE(RBD_FEATURE_JOURNALING);
-  TestImageRemoveSetUp();
-
-  C_SaferCond ctx;
-  librbd::NoOpProgressContext no_op;
-  ContextWQ op_work_queue;
-  MockTrimRequest mock_trim_request;
-  MockJournalRemoveRequest mock_journal_remove_request;
-  MockMirrorDisableRequest mock_mirror_disable_request;
+  REQUIRE_FEATURE(RBD_FEATURE_LAYERING);
+  expect_op_work_queue(*m_mock_imctx);
 
   m_mock_imctx->parent_md.spec.pool_id = m_ioctx.get_id();
   m_mock_imctx->parent_md.spec.image_id = "parent id";
@@ -349,38 +373,37 @@ TEST_F(TestMockImageRemoveRequest, SuccessV2CloneV1) {
   expect_state_open(*m_mock_imctx, 0);
   expect_mirror_image_get(*m_mock_imctx, 0);
   expect_get_group(*m_mock_imctx, 0);
+
+  MockTrimRequest mock_trim_request;
   expect_trim(*m_mock_imctx, mock_trim_request, 0);
-  expect_op_work_queue(*m_mock_imctx);
 
   MockDetachChildRequest mock_detach_child_request;
   expect_detach_child(*m_mock_imctx, mock_detach_child_request, 0);
 
+  MockMirrorDisableRequest mock_mirror_disable_request;
   expect_mirror_disable(*m_mock_imctx, mock_mirror_disable_request, 0);
+
   expect_state_close(*m_mock_imctx);
-  expect_wq_queue(op_work_queue, 0);
+
+  MockJournalRemoveRequest mock_journal_remove_request;
   expect_journal_remove(*m_mock_imctx, mock_journal_remove_request, 0);
+
   expect_remove_mirror_image(m_ioctx, 0);
   expect_dir_remove_image(m_ioctx, 0);
 
+  C_SaferCond ctx;
+  librbd::NoOpProgressContext no_op;
+  ContextWQ op_work_queue;
   MockRemoveRequest *req = MockRemoveRequest::create(m_ioctx, m_image_name, "",
                                              true, false, no_op, &op_work_queue, &ctx);
   req->send();
 
   ASSERT_EQ(0, ctx.wait());
-
-  TestImageRemoveTearDown();
 }
 
 TEST_F(TestMockImageRemoveRequest, SuccessV2CloneV2) {
-  REQUIRE_FEATURE(RBD_FEATURE_JOURNALING);
-  TestImageRemoveSetUp();
-
-  C_SaferCond ctx;
-  librbd::NoOpProgressContext no_op;
-  ContextWQ op_work_queue;
-  MockTrimRequest mock_trim_request;
-  MockJournalRemoveRequest mock_journal_remove_request;
-  MockMirrorDisableRequest mock_mirror_disable_request;
+  REQUIRE_FEATURE(RBD_FEATURE_LAYERING);
+  expect_op_work_queue(*m_mock_imctx);
 
   m_mock_imctx->parent_md.spec.pool_id = m_ioctx.get_id();
   m_mock_imctx->parent_md.spec.image_id = "parent id";
@@ -390,38 +413,37 @@ TEST_F(TestMockImageRemoveRequest, SuccessV2CloneV2) {
   expect_state_open(*m_mock_imctx, 0);
   expect_mirror_image_get(*m_mock_imctx, 0);
   expect_get_group(*m_mock_imctx, 0);
+
+  MockTrimRequest mock_trim_request;
   expect_trim(*m_mock_imctx, mock_trim_request, 0);
-  expect_op_work_queue(*m_mock_imctx);
 
   MockDetachChildRequest mock_detach_child_request;
   expect_detach_child(*m_mock_imctx, mock_detach_child_request, 0);
 
+  MockMirrorDisableRequest mock_mirror_disable_request;
   expect_mirror_disable(*m_mock_imctx, mock_mirror_disable_request, 0);
+
   expect_state_close(*m_mock_imctx);
-  expect_wq_queue(op_work_queue, 0);
+
+  MockJournalRemoveRequest mock_journal_remove_request;
   expect_journal_remove(*m_mock_imctx, mock_journal_remove_request, 0);
+
   expect_remove_mirror_image(m_ioctx, 0);
   expect_dir_remove_image(m_ioctx, 0);
 
+  C_SaferCond ctx;
+  librbd::NoOpProgressContext no_op;
+  ContextWQ op_work_queue;
   MockRemoveRequest *req = MockRemoveRequest::create(m_ioctx, m_image_name, "",
                                              true, false, no_op, &op_work_queue, &ctx);
   req->send();
 
   ASSERT_EQ(0, ctx.wait());
-
-  TestImageRemoveTearDown();
 }
 
 TEST_F(TestMockImageRemoveRequest, NotExistsV2) {
   REQUIRE_FEATURE(RBD_FEATURE_JOURNALING);
-  TestImageRemoveSetUp();
-
-  C_SaferCond ctx;
-  librbd::NoOpProgressContext no_op;
-  ContextWQ op_work_queue;
-  MockTrimRequest mock_trim_request;
-  MockJournalRemoveRequest mock_journal_remove_request;
-  MockMirrorDisableRequest mock_mirror_disable_request;
+  expect_op_work_queue(*m_mock_imctx);
 
   m_mock_imctx->parent_md.spec.pool_id = m_ioctx.get_id();
   m_mock_imctx->parent_md.spec.image_id = "parent id";
@@ -431,25 +453,92 @@ TEST_F(TestMockImageRemoveRequest, NotExistsV2) {
   expect_state_open(*m_mock_imctx, 0);
   expect_mirror_image_get(*m_mock_imctx, 0);
   expect_get_group(*m_mock_imctx, 0);
+
+  MockTrimRequest mock_trim_request;
   expect_trim(*m_mock_imctx, mock_trim_request, 0);
-  expect_op_work_queue(*m_mock_imctx);
 
   MockDetachChildRequest mock_detach_child_request;
   expect_detach_child(*m_mock_imctx, mock_detach_child_request, 0);
 
+  MockMirrorDisableRequest mock_mirror_disable_request;
   expect_mirror_disable(*m_mock_imctx, mock_mirror_disable_request, 0);
+
   expect_state_close(*m_mock_imctx);
-  expect_wq_queue(op_work_queue, 0);
+
+  MockJournalRemoveRequest mock_journal_remove_request;
   expect_journal_remove(*m_mock_imctx, mock_journal_remove_request, 0);
+
   expect_remove_mirror_image(m_ioctx, 0);
   expect_dir_remove_image(m_ioctx, -ENOENT);
 
+  C_SaferCond ctx;
+  librbd::NoOpProgressContext no_op;
+  ContextWQ op_work_queue;
   MockRemoveRequest *req = MockRemoveRequest::create(m_ioctx, m_image_name, "",
                                              true, false, no_op, &op_work_queue, &ctx);
   req->send();
   ASSERT_EQ(-ENOENT, ctx.wait());
+}
+
+TEST_F(TestMockImageRemoveRequest, Snapshots) {
+  m_mock_imctx->snap_info = {
+    {123, {"snap1", {cls::rbd::UserSnapshotNamespace{}}, {}, {}, {}, {}, {}}}};
+
+  InSequence seq;
+  expect_state_open(*m_mock_imctx, 0);
+  expect_state_close(*m_mock_imctx);
+
+  C_SaferCond ctx;
+  librbd::NoOpProgressContext no_op;
+  ContextWQ op_work_queue;
+  MockRemoveRequest *req = MockRemoveRequest::create(
+    m_ioctx, m_image_name, "", true, false, no_op, &op_work_queue, &ctx);
+  req->send();
+
+  ASSERT_EQ(-ENOTEMPTY, ctx.wait());
+}
+
+TEST_F(TestMockImageRemoveRequest, AutoDeleteSnapshots) {
+  REQUIRE_FORMAT_V2();
+  expect_op_work_queue(*m_mock_imctx);
+
+  m_mock_imctx->snap_info = {
+    {123, {"snap1", {cls::rbd::TrashSnapshotNamespace{}}, {}, {}, {}, {}, {}}}};
+
+  InSequence seq;
+  expect_state_open(*m_mock_imctx, 0);
+  expect_mirror_image_get(*m_mock_imctx, 0);
+  expect_get_group(*m_mock_imctx, 0);
+
+  MockSnapshotRemoveRequest mock_snap_remove_request;
+  expect_remove_snap(*m_mock_imctx, mock_snap_remove_request, 0);
+
+  MockTrimRequest mock_trim_request;
+  expect_trim(*m_mock_imctx, mock_trim_request, 0);
+
+  MockDetachChildRequest mock_detach_child_request;
+  expect_detach_child(*m_mock_imctx, mock_detach_child_request, 0);
+
+  MockMirrorDisableRequest mock_mirror_disable_request;
+  expect_mirror_disable(*m_mock_imctx, mock_mirror_disable_request, 0);
+
+  expect_state_close(*m_mock_imctx);
+
+  MockJournalRemoveRequest mock_journal_remove_request;
+  expect_journal_remove(*m_mock_imctx, mock_journal_remove_request, 0);
+
+  expect_remove_mirror_image(m_ioctx, 0);
+  expect_dir_remove_image(m_ioctx, 0);
+
+  C_SaferCond ctx;
+  librbd::NoOpProgressContext no_op;
+  ContextWQ op_work_queue;
+  MockRemoveRequest *req = MockRemoveRequest::create(
+    m_ioctx, m_image_name, "", true, false, no_op, &op_work_queue, &ctx);
+  req->send();
+
+  ASSERT_EQ(0, ctx.wait());
 
-  TestImageRemoveTearDown();
 }
 
 } // namespace image
index 5946ec571dd5b878c75fa7e8a82af044552edb27..397b05a75fd5314af17f1b4efbe523d3267c5b76 100644 (file)
@@ -13,6 +13,26 @@ namespace rbd {
 namespace action {
 namespace remove {
 
+namespace {
+
+bool is_auto_delete_snapshot(librbd::Image* image,
+                             const librbd::snap_info_t &snap_info) {
+  librbd::snap_namespace_type_t namespace_type;
+  int r = image->snap_get_namespace_type(snap_info.id, &namespace_type);
+  if (r < 0) {
+    return false;
+  }
+
+  switch (namespace_type) {
+  case RBD_SNAP_NAMESPACE_TYPE_TRASH:
+    return true;
+  default:
+    return false;
+  }
+}
+
+} // anonymous namespace
+
 namespace at = argument_types;
 namespace po = boost::program_options;
 
@@ -62,9 +82,30 @@ int execute(const po::variables_map &vm,
                 vm[at::NO_PROGRESS].as<bool>());
   if (r < 0) {
     if (r == -ENOTEMPTY) {
-      std::cerr << "rbd: image has snapshots - these must be deleted"
-                << " with 'rbd snap purge' before the image can be removed."
-                << std::endl;
+      librbd::Image image;
+      std::vector<librbd::snap_info_t> snaps;
+      r = utils::open_image(io_ctx, image_name, true, &image);
+      if (r >= 0) {
+        r = image.snap_list(snaps);
+      }
+      if (r >= 0) {
+        snaps.erase(std::remove_if(snaps.begin(), snaps.end(),
+                                  [&image](const librbd::snap_info_t& snap) {
+                                     return is_auto_delete_snapshot(&image,
+                                                                    snap);
+                                   }),
+                    snaps.end());
+      }
+
+      if (!snaps.empty()) {
+        std::cerr << "rbd: image has snapshots - these must be deleted"
+                  << " with 'rbd snap purge' before the image can be removed."
+                  << std::endl;
+      } else {
+        std::cerr << "rbd: image has snapshots with linked clones - these must "
+                  << "be deleted or flattened before the image can be removed."
+                  << std::endl;
+      }
     } else if (r == -EBUSY) {
       std::cerr << "rbd: error: image still has watchers"
                 << std::endl