# 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
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
</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>
-
/* Configuration */
configuration: RbdConfigurationEntry[];
+
+ /* Deletion process */
+ source?: string;
}
<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>
+@import '../../../../defaults';
+
+.warn {
+ color: $color-bright-yellow;
+}
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';
});
});
});
+
+ 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');
+ });
});
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';
nameTpl: TemplateRef<any>;
@ViewChild('flattenTpl')
flattenTpl: TemplateRef<any>;
+ @ViewChild('removingStatTpl')
+ removingStatTpl: TemplateRef<any>;
permission: Permission;
tableActions: CdTableAction[];
retries: number;
viewCacheStatusList: any[];
selection = new CdTableSelection();
+ icons = Icons;
modalRef: BsModalRef;
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',
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',
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,
name: this.i18n('Name'),
prop: 'name',
flexGrow: 2,
- cellTransformation: CellTemplate.executing
+ cellTemplate: this.removingStatTpl
},
{
name: this.i18n('Pool'),
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'`);
+ }
+ }
}
--- /dev/null
+# -*- 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'
+ }]))