Skip to content

Commit

Permalink
Merge pull request #23351 from tspmelo/wip-rbd-trash
Browse files Browse the repository at this point in the history
mgr/dashboard: Add support for RBD Trash

Reviewed-by: Sebastian Wagner <swagner@suse.com>
Reviewed-by: Volker Theile <vtheile@suse.com>
  • Loading branch information
LenzGr committed Sep 27, 2018
2 parents 925273d + 1eeaa50 commit 75c1d8e
Show file tree
Hide file tree
Showing 41 changed files with 1,450 additions and 33 deletions.
107 changes: 107 additions & 0 deletions qa/tasks/mgr/dashboard/test_rbd.py
Expand Up @@ -3,6 +3,8 @@

from __future__ import absolute_import

import time

from .helper import DashboardTestCase, JObj, JLeaf, JList


Expand Down Expand Up @@ -136,6 +138,31 @@ def tearDownClass(cls):
cls._ceph_cmd(['osd', 'pool', 'delete', 'rbd_data', 'rbd_data',
'--yes-i-really-really-mean-it'])

@classmethod
def create_image_in_trash(cls, pool, name, delay=0, **kwargs):
cls.create_image(pool, name, 10240)
img = cls._get('/api/block/image/{}/{}'.format(pool, name))

cls._task_post("/api/block/image/{}/{}/move_trash".format(pool, name),
{'delay': delay})

return img['id']

@classmethod
def remove_trash(cls, pool, image_id, image_name, force=False):
return cls._task_delete('/api/block/image/trash/{}/{}/?image_name={}&force={}'.format('rbd', image_id, image_name, force))

@classmethod
def get_trash(cls, pool, image_id):
trash = cls._get('/api/block/image/trash/?pool_name={}'.format(pool))
if isinstance(trash, list):
for pool in trash:
for image in pool['value']:
if image['id'] == image_id:
return image

return None

