From: Jason Dillaman Date: Thu, 8 Feb 2018 22:12:00 +0000 (-0500) Subject: librbd: auto-remove trash snapshots when image is deleted X-Git-Tag: v13.0.2~297^2 X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=refs%2Fpull%2F20376%2Fhead;p=ceph.git librbd: auto-remove trash snapshots when image is deleted Fixes: http://tracker.ceph.com/issues/22873 Signed-off-by: Jason Dillaman --- diff --git a/qa/workunits/rbd/cli_generic.sh b/qa/workunits/rbd/cli_generic.sh index 27e313d955de..3a4bdb39ebbd 100755 --- a/qa/workunits/rbd/cli_generic.sh +++ b/qa/workunits/rbd/cli_generic.sh @@ -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 diff --git a/src/librbd/image/RemoveRequest.cc b/src/librbd/image/RemoveRequest.cc index 0013bee5c0fe..91243f55d528 100644 --- a/src/librbd/image/RemoveRequest.cc +++ b/src/librbd/image/RemoveRequest.cc @@ -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 @@ -23,6 +24,21 @@ 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 void RemoveRequest::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::check_image_watchers() { template void RemoveRequest::check_group() { + if (m_old_format) { + trim_image(); + return; + } + ldout(m_cct, 20) << dendl; librados::ObjectReadOperation op; @@ -333,7 +362,50 @@ void RemoveRequest::handle_check_group(int r) { return; } - trim_image(); + remove_snapshot(); +} + +template +void RemoveRequest::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, &RemoveRequest::handle_remove_snapshot>(this); + auto req = librbd::operation::SnapshotRemoveRequest::create( + *m_image_ctx, snap_info.snap_namespace, snap_info.name, + snap_id, ctx); + req->send(); +} + +template +void RemoveRequest::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 diff --git a/src/librbd/image/RemoveRequest.h b/src/librbd/image/RemoveRequest.h index ab16ed72d153..d85e301cfd97 100644 --- a/src/librbd/image/RemoveRequest.h +++ b/src/librbd/image/RemoveRequest.h @@ -59,7 +59,10 @@ private: * | | v | | * | | VALIDATE IMAGE REMOVAL<-/ | * | | / | v - * | \------<--------/ | | + * | \------<--------/ | /------\ | + * | v v | | + * | REMOVE SNAPS ----/ | + * | | | * | v | * | TRIM IMAGE | * | | | @@ -128,6 +131,8 @@ private: std::list m_watchers; std::list m_mirror_watchers; + std::map 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); diff --git a/src/test/librbd/image/test_mock_RemoveRequest.cc b/src/test/librbd/image/test_mock_RemoveRequest.cc index 3b565d594b90..a35d53c3c093 100644 --- a/src/test/librbd/image/test_mock_RemoveRequest.cc +++ b/src/test/librbd/image/test_mock_RemoveRequest.cc @@ -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 @@ -77,6 +78,30 @@ DetachChildRequest *DetachChildRequest::s_in namespace operation { +template <> +class SnapshotRemoveRequest { +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 *SnapshotRemoveRequest::s_instance; + template <> class TrimRequest { public: @@ -181,6 +206,7 @@ public: typedef typename TypeTraits::ContextWQ ContextWQ; typedef RemoveRequest MockRemoveRequest; typedef DetachChildRequest MockDetachChildRequest; + typedef librbd::operation::SnapshotRemoveRequest MockSnapshotRemoveRequest; typedef librbd::operation::TrimRequest MockTrimRequest; typedef librbd::journal::RemoveRequest MockJournalRemoveRequest; typedef librbd::mirror::DisableRequest 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 diff --git a/src/tools/rbd/action/Remove.cc b/src/tools/rbd/action/Remove.cc index 5946ec571dd5..397b05a75fd5 100644 --- a/src/tools/rbd/action/Remove.cc +++ b/src/tools/rbd/action/Remove.cc @@ -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()); 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 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