]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: show partially deleted RBDs 41738/head
authorTatjana Dehler <tdehler@suse.com>
Thu, 27 May 2021 09:46:50 +0000 (11:46 +0200)
committerTatjana Dehler <tdehler@suse.com>
Tue, 8 Jun 2021 08:41:07 +0000 (10:41 +0200)
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 <tdehler@suse.com>
(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.

src/pybind/mgr/dashboard/controllers/rbd.py
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-form/rbd-form.model.ts
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.scss
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/tests/test_rbd.py [new file with mode: 0644]

index ea5714a6b72466ab863b19bf477315ebb213999e..f28ee102ee63d0d716c7d95a3664eb57788f28e5 100644 (file)
@@ -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
 
index 716539f9b7e87170411d2f357eb8bf1e1308cef8..aedb2f7bd2463cc4ff43d34191a8fc637adc2560 100644 (file)
@@ -3,7 +3,7 @@
 </ng-template>
 
 
-<tabset *ngIf="selection?.hasSingleSelection">
+<tabset *ngIf="selection?.hasSingleSelection && selectedItem.source !== 'REMOVING'">
   <tab i18n-heading
        heading="Details">
     <table class="table table-striped table-bordered">
   </tab>
 </tabset>
 
+<cd-warning-panel *ngIf="selection?.hasSingleSelection && selectedItem.source === 'REMOVING'"
+                  i18n>Information can not be displayed for RBD in status 'Removing'.</cd-warning-panel>
+
 <ng-template
   #poolConfigurationSourceTpl
   let-row="row"
   </ng-container>
   <ng-template #global><span i18n i18n-tooltip tooltip="This is the global value. No value for this option has been set for this image.">Global</span></ng-template>
 </ng-template>
-
index 9511a00d003022dcb2ca2d502f7fb12dc19ec6da..499675112fed7126691ac735b7dd3d6f8777caaf 100644 (file)
@@ -13,4 +13,7 @@ export class RbdFormModel {
 
   /* Configuration */
   configuration: RbdConfigurationEntry[];
+
+  /* Deletion process */
+  source?: string;
 }
index 4dd424e23df81451dc47a45ff65a0279c39827ea..6f18e49534c32d99fee3388ec54aaf310b090810 100644 (file)
   <strong>{{ value.parent }}</strong> to child
   <strong>{{ value.child }}</strong>.
 </ng-template>
+
+<ng-template #removingStatTpl
+             let-column="column"
+             let-value="value"
+             let-row="row">
+
+  <i [ngClass]="[icons.spinner, icons.spin]"
+     *ngIf="row.cdExecuting"></i>
+  <span [ngClass]="column?.customTemplateConfig?.valueClass">
+    {{ value }}
+  </span>
+  <span *ngIf="row.cdExecuting"
+        [ngClass]="column?.customTemplateConfig?.executingClass ?
+        column.customTemplateConfig.executingClass :
+        'text-muted italic'">
+    ({{ row.cdExecuting }})
+  </span>
+  <i *ngIf="row.source && row.source === 'REMOVING'"
+     i18n-title
+     title="RBD in status 'Removing'"
+     class="{{ icons.warning }} warn"></i>
+</ng-template>
index 4b80c3b9b85717573139de7cda0b585afc2941f8..bf56257613bc2d28091c92bfcd86199f0d2b6487 100644 (file)
@@ -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');
+  });
 });
index 596941912cd80cf8f36333b7a1cf296ecdba4071..71840f0093440f87e4de0c8b0065d1e2a3f6faa3 100644 (file)
@@ -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<any>;
   @ViewChild('flattenTpl')
   flattenTpl: TemplateRef<any>;
+  @ViewChild('removingStatTpl')
+  removingStatTpl: TemplateRef<any>;
 
   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 (file)
index 0000000..af2a021
--- /dev/null
@@ -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'
+        }]))