def _validate_image(self, img, **kwargs):
"""
Example of an RBD image json:
Expand Down Expand Up @@ -599,3 +626,83 @@ def test_image_with_special_name(self):
'object-map'])

self.remove_image('rbd', rbd_name_encoded)

def test_move_image_to_trash(self):
id = self.create_image_in_trash('rbd', 'test_rbd')
self.assertStatus(200)

self._get('/api/block/image/rbd/test_rbd')
self.assertStatus(404)

time.sleep(1)

image = self.get_trash('rbd', id)
self.assertIsNotNone(image)

self.remove_trash('rbd', id, 'test_rbd')

def test_list_trash(self):
id = self.create_image_in_trash('rbd', 'test_rbd', 0)
data = self._get('/api/block/image/trash/?pool_name={}'.format('rbd'))
self.assertStatus(200)
self.assertIsInstance(data, list)
self.assertIsNotNone(data)

self.remove_trash('rbd', id, 'test_rbd')
self.assertStatus(204)

def test_restore_trash(self):
id = self.create_image_in_trash('rbd', 'test_rbd')

self._task_post('/api/block/image/trash/{}/{}/restore'.format('rbd', id), {'new_image_name': 'test_rbd'})

self._get('/api/block/image/rbd/test_rbd')
self.assertStatus(200)

image = self.get_trash('rbd', id)
self.assertIsNone(image)

self.remove_image('rbd', 'test_rbd')

def test_remove_expired_trash(self):
id = self.create_image_in_trash('rbd', 'test_rbd', 0)
self.remove_trash('rbd', id, 'test_rbd', False)
self.assertStatus(204)

image = self.get_trash('rbd', id)
self.assertIsNone(image)

def test_remove_not_expired_trash(self):
id = self.create_image_in_trash('rbd', 'test_rbd', 9999)
self.remove_trash('rbd', id, 'test_rbd', False)
self.assertStatus(400)

image = self.get_trash('rbd', id)
self.assertIsNotNone(image)

self.remove_trash('rbd', id, 'test_rbd', True)

def test_remove_not_expired_trash_with_force(self):
id = self.create_image_in_trash('rbd', 'test_rbd', 9999)
self.remove_trash('rbd', id, 'test_rbd', True)
self.assertStatus(204)

image = self.get_trash('rbd', id)
self.assertIsNone(image)

def test_purge_trash(self):
id_expired = self.create_image_in_trash('rbd', 'test_rbd_expired', 0)
id_not_expired = self.create_image_in_trash('rbd', 'test_rbd', 9999)

time.sleep(1)

self._task_post('/api/block/image/trash/purge?pool_name={}'.format('rbd'))
self.assertStatus(200)

time.sleep(1)

trash_not_expired = self.get_trash('rbd', id_not_expired)
self.assertIsNotNone(trash_not_expired)

trash_expired = self.get_trash('rbd', id_expired)
self.assertIsNone(trash_expired)
78 changes: 77 additions & 1 deletion src/pybind/mgr/dashboard/controllers/rbd.py
Expand Up @@ -5,6 +5,7 @@

import math
from functools import partial
from datetime import datetime

import cherrypy
import six
Expand All @@ -15,7 +16,7 @@
from .. import mgr
from ..security import Scope
from ..services.ceph_service import CephService
from ..tools import ViewCache
from ..tools import ViewCache, str_to_bool
from ..services.exception import handle_rados_error, handle_rbd_error, \
serialize_dashboard_exception

Expand Down Expand Up @@ -374,6 +375,16 @@ def default_features(self):
rbd_default_features = mgr.get('config')['rbd_default_features']
return _format_bitmask(int(rbd_default_features))

@RbdTask('trash/move', ['{pool_name}', '{image_name}'], 2.0)
@RESTController.Resource('POST')
def move_trash(self, pool_name, image_name, delay=0):
"""Move an image to the trash.
Images, even ones actively in-use by clones,
can be moved to the trash and deleted at a later time.
"""
rbd_inst = rbd.RBD()
return _rbd_call(pool_name, rbd_inst.trash_move, image_name, delay)


@ApiController('/block/image/{pool_name}/{image_name}/snap', Scope.RBD_IMAGE)
class RbdSnapshot(RESTController):
Expand Down Expand Up @@ -453,3 +464,68 @@ def _clone(ioctx):
return _rbd_call(child_pool_name, _clone)

return _rbd_call(pool_name, _parent_clone)


@ApiController('/block/image/trash')
class RbdTrash(RESTController):
RESOURCE_ID = "pool_name/image_id"
rbd_inst = rbd.RBD()

@ViewCache()
def _trash_pool_list(self, pool_name):
with mgr.rados.open_ioctx(pool_name) as ioctx:
images = self.rbd_inst.trash_list(ioctx)
result = []
for trash in images:
trash['pool_name'] = pool_name
trash['deletion_time'] = "{}Z".format(trash['deletion_time'].isoformat())
trash['deferment_end_time'] = "{}Z".format(trash['deferment_end_time'].isoformat())
result.append(trash)
return result

def _trash_list(self, pool_name=None):
if pool_name:
pools = [pool_name]
else:
pools = [p['pool_name'] for p in CephService.get_pool_list('rbd')]

result = []
for pool in pools:
# pylint: disable=unbalanced-tuple-unpacking
status, value = self._trash_pool_list(pool)
result.append({'status': status, 'value': value, 'pool_name': pool})
return result

@handle_rbd_error()
@handle_rados_error('pool')
def list(self, pool_name=None):
"""List all entries from trash."""
return self._trash_list(pool_name)

@handle_rbd_error()
@handle_rados_error('pool')
@RbdTask('trash/purge', ['{pool_name}'], 2.0)
@RESTController.Collection('POST', query_params=['pool_name'])
def purge(self, pool_name=None):
"""Remove all expired images from trash."""
now = "{}Z".format(datetime.now().isoformat())
pools = self._trash_list(pool_name)

for pool in pools:
for image in pool['value']:
if image['deferment_end_time'] < now:
_rbd_call(pool['pool_name'], self.rbd_inst.trash_remove, image['id'], 0)

@RbdTask('trash/restore', ['{pool_name}', '{image_id}', '{new_image_name}'], 2.0)
@RESTController.Resource('POST')
def restore(self, pool_name, image_id, new_image_name):
"""Restore an image from trash."""
return _rbd_call(pool_name, self.rbd_inst.trash_restore, image_id, new_image_name)

@RbdTask('trash/remove', ['{pool_name}', '{image_id}', '{image_name}'], 2.0)
def delete(self, pool_name, image_id, image_name, force=False):
"""Delete an image from trash.
If image deferment time has not expired you can not removed it unless use force.
But an actively in-use by clones or has snapshots can not be removed.
"""
return _rbd_call(pool_name, self.rbd_inst.trash_remove, image_id, int(str_to_bool(force)))
1 change: 1 addition & 0 deletions src/pybind/mgr/dashboard/frontend/angular.json
Expand Up @@ -25,6 +25,7 @@
"node_modules/ng2-toastr/bundles/ng2-toastr.min.css",
"node_modules/fork-awesome/css/fork-awesome.css",
"node_modules/awesome-bootstrap-checkbox/awesome-bootstrap-checkbox.css",
"node_modules/ngx-bootstrap/datepicker/bs-datepicker.css",
"src/styles.scss"
],
"scripts": [
Expand Down
Expand Up @@ -4,7 +4,7 @@ import { ActivatedRouteSnapshot, RouterModule, Routes } from '@angular/router';
import { IscsiComponent } from './ceph/block/iscsi/iscsi.component';
import { MirroringComponent } from './ceph/block/mirroring/mirroring.component';
import { RbdFormComponent } from './ceph/block/rbd-form/rbd-form.component';
import { RbdListComponent } from './ceph/block/rbd-list/rbd-list.component';
import { RbdImagesComponent } from './ceph/block/rbd-images/rbd-images.component';
import { CephfsListComponent } from './ceph/cephfs/cephfs-list/cephfs-list.component';
import { ConfigurationComponent } from './ceph/cluster/configuration/configuration.component';
import { HostsComponent } from './ceph/cluster/hosts/hosts.component';
Expand Down Expand Up @@ -107,7 +107,7 @@ const routes: Routes = [
path: 'rbd',
data: { breadcrumbs: 'Images' },
children: [
{ path: '', component: RbdListComponent },
{ path: '', component: RbdImagesComponent },
{ path: 'add', component: RbdFormComponent, data: { breadcrumbs: 'Add' } },
{ path: 'edit/:pool/:name', component: RbdFormComponent, data: { breadcrumbs: 'Edit' } },
{
Expand Down
Expand Up @@ -4,6 +4,7 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router';

import { BsDropdownModule, ModalModule, TabsModule, TooltipModule } from 'ngx-bootstrap';
import { BsDatepickerModule } from 'ngx-bootstrap/datepicker';
import { ProgressbarModule } from 'ngx-bootstrap/progressbar';

import { SharedModule } from '../../shared/shared.module';
Expand All @@ -12,19 +13,31 @@ import { MirrorHealthColorPipe } from './mirror-health-color.pipe';
import { MirroringComponent } from './mirroring/mirroring.component';
import { RbdDetailsComponent } from './rbd-details/rbd-details.component';
import { RbdFormComponent } from './rbd-form/rbd-form.component';
import { RbdImagesComponent } from './rbd-images/rbd-images.component';
import { RbdListComponent } from './rbd-list/rbd-list.component';
import { RbdSnapshotFormComponent } from './rbd-snapshot-form/rbd-snapshot-form.component';
import { RbdSnapshotListComponent } from './rbd-snapshot-list/rbd-snapshot-list.component';
import { RbdTrashListComponent } from './rbd-trash-list/rbd-trash-list.component';
import { RbdTrashMoveModalComponent } from './rbd-trash-move-modal/rbd-trash-move-modal.component';
import { RbdTrashPurgeModalComponent } from './rbd-trash-purge-modal/rbd-trash-purge-modal.component';
import { RbdTrashRestoreModalComponent } from './rbd-trash-restore-modal/rbd-trash-restore-modal.component';

@NgModule({
entryComponents: [RbdDetailsComponent, RbdSnapshotFormComponent],
entryComponents: [
RbdDetailsComponent,
RbdSnapshotFormComponent,
RbdTrashMoveModalComponent,
RbdTrashRestoreModalComponent,
RbdTrashPurgeModalComponent
],
imports: [
CommonModule,
FormsModule,
ReactiveFormsModule,
TabsModule.forRoot(),
ProgressbarModule.forRoot(),
BsDropdownModule.forRoot(),
BsDatepickerModule.forRoot(),
TooltipModule.forRoot(),
ModalModule.forRoot(),
SharedModule,
Expand All @@ -38,7 +51,12 @@ import { RbdSnapshotListComponent } from './rbd-snapshot-list/rbd-snapshot-list.
RbdDetailsComponent,
RbdFormComponent,
RbdSnapshotListComponent,
RbdSnapshotFormComponent
RbdSnapshotFormComponent,
RbdTrashListComponent,
RbdTrashMoveModalComponent,
RbdImagesComponent,
RbdTrashRestoreModalComponent,
RbdTrashPurgeModalComponent
]
})
export class BlockModule {}
@@ -0,0 +1,13 @@
<div>
<tabset>
<tab heading="Images"
i18n-heading
id="tab1">
<cd-rbd-list></cd-rbd-list>
</tab>
<tab heading="Trash"
i18n-heading>
<cd-rbd-trash-list></cd-rbd-trash-list>
</tab>
</tabset>
</div>
Empty file.
@@ -0,0 +1,50 @@
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';

import { ToastModule } from 'ng2-toastr';
import { TabsModule, TooltipModule } from 'ngx-bootstrap';

import { TaskListService } from '../../../shared/services/task-list.service';
import { SharedModule } from '../../../shared/shared.module';
import { RbdDetailsComponent } from '../rbd-details/rbd-details.component';
import { RbdListComponent } from '../rbd-list/rbd-list.component';
import { RbdSnapshotListComponent } from '../rbd-snapshot-list/rbd-snapshot-list.component';
import { RbdTrashListComponent } from '../rbd-trash-list/rbd-trash-list.component';
import { RbdImagesComponent } from './rbd-images.component';

describe('RbdImagesComponent', () => {
let component: RbdImagesComponent;
let fixture: ComponentFixture<RbdImagesComponent>;

beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [
RbdDetailsComponent,
RbdImagesComponent,
RbdListComponent,
RbdSnapshotListComponent,
RbdTrashListComponent
],
imports: [
HttpClientTestingModule,
RouterTestingModule,
SharedModule,
TabsModule.forRoot(),
ToastModule.forRoot(),
TooltipModule.forRoot()
],
providers: [TaskListService]
}).compileComponents();
}));

beforeEach(() => {
fixture = TestBed.createComponent(RbdImagesComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,12 @@
import { Component, OnInit } from '@angular/core';

@Component({
selector: 'cd-rbd-images',
templateUrl: './rbd-images.component.html',
styleUrls: ['./rbd-images.component.scss']
})
export class RbdImagesComponent implements OnInit {
constructor() {}

ngOnInit() {}
}

0 comments on commit 75c1d8e

Please sign in to comment.