import { IscsiComponent } from './ceph/block/iscsi/iscsi.component';
import { MirroringComponent } from './ceph/block/mirroring/mirroring.component';
-import { PoolDetailComponent } from './ceph/block/pool-detail/pool-detail.component';
+import { RbdFormComponent } from './ceph/block/rbd-form/rbd-form.component';
+import { RbdListComponent } from './ceph/block/rbd-list/rbd-list.component';
import { CephfsComponent } from './ceph/cephfs/cephfs/cephfs.component';
import { ClientsComponent } from './ceph/cephfs/clients/clients.component';
import { ConfigurationComponent } from './ceph/cluster/configuration/configuration.component';
canActivate: [AuthGuardService]
},
{ path: 'block/iscsi', component: IscsiComponent, canActivate: [AuthGuardService] },
- { path: 'block/pool/:name', component: PoolDetailComponent, canActivate: [AuthGuardService] },
+ { path: 'block/rbd', component: RbdListComponent, canActivate: [AuthGuardService] },
+ { path: 'rbd/add', component: RbdFormComponent, canActivate: [AuthGuardService] },
+ { path: 'rbd/edit/:pool/:name', component: RbdFormComponent, canActivate: [AuthGuardService] },
{
path: 'perf_counters/:type/:id',
component: PerformanceCounterComponent,
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
-import { FormsModule } from '@angular/forms';
+import { FormsModule, ReactiveFormsModule } from '@angular/forms';
+import { RouterModule } from '@angular/router';
+import { BsDropdownModule, ModalModule, TabsModule, TooltipModule } from 'ngx-bootstrap';
import { ProgressbarModule } from 'ngx-bootstrap/progressbar';
-import { TabsModule } from 'ngx-bootstrap/tabs';
import { SharedModule } from '../../shared/shared.module';
import { IscsiComponent } from './iscsi/iscsi.component';
import { MirrorHealthColorPipe } from './mirror-health-color.pipe';
import { MirroringComponent } from './mirroring/mirroring.component';
-import { PoolDetailComponent } from './pool-detail/pool-detail.component';
+import { RbdDetailsComponent } from './rbd-details/rbd-details.component';
+import { RbdFormComponent } from './rbd-form/rbd-form.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 {
+ RollbackConfirmationModalComponent
+} from './rollback-confirmation-modal/rollback-confimation-modal.component';
@NgModule({
+ entryComponents: [
+ RbdDetailsComponent,
+ RbdSnapshotFormComponent,
+ RollbackConfirmationModalComponent
+ ],
imports: [
CommonModule,
FormsModule,
+ ReactiveFormsModule,
TabsModule.forRoot(),
ProgressbarModule.forRoot(),
- SharedModule
+ BsDropdownModule.forRoot(),
+ TooltipModule.forRoot(),
+ ModalModule.forRoot(),
+ SharedModule,
+ RouterModule
],
declarations: [
- PoolDetailComponent,
+ RbdListComponent,
IscsiComponent,
MirroringComponent,
- MirrorHealthColorPipe
+ MirrorHealthColorPipe,
+ RbdDetailsComponent,
+ RbdFormComponent,
+ RbdSnapshotListComponent,
+ RbdSnapshotFormComponent,
+ RollbackConfirmationModalComponent
]
})
export class BlockModule { }
+++ /dev/null
-<nav aria-label="breadcrumb">
- <ol class="breadcrumb">
- <li i18n
- class="breadcrumb-item">Block</li>
- <li i18n
- class="breadcrumb-item">Pools</li>
- <li class="breadcrumb-item active"
- aria-current="page">{{ name }}</li>
- </ol>
-</nav>
-
-<cd-view-cache [status]="viewCacheStatus"></cd-view-cache>
-
-<cd-table [data]="images"
- columnMode="flex"
- [columns]="columns"
- (fetchData)="loadImages()">
-</cd-table>
-
-<ng-template #parentTpl
- let-value="value">
- <span *ngIf="value">{{ value.pool_name }}/{{ value.image_name }}@{{ value.snap_name }}</span>
- <span *ngIf="!value">-</span>
-</ng-template>
+++ /dev/null
-import { HttpClientTestingModule } from '@angular/common/http/testing';
-import { async, ComponentFixture, TestBed } from '@angular/core/testing';
-import { RouterTestingModule } from '@angular/router/testing';
-
-import { AlertModule, BsDropdownModule, TabsModule } from 'ngx-bootstrap';
-
-import { ComponentsModule } from '../../../shared/components/components.module';
-import { SharedModule } from '../../../shared/shared.module';
-import { PoolDetailComponent } from './pool-detail.component';
-
-describe('PoolDetailComponent', () => {
- let component: PoolDetailComponent;
- let fixture: ComponentFixture<PoolDetailComponent>;
-
- beforeEach(async(() => {
- TestBed.configureTestingModule({
- imports: [
- SharedModule,
- BsDropdownModule.forRoot(),
- TabsModule.forRoot(),
- AlertModule.forRoot(),
- ComponentsModule,
- RouterTestingModule,
- HttpClientTestingModule
- ],
- declarations: [ PoolDetailComponent ]
- })
- .compileComponents();
- }));
-
- beforeEach(() => {
- fixture = TestBed.createComponent(PoolDetailComponent);
- component = fixture.componentInstance;
- fixture.detectChanges();
- });
-
- it('should create', () => {
- expect(component).toBeTruthy();
- });
-});
+++ /dev/null
-import { Component, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/core';
-import { ActivatedRoute } from '@angular/router';
-
-import { PoolService } from '../../../shared/api/pool.service';
-import { ViewCacheStatus } from '../../../shared/enum/view-cache-status.enum';
-import { CdTableColumn } from '../../../shared/models/cd-table-column';
-import { DimlessBinaryPipe } from '../../../shared/pipes/dimless-binary.pipe';
-import { DimlessPipe } from '../../../shared/pipes/dimless.pipe';
-
-@Component({
- selector: 'cd-pool-detail',
- templateUrl: './pool-detail.component.html',
- styleUrls: ['./pool-detail.component.scss']
-})
-export class PoolDetailComponent implements OnInit, OnDestroy {
- @ViewChild('parentTpl') parentTpl: TemplateRef<any>;
-
- name: string;
- images: any;
- columns: CdTableColumn[];
- retries: number;
- routeParamsSubscribe: any;
- viewCacheStatus: ViewCacheStatus;
-
- constructor(
- private route: ActivatedRoute,
- private poolService: PoolService,
- dimlessBinaryPipe: DimlessBinaryPipe,
- dimlessPipe: DimlessPipe
- ) {
- this.columns = [
- {
- name: 'Name',
- prop: 'name',
- cellTemplate: this.parentTpl,
- flexGrow: 2
- },
- {
- name: 'Size',
- prop: 'size',
- flexGrow: 1,
- cellClass: 'text-right',
- pipe: dimlessBinaryPipe
- },
- {
- name: 'Objects',
- prop: 'num_objs',
- flexGrow: 1,
- cellClass: 'text-right',
- pipe: dimlessPipe
- },
- {
- name: 'Object size',
- prop: 'obj_size',
- flexGrow: 1,
- cellClass: 'text-right',
- pipe: dimlessBinaryPipe
- },
- {
- name: 'Features',
- prop: 'features_name',
- flexGrow: 3
- },
- {
- name: 'Parent',
- prop: 'parent',
- cellTemplate: this.parentTpl,
- flexGrow: 2
- }
- ];
- }
-
- ngOnInit() {
- this.routeParamsSubscribe = this.route.params.subscribe((params: { name: string }) => {
- this.name = params.name;
- this.images = [];
- this.retries = 0;
- });
- }
-
- ngOnDestroy() {
- this.routeParamsSubscribe.unsubscribe();
- }
-
- loadImages() {
- this.poolService.rbdPoolImages(this.name).then(
- resp => {
- this.viewCacheStatus = resp[0].status;
- this.images = resp[0].value;
- },
- () => {
- this.viewCacheStatus = ViewCacheStatus.ValueException;
- }
- );
- }
-}
--- /dev/null
+<ng-template #usageNotAvailableTooltipTpl>
+ <div [innerHtml]="'Only available for RBD images with <strong>fast-diff</strong> enabled'"></div>
+</ng-template>
+
+
+<tabset *ngIf="selection?.hasSingleSelection">
+ <tab i18n-heading
+ heading="Details">
+ <table class="table table-striped table-bordered">
+ <tbody>
+ <tr>
+ <td i18n
+ class="bold col-sm-1">Name
+ </td>
+ <td class="col-sm-3">{{ selectedItem.name }}</td>
+ </tr>
+ <tr>
+ <td i18n
+ class="bold col-sm-1">Pool
+ </td>
+ <td class="col-sm-3">{{ selectedItem.pool_name }}</td>
+ </tr>
+ <tr>
+ <td i18n
+ class="bold col-sm-1">Data Pool
+ </td>
+ <td class="col-sm-3">{{ selectedItem.data_pool | empty }}</td>
+ </tr>
+ <tr>
+ <td i18n
+ class="bold col-sm-1">Created
+ </td>
+ <td class="col-sm-3">{{ selectedItem.timestamp | cdDate }}</td>
+ </tr>
+ <tr>
+ <td i18n
+ class="bold col-sm-1">Size
+ </td>
+ <td class="col-sm-3">{{ selectedItem.size | dimlessBinary }}</td>
+ </tr>
+ <tr>
+ <td i18n
+ class="bold col-sm-1">Objects
+ </td>
+ <td class="col-sm-3">{{ selectedItem.num_objs | dimless }}</td>
+ </tr>
+ <tr>
+ <td i18n
+ class="bold col-sm-1">Object size
+ </td>
+ <td class="col-sm-3">{{ selectedItem.obj_size | dimlessBinary }}</td>
+ </tr>
+ <tr>
+ <td i18n
+ class="bold col-sm-1">Features
+ </td>
+ <td class="col-sm-3">
+ <span *ngFor="let feature of selectedItem.features_name">
+ <span class="badge badge-pill badge-primary">{{ feature }}</span>
+ </span>
+ </td>
+ </tr>
+ <tr>
+ <td i18n
+ class="bold col-sm-1">Provisioned
+ </td>
+ <td class="col-sm-3">
+ <span *ngIf="selectedItem.features_name?.indexOf('fast-diff') === -1">
+ <span class="text-muted"
+ [tooltip]="usageNotAvailableTooltipTpl"
+ placement="right">N/A</span>
+ </span>
+ <span *ngIf="selectedItem.features_name?.indexOf('fast-diff') !== -1">
+ {{ selectedItem.disk_usage | dimlessBinary }}
+ </span>
+ </td>
+ </tr>
+ <tr>
+ <td i18n
+ class="bold col-sm-1">Total provisioned
+ </td>
+ <td class="col-sm-3">
+ <span *ngIf="selectedItem.features_name?.indexOf('fast-diff') === -1">
+ <span class="text-muted"
+ [tooltip]="usageNotAvailableTooltipTpl"
+ placement="right">N/A</span>
+ </span>
+ <span *ngIf="selectedItem.features_name?.indexOf('fast-diff') !== -1">
+ {{ selectedItem.total_disk_usage | dimlessBinary }}
+ </span>
+ </td>
+ </tr>
+ <tr>
+ <td i18n
+ class="bold col-sm-1">Striping unit
+ </td>
+ <td class="col-sm-3">{{ selectedItem.stripe_unit | dimlessBinary }}</td>
+ </tr>
+ <tr>
+ <td i18n
+ class="bold col-sm-1">Striping count
+ </td>
+ <td class="col-sm-3">{{ selectedItem.stripe_count }}</td>
+ </tr>
+ <tr>
+ <td i18n
+ class="bold col-sm-1">Parent
+ </td>
+ <td class="col-sm-3">
+ <span *ngIf="selectedItem.parent">{{ selectedItem.parent.pool_name }}/{{ selectedItem.parent.image_name }}@{{ selectedItem.parent.snap_name }}</span>
+ <span *ngIf="!selectedItem.parent">-</span>
+ </td>
+ </tr>
+ <tr>
+ <td i18n
+ class="bold col-sm-1">Block name prefix
+ </td>
+ <td class="col-sm-3">{{ selectedItem.block_name_prefix }}</td>
+ </tr>
+ <tr>
+ <td i18n
+ class="bold col-sm-1">Order
+ </td>
+ <td class="col-sm-3">{{ selectedItem.order }}</td>
+ </tr>
+ </tbody>
+ </table>
+ </tab>
+ <tab i18n-heading
+ heading="Snapshots">
+ <cd-rbd-snapshot-list [snapshots]="selectedItem.snapshots"
+ [poolName]="selectedItem.pool_name"
+ [rbdName]="selectedItem.name"
+ [executingTasks]="selectedItem.executingTasks"></cd-rbd-snapshot-list>
+ </tab>
+</tabset>
--- /dev/null
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { TabsModule, TooltipModule } from 'ngx-bootstrap';
+
+import { SharedModule } from '../../../shared/shared.module';
+import { RbdSnapshotListComponent } from '../rbd-snapshot-list/rbd-snapshot-list.component';
+import { RbdDetailsComponent } from './rbd-details.component';
+
+describe('RbdDetailsComponent', () => {
+ let component: RbdDetailsComponent;
+ let fixture: ComponentFixture<RbdDetailsComponent>;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ RbdDetailsComponent, RbdSnapshotListComponent ],
+ imports: [ SharedModule, TabsModule.forRoot(), TooltipModule.forRoot(), ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(RbdDetailsComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
--- /dev/null
+import { Component, Input, OnChanges } from '@angular/core';
+
+import { CdTableSelection } from '../../../shared/models/cd-table-selection';
+
+@Component({
+ selector: 'cd-rbd-details',
+ templateUrl: './rbd-details.component.html',
+ styleUrls: ['./rbd-details.component.scss']
+})
+export class RbdDetailsComponent implements OnChanges {
+
+ @Input() selection: CdTableSelection;
+ selectedItem: any;
+
+ constructor() { }
+
+ ngOnChanges() {
+ if (this.selection.hasSelection) {
+ this.selectedItem = this.selection.first();
+ }
+ }
+}
--- /dev/null
+import { RbdFormModel } from './rbd-form.model';
+
+export class RbdFormCreateRequestModel extends RbdFormModel {
+ features: Array<string> = [];
+}
--- /dev/null
+export class RbdFormEditRequestModel {
+ name: string;
+ size: number;
+ features: Array<string> = [];
+}
--- /dev/null
+import { RbdFormModel } from './rbd-form.model';
+
+export class RbdFormResponseModel extends RbdFormModel {
+ features_name: string[];
+}
--- /dev/null
+<nav aria-label="breadcrumb">
+ <ol class="breadcrumb">
+ <li class="breadcrumb-item">Block</li>
+ <li class="breadcrumb-item">
+ <a routerLink="/block/rbd">Images</a></li>
+ <li class="breadcrumb-item active"
+ i18n>{editing, select, 1 {Edit} other {Add}}</li>
+ </ol>
+</nav>
+
+<div class="col-sm-12 col-lg-6">
+ <form name="rbdForm"
+ class="form-horizontal"
+ #formDir="ngForm"
+ [formGroup]="rbdForm"
+ novalidate>
+ <div class="panel panel-default">
+ <div class="panel-heading">
+ <h3 class="panel-title">
+ <span i18n>{editing, select, 1 {Edit} other {Add}}</span> RBD
+ </h3>
+ </div>
+ <div class="panel-body">
+
+ <!-- Name -->
+ <div class="form-group"
+ [ngClass]="{'has-error': (formDir.submitted || rbdForm.controls.name.dirty) && rbdForm.controls.name.invalid}">
+ <label i18n
+ class="control-label col-sm-3"
+ for="name">Name
+ <span class="required"></span>
+ </label>
+ <div class="col-sm-9">
+ <input class="form-control"
+ type="text"
+ placeholder="Name..."
+ id="name"
+ name="name"
+ formControlName="name"
+ autofocus>
+ <span i18n
+ class="help-block"
+ *ngIf="(formDir.submitted || rbdForm.controls.name.dirty) && rbdForm.controls.name.hasError('required')">
+ This field is required.
+ </span>
+ </div>
+ </div>
+
+ <!-- Pool -->
+ <div class="form-group"
+ [ngClass]="{'has-error': (formDir.submitted || rbdForm.controls.pool.dirty) && rbdForm.controls.pool.invalid}"
+ (change)="onPoolChange($event.target.value)">
+ <label class="control-label col-sm-3"
+ for="pool">
+ Pool
+ <span class="required"></span>
+ </label>
+ <div class="col-sm-9">
+ <select id="pool"
+ name="pool"
+ class="form-control"
+ formControlName="pool">
+ <option *ngIf="pools === null"
+ [ngValue]="null">Loading...
+ </option>
+ <option *ngIf="pools !== null && pools.length === 0"
+ [ngValue]="null">-- No pools available --
+ </option>
+ <option *ngIf="pools !== null && pools.length > 0"
+ [ngValue]="null">-- Select a pool --
+ </option>
+ <option *ngFor="let pool of pools"
+ [value]="pool.pool_name">{{ pool.pool_name }}</option>
+ </select>
+ <span i18n
+ class="help-block"
+ *ngIf="(formDir.submitted || rbdForm.controls.pool.dirty) && rbdForm.controls.pool.hasError('required')">
+ This field is required.
+ </span>
+ </div>
+ </div>
+
+ <!-- Use a dedicated pool -->
+ <div class="form-group">
+ <div class="col-sm-offset-3 col-sm-9">
+ <div class="checkbox checkbox-primary">
+ <input type="checkbox"
+ id="useDataPool"
+ name="useDataPool"
+ formControlName="useDataPool"
+ (change)="onUseDataPoolChange()">
+ <label i18n
+ for="useDataPool">Use a dedicated data pool</label>
+ </div>
+ </div>
+ </div>
+
+ <!-- Data Pool -->
+ <div class="form-group"
+ [ngClass]="{'has-error': (formDir.submitted || rbdForm.controls.dataPool.dirty) && rbdForm.controls.dataPool.invalid}"
+ *ngIf="rbdForm.controls.useDataPool.value">
+ <label class="control-label col-sm-3"
+ for="dataPool">
+ Data pool
+ <span class="required"></span>
+ <cd-helper i18n-html
+ html="Dedicated pool that stores the object-data of the RBD.">
+ </cd-helper>
+ </label>
+ <div class="col-sm-9">
+ <select id="dataPool"
+ name="dataPool"
+ class="form-control"
+ formControlName="dataPool"
+ (change)="onDataPoolChange($event.target.value)">
+ <option *ngIf="dataPools === null"
+ [ngValue]="null">Loading...
+ </option>
+ <option *ngIf="dataPools !== null && dataPools.length === 0"
+ [ngValue]="null">-- No data pools available --
+ </option>
+ <option *ngIf="dataPools !== null && dataPools.length > 0"
+ [ngValue]="null">-- Select a data pool --
+ </option>
+ <option *ngFor="let dataPool of dataPools"
+ [value]="dataPool.pool_name">{{ dataPool.pool_name }}</option>
+ </select>
+ <span i18n
+ class="help-block"
+ *ngIf="(formDir.submitted || rbdForm.controls.dataPool.dirty) && rbdForm.controls.dataPool.hasError('required')">
+ This field is required.
+ </span>
+ </div>
+ </div>
+
+ <!-- Size -->
+ <div class="form-group"
+ [ngClass]="{'has-error': (formDir.submitted || rbdForm.controls.size.dirty) && rbdForm.controls.size.invalid}">
+ <label i18n
+ class="control-label col-sm-3"
+ for="size">Size
+ <span class="required"></span>
+ </label>
+ <div class="col-sm-9">
+ <input id="size"
+ name="size"
+ class="form-control"
+ type="text"
+ formControlName="size"
+ ì18n-placeholder
+ placeholder="e.g., 10GiB"
+ defaultUnit="GiB"
+ cdDimlessBinary>
+ <span i18n
+ class="help-block"
+ *ngIf="(formDir.submitted || rbdForm.controls.size.dirty) && rbdForm.controls.size.hasError('required')">
+ This field is required.
+ </span>
+ <span i18n
+ class="help-block"
+ *ngIf="(formDir.submitted || rbdForm.controls.size.dirty) && rbdForm.controls.size.hasError('invalidSizeObject')">
+ You have to increase the size.
+ </span>
+ </div>
+ </div>
+
+ <!-- Features -->
+ <div class="form-group"
+ [ngClass]="{'has-error': (formDir.submitted || featuresFormGroups.dirty) && featuresFormGroups.invalid}"
+ formGroupName="features">
+ <label i18n
+ class="col-sm-3 control-label"
+ for="features">
+ Features
+ </label>
+ <div class="col-sm-9">
+ <div class="checkbox checkbox-primary"
+ *ngIf="!editing">
+ <input type="checkbox"
+ id="default-features"
+ name="default-features"
+ formControlName="defaultFeatures">
+ <label i18n
+ for="default-features">Use default features</label>
+ </div>
+ <div *ngIf="!featuresFormGroups.value.defaultFeatures">
+ <br *ngIf="!editing">
+ <div class="checkbox checkbox-primary"
+ *ngFor="let feature of featuresList">
+ <input type="checkbox"
+ id="{{ feature.key }}"
+ name="{{ feature.key }}"
+ formControlName="{{ feature.key }}">
+ <label for="{{ feature.key }}">{{ feature.desc }}</label>
+ <cd-helper *ngIf="feature.helperHtml"
+ html="{{ feature.helperHtml }}">
+ </cd-helper>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <!-- Advanced -->
+ <div class="row">
+ <div class="col-sm-12">
+ <a i18n
+ class="pull-right margin-right-md"
+ (click)="advancedEnabled = true"
+ *ngIf="!advancedEnabled">
+ Advanced...
+ </a>
+ </div>
+ </div>
+ <div *ngIf="advancedEnabled">
+
+ <h2 i18n
+ class="page-header">Advanced</h2>
+
+ <!-- Object Size -->
+ <div class="form-group"
+ [ngClass]="{'has-error': (formDir.submitted || rbdForm.controls.obj_size.dirty) && rbdForm.controls.obj_size.invalid}">
+ <label i18n
+ class="control-label col-sm-3"
+ for="size">Object size
+ </label>
+ <div class="col-sm-9">
+ <select id="obj_size"
+ name="obj_size"
+ class="form-control"
+ formControlName="obj_size">
+ <option *ngFor="let objectSize of objectSizes"
+ [value]="objectSize">{{ objectSize }}</option>
+ </select>
+ </div>
+ </div>
+
+ <!-- Strippe Unit -->
+ <div class="form-group"
+ [ngClass]="{'has-error': (formDir.submitted || rbdForm.controls.stripingUnit.dirty) && rbdForm.controls.stripingUnit.invalid}">
+ <label class="control-label col-sm-3"
+ for="stripingUnit">
+ <span i18n>Stripe unit</span><span class="required"
+ *ngIf="rbdForm.controls.stripingCount.value !== null"></span>
+ </label>
+ <div class="col-sm-9">
+ <select id="stripingUnit"
+ name="stripingUnit"
+ class="form-control"
+ formControlName="stripingUnit">
+ <option i18n [ngValue]="null">-- Select stripe unit --</option>
+ <option *ngFor="let objectSize of objectSizes"
+ [value]="objectSize">{{ objectSize }}</option>
+ </select>
+ <span i18n
+ class="help-block"
+ *ngIf="(formDir.submitted || rbdForm.controls.stripingUnit.dirty) && rbdForm.controls.stripingUnit.hasError('required')">
+ This field is required because stripe count is defined!
+ </span>
+ <span i18n
+ class="help-block"
+ *ngIf="(formDir.submitted || rbdForm.controls.stripingUnit.dirty) && rbdForm.controls.stripingUnit.hasError('invalidStripingUnit')">
+ Stripe unit is greater than object size.
+ </span>
+ </div>
+ </div>
+
+ <!-- Strippe Count -->
+ <div class="form-group"
+ [ngClass]="{'has-error': (formDir.submitted || rbdForm.controls.stripingCount.dirty) && rbdForm.controls.stripingCount.invalid}">
+ <label class="control-label col-sm-3"
+ for="stripingCount">
+ <span i18n>Stripe count</span><span class="required"
+ *ngIf="rbdForm.controls.stripingUnit.value !== null"></span>
+ </label>
+ <div class="col-sm-9">
+ <input id="stripingCount"
+ name="stripingCount"
+ formControlName="stripingCount"
+ class="form-control"
+ type="number">
+ <span i18n
+ class="help-block"
+ *ngIf="(formDir.submitted || rbdForm.controls.stripingCount.dirty) && rbdForm.controls.stripingCount.hasError('required')">
+ This field is required because stripe unit is defined!
+ </span>
+ <span i18n
+ class="help-block"
+ *ngIf="(formDir.submitted || rbdForm.controls.stripingCount.dirty) && rbdForm.controls.stripingCount.hasError('min')">
+ Stripe count must be greater than 0.
+ </span>
+ </div>
+ </div>
+
+ </div>
+
+ </div>
+ <div class="panel-footer">
+ <div class="button-group text-right">
+ <cd-submit-button [form]="rbdForm"
+ type="button"
+ (submitAction)="submit()">
+ <span i18n>{editing, select, 1 {Update} other {Create}}</span> RBD
+ </cd-submit-button>
+ <button i18n
+ type="button"
+ class="btn btn-sm btn-default"
+ routerLink="/block/rbd">
+ Back
+ </button>
+ </div>
+ </div>
+ </div>
+ </form>
+</div>
--- /dev/null
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { ReactiveFormsModule } from '@angular/forms';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { ToastModule } from 'ng2-toastr';
+
+import { ApiModule } from '../../../shared/api/api.module';
+import { ComponentsModule } from '../../../shared/components/components.module';
+import { ServicesModule } from '../../../shared/services/services.module';
+import { RbdFormComponent } from './rbd-form.component';
+
+describe('RbdFormComponent', () => {
+ let component: RbdFormComponent;
+ let fixture: ComponentFixture<RbdFormComponent>;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ ReactiveFormsModule,
+ HttpClientTestingModule,
+ RouterTestingModule,
+ ComponentsModule,
+ ServicesModule,
+ ApiModule,
+ ToastModule.forRoot()
+ ],
+ declarations: [ RbdFormComponent ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(RbdFormComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
--- /dev/null
+import { Component, OnInit } from '@angular/core';
+import { FormControl, FormGroup, Validators } from '@angular/forms';
+import { ActivatedRoute, Router } from '@angular/router';
+
+import * as _ from 'lodash';
+
+import { PoolService } from '../../../shared/api/pool.service';
+import { RbdService } from '../../../shared/api/rbd.service';
+import { NotificationType } from '../../../shared/enum/notification-type.enum';
+import { FinishedTask } from '../../../shared/models/finished-task';
+import { DimlessBinaryPipe } from '../../../shared/pipes/dimless-binary.pipe';
+import { FormatterService } from '../../../shared/services/formatter.service';
+import { NotificationService } from '../../../shared/services/notification.service';
+import { TaskManagerMessageService } from '../../../shared/services/task-manager-message.service';
+import { TaskManagerService } from '../../../shared/services/task-manager.service';
+import { RbdFormCreateRequestModel } from './rbd-form-create-request.model';
+import { RbdFormEditRequestModel } from './rbd-form-edit-request.model';
+import { RbdFormResponseModel } from './rbd-form-response.model';
+
+@Component({
+ selector: 'cd-rbd-form',
+ templateUrl: './rbd-form.component.html',
+ styleUrls: ['./rbd-form.component.scss']
+})
+export class RbdFormComponent implements OnInit {
+
+ rbdForm: FormGroup;
+ featuresFormGroups: FormGroup;
+ defaultFeaturesFormControl: FormControl;
+ deepFlattenFormControl: FormControl;
+ layeringFormControl: FormControl;
+ exclusiveLockFormControl: FormControl;
+ objectMapFormControl: FormControl;
+ journalingFormControl: FormControl;
+ fastDiffFormControl: FormControl;
+
+ pools: Array<string> = null;
+ allPools: Array<string> = null;
+ dataPools: Array<string> = null;
+ allDataPools: Array<string> = null;
+ features: any;
+ featuresList = [];
+
+ routeParamsSubscribe: any;
+ pool: string;
+
+ advancedEnabled = false;
+
+ editing = false;
+
+ response: RbdFormResponseModel;
+
+ defaultObjectSize = '4MiB';
+
+ objectSizes: Array<string> = [
+ '4KiB',
+ '8KiB',
+ '16KiB',
+ '32KiB',
+ '64KiB',
+ '128KiB',
+ '256KiB',
+ '512KiB',
+ '1MiB',
+ '2MiB',
+ '4MiB',
+ '8MiB',
+ '16MiB',
+ '32MiB'
+ ];
+
+ constructor(private route: ActivatedRoute,
+ private router: Router,
+ private poolService: PoolService,
+ private rbdService: RbdService,
+ private formatter: FormatterService,
+ private dimlessBinaryPipe: DimlessBinaryPipe,
+ private taskManagerService: TaskManagerService,
+ private taskManagerMessageService: TaskManagerMessageService,
+ private notificationService: NotificationService) {
+ this.features = {
+ 'deep-flatten': {
+ desc: 'Deep flatten',
+ requires: null,
+ allowEnable: false,
+ allowDisable: true
+ },
+ 'layering': {
+ desc: 'Layering',
+ requires: null,
+ allowEnable: false,
+ allowDisable: false
+ },
+ 'exclusive-lock': {
+ desc: 'Exclusive lock',
+ requires: null,
+ allowEnable: true,
+ allowDisable: true
+ },
+ 'object-map': {
+ desc: 'Object map (requires exclusive-lock)',
+ requires: 'exclusive-lock',
+ allowEnable: true,
+ allowDisable: true
+ },
+ 'journaling': {
+ desc: 'Journaling (requires exclusive-lock)',
+ requires: 'exclusive-lock',
+ allowEnable: true,
+ allowDisable: true
+ },
+ 'fast-diff': {
+ desc: 'Fast diff (requires object-map)',
+ requires: 'object-map',
+ allowEnable: true,
+ allowDisable: true
+ }
+ };
+ this.createForm();
+ for (const key of Object.keys(this.features)) {
+ const listItem = this.features[key];
+ listItem.key = key;
+ this.featuresList.push(listItem);
+ }
+ }
+
+ createForm() {
+ this.defaultFeaturesFormControl = new FormControl(true);
+ this.deepFlattenFormControl = new FormControl(false);
+ this.layeringFormControl = new FormControl(false);
+ this.exclusiveLockFormControl = new FormControl(false);
+ this.objectMapFormControl = new FormControl({value: false, disabled: true});
+ this.journalingFormControl = new FormControl({value: false, disabled: true});
+ this.fastDiffFormControl = new FormControl({value: false, disabled: true});
+ this.featuresFormGroups = new FormGroup({
+ defaultFeatures: this.defaultFeaturesFormControl,
+ 'deep-flatten': this.deepFlattenFormControl,
+ 'layering': this.layeringFormControl,
+ 'exclusive-lock': this.exclusiveLockFormControl,
+ 'object-map': this.objectMapFormControl,
+ 'journaling': this.journalingFormControl,
+ 'fast-diff': this.fastDiffFormControl,
+ });
+ this.rbdForm = new FormGroup({
+ name: new FormControl('', {
+ validators: [
+ Validators.required
+ ]
+ }),
+ pool: new FormControl(null, {
+ validators: [
+ Validators.required
+ ]
+ }),
+ useDataPool: new FormControl(false),
+ dataPool: new FormControl(null),
+ size: new FormControl(null, {
+ updateOn: 'blur'
+ }),
+ obj_size: new FormControl(this.defaultObjectSize),
+ features: this.featuresFormGroups,
+ stripingUnit: new FormControl(null),
+ stripingCount: new FormControl(null, {
+ updateOn: 'blur'
+ })
+ }, this.validateRbdForm(this.formatter));
+ }
+
+ disableForEdit() {
+ this.rbdForm.get('pool').disable();
+ this.rbdForm.get('useDataPool').disable();
+ this.rbdForm.get('dataPool').disable();
+ this.rbdForm.get('obj_size').disable();
+ this.rbdForm.get('stripingUnit').disable();
+ this.rbdForm.get('stripingCount').disable();
+ }
+
+ ngOnInit() {
+ if (this.router.url.startsWith('/rbd/edit')) {
+ this.editing = true;
+ }
+ if (this.editing) {
+ this.disableForEdit();
+ this.routeParamsSubscribe = this.route.params.subscribe(
+ (params: { pool: string, name: string }) => {
+ const poolName = params.pool;
+ const rbdName = params.name;
+ this.rbdService.get(poolName, rbdName)
+ .subscribe((resp: RbdFormResponseModel) => {
+ this.setResponse(resp);
+ });
+ }
+ );
+ }
+ this.poolService.list(['pool_name', 'type', 'flags_names', 'application_metadata']).then(
+ resp => {
+ const pools = [];
+ const dataPools = [];
+ for (const pool of resp) {
+ if (!_.isUndefined(pool.application_metadata.rbd)) {
+ if (pool.type === 'replicated') {
+ pools.push(pool);
+ dataPools.push(pool);
+ } else if (pool.type === 'erasure' &&
+ pool.flags_names.indexOf('ec_overwrites') !== -1) {
+ dataPools.push(pool);
+ }
+ }
+ }
+ this.pools = pools;
+ this.allPools = pools;
+ this.dataPools = dataPools;
+ this.allDataPools = dataPools;
+ if (this.pools.length === 1) {
+ const poolName = this.pools[0]['pool_name'];
+ this.rbdForm.get('pool').setValue(poolName);
+ this.onPoolChange(poolName);
+ }
+ }
+ );
+ this.defaultFeaturesFormControl.valueChanges.subscribe((value) => {
+ this.watchDataFeatures(null, value);
+ });
+ this.deepFlattenFormControl.valueChanges.subscribe((value) => {
+ this.watchDataFeatures('deep-flatten', value);
+ });
+ this.layeringFormControl.valueChanges.subscribe((value) => {
+ this.watchDataFeatures('layering', value);
+ });
+ this.exclusiveLockFormControl.valueChanges.subscribe((value) => {
+ this.watchDataFeatures('exclusive-lock', value);
+ });
+ this.objectMapFormControl.valueChanges.subscribe((value) => {
+ this.watchDataFeatures('object-map', value);
+ });
+ this.journalingFormControl.valueChanges.subscribe((value) => {
+ this.watchDataFeatures('journaling', value);
+ });
+ this.fastDiffFormControl.valueChanges.subscribe((value) => {
+ this.watchDataFeatures('fast-diff', value);
+ });
+ }
+
+ onPoolChange(selectedPoolName) {
+ const newDataPools = this.allDataPools.filter((dataPool: any) => {
+ return dataPool.pool_name !== selectedPoolName;
+ });
+ if (this.rbdForm.get('dataPool').value === selectedPoolName) {
+ this.rbdForm.get('dataPool').setValue(null);
+ }
+ this.dataPools = newDataPools;
+ }
+
+ onUseDataPoolChange () {
+ if (!this.rbdForm.get('useDataPool').value) {
+ this.rbdForm.get('dataPool').setValue(null);
+ this.onDataPoolChange(null);
+ }
+ }
+
+ onDataPoolChange(selectedDataPoolName) {
+ const newPools = this.allPools.filter((pool: any) => {
+ return pool.pool_name !== selectedDataPoolName;
+ });
+ if (this.rbdForm.get('pool').value === selectedDataPoolName) {
+ this.rbdForm.get('pool').setValue(null);
+ }
+ this.pools = newPools;
+ }
+
+ validateRbdForm(formatter: FormatterService) {
+ return (formGroup: FormGroup) => {
+ // Data Pool
+ const useDataPoolControl = formGroup.get('useDataPool');
+ const dataPoolControl = formGroup.get('dataPool');
+ let dataPoolControlErrors = null;
+ if (useDataPoolControl.value && dataPoolControl.value == null) {
+ dataPoolControlErrors = {'required': true};
+ }
+ dataPoolControl.setErrors(dataPoolControlErrors);
+ // Size
+ const sizeControl = formGroup.get('size');
+ const objectSizeControl = formGroup.get('obj_size');
+ const objectSizeInBytes = formatter.toBytes(
+ objectSizeControl.value != null ? objectSizeControl.value : this.defaultObjectSize);
+ const stripingCountControl = formGroup.get('stripingCount');
+ const stripingCount = stripingCountControl.value != null ? stripingCountControl.value : 1;
+ let sizeControlErrors = null;
+ if (sizeControl.value === null) {
+ sizeControlErrors = {'required': true};
+ } else {
+ const sizeInBytes = formatter.toBytes(sizeControl.value);
+ if (stripingCount * objectSizeInBytes > sizeInBytes) {
+ sizeControlErrors = {'invalidSizeObject': true};
+ }
+ }
+ sizeControl.setErrors(sizeControlErrors);
+ // Striping Unit
+ const stripingUnitControl = formGroup.get('stripingUnit');
+ let stripingUnitControlErrors = null;
+ if (stripingUnitControl.value === null && stripingCountControl.value !== null) {
+ stripingUnitControlErrors = {'required': true};
+ } else if (stripingUnitControl.value !== null) {
+ const stripingUnitInBytes = formatter.toBytes(stripingUnitControl.value);
+ if (stripingUnitInBytes > objectSizeInBytes) {
+ stripingUnitControlErrors = {'invalidStripingUnit': true};
+ }
+ }
+ stripingUnitControl.setErrors(stripingUnitControlErrors);
+ // Striping Count
+ let stripingCountControlErrors = null;
+ if (stripingCountControl.value === null && stripingUnitControl.value !== null) {
+ stripingCountControlErrors = {'required': true};
+ } else if (stripingCount < 1) {
+ stripingCountControlErrors = {'min': true};
+ }
+ stripingCountControl.setErrors(stripingCountControlErrors);
+ };
+ }
+
+ deepBoxCheck(key, checked) {
+ _.forIn(this.features, (details, feature) => {
+ if (details.requires === key) {
+ if (checked) {
+ this.featuresFormGroups.get(feature).enable();
+ } else {
+ this.featuresFormGroups.get(feature).disable();
+ this.featuresFormGroups.get(feature).setValue(checked);
+ this.watchDataFeatures(feature, checked);
+ this.deepBoxCheck(feature, checked);
+ }
+ }
+ if (this.editing && this.featuresFormGroups.get(feature).enabled) {
+
+ if (this.response.features_name.indexOf(feature) !== -1 && !details.allowDisable) {
+ this.featuresFormGroups.get(feature).disable();
+
+ } else if (this.response.features_name.indexOf(feature) === -1 && !details.allowEnable) {
+ this.featuresFormGroups.get(feature).disable();
+ }
+ }
+ });
+ }
+
+ featureFormUpdate(key, checked) {
+ if (checked) {
+ const required = this.features[key].requires;
+ if (required && !this.featuresFormGroups.get(required).value) {
+ this.featuresFormGroups.get(key).setValue(false);
+ return;
+ }
+ }
+ this.deepBoxCheck(key, checked);
+ }
+
+ watchDataFeatures(key, checked) {
+ if (!this.defaultFeaturesFormControl.value && key) {
+ this.featureFormUpdate(key, checked);
+ }
+ }
+
+ setResponse(response: RbdFormResponseModel) {
+ this.response = response;
+ this.rbdForm.get('name').setValue(response.name);
+ this.rbdForm.get('pool').setValue(response.pool_name);
+ if (response.data_pool) {
+ this.rbdForm.get('useDataPool').setValue(true);
+ this.rbdForm.get('dataPool').setValue(response.data_pool);
+ }
+ this.rbdForm.get('size').setValue(this.dimlessBinaryPipe.transform(response.size));
+ this.rbdForm.get('obj_size').setValue(this.dimlessBinaryPipe.transform(response.obj_size));
+ const featuresControl = this.rbdForm.get('features');
+ featuresControl.get('defaultFeatures').setValue(false);
+ _.forIn(this.features, (feature) => {
+ if (response.features_name.indexOf(feature.key) !== -1) {
+ featuresControl.get(feature.key).setValue(true);
+ }
+ });
+ this.rbdForm.get('stripingUnit').setValue(
+ this.dimlessBinaryPipe.transform(response.stripe_unit));
+ this.rbdForm.get('stripingCount').setValue(response.stripe_count);
+ }
+
+ createRequest() {
+ const request = new RbdFormCreateRequestModel();
+ request.pool_name = this.rbdForm.get('pool').value;
+ request.name = this.rbdForm.get('name').value;
+ request.size = this.formatter.toBytes(this.rbdForm.get('size').value);
+ request.obj_size = this.formatter.toBytes(this.rbdForm.get('obj_size').value);
+ if (!this.defaultFeaturesFormControl.value) {
+ _.forIn(this.features, (feature) => {
+ if (this.featuresFormGroups.get(feature.key).value) {
+ request.features.push(feature.key);
+ }
+ });
+ } else {
+ request.features = null;
+ }
+ request.stripe_unit = this.formatter.toBytes(this.rbdForm.get('stripingUnit').value);
+ request.stripe_count = this.rbdForm.get('stripingCount').value;
+ request.data_pool = this.rbdForm.get('dataPool').value;
+ return request;
+ }
+
+ createAction() {
+ const request = this.createRequest();
+ const finishedTask = new FinishedTask();
+ finishedTask.name = 'rbd/create';
+ finishedTask.metadata = {'pool_name': request.pool_name, 'image_name': request.name};
+ this.rbdService.create(request).toPromise().then((resp) => {
+ if (resp.status === 202) {
+ this.notificationService.show(NotificationType.info,
+ `RBD creation in progress...`,
+ this.taskManagerMessageService.getDescription(finishedTask));
+ this.taskManagerService.subscribe(finishedTask.name, finishedTask.metadata,
+ (asyncFinishedTask: FinishedTask) => {
+ this.notificationService.notifyTask(asyncFinishedTask);
+ });
+ } else {
+ finishedTask.success = true;
+ this.notificationService.notifyTask(finishedTask);
+ }
+ this.router.navigate(['/block/rbd']);
+ }, (resp) => {
+ this.rbdForm.setErrors({'cdSubmitButton': true});
+ finishedTask.success = false;
+ finishedTask.exception = resp.error;
+ this.notificationService.notifyTask(finishedTask);
+ });
+ }
+
+ editRequest() {
+ const request = new RbdFormEditRequestModel();
+ request.name = this.rbdForm.get('name').value;
+ request.size = this.formatter.toBytes(this.rbdForm.get('size').value);
+ if (!this.defaultFeaturesFormControl.value) {
+ _.forIn(this.features, (feature) => {
+ if (this.featuresFormGroups.get(feature.key).value) {
+ request.features.push(feature.key);
+ }
+ });
+ }
+ return request;
+ }
+
+ editAction() {
+ const request = this.editRequest();
+ const finishedTask = new FinishedTask();
+ finishedTask.name = 'rbd/edit';
+ finishedTask.metadata = {
+ 'pool_name': this.response.pool_name,
+ 'image_name': this.response.name
+ };
+ this.rbdService.update(this.response.pool_name, this.response.name, request)
+ .toPromise().then((resp) => {
+ if (resp.status === 202) {
+ this.notificationService.show(NotificationType.info,
+ `RBD update in progress...`,
+ this.taskManagerMessageService.getDescription(finishedTask));
+ this.taskManagerService.subscribe(finishedTask.name, finishedTask.metadata,
+ (asyncFinishedTask: FinishedTask) => {
+ this.notificationService.notifyTask(asyncFinishedTask);
+ });
+ } else {
+ finishedTask.success = true;
+ this.notificationService.notifyTask(finishedTask);
+ }
+ this.router.navigate(['/block/rbd']);
+ }).catch((resp) => {
+ this.rbdForm.setErrors({'cdSubmitButton': true});
+ finishedTask.success = false;
+ finishedTask.exception = resp.error;
+ this.notificationService.notifyTask(finishedTask);
+ });
+ }
+
+ submit() {
+ if (this.editing) {
+ this.editAction();
+ } else {
+ this.createAction();
+ }
+ }
+
+}
--- /dev/null
+export class RbdFormModel {
+ name: string;
+ pool_name: string;
+ data_pool: string;
+ size: number;
+ obj_size: number;
+ stripe_unit: number;
+ stripe_count: number;
+}
--- /dev/null
+<nav aria-label="breadcrumb">
+ <ol class="breadcrumb">
+ <li i18n
+ class="breadcrumb-item">Block</li>
+ <li i18n
+ class="breadcrumb-item active"
+ aria-current="page">Images</li>
+ </ol>
+</nav>
+
+<cd-view-cache *ngFor="let viewCacheStatus of viewCacheStatusList"
+ [status]="viewCacheStatus.status"
+ [statusFor]="viewCacheStatus.statusFor"></cd-view-cache>
+
+<cd-table [data]="images"
+ columnMode="flex"
+ [columns]="columns"
+ identifier="name"
+ selectionType="single"
+ (updateSelection)="updateSelection($event)">
+ <div class="table-actions">
+ <div class="btn-group" dropdown>
+ <button type="button"
+ class="btn btn-sm btn-primary"
+ *ngIf="!selection.hasSingleSelection"
+ routerLink="/rbd/add">
+ <i class="fa fa-fw fa-plus"></i><span i18n>Add</span>
+ </button>
+ <button type="button"
+ class="btn btn-sm btn-primary"
+ *ngIf="selection.hasSingleSelection"
+ [ngClass]="{'disabled': selection.first().executing}"
+ routerLink="/rbd/edit/{{ selection.first()?.pool_name }}/{{ selection.first()?.name }}">
+ <i class="fa fa-fw fa-pencil"></i><span i18n>Edit</span>
+ </button>
+ <button type="button" dropdownToggle class="btn btn-sm btn-primary dropdown-toggle dropdown-toggle-split">
+ <span class="caret"></span>
+ <span class="sr-only"></span>
+ </button>
+ <ul *dropdownMenu class="dropdown-menu" role="menu">
+ <li role="menuitem">
+ <a class="dropdown-item" routerLink="/rbd/add"><i class="fa fa-fw fa-plus"></i><span i18n>Add</span></a>
+ </li>
+ <li role="menuitem"
+ [ngClass]="{'disabled': !selection.hasSingleSelection || selection.first().executing}">
+ <a class="dropdown-item" routerLink="/rbd/edit/{{ selection.first()?.pool_name }}/{{ selection.first()?.name }}"><i class="fa fa-fw fa-pencil"></i><span i18n>Edit</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>
+ </li>
+ </ul>
+ </div>
+ </div>
+ <cd-rbd-details cdTableDetail
+ [selection]="selection">
+ </cd-rbd-details>
+</cd-table>
+
+<ng-template #usageNotAvailableTooltipTpl>
+ <div i18n [innerHtml]="'Only available for RBD images with <strong>fast-diff</strong> enabled'"></div>
+</ng-template>
+
+<ng-template #parentTpl
+ let-value="value">
+ <span *ngIf="value">{{ value.pool_name }}/{{ value.image_name }}@{{ value.snap_name }}</span>
+ <span *ngIf="!value">-</span>
+</ng-template>
--- /dev/null
+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 {
+ AlertModule,
+ BsDropdownModule,
+ ModalModule,
+ TabsModule,
+ TooltipModule
+} from 'ngx-bootstrap';
+
+import { ComponentsModule } from '../../../shared/components/components.module';
+import { SharedModule } from '../../../shared/shared.module';
+import { RbdDetailsComponent } from '../rbd-details/rbd-details.component';
+import { RbdSnapshotListComponent } from '../rbd-snapshot-list/rbd-snapshot-list.component';
+import { RbdListComponent } from './rbd-list.component';
+
+describe('RbdListComponent', () => {
+ let component: RbdListComponent;
+ let fixture: ComponentFixture<RbdListComponent>;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ SharedModule,
+ BsDropdownModule.forRoot(),
+ TabsModule.forRoot(),
+ ModalModule.forRoot(),
+ TooltipModule.forRoot(),
+ ToastModule.forRoot(),
+ AlertModule.forRoot(),
+ ComponentsModule,
+ RouterTestingModule,
+ HttpClientTestingModule
+ ],
+ declarations: [ RbdListComponent, RbdDetailsComponent, RbdSnapshotListComponent ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(RbdListComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
--- /dev/null
+import { Component, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/core';
+
+import * as _ from 'lodash';
+import { BsModalRef, BsModalService } from 'ngx-bootstrap';
+
+import { RbdService } from '../../../shared/api/rbd.service';
+import {
+ DeleteConfirmationComponent
+} from '../../../shared/components/delete-confirmation-modal/delete-confirmation-modal.component';
+import { CellTemplate } from '../../../shared/enum/cell-template.enum';
+import { NotificationType } from '../../../shared/enum/notification-type.enum';
+import { ViewCacheStatus } from '../../../shared/enum/view-cache-status.enum';
+import { CdTableColumn } from '../../../shared/models/cd-table-column';
+import { CdTableSelection } from '../../../shared/models/cd-table-selection';
+import { ExecutingTask } from '../../../shared/models/executing-task';
+import { FinishedTask } from '../../../shared/models/finished-task';
+import { DimlessBinaryPipe } from '../../../shared/pipes/dimless-binary.pipe';
+import { DimlessPipe } from '../../../shared/pipes/dimless.pipe';
+import {
+ NotificationService
+} from '../../../shared/services/notification.service';
+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 { RbdModel } from './rbd-model';
+
+@Component({
+ selector: 'cd-rbd-list',
+ templateUrl: './rbd-list.component.html',
+ styleUrls: ['./rbd-list.component.scss']
+})
+export class RbdListComponent implements OnInit, OnDestroy {
+
+ @ViewChild('usageTpl') usageTpl: TemplateRef<any>;
+ @ViewChild('parentTpl') parentTpl: TemplateRef<any>;
+ @ViewChild('nameTpl') nameTpl: TemplateRef<any>;
+
+ images: any;
+ executingTasks: ExecutingTask[] = [];
+ columns: CdTableColumn[];
+ retries: number;
+ viewCacheStatusList: any[];
+ selection = new CdTableSelection();
+
+ summaryDataSubscription = null;
+
+ modalRef: BsModalRef;
+
+ constructor(private rbdService: RbdService,
+ private dimlessBinaryPipe: DimlessBinaryPipe,
+ private dimlessPipe: DimlessPipe,
+ private summaryService: SummaryService,
+ private modalService: BsModalService,
+ private notificationService: NotificationService,
+ private taskManagerMessageService: TaskManagerMessageService,
+ private taskManagerService: TaskManagerService) {
+ }
+
+ ngOnInit() {
+ this.columns = [
+ {
+ name: 'Name',
+ prop: 'name',
+ flexGrow: 2,
+ cellTransformation: CellTemplate.executing
+ },
+ {
+ name: 'Pool',
+ prop: 'pool_name',
+ flexGrow: 2
+ },
+ {
+ name: 'Size',
+ prop: 'size',
+ flexGrow: 1,
+ cellClass: 'text-right',
+ pipe: this.dimlessBinaryPipe
+ },
+ {
+ name: 'Objects',
+ prop: 'num_objs',
+ flexGrow: 1,
+ cellClass: 'text-right',
+ pipe: this.dimlessPipe
+ },
+ {
+ name: 'Object size',
+ prop: 'obj_size',
+ flexGrow: 1,
+ cellClass: 'text-right',
+ pipe: this.dimlessBinaryPipe
+ },
+ {
+ name: 'Provisioned',
+ prop: 'disk_usage',
+ cellClass: 'text-center',
+ flexGrow: 1,
+ pipe: this.dimlessBinaryPipe
+ },
+ {
+ name: 'Total provisioned',
+ prop: 'total_disk_usage',
+ cellClass: 'text-center',
+ flexGrow: 1,
+ pipe: this.dimlessBinaryPipe
+ },
+ {
+ name: 'Parent',
+ prop: 'parent',
+ flexGrow: 2,
+ cellTemplate: this.parentTpl
+ }
+ ];
+
+ this.summaryService.get().then(resp => {
+ this.loadImages(resp.executing_tasks);
+ this.summaryDataSubscription = this.summaryService.summaryData$.subscribe((data: any) => {
+ this.loadImages(data.executing_tasks);
+ });
+ });
+
+ }
+
+ ngOnDestroy() {
+ if (this.summaryDataSubscription) {
+ this.summaryDataSubscription.unsubscribe();
+ }
+ }
+
+ loadImages(executingTasks) {
+ if (executingTasks === null) {
+ executingTasks = this.executingTasks;
+ }
+ this.rbdService.list()
+ .subscribe(
+ (resp: any[]) => {
+ let images = [];
+ const viewCacheStatusMap = {};
+ resp.forEach(pool => {
+ if (_.isUndefined(viewCacheStatusMap[pool.status])) {
+ viewCacheStatusMap[pool.status] = [];
+ }
+ viewCacheStatusMap[pool.status].push(pool.pool_name);
+ images = images.concat(pool.value);
+ });
+ const viewCacheStatusList = [];
+ _.forEach(viewCacheStatusMap, (value, key) => {
+ viewCacheStatusList.push({
+ status: parseInt(key, 10),
+ statusFor: (value.length > 1 ? 'pools ' : 'pool ') +
+ '<strong>' + value.join('</strong>, <strong>') + '</strong>'
+ });
+ });
+ this.viewCacheStatusList = viewCacheStatusList;
+ images.forEach(image => {
+ image.executingTasks = this._getExecutingTasks(executingTasks,
+ image.pool_name, image.name);
+ });
+ this.images = this.merge(images, executingTasks);
+ this.executingTasks = executingTasks;
+ },
+ () => {
+ this.viewCacheStatusList = [{status: ViewCacheStatus.ValueException}];
+ }
+ );
+ }
+
+ _getExecutingTasks(executingTasks: ExecutingTask[], poolName, imageName): ExecutingTask[] {
+ const result: ExecutingTask[] = [];
+ executingTasks.forEach(executingTask => {
+ if (executingTask.name === 'rbd/snap/create' ||
+ executingTask.name === 'rbd/snap/delete' ||
+ executingTask.name === 'rbd/snap/edit' ||
+ executingTask.name === 'rbd/snap/rollback') {
+ if (poolName === executingTask.metadata['pool_name'] &&
+ imageName === executingTask.metadata['image_name']) {
+ result.push(executingTask);
+ }
+ }
+ });
+ return result;
+ }
+
+ private merge(rbds: RbdModel[], executingTasks: ExecutingTask[] = []) {
+ const resultRBDs = _.clone(rbds);
+ executingTasks.forEach((executingTask) => {
+ const rbdExecuting = resultRBDs.find((rbd) => {
+ return rbd.pool_name === executingTask.metadata['pool_name'] &&
+ rbd.name === executingTask.metadata['image_name'];
+ });
+ if (rbdExecuting) {
+ if (executingTask.name === 'rbd/delete') {
+ rbdExecuting.cdExecuting = 'deleting';
+
+ } else if (executingTask.name === 'rbd/edit') {
+ rbdExecuting.cdExecuting = 'updating';
+ }
+ } else if (executingTask.name === 'rbd/create') {
+ const rbdModel = new RbdModel();
+ rbdModel.name = executingTask.metadata['image_name'];
+ rbdModel.pool_name = executingTask.metadata['pool_name'];
+ rbdModel.cdExecuting = 'creating';
+ resultRBDs.push(rbdModel);
+ }
+ });
+ return resultRBDs;
+ }
+
+ updateSelection(selection: CdTableSelection) {
+ this.selection = selection;
+ }
+
+ deleteRbd(poolName: string, imageName: string) {
+ const finishedTask = new FinishedTask();
+ finishedTask.name = 'rbd/delete';
+ finishedTask.metadata = {'pool_name': poolName, 'image_name': imageName};
+ this.rbdService.delete(poolName, imageName)
+ .toPromise().then((resp) => {
+ if (resp.status === 202) {
+ this.notificationService.show(NotificationType.info,
+ `RBD deletion 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);
+ });
+ }
+
+ deleteRbdModal() {
+ const poolName = this.selection.first().pool_name;
+ const imageName = this.selection.first().name;
+ this.modalRef = this.modalService.show(DeleteConfirmationComponent);
+ this.modalRef.content.itemName = `${poolName}/${imageName}`;
+ this.modalRef.content.onSubmit.subscribe(() => {
+ this.deleteRbd(poolName, imageName);
+ });
+ }
+}
--- /dev/null
+export class RbdModel {
+ name: string;
+ pool_name: string;
+
+ cdExecuting: string;
+}
--- /dev/null
+<div class="modal-header">
+ <h4 class="modal-title pull-left">{{ editing ? 'Rename' : 'Create' }} RBD Snapshot</h4>
+ <button type="button" class="close pull-right" aria-label="Close" (click)="modalRef.hide()">
+ <span aria-hidden="true">×</span>
+ </button>
+</div>
+<form name="snapshotForm"
+ class="form-horizontal"
+ #formDir="ngForm"
+ [formGroup]="snapshotForm"
+ novalidate>
+ <div class="modal-body">
+
+ <!-- Name -->
+ <div class="form-group"
+ [ngClass]="{'has-error': (formDir.submitted || snapshotForm.controls.snapshotName.dirty) && snapshotForm.controls.snapshotName.invalid}">
+ <label i18n
+ class="control-label col-sm-3"
+ for="snapshotName">Name
+ <span class="required"></span>
+ </label>
+ <div class="col-sm-9">
+ <input class="form-control"
+ type="text"
+ placeholder="Snapshot name..."
+ id="snapshotName"
+ name="snapshotName"
+ formControlName="snapshotName"
+ autofocus>
+ <span i18n
+ class="help-block"
+ *ngIf="(formDir.submitted || snapshotForm.controls.snapshotName.dirty) && snapshotForm.controls.snapshotName.hasError('required')">
+ This field is required.
+ </span>
+ </div>
+ </div>
+
+ </div>
+
+ <div class="modal-footer">
+ <div class="button-group text-right">
+ <cd-submit-button [form]="snapshotForm"
+ (submitAction)="submit()">
+ {{ editing ? 'Rename' : 'Create' }} Snapshot
+ </cd-submit-button>
+ <button type="button" class="btn btn-sm btn-default" (click)="modalRef.hide()">Close</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 { ComponentsModule } from '../../../shared/components/components.module';
+import { AuthStorageService } from '../../../shared/services/auth-storage.service';
+import { ServicesModule } from '../../../shared/services/services.module';
+import { RbdSnapshotFormComponent } from './rbd-snapshot-form.component';
+
+describe('RbdSnapshotFormComponent', () => {
+ let component: RbdSnapshotFormComponent;
+ let fixture: ComponentFixture<RbdSnapshotFormComponent>;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ ReactiveFormsModule,
+ ComponentsModule,
+ HttpClientTestingModule,
+ ServicesModule,
+ ApiModule,
+ ToastModule.forRoot()
+ ],
+ declarations: [ RbdSnapshotFormComponent ],
+ providers: [ BsModalRef, BsModalService, AuthStorageService ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(RbdSnapshotFormComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
--- /dev/null
+import { Component, OnInit } from '@angular/core';
+import { FormControl, FormGroup, Validators } from '@angular/forms';
+
+import { BsModalRef } from 'ngx-bootstrap';
+import { Subject } from 'rxjs/Subject';
+
+import { RbdService } from '../../../shared/api/rbd.service';
+import { FinishedTask } from '../../../shared/models/finished-task';
+import {
+ NotificationService
+} from '../../../shared/services/notification.service';
+import { TaskManagerService } from '../../../shared/services/task-manager.service';
+
+@Component({
+ selector: 'cd-rbd-snapshot-form',
+ templateUrl: './rbd-snapshot-form.component.html',
+ styleUrls: ['./rbd-snapshot-form.component.scss']
+})
+export class RbdSnapshotFormComponent implements OnInit {
+
+ poolName: string;
+ imageName: string;
+ snapName: string;
+
+ snapshotForm: FormGroup;
+
+ editing = false;
+
+ public onSubmit: Subject<string>;
+
+ constructor(public modalRef: BsModalRef,
+ private rbdService: RbdService,
+ private taskManagerService: TaskManagerService,
+ private notificationService: NotificationService) {
+ this.createForm();
+ }
+
+ createForm() {
+ this.snapshotForm = new FormGroup({
+ snapshotName: new FormControl('', {
+ validators: [
+ Validators.required
+ ]
+ })
+ });
+ }
+
+ ngOnInit() {
+ this.onSubmit = new Subject();
+ }
+
+ setSnapName(snapName) {
+ this.snapName = snapName;
+ this.snapshotForm.get('snapshotName').setValue(snapName);
+ this.editing = true;
+ }
+
+ editAction() {
+ const snapshotName = this.snapshotForm.get('snapshotName').value;
+ const finishedTask = new FinishedTask();
+ finishedTask.name = 'rbd/snap/edit';
+ finishedTask.metadata = {
+ 'pool_name': this.poolName,
+ 'image_name': this.imageName,
+ 'snapshot_name': snapshotName
+ };
+ this.rbdService.renameSnapshot(this.poolName, this.imageName, this.snapName, snapshotName)
+ .toPromise().then((resp) => {
+ this.taskManagerService.subscribe(finishedTask.name, finishedTask.metadata,
+ (asyncFinishedTask: FinishedTask) => {
+ this.notificationService.notifyTask(asyncFinishedTask);
+ });
+ this.modalRef.hide();
+ this.onSubmit.next(this.snapName);
+ }).catch((resp) => {
+ this.snapshotForm.setErrors({'cdSubmitButton': true});
+ finishedTask.success = false;
+ finishedTask.exception = resp.error;
+ this.notificationService.notifyTask(finishedTask);
+ });
+ }
+
+ createAction() {
+ const snapshotName = this.snapshotForm.get('snapshotName').value;
+ const finishedTask = new FinishedTask();
+ finishedTask.name = 'rbd/snap/create';
+ finishedTask.metadata = {
+ 'pool_name': this.poolName,
+ 'image_name': this.imageName,
+ 'snapshot_name': snapshotName
+ };
+ this.rbdService.createSnapshot(this.poolName, this.imageName, snapshotName)
+ .toPromise().then((resp) => {
+ this.taskManagerService.subscribe(finishedTask.name, finishedTask.metadata,
+ (asyncFinishedTask: FinishedTask) => {
+ this.notificationService.notifyTask(asyncFinishedTask);
+ });
+ this.modalRef.hide();
+ this.onSubmit.next(snapshotName);
+ }).catch((resp) => {
+ this.snapshotForm.setErrors({'cdSubmitButton': true});
+ finishedTask.success = false;
+ finishedTask.exception = resp.error;
+ this.notificationService.notifyTask(finishedTask);
+ });
+ }
+
+ submit() {
+ if (this.editing) {
+ this.editAction();
+ } else {
+ this.createAction();
+ }
+ }
+}
--- /dev/null
+<cd-table [data]="data"
+ columnMode="flex"
+ selectionType="single"
+ (updateSelection)="updateSelection($event)"
+ [columns]="columns">
+ <div class="table-actions">
+ <div class="btn-group" dropdown>
+ <button type="button"
+ class="btn btn-sm btn-primary"
+ *ngIf="!selection.hasSingleSelection"
+ (click)="openCreateSnapshotModal()">
+ <i class="fa fa-fw fa-plus"></i><span i18n>Create</span>
+ </button>
+ <button type="button"
+ class="btn btn-sm btn-primary"
+ *ngIf="selection.hasSingleSelection"
+ [ngClass]="{'disabled': selection.first().executing}"
+ (click)="openEditSnapshotModal()">
+ <i class="fa fa-fw fa-pencil"></i><span i18n>Rename</span>
+ </button>
+ <button type="button" dropdownToggle class="btn btn-sm btn-primary dropdown-toggle dropdown-toggle-split">
+ <span class="caret"></span>
+ <span class="sr-only"></span>
+ </button>
+ <ul *dropdownMenu class="dropdown-menu" role="menu">
+ <li role="menuitem"><a class="dropdown-item" (click)="openCreateSnapshotModal()"><i class="fa fa-fw fa-plus"></i><span i18n>Create</span></a></li>
+ <li role="menuitem"
+ [ngClass]="{'disabled': !selection.hasSingleSelection || selection.first().executing}"><a class="dropdown-item" (click)="openEditSnapshotModal()"><i class="fa fa-fw fa-pencil"></i><span i18n>Rename</span></a></li>
+ <li role="menuitem"
+ [ngClass]="{'disabled': !selection.hasSingleSelection || selection.first().executing}">
+ <a class="dropdown-item" (click)="toggleProtection()">
+ <span *ngIf="!selection.first()?.is_protected"><i class="fa fa-fw fa-lock"></i><span i18n>Protect</span></span>
+ <span *ngIf="selection.first()?.is_protected"><i class="fa fa-fw fa-unlock"></i><span i18n>Unprotect</span></span>
+ </a>
+ </li>
+ <li role="menuitem"
+ [ngClass]="{'disabled': !selection.hasSingleSelection || selection.first().executing}">
+ <a class="dropdown-item" (click)="rollbackModal()"><i class="fa fa-fw fa-undo"></i><span i18n>Rollback</span></a>
+ </li>
+ <li role="menuitem"
+ [ngClass]="{'disabled': !selection.hasSingleSelection || selection.first().executing || selection.first().is_protected}">
+ <a class="dropdown-item" (click)="deleteSnapshotModal()"><i class="fa fa-fw fa-trash-o"></i><span i18n>Delete</span></a>
+ </li>
+ </ul>
+ </div>
+ </div>
+</cd-table>
+
+<ng-template #protectTpl
+ let-value="value">
+ <span *ngIf="value" class="label label-success">PROTECTED</span>
+ <span *ngIf="!value" class="label label-info">UNPROTECTED</span>
+</ng-template>
--- /dev/null
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { ToastModule } from 'ng2-toastr';
+import { ModalModule } from 'ngx-bootstrap';
+
+import { ApiModule } from '../../../shared/api/api.module';
+import { ComponentsModule } from '../../../shared/components/components.module';
+import { DataTableModule } from '../../../shared/datatable/datatable.module';
+import { AuthStorageService } from '../../../shared/services/auth-storage.service';
+import { ServicesModule } from '../../../shared/services/services.module';
+import { RbdSnapshotListComponent } from './rbd-snapshot-list.component';
+
+describe('RbdSnapshotListComponent', () => {
+ let component: RbdSnapshotListComponent;
+ let fixture: ComponentFixture<RbdSnapshotListComponent>;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ RbdSnapshotListComponent ],
+ imports: [
+ DataTableModule,
+ ComponentsModule,
+ ModalModule.forRoot(),
+ ToastModule.forRoot(),
+ ServicesModule,
+ ApiModule,
+ HttpClientTestingModule
+ ],
+ providers: [ AuthStorageService ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(RbdSnapshotListComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
--- /dev/null
+import {
+ Component,
+ Input,
+ OnChanges,
+ OnInit,
+ TemplateRef,
+ ViewChild
+} from '@angular/core';
+
+import * as _ from 'lodash';
+import { ToastsManager } from 'ng2-toastr';
+import { BsModalRef, BsModalService } from 'ngx-bootstrap';
+
+import {
+ RbdService
+} from '../../../shared/api/rbd.service';
+import {
+ DeleteConfirmationComponent
+} from '../../../shared/components/delete-confirmation-modal/delete-confirmation-modal.component';
+import { CellTemplate } from '../../../shared/enum/cell-template.enum';
+import { CdTableColumn } from '../../../shared/models/cd-table-column';
+import { CdTableSelection } from '../../../shared/models/cd-table-selection';
+import { ExecutingTask } from '../../../shared/models/executing-task';
+import { FinishedTask } from '../../../shared/models/finished-task';
+import { CdDatePipe } from '../../../shared/pipes/cd-date.pipe';
+import { DimlessBinaryPipe } from '../../../shared/pipes/dimless-binary.pipe';
+import {
+ NotificationService
+} from '../../../shared/services/notification.service';
+import { TaskManagerService } from '../../../shared/services/task-manager.service';
+import { RbdSnapshotFormComponent } from '../rbd-snapshot-form/rbd-snapshot-form.component';
+import {
+ RollbackConfirmationModalComponent
+} from '../rollback-confirmation-modal/rollback-confimation-modal.component';
+import { RbdSnapshotModel } from './rbd-snapshot.model';
+
+@Component({
+ selector: 'cd-rbd-snapshot-list',
+ templateUrl: './rbd-snapshot-list.component.html',
+ styleUrls: ['./rbd-snapshot-list.component.scss']
+})
+export class RbdSnapshotListComponent implements OnInit, OnChanges {
+
+ @Input() snapshots: RbdSnapshotModel[] = [];
+ @Input() poolName: string;
+ @Input() rbdName: string;
+ @Input() executingTasks: ExecutingTask[] = [];
+
+ @ViewChild('nameTpl') nameTpl: TemplateRef<any>;
+ @ViewChild('protectTpl') protectTpl: TemplateRef<any>;
+
+ data: RbdSnapshotModel[];
+
+ columns: CdTableColumn[];
+
+ modalRef: BsModalRef;
+
+ selection = new CdTableSelection();
+
+ constructor(private modalService: BsModalService,
+ private dimlessBinaryPipe: DimlessBinaryPipe,
+ private cdDatePipe: CdDatePipe,
+ private rbdService: RbdService,
+ private toastr: ToastsManager,
+ private taskManagerService: TaskManagerService,
+ private notificationService: NotificationService) { }
+
+ ngOnInit() {
+ this.columns = [
+ {
+ name: 'Name',
+ prop: 'name',
+ cellTransformation: CellTemplate.executing,
+ flexGrow: 2
+ },
+ {
+ name: 'Size',
+ prop: 'size',
+ flexGrow: 1,
+ cellClass: 'text-right',
+ pipe: this.dimlessBinaryPipe
+ },
+ {
+ name: 'Provisioned',
+ prop: 'disk_usage',
+ flexGrow: 1,
+ cellClass: 'text-right',
+ pipe: this.dimlessBinaryPipe
+ },
+ {
+ name: 'State',
+ prop: 'is_protected',
+ flexGrow: 1,
+ cellClass: 'text-center',
+ cellTemplate: this.protectTpl
+ },
+ {
+ name: 'Created',
+ prop: 'timestamp',
+ flexGrow: 1,
+ pipe: this.cdDatePipe
+ }
+ ];
+ }
+
+ ngOnChanges() {
+ this.data = this.merge(this.snapshots, this.executingTasks);
+ }
+
+ private merge(snapshots: RbdSnapshotModel[], executingTasks: ExecutingTask[] = []) {
+ const resultSnapshots = _.clone(snapshots);
+ executingTasks.forEach((executingTask) => {
+ const snapshotExecuting = resultSnapshots.find((snapshot) => {
+ return snapshot.name === executingTask.metadata['snapshot_name'];
+ });
+ if (snapshotExecuting) {
+ if (executingTask.name === 'rbd/snap/delete') {
+ snapshotExecuting.cdExecuting = 'deleting';
+
+ } else if (executingTask.name === 'rbd/snap/edit') {
+ snapshotExecuting.cdExecuting = 'updating';
+
+ } else if (executingTask.name === 'rbd/snap/rollback') {
+ snapshotExecuting.cdExecuting = 'rolling back';
+ }
+ } else if (executingTask.name === 'rbd/snap/create') {
+ const rbdSnapshotModel = new RbdSnapshotModel();
+ rbdSnapshotModel.name = executingTask.metadata['snapshot_name'];
+ rbdSnapshotModel.cdExecuting = 'creating';
+ resultSnapshots.push(rbdSnapshotModel);
+ }
+ });
+ return resultSnapshots;
+ }
+
+ private openSnapshotModal(taskName: string, oldSnapshotName: string = null) {
+ this.modalRef = this.modalService.show(RbdSnapshotFormComponent);
+ this.modalRef.content.poolName = this.poolName;
+ this.modalRef.content.imageName = this.rbdName;
+ if (oldSnapshotName) {
+ this.modalRef.content.setSnapName(this.selection.first().name);
+ }
+ this.modalRef.content.onSubmit.subscribe((snapshotName: string) => {
+ const executingTask = new ExecutingTask();
+ executingTask.name = taskName;
+ executingTask.metadata = {'snapshot_name': snapshotName};
+ this.executingTasks.push(executingTask);
+ this.ngOnChanges();
+ });
+ }
+
+ openCreateSnapshotModal() {
+ this.openSnapshotModal('rbd/snap/create');
+ }
+
+ openEditSnapshotModal() {
+ this.openSnapshotModal('rbd/snap/edit', this.selection.first().name);
+ }
+
+ toggleProtection() {
+ const snapshotName = this.selection.first().name;
+ const isProtected = this.selection.first().is_protected;
+ const finishedTask = new FinishedTask();
+ finishedTask.name = 'rbd/snap/edit';
+ finishedTask.metadata = {
+ 'pool_name': this.poolName,
+ 'image_name': this.rbdName,
+ 'snapshot_name': snapshotName
+ };
+ this.rbdService.protectSnapshot(this.poolName, this.rbdName, snapshotName, !isProtected)
+ .toPromise().then((resp) => {
+ const executingTask = new ExecutingTask();
+ executingTask.name = finishedTask.name;
+ executingTask.metadata = finishedTask.metadata;
+ this.executingTasks.push(executingTask);
+ this.ngOnChanges();
+ this.taskManagerService.subscribe(finishedTask.name, finishedTask.metadata,
+ (asyncFinishedTask: FinishedTask) => {
+ this.notificationService.notifyTask(asyncFinishedTask);
+ });
+ }).catch((resp) => {
+ finishedTask.success = false;
+ finishedTask.exception = resp.error;
+ this.notificationService.notifyTask(finishedTask);
+ });
+ }
+
+ private asyncTask(task: string, taskName: string, snapshotName: string) {
+ const finishedTask = new FinishedTask();
+ finishedTask.name = taskName;
+ finishedTask.metadata = {
+ 'pool_name': this.poolName,
+ 'image_name': this.rbdName,
+ 'snapshot_name': snapshotName
+ };
+ this.rbdService[task](this.poolName, this.rbdName, snapshotName)
+ .toPromise().then(() => {
+ const executingTask = new ExecutingTask();
+ executingTask.name = finishedTask.name;
+ executingTask.metadata = finishedTask.metadata;
+ this.executingTasks.push(executingTask);
+ this.modalRef.hide();
+ this.ngOnChanges();
+ this.taskManagerService.subscribe(executingTask.name, executingTask.metadata,
+ (asyncFinishedTask: FinishedTask) => {
+ this.notificationService.notifyTask(asyncFinishedTask);
+ });
+ })
+ .catch((resp) => {
+ this.modalRef.hide();
+ finishedTask.success = false;
+ finishedTask.exception = resp.error;
+ this.notificationService.notifyTask(finishedTask);
+ });
+ }
+
+ rollbackModal() {
+ const snapshotName = this.selection.selected[0].name;
+ this.modalRef = this.modalService.show(RollbackConfirmationModalComponent);
+ this.modalRef.content.snapName = `${this.poolName}/${this.rbdName}@${snapshotName}`;
+ this.modalRef.content.onSubmit.subscribe((itemName: string) => {
+ this.asyncTask('rollbackSnapshot', 'rbd/snap/rollback', snapshotName);
+ });
+ }
+
+ deleteSnapshotModal() {
+ const snapshotName = this.selection.selected[0].name;
+ this.modalRef = this.modalService.show(DeleteConfirmationComponent);
+ this.modalRef.content.itemName = snapshotName;
+ this.modalRef.content.onSubmit.subscribe((itemName: string) => {
+ this.asyncTask('deleteSnapshot', 'rbd/snap/delete', snapshotName);
+ });
+ }
+
+ updateSelection(selection: CdTableSelection) {
+ this.selection = selection;
+ }
+}
--- /dev/null
+export class RbdSnapshotModel {
+ id: number;
+ name: string;
+ size: number;
+ timestamp: string;
+ is_protected: boolean;
+
+ cdExecuting: string;
+}
--- /dev/null
+<div class="modal-header">
+ <h4 i18n
+ class="modal-title pull-left">RBD snapshot rollback</h4>
+ <button type="button" class="close pull-right" aria-label="Close" (click)="modalRef.hide()">
+ <span aria-hidden="true">×</span>
+ </button>
+</div>
+<form name="rollbackForm"
+ class="form-horizontal"
+ #formDir="ngForm"
+ [formGroup]="rollbackForm"
+ novalidate>
+ <div class="modal-body">
+ You are about to rollback <strong>{{ snapName }}</strong>.
+ </div>
+ <div class="modal-footer">
+ <div class="button-group text-right">
+ <cd-submit-button i18n
+ [form]="rollbackForm"
+ (submitAction)="submit()">
+ Rollback
+ </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 { RollbackConfirmationModalComponent } from './rollback-confimation-modal.component';
+
+describe('RollbackConfirmationModalComponent', () => {
+ let component: RollbackConfirmationModalComponent;
+ let fixture: ComponentFixture<RollbackConfirmationModalComponent>;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ ReactiveFormsModule,
+ HttpClientTestingModule,
+ SharedModule,
+ ServicesModule,
+ ApiModule,
+ ToastModule.forRoot()
+ ],
+ declarations: [ RollbackConfirmationModalComponent ],
+ providers: [ BsModalRef, BsModalService ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(RollbackConfirmationModalComponent);
+ 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-rollback-confimation-modal',
+ templateUrl: './rollback-confimation-modal.component.html',
+ styleUrls: ['./rollback-confimation-modal.component.scss']
+})
+export class RollbackConfirmationModalComponent implements OnInit {
+
+ snapName: string;
+
+ rollbackForm: FormGroup;
+
+ public onSubmit: Subject<string>;
+
+ constructor(public modalRef: BsModalRef) {
+ this.createForm();
+ }
+
+ createForm() {
+ this.rollbackForm = new FormGroup({});
+ }
+
+ ngOnInit() {
+ this.onSubmit = new Subject();
+ }
+
+ submit() {
+ this.onSubmit.next(this.snapName);
+ }
+}
</a>
<ul class="dropdown-menu">
+ <li routerLinkActive="active">
+ <a i18n
+ class="dropdown-item"
+ routerLink="/block/rbd">Images</a>
+ </li>
+
<li routerLinkActive="active"
class="tc_submenuitem tc_submenuitem_block_mirroring">
<a i18n
routerLink="/block/iscsi">iSCSI</a>
</li>
- <li class="dropdown-submenu">
- <a class="dropdown-toggle"
- data-toggle="dropdown">Pools</a>
- <ul *dropdownMenu
- class="dropdown-menu">
- <li routerLinkActive="active"
- class="tc_submenuitem tc_submenuitem_pools"
- *ngFor="let rbdPool of rbdPools">
- <a i18n
- class="dropdown-item"
- routerLink="/block/pool/{{ rbdPool }}">{{ rbdPool }}
- </a>
- </li>
- <li class="tc_submenuitem tc_submenuitem_cephfs_nofs"
- *ngIf="rbdPools.length === 0">
- <a class="dropdown-item disabled"
- i18n>There are no pools</a>
- </li>
- </ul>
- </li>
</ul>
</li>
import { OsdService } from './osd.service';
import { PoolService } from './pool.service';
import { RbdMirroringService } from './rbd-mirroring.service';
+import { RbdService } from './rbd.service';
import { RgwDaemonService } from './rgw-daemon.service';
import { TablePerformanceCounterService } from './table-performance-counter.service';
import { TcmuIscsiService } from './tcmu-iscsi.service';
MonitorService,
OsdService,
PoolService,
+ RbdService,
RbdMirroringService,
RgwDaemonService,
TablePerformanceCounterService,
constructor(private http: HttpClient) {
}
- rbdPoolImages(pool) {
- return this.http.get(`api/block/image?pool_name=${pool}`).toPromise().then((resp: any) => {
+ list(attrs = []) {
+ const attrsStr = attrs.join(',');
+ return this.http.get(`api/pool?attrs=${attrsStr}`).toPromise().then((resp: any) => {
return resp;
});
}
--- /dev/null
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+
+@Injectable()
+export class RbdService {
+
+ constructor(private http: HttpClient) {
+ }
+
+ create(rbd) {
+ return this.http.post('api/block/image', rbd, { observe: 'response' });
+ }
+
+ delete(poolName, rbdName) {
+ return this.http.delete(`api/block/image/${poolName}/${rbdName}`, { observe: 'response' });
+ }
+
+ update(poolName, rbdName, rbd) {
+ return this.http.put(`api/block/image/${poolName}/${rbdName}`, rbd, { observe: 'response' });
+ }
+
+ get(poolName, rbdName) {
+ return this.http.get(`api/block/image/${poolName}/${rbdName}`);
+ }
+
+ list() {
+ return this.http.get('api/block/image');
+ }
+
+ createSnapshot(poolName, rbdName, snapshotName) {
+ const request = {
+ snapshot_name: snapshotName
+ };
+ return this.http.post(`api/block/image/${poolName}/${rbdName}/snap`, request,
+ { observe: 'response' });
+ }
+
+ renameSnapshot(poolName, rbdName, snapshotName, newSnapshotName) {
+ const request = {
+ new_snap_name: newSnapshotName
+ };
+ return this.http.put(
+ `api/block/image/${poolName}/${rbdName}/snap/${snapshotName}`, request,
+ { observe: 'response' });
+ }
+
+ protectSnapshot(poolName, rbdName, snapshotName, isProtected) {
+ const request = {
+ is_protected: isProtected
+ };
+ return this.http.put(
+ `api/block/image/${poolName}/${rbdName}/snap/${snapshotName}`, request,
+ { observe: 'response' });
+ }
+
+ rollbackSnapshot(poolName, rbdName, snapshotName) {
+ return this.http.post(
+ `api/block/image/${poolName}/${rbdName}/snap/${snapshotName}/rollback`, null,
+ { observe: 'response' });
+ }
+
+ deleteSnapshot(poolName, rbdName, snapshotName) {
+ return this.http.delete(
+ `api/block/image/${poolName}/${rbdName}/snap/${snapshotName}`,
+ { observe: 'response' });
+ }
+}
--- /dev/null
+import { EmptyPipe } from './empty.pipe';
+
+describe('EmptyPipe', () => {
+ it('create an instance', () => {
+ const pipe = new EmptyPipe();
+ expect(pipe).toBeTruthy();
+ });
+});
--- /dev/null
+import { Pipe, PipeTransform } from '@angular/core';
+
+import * as _ from 'lodash';
+
+@Pipe({
+ name: 'empty'
+})
+export class EmptyPipe implements PipeTransform {
+
+ transform(value: any, args?: any): any {
+ return _.isUndefined(value) || _.isNull(value) ? '-' : value;
+ }
+
+}
--- /dev/null
+// http://www.virtsync.com/c-error-codes-include-errno
+export enum UnixErrno {
+ EEXIST = 17, // File exists
+}
import { CommonModule, DatePipe } from '@angular/common';
import { NgModule } from '@angular/core';
+import { EmptyPipe } from '../empty.pipe';
import { CdDatePipe } from './cd-date.pipe';
import { CephShortVersionPipe } from './ceph-short-version.pipe';
import { DimlessBinaryPipe } from './dimless-binary.pipe';
RelativeDatePipe,
ListPipe,
FilterPipe,
- CdDatePipe
+ CdDatePipe,
+ EmptyPipe
],
exports: [
DimlessBinaryPipe,
RelativeDatePipe,
ListPipe,
FilterPipe,
- CdDatePipe
+ CdDatePipe,
+ EmptyPipe
],
providers: [
DatePipe,
DimlessPipe,
RelativeDatePipe,
ListPipe,
- CdDatePipe
+ CdDatePipe,
+ EmptyPipe
]
})
export class PipesModule {}
export class TaskManagerMessageService {
messages = {
+ 'rbd/create': new TaskManagerMessage(
+ (metadata) => `Create RBD '${metadata.pool_name}/${metadata.image_name}'`,
+ (metadata) => `RBD '${metadata.pool_name}/${metadata.image_name}'
+ has been created successfully`,
+ (metadata) => {
+ return {
+ '17': `Name '${metadata.pool_name}/${metadata.image_name}' is already
+ in use.`
+ };
+ }
+ ),
+ 'rbd/edit': new TaskManagerMessage(
+ (metadata) => `Update RBD '${metadata.pool_name}/${metadata.image_name}'`,
+ (metadata) => `RBD '${metadata.pool_name}/${metadata.image_name}'
+ has been updated successfully`,
+ (metadata) => {
+ return {
+ '17': `Name '${metadata.pool_name}/${metadata.image_name}' is already
+ in use.`
+ };
+ }
+ ),
+ 'rbd/delete': new TaskManagerMessage(
+ (metadata) => `Delete RBD '${metadata.pool_name}/${metadata.image_name}'`,
+ (metadata) => `RBD '${metadata.pool_name}/${metadata.image_name}'
+ has been deleted successfully`,
+ (metadata) => {
+ return {
+ '39': `RBD image contains snapshots.`
+ };
+ }
+ ),
+ 'rbd/snap/create': new TaskManagerMessage(
+ (metadata) => `Create snapshot ` +
+ `'${metadata.pool_name}/${metadata.image_name}@${metadata.snapshot_name}'`,
+ (metadata) => `Snapshot ` +
+ `'${metadata.pool_name}/${metadata.image_name}@${metadata.snapshot_name}' ` +
+ `has been created successfully`,
+ (metadata) => {
+ return {
+ '17': `Name '${metadata.snapshot_name}' is already in use.`
+ };
+ }
+ ),
+ 'rbd/snap/edit': new TaskManagerMessage(
+ (metadata) => `Update snapshot ` +
+ `'${metadata.pool_name}/${metadata.image_name}@${metadata.snapshot_name}'`,
+ (metadata) => `Snapshot ` +
+ `'${metadata.pool_name}/${metadata.image_name}@${metadata.snapshot_name}' ` +
+ `has been updated successfully`,
+ () => {
+ return {
+ '16': `Cannot unprotect snapshot because it contains child images.`
+ };
+ }
+ ),
+ 'rbd/snap/delete': new TaskManagerMessage(
+ (metadata) => `Delete snapshot ` +
+ `'${metadata.pool_name}/${metadata.image_name}@${metadata.snapshot_name}'`,
+ (metadata) => `Snapshot ` +
+ `'${metadata.pool_name}/${metadata.image_name}@${metadata.snapshot_name}' ` +
+ `has been deleted successfully`,
+ () => {
+ return {
+ '16': `Snapshot is protected.`
+ };
+ }
+ ),
+ 'rbd/snap/rollback': new TaskManagerMessage(
+ (metadata) => `Rollback snapshot ` +
+ `'${metadata.pool_name}/${metadata.image_name}@${metadata.snapshot_name}'`,
+ (metadata) => `Snapshot ` +
+ `'${metadata.pool_name}/${metadata.image_name}@${metadata.snapshot_name}' ` +
+ `has been rolled back successfully`,
+ () => {
+ return {
+ };
+ }
+ ),
};
defaultMessage = new TaskManagerMessage(
import { DimlessBinaryDirective } from './directives/dimless-binary.directive';
import { PasswordButtonDirective } from './directives/password-button.directive';
import { PipesModule } from './pipes/pipes.module';
+import { AuthGuardService } from './services/auth-guard.service';
+import { AuthStorageService } from './services/auth-storage.service';
+import { FormatterService } from './services/formatter.service';
import { ServicesModule } from './services/services.module';
@NgModule({
DimlessBinaryDirective,
DataTableModule,
ApiModule
+ ],
+ providers: [
+ AuthStorageService,
+ AuthGuardService,
+ FormatterService
]
})
export class SharedModule {}