return cls._task_put('/api/block/image/{}/{}'.format(pool, image),
{'name': name, 'size': size, 'features': features})
+ @classmethod
+ def flatten_image(cls, pool, image):
+ return cls._task_post('/api/block/image/{}/{}/flatten'.format(pool, image))
+
@classmethod
def create_snapshot(cls, pool, image, snapshot):
return cls._task_post('/api/block/image/{}/{}/snap'.format(pool, image),
self.assertStatus(204)
self.remove_image('rbd_iscsi', 'coimg-copy')
self.assertStatus(204)
+
+ def test_flatten(self):
+ self.create_snapshot('rbd', 'img1', 'snapf')
+ self.update_snapshot('rbd', 'img1', 'snapf', None, True)
+ self.clone_image('rbd', 'img1', 'snapf', 'rbd_iscsi', 'img1_snapf_clone')
+
+ img = self._get('/api/block/image/rbd_iscsi/img1_snapf_clone')
+ self.assertStatus(200)
+ self.assertIsNotNone(img['parent'])
+
+ self.flatten_image('rbd_iscsi', 'img1_snapf_clone')
+ self.assertStatus(200)
+
+ img = self._get('/api/block/image/rbd_iscsi/img1_snapf_clone')
+ self.assertStatus(200)
+ self.assertIsNone(img['parent'])
+
+ self.update_snapshot('rbd', 'img1', 'snapf', None, False)
+ self.remove_snapshot('rbd', 'img1', 'snapf')
+ self.assertStatus(204)
+
+ self.remove_image('rbd_iscsi', 'img1_snapf_clone')
+ self.assertStatus(204)
return _rbd_image_call(pool_name, image_name, _src_copy)
+ @RbdTask('flatten', ['{pool_name}', '{image_name}'], 2.0)
+ @RESTController.resource(['POST'])
+ def flatten(self, pool_name, image_name):
+
+ def _flatten(ioctx, image):
+ image.flatten()
+
+ return _rbd_image_call(pool_name, image_name, _flatten)
+
@ApiController('block/image/:pool_name/:image_name/snap')
@AuthRequired()
import { ProgressbarModule } from 'ngx-bootstrap/progressbar';
import { SharedModule } from '../../shared/shared.module';
+import {
+ FlattenConfirmationModalComponent
+} from './flatten-confirmation-modal/flatten-confimation-modal.component';
import { IscsiComponent } from './iscsi/iscsi.component';
import { MirrorHealthColorPipe } from './mirror-health-color.pipe';
import { MirroringComponent } from './mirroring/mirroring.component';
entryComponents: [
RbdDetailsComponent,
RbdSnapshotFormComponent,
- RollbackConfirmationModalComponent
+ RollbackConfirmationModalComponent,
+ FlattenConfirmationModalComponent
],
imports: [
CommonModule,
RbdFormComponent,
RbdSnapshotListComponent,
RbdSnapshotFormComponent,
- RollbackConfirmationModalComponent
+ RollbackConfirmationModalComponent,
+ FlattenConfirmationModalComponent
]
})
export class BlockModule { }
--- /dev/null
+<div class="modal-header">
+ <h4 i18n
+ class="modal-title pull-left">RBD flatten</h4>
+ <button type="button" class="close pull-right" aria-label="Close" (click)="modalRef.hide()">
+ <span aria-hidden="true">×</span>
+ </button>
+</div>
+<form name="flattenForm"
+ class="form-horizontal"
+ #formDir="ngForm"
+ [formGroup]="flattenForm"
+ novalidate>
+ <div class="modal-body">
+ You are about to flatten <strong>{{ child }}</strong>.
+ <br>
+ <br>
+ All blocks will be copied from parent <strong>{{ parent }}</strong> to child <strong>{{ child }}</strong>.
+ </div>
+ <div class="modal-footer">
+ <div class="button-group text-right">
+ <cd-submit-button i18n
+ [form]="flattenForm"
+ (submitAction)="submit()">
+ Flatten
+ </cd-submit-button>
+ <button i18n type="button" class="btn btn-sm btn-default" (click)="modalRef.hide()">Cancel</button>
+ </div>
+ </div>
+</form>
+
--- /dev/null
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { ReactiveFormsModule } from '@angular/forms';
+
+import { ToastModule } from 'ng2-toastr';
+import { BsModalRef, BsModalService } from 'ngx-bootstrap';
+
+import { ApiModule } from '../../../shared/api/api.module';
+import { ServicesModule } from '../../../shared/services/services.module';
+import { SharedModule } from '../../../shared/shared.module';
+import { FlattenConfirmationModalComponent } from './flatten-confimation-modal.component';
+
+describe('FlattenConfirmationModalComponent', () => {
+ let component: FlattenConfirmationModalComponent;
+ let fixture: ComponentFixture<FlattenConfirmationModalComponent>;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ ReactiveFormsModule,
+ HttpClientTestingModule,
+ SharedModule,
+ ServicesModule,
+ ApiModule,
+ ToastModule.forRoot()
+ ],
+ declarations: [ FlattenConfirmationModalComponent ],
+ providers: [ BsModalRef, BsModalService ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(FlattenConfirmationModalComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
--- /dev/null
+import { Component, OnInit } from '@angular/core';
+import { FormGroup } from '@angular/forms';
+
+import { BsModalRef } from 'ngx-bootstrap';
+import { Subject } from 'rxjs/Subject';
+
+@Component({
+ selector: 'cd-flatten-confimation-modal',
+ templateUrl: './flatten-confimation-modal.component.html',
+ styleUrls: ['./flatten-confimation-modal.component.scss']
+})
+export class FlattenConfirmationModalComponent implements OnInit {
+
+ child: string;
+ parent: string;
+
+ flattenForm: FormGroup;
+
+ public onSubmit: Subject<string>;
+
+ constructor(public modalRef: BsModalRef) {
+ this.createForm();
+ }
+
+ createForm() {
+ this.flattenForm = new FormGroup({});
+ }
+
+ ngOnInit() {
+ this.onSubmit = new Subject();
+ }
+
+ submit() {
+ this.onSubmit.next();
+ }
+}
[ngClass]="{'disabled': !selection.hasSingleSelection || selection.first().executing}">
<a class="dropdown-item" routerLink="/rbd/copy/{{ selection.first()?.pool_name }}/{{ selection.first()?.name }}"><i class="fa fa-fw fa-copy"></i><span i18n>Copy</span></a>
</li>
+ <li role="menuitem"
+ [ngClass]="{'disabled': !selection.hasSingleSelection || selection.first().executing || !selection.first().parent}">
+ <a class="dropdown-item" (click)="flattenRbdModal()"><i class="fa fa-fw fa-chain-broken"></i><span i18n>Flatten</span></a>
+ </li>
<li role="menuitem"
[ngClass]="{'disabled': !selection.hasSingleSelection || selection.first().executing}">
<a class="dropdown-item" (click)="deleteRbdModal()"><i class="fa fa-fw fa-trash-o"></i><span i18n>Delete</span></a>
import { SummaryService } from '../../../shared/services/summary.service';
import { TaskManagerMessageService } from '../../../shared/services/task-manager-message.service';
import { TaskManagerService } from '../../../shared/services/task-manager.service';
+import {
+ FlattenConfirmationModalComponent
+} from '../flatten-confirmation-modal/flatten-confimation-modal.component';
+import { RbdParentModel } from '../rbd-form/rbd-parent.model';
import { RbdModel } from './rbd-model';
@Component({
} else if (executingTask.name === 'rbd/edit') {
rbdExecuting.cdExecuting = 'updating';
+
+ } else if (executingTask.name === 'rbd/flatten') {
+ rbdExecuting.cdExecuting = 'flattening';
}
+
} else if (executingTask.name === 'rbd/create') {
const rbdModel = new RbdModel();
rbdModel.name = executingTask.metadata['image_name'];
modalRef: this.modalRef
});
}
+
+ flattenRbd(poolName, imageName) {
+ const finishedTask = new FinishedTask();
+ finishedTask.name = 'rbd/flatten';
+ finishedTask.metadata = {'pool_name': poolName, 'image_name': imageName};
+ this.rbdService.flatten(poolName, imageName)
+ .toPromise().then((resp) => {
+ if (resp.status === 202) {
+ this.notificationService.show(NotificationType.info,
+ `RBD flatten in progress...`,
+ this.taskManagerMessageService.getDescription(finishedTask));
+ const executingTask = new ExecutingTask();
+ executingTask.name = finishedTask.name;
+ executingTask.metadata = finishedTask.metadata;
+ this.executingTasks.push(executingTask);
+ this.taskManagerService.subscribe(executingTask.name, executingTask.metadata,
+ (asyncFinishedTask: FinishedTask) => {
+ this.notificationService.notifyTask(asyncFinishedTask);
+ });
+ } else {
+ finishedTask.success = true;
+ this.notificationService.notifyTask(finishedTask);
+ }
+ this.modalRef.hide();
+ this.loadImages(null);
+ }).catch((resp) => {
+ finishedTask.success = false;
+ finishedTask.exception = resp.error;
+ this.notificationService.notifyTask(finishedTask);
+ });
+ }
+
+ flattenRbdModal() {
+ const poolName = this.selection.first().pool_name;
+ const imageName = this.selection.first().name;
+ this.modalRef = this.modalService.show(FlattenConfirmationModalComponent);
+ const parent: RbdParentModel = this.selection.first().parent;
+ this.modalRef.content.parent = `${parent.pool_name}/${parent.image_name}@${parent.snap_name}`;
+ this.modalRef.content.child = `${poolName}/${imageName}`;
+ this.modalRef.content.onSubmit.subscribe(() => {
+ this.flattenRbd(poolName, imageName);
+ });
+ }
}
{ observe: 'response' });
}
+ flatten(poolName, rbdName) {
+ return this.http.post(`api/block/image/${poolName}/${rbdName}/flatten`, null,
+ { observe: 'response' });
+ }
+
createSnapshot(poolName, rbdName, snapshotName) {
const request = {
snapshot_name: snapshotName
};
}
),
+ 'rbd/flatten': new TaskManagerMessage(
+ (metadata) => `Flatten RBD '${metadata.pool_name}/${metadata.image_name}'`,
+ (metadata) => `RBD '${metadata.pool_name}/${metadata.image_name}'
+ has been flattened successfully`,
+ () => {
+ return {
+ };
+ }
+ ),
'rbd/snap/create': new TaskManagerMessage(
(metadata) => `Create snapshot ` +
`'${metadata.pool_name}/${metadata.image_name}@${metadata.snapshot_name}'`,