From: Tatjana Dehler Date: Thu, 27 May 2021 09:46:50 +0000 (+0200) Subject: mgr/dashboard: show partially deleted RBDs X-Git-Tag: v14.2.22~10^2 X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=51a964eccda6d47a7bf88c0a99023156958fe3f0;p=ceph.git mgr/dashboard: show partially deleted RBDs An RBD might be partially deleted if the deletion process has been started but was interrupted. In this case return the RBD as part of the RBD list and mark it as partially deleted. Fixes: https://tracker.ceph.com/issues/48603 Signed-off-by: Tatjana Dehler (cherry picked from commit d83c277ac1861df31d2a39d16e20c7bebbea676e) Conflicts: src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-details/rbd-details.component.html src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.html src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.spec.ts src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.ts src/pybind/mgr/dashboard/services/rbd.py src/pybind/mgr/dashboard/tests/test_rbd_service.py Resolved various conflicts because nautilus and master diverged a lot. --- diff --git a/src/pybind/mgr/dashboard/controllers/rbd.py b/src/pybind/mgr/dashboard/controllers/rbd.py index ea5714a6b724..f28ee102ee63 100644 --- a/src/pybind/mgr/dashboard/controllers/rbd.py +++ b/src/pybind/mgr/dashboard/controllers/rbd.py @@ -3,6 +3,7 @@ # pylint: disable=too-many-statements,too-many-branches from __future__ import absolute_import +import errno import math from functools import partial from datetime import datetime @@ -189,19 +190,39 @@ class Rbd(RESTController): return stat + # pylint: disable=inconsistent-return-statements + @classmethod + def _rbd_image_removing(cls, ioctx, pool_name, image_id): + rbd_inst = rbd.RBD() + img = rbd_inst.trash_get(ioctx, image_id) + img_spec = '{}/{}'.format(pool_name, image_id) + + if img['source'] == 'REMOVING': + img['unique_id'] = img_spec + img['pool_name'] = pool_name + img['deletion_time'] = "{}Z".format(img['deletion_time'].isoformat()) + img['deferment_end_time'] = "{}Z".format(img['deferment_end_time'].isoformat()) + return img + raise rbd.ImageNotFound('No image {} in status `REMOVING` found.'.format(img_spec), + errno=errno.ENOENT) + @classmethod @ViewCache() def _rbd_pool_list(cls, pool_name): rbd_inst = rbd.RBD() with mgr.rados.open_ioctx(pool_name) as ioctx: - names = rbd_inst.list(ioctx) + image_refs = rbd_inst.list2(ioctx) result = [] - for name in names: + for image_ref in image_refs: try: - stat = cls._rbd_image(ioctx, pool_name, name) + stat = cls._rbd_image(ioctx, pool_name, image_ref['name']) except rbd.ImageNotFound: - # may have been removed in the meanwhile - continue + # Check if the RBD has been deleted partially. This happens for example if + # the deletion process of the RBD has been started and was interrupted. + try: + stat = cls._rbd_image_removing(ioctx, pool_name, image_ref['id']) + except rbd.ImageNotFound: + continue result.append(stat) return result diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-details/rbd-details.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-details/rbd-details.component.html index 716539f9b7e8..aedb2f7bd246 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-details/rbd-details.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-details/rbd-details.component.html @@ -3,7 +3,7 @@ - + @@ -132,6 +132,9 @@ +Information can not be displayed for RBD in status 'Removing'. + Global - diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.model.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.model.ts index 9511a00d0030..499675112fed 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.model.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.model.ts @@ -13,4 +13,7 @@ export class RbdFormModel { /* Configuration */ configuration: RbdConfigurationEntry[]; + + /* Deletion process */ + source?: string; } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.html index 4dd424e23df8..6f18e49534c3 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.html @@ -41,3 +41,25 @@ {{ value.parent }} to child {{ value.child }}. + + + + + + {{ value }} + + + ({{ row.cdExecuting }}) + + + diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.scss index e69de29bb2d1..348927d953c6 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.scss +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.scss @@ -0,0 +1,5 @@ +@import '../../../../defaults'; + +.warn { + color: $color-bright-yellow; +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.spec.ts index 4b80c3b9b857..bf56257613bc 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.spec.ts @@ -21,6 +21,7 @@ import { RbdService } from '../../../shared/api/rbd.service'; import { ActionLabels } from '../../../shared/constants/app.constants'; import { TableActionsComponent } from '../../../shared/datatable/table-actions/table-actions.component'; import { ViewCacheStatus } from '../../../shared/enum/view-cache-status.enum'; +import { CdTableSelection } from '../../../shared/models/cd-table-selection'; import { ExecutingTask } from '../../../shared/models/executing-task'; import { SummaryService } from '../../../shared/services/summary.service'; import { TaskListService } from '../../../shared/services/task-list.service'; @@ -364,4 +365,29 @@ describe('RbdListComponent', () => { }); }); }); + + it('should disable edit, copy, flatten and move action if RBD is in status `Removing`', () => { + const checkAction = (name: string) => { + const message = `Action not possible for an RBD in status 'Removing'`; + const action = component.tableActions.find((o) => o.name === name); + expect(action.disable(component.selection)).toBeTruthy(); + expect(action.disableDesc(undefined)).toBe(message); + }; + + component.selection = new CdTableSelection(); + component.selection.selected = [ + { + name: 'foobar', + pool_name: 'rbd', + snapshots: [], + source: 'REMOVING' + } + ]; + component.selection.update(); + + checkAction('Edit'); + checkAction('Copy'); + checkAction('Flatten'); + checkAction('Move to Trash'); + }); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.ts index 596941912cd8..71840f009344 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.ts @@ -9,7 +9,7 @@ import { ConfirmationModalComponent } from '../../../shared/components/confirmat import { CriticalConfirmationModalComponent } from '../../../shared/components/critical-confirmation-modal/critical-confirmation-modal.component'; import { ActionLabelsI18n } from '../../../shared/constants/app.constants'; import { TableComponent } from '../../../shared/datatable/table/table.component'; -import { CellTemplate } from '../../../shared/enum/cell-template.enum'; +import { Icons } from '../../../shared/enum/icons.enum'; import { ViewCacheStatus } from '../../../shared/enum/view-cache-status.enum'; import { CdTableAction } from '../../../shared/models/cd-table-action'; import { CdTableColumn } from '../../../shared/models/cd-table-column'; @@ -48,6 +48,8 @@ export class RbdListComponent implements OnInit { nameTpl: TemplateRef; @ViewChild('flattenTpl') flattenTpl: TemplateRef; + @ViewChild('removingStatTpl') + removingStatTpl: TemplateRef; permission: Permission; tableActions: CdTableAction[]; @@ -56,6 +58,7 @@ export class RbdListComponent implements OnInit { retries: number; viewCacheStatusList: any[]; selection = new CdTableSelection(); + icons = Icons; modalRef: BsModalRef; @@ -107,7 +110,9 @@ export class RbdListComponent implements OnInit { permission: 'update', icon: 'fa-pencil', routerLink: () => this.urlBuilder.getEdit(getImageUri()), - name: this.actionLabels.EDIT + name: this.actionLabels.EDIT, + disable: () => !this.selection.first() || !_.isUndefined(this.getRemovingStatusDesc()), + disableDesc: () => this.getRemovingStatusDesc() }; const deleteAction: CdTableAction = { permission: 'delete', @@ -119,18 +124,25 @@ export class RbdListComponent implements OnInit { permission: 'create', canBePrimary: (selection: CdTableSelection) => selection.hasSingleSelection, disable: (selection: CdTableSelection) => - !selection.hasSingleSelection || selection.first().cdExecuting, + !selection.hasSingleSelection || + selection.first().cdExecuting || + !_.isUndefined(this.getRemovingStatusDesc()), icon: 'fa-copy', routerLink: () => `/block/rbd/copy/${getImageUri()}`, - name: this.actionLabels.COPY + name: this.actionLabels.COPY, + disableDesc: () => this.getRemovingStatusDesc() }; const flattenAction: CdTableAction = { permission: 'update', disable: (selection: CdTableSelection) => - !selection.hasSingleSelection || selection.first().cdExecuting || !selection.first().parent, + !selection.hasSingleSelection || + selection.first().cdExecuting || + !selection.first().parent || + !_.isUndefined(this.getRemovingStatusDesc()), icon: 'fa-chain-broken', click: () => this.flattenRbdModal(), - name: this.actionLabels.FLATTEN + name: this.actionLabels.FLATTEN, + disableDesc: () => this.getRemovingStatusDesc() }; const moveAction: CdTableAction = { permission: 'delete', @@ -140,7 +152,9 @@ export class RbdListComponent implements OnInit { disable: (selection: CdTableSelection) => !selection.first() || !selection.hasSingleSelection || - selection.first().image_format === RBDImageFormat.V1 + selection.first().image_format === RBDImageFormat.V1 || + !_.isUndefined(this.getRemovingStatusDesc()), + disableDesc: () => this.getRemovingStatusDesc() }; this.tableActions = [ addAction, @@ -158,7 +172,7 @@ export class RbdListComponent implements OnInit { name: this.i18n('Name'), prop: 'name', flexGrow: 2, - cellTransformation: CellTemplate.executing + cellTemplate: this.removingStatTpl }, { name: this.i18n('Pool'), @@ -350,4 +364,11 @@ export class RbdListComponent implements OnInit { this.modalRef = this.modalService.show(ConfirmationModalComponent, { initialState }); } + + getRemovingStatusDesc(): string | undefined { + const first = this.selection.first(); + if (first && first.source && first.source === 'REMOVING') { + return this.i18n(`Action not possible for an RBD in status 'Removing'`); + } + } } diff --git a/src/pybind/mgr/dashboard/tests/test_rbd.py b/src/pybind/mgr/dashboard/tests/test_rbd.py new file mode 100644 index 000000000000..af2a0214b906 --- /dev/null +++ b/src/pybind/mgr/dashboard/tests/test_rbd.py @@ -0,0 +1,107 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import + +from datetime import datetime + +import unittest +try: + import mock +except ImportError: + import unittest.mock as mock +from mock import MagicMock + +from .. import mgr +from ..controllers.rbd import Rbd + + +class ImageNotFoundStub(Exception): + def __init__(self, message, errno=None): + super(ImageNotFoundStub, self).__init__( + 'RBD image not found (%s)' % message, errno) + + +class RbdTest(unittest.TestCase): + + @mock.patch('dashboard.controllers.rbd.rbd.RBD') + def test_rbd_image_removing(self, rbd_mock): + time = datetime.utcnow() + rbd_inst_mock = rbd_mock.return_value + rbd_inst_mock.trash_get.return_value = { + 'id': '3c1a5ee60a88', + 'name': 'test_rbd', + 'source': 'REMOVING', + 'deletion_time': time, + 'deferment_end_time': time + } + + ioctx_mock = MagicMock() + + # pylint: disable=protected-access + rbd = Rbd._rbd_image_removing(ioctx_mock, 'test_pool', '3c1a5ee60a88') + self.assertEqual(rbd, { + 'id': '3c1a5ee60a88', + 'unique_id': 'test_pool/3c1a5ee60a88', + 'name': 'test_rbd', + 'source': 'REMOVING', + 'deletion_time': '{}Z'.format(time.isoformat()), + 'deferment_end_time': '{}Z'.format(time.isoformat()), + 'pool_name': 'test_pool' + }) + + @mock.patch('dashboard.controllers.rbd.rbd.ImageNotFound', + new_callable=lambda: ImageNotFoundStub) + @mock.patch('dashboard.controllers.rbd.rbd.RBD') + def test_rbd_image_stat_filter_source_user(self, rbd_mock, _): + rbd_inst_mock = rbd_mock.return_value + rbd_inst_mock.trash_get.return_value = { + 'id': '3c1a5ee60a88', + 'name': 'test_rbd', + 'source': 'USER' + } + + ioctx_mock = MagicMock() + with self.assertRaises(ImageNotFoundStub) as ctx: + # pylint: disable=protected-access + Rbd._rbd_image_removing(ioctx_mock, 'test_pool', '3c1a5ee60a88') + self.assertIn('No image test_pool/3c1a5ee60a88 in status `REMOVING` found.', + str(ctx.exception)) + + @mock.patch('dashboard.controllers.rbd.rbd.ImageNotFound', + new_callable=lambda: ImageNotFoundStub) + @mock.patch('dashboard.controllers.rbd.Rbd._rbd_image_removing') + @mock.patch('dashboard.controllers.rbd.Rbd._rbd_image') + @mock.patch('dashboard.controllers.rbd.rbd.RBD') + def test_rbd_pool_list(self, rbd_mock, rbd_image_mock, rbd_image_removing_mock, _): + time = datetime.utcnow() + + ioctx_mock = MagicMock() + mgr.rados = MagicMock() + mgr.rados.open_ioctx.return_value = ioctx_mock + + rbd_inst_mock = rbd_mock.return_value + rbd_inst_mock.list2.return_value = [{'name': 'test_rbd', 'id': '3c1a5ee60a88'}] + + rbd_image_mock.side_effect = mock.Mock(side_effect=ImageNotFoundStub( + 'RBD image not found test_pool/3c1a5ee60a88')) + + rbd_image_removing_mock.return_value = { + 'id': '3c1a5ee60a88', + 'unique_id': 'test_pool/3c1a5ee60a88', + 'name': 'test_rbd', + 'source': 'REMOVING', + 'deletion_time': '{}Z'.format(time.isoformat()), + 'deferment_end_time': '{}Z'.format(time.isoformat()), + 'pool_name': 'test_pool' + } + + # pylint: disable=protected-access + rbd_pool_list = Rbd._rbd_pool_list('test_pool') + self.assertEqual(rbd_pool_list, (0, [{ + 'id': '3c1a5ee60a88', + 'unique_id': 'test_pool/3c1a5ee60a88', + 'name': 'test_rbd', + 'source': 'REMOVING', + 'deletion_time': '{}Z'.format(time.isoformat()), + 'deferment_end_time': '{}Z'.format(time.isoformat()), + 'pool_name': 'test_pool' + }]))