--- /dev/null
+<cd-modal [modalRef]="modalRef">
+ <ng-container i18n
+ class="modal-title">Create Bootstrap Token</ng-container>
+
+ <ng-container class="modal-content">
+ <form name="createBootstrapForm"
+ class="form"
+ #formDir="ngForm"
+ [formGroup]="createBootstrapForm"
+ novalidate>
+ <div class="modal-body">
+ <p>
+ <ng-container i18n>To create a bootstrap token which can be imported
+ by a peer site cluster, provide the local site's name, select
+ which pools will have mirroring enabled, and click
+ <kbd>Generate</kbd>.</ng-container>
+ </p>
+
+ <div class="form-group">
+ <label class="col-form-label"
+ for="siteName">
+ <span i18n>Site Name</span>
+ <span class="required"></span>
+ </label>
+ <input class="form-control"
+ type="text"
+ placeholder="Name..."
+ i18n-placeholder
+ id="siteName"
+ name="siteName"
+ formControlName="siteName"
+ autofocus>
+ <span *ngIf="createBootstrapForm.showError('siteName', formDir, 'required')"
+ class="invalid-feedback"
+ i18n>This field is required.</span>
+ </div>
+
+ <div class="form-group"
+ formGroupName="pools">
+ <label class="col-form-label"
+ for="pools">
+ <span i18n>Pools</span>
+ <span class="required"></span>
+ </label>
+ <div class="custom-control custom-checkbox"
+ *ngFor="let pool of pools">
+ <input type="checkbox"
+ class="custom-control-input"
+ id="{{ pool.name }}"
+ name="{{ pool.name }}"
+ formControlName="{{ pool.name }}">
+ <label class="custom-control-label"
+ for="{{ pool.name }}">{{ pool.name }}</label>
+ </div>
+ <span *ngIf="createBootstrapForm.showError('pools', formDir, 'requirePool')"
+ class="invalid-feedback"
+ i18n>At least one pool is required.</span>
+ </div>
+
+ <div class="button-group text-right">
+ <cd-submit-button i18n
+ [form]="createBootstrapForm"
+ (submitAction)="generate()">Generate</cd-submit-button>
+ </div>
+
+ <div class="form-group">
+ <label class="col-form-label"
+ for="token">
+ <span i18n>Token</span>
+ </label>
+ <textarea class="form-control resize-vertical"
+ placeholder="Generated token..."
+ i18n-placeholder
+ id="token"
+ formControlName="token"
+ readonly>
+ </textarea>
+ </div>
+ </div>
+
+ <div class="modal-footer">
+ <div class="button-group text-right">
+ <cd-back-button [back]="modalRef.hide"
+ name="Close"
+ i18n-name>
+ </cd-back-button>
+ </div>
+ </div>
+ </form>
+ </ng-container>
+</cd-modal>
--- /dev/null
+.form-group.ng-invalid .invalid-feedback {
+ display: block;
+}
--- /dev/null
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ReactiveFormsModule } from '@angular/forms';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { BsModalRef, BsModalService } from 'ngx-bootstrap/modal';
+import { ToastrModule } from 'ngx-toastr';
+import { of } from 'rxjs';
+
+import {
+ configureTestBed,
+ FormHelper,
+ i18nProviders
+} from '../../../../../testing/unit-test-helper';
+import { RbdMirroringService } from '../../../../shared/api/rbd-mirroring.service';
+import { NotificationService } from '../../../../shared/services/notification.service';
+import { SharedModule } from '../../../../shared/shared.module';
+import { BootstrapCreateModalComponent } from './bootstrap-create-modal.component';
+
+describe('BootstrapCreateModalComponent', () => {
+ let component: BootstrapCreateModalComponent;
+ let fixture: ComponentFixture<BootstrapCreateModalComponent>;
+ let notificationService: NotificationService;
+ let rbdMirroringService: RbdMirroringService;
+ let formHelper: FormHelper;
+
+ configureTestBed({
+ declarations: [BootstrapCreateModalComponent],
+ imports: [
+ HttpClientTestingModule,
+ ReactiveFormsModule,
+ RouterTestingModule,
+ SharedModule,
+ ToastrModule.forRoot()
+ ],
+ providers: [BsModalRef, BsModalService, i18nProviders]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(BootstrapCreateModalComponent);
+ component = fixture.componentInstance;
+ component.siteName = 'site-A';
+
+ notificationService = TestBed.get(NotificationService);
+ spyOn(notificationService, 'show').and.stub();
+
+ rbdMirroringService = TestBed.get(RbdMirroringService);
+
+ formHelper = new FormHelper(component.createBootstrapForm);
+
+ spyOn(rbdMirroringService, 'getSiteName').and.callFake(() => of({ site_name: 'site-A' }));
+ spyOn(rbdMirroringService, 'subscribeSummary').and.callFake((call) =>
+ of({
+ content_data: {
+ pools: [
+ { name: 'pool1', mirror_mode: 'disabled' },
+ { name: 'pool2', mirror_mode: 'disabled' },
+ { name: 'pool3', mirror_mode: 'disabled' }
+ ]
+ }
+ }).subscribe(call)
+ );
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ describe('generate token', () => {
+ beforeEach(() => {
+ spyOn(rbdMirroringService, 'refresh').and.stub();
+ spyOn(component.modalRef, 'hide').and.callThrough();
+ fixture.detectChanges();
+ });
+
+ afterEach(() => {
+ expect(rbdMirroringService.getSiteName).toHaveBeenCalledTimes(1);
+ expect(rbdMirroringService.subscribeSummary).toHaveBeenCalledTimes(1);
+ expect(rbdMirroringService.refresh).toHaveBeenCalledTimes(1);
+ });
+
+ it('should generate a bootstrap token', () => {
+ spyOn(rbdMirroringService, 'setSiteName').and.callFake(() => of({ site_name: 'new-site-A' }));
+ spyOn(rbdMirroringService, 'updatePool').and.callFake(() => of({}));
+ spyOn(rbdMirroringService, 'createBootstrapToken').and.callFake(() => of({ token: 'token' }));
+
+ component.createBootstrapForm.patchValue({
+ siteName: 'new-site-A',
+ pools: { pool1: true, pool3: true }
+ });
+ component.generate();
+ expect(rbdMirroringService.setSiteName).toHaveBeenCalledWith('new-site-A');
+ expect(rbdMirroringService.updatePool).toHaveBeenCalledWith('pool1', {
+ mirror_mode: 'image'
+ });
+ expect(rbdMirroringService.updatePool).toHaveBeenCalledWith('pool3', {
+ mirror_mode: 'image'
+ });
+ expect(rbdMirroringService.createBootstrapToken).toHaveBeenCalledWith('pool3');
+ expect(component.createBootstrapForm.getValue('token')).toBe('token');
+ });
+ });
+
+ describe('form validation', () => {
+ beforeEach(() => {
+ fixture.detectChanges();
+ });
+
+ it('should require a site name', () => {
+ formHelper.expectErrorChange('siteName', '', 'required');
+ });
+
+ it('should require at least one pool', () => {
+ formHelper.expectError(component.createBootstrapForm.get('pools'), 'requirePool');
+ });
+ });
+});
--- /dev/null
+import { Component, OnDestroy, OnInit } from '@angular/core';
+import { FormControl, FormGroup, ValidatorFn, Validators } from '@angular/forms';
+
+import * as _ from 'lodash';
+import { BsModalRef } from 'ngx-bootstrap/modal';
+import { concat, forkJoin, Subscription } from 'rxjs';
+import { last, tap } from 'rxjs/operators';
+
+import { RbdMirroringService } from '../../../../shared/api/rbd-mirroring.service';
+import { CdFormGroup } from '../../../../shared/forms/cd-form-group';
+import { FinishedTask } from '../../../../shared/models/finished-task';
+import { TaskWrapperService } from '../../../../shared/services/task-wrapper.service';
+
+@Component({
+ selector: 'cd-bootstrap-create-modal',
+ templateUrl: './bootstrap-create-modal.component.html',
+ styleUrls: ['./bootstrap-create-modal.component.scss']
+})
+export class BootstrapCreateModalComponent implements OnDestroy, OnInit {
+ siteName: string;
+ pools: any[] = [];
+ token: string;
+
+ subs: Subscription;
+
+ createBootstrapForm: CdFormGroup;
+
+ constructor(
+ public modalRef: BsModalRef,
+ private rbdMirroringService: RbdMirroringService,
+ private taskWrapper: TaskWrapperService
+ ) {
+ this.createForm();
+ }
+
+ createForm() {
+ this.createBootstrapForm = new CdFormGroup({
+ siteName: new FormControl('', {
+ validators: [Validators.required]
+ }),
+ pools: new FormGroup(
+ {},
+ {
+ validators: [this.validatePools()]
+ }
+ ),
+ token: new FormControl('', {})
+ });
+ }
+
+ ngOnInit() {
+ this.createBootstrapForm.get('siteName').setValue(this.siteName);
+ this.rbdMirroringService.getSiteName().subscribe((response: any) => {
+ this.createBootstrapForm.get('siteName').setValue(response.site_name);
+ });
+
+ this.subs = this.rbdMirroringService.subscribeSummary((data: any) => {
+ if (!data) {
+ return;
+ }
+
+ const pools = data.content_data.pools;
+ this.pools = pools.reduce((acc, pool) => {
+ acc.push({
+ name: pool['name'],
+ mirror_mode: pool['mirror_mode']
+ });
+ return acc;
+ }, []);
+
+ const poolsControl = this.createBootstrapForm.get('pools') as FormGroup;
+ _.each(this.pools, (pool) => {
+ const poolName = pool['name'];
+ const mirroring_disabled = pool['mirror_mode'] === 'disabled';
+ const control = poolsControl.controls[poolName];
+ if (control) {
+ if (mirroring_disabled && control.disabled) {
+ control.enable();
+ } else if (!mirroring_disabled && control.enabled) {
+ control.disable();
+ control.setValue(true);
+ }
+ } else {
+ poolsControl.addControl(
+ poolName,
+ new FormControl({ value: !mirroring_disabled, disabled: !mirroring_disabled })
+ );
+ }
+ });
+ });
+ }
+
+ ngOnDestroy() {
+ if (this.subs) {
+ this.subs.unsubscribe();
+ }
+ }
+
+ validatePools(): ValidatorFn {
+ return (poolsControl: FormGroup): { [key: string]: any } => {
+ let checkedCount = 0;
+ _.each(poolsControl.controls, (control) => {
+ if (control.value === true) {
+ ++checkedCount;
+ }
+ });
+
+ if (checkedCount > 0) {
+ return null;
+ }
+
+ return { requirePool: true };
+ };
+ }
+
+ generate() {
+ this.createBootstrapForm.get('token').setValue('');
+
+ let bootstrapPoolName = '';
+ const poolNames: string[] = [];
+ const poolsControl = this.createBootstrapForm.get('pools') as FormGroup;
+ _.each(poolsControl.controls, (control, poolName) => {
+ if (control.value === true) {
+ bootstrapPoolName = poolName;
+ if (!control.disabled) {
+ poolNames.push(poolName);
+ }
+ }
+ });
+
+ const poolModeRequest = {
+ mirror_mode: 'image'
+ };
+
+ const apiActionsObs = concat(
+ this.rbdMirroringService.setSiteName(this.createBootstrapForm.getValue('siteName')),
+ forkJoin(
+ poolNames.map((poolName) => this.rbdMirroringService.updatePool(poolName, poolModeRequest))
+ ),
+ this.rbdMirroringService
+ .createBootstrapToken(bootstrapPoolName)
+ .pipe(tap((data) => this.createBootstrapForm.get('token').setValue(data['token'])))
+ ).pipe(last());
+
+ const finishHandler = () => {
+ this.rbdMirroringService.refresh();
+ this.createBootstrapForm.setErrors({ cdSubmitButton: true });
+ };
+
+ const taskObs = this.taskWrapper.wrapTaskAroundCall({
+ task: new FinishedTask('rbd/mirroring/bootstrap/create', {}),
+ call: apiActionsObs
+ });
+ taskObs.subscribe(undefined, finishHandler, finishHandler);
+ }
+}
--- /dev/null
+<cd-modal [modalRef]="modalRef">
+ <ng-container i18n
+ class="modal-title">Import Bootstrap Token</ng-container>
+
+ <ng-container class="modal-content">
+ <form name="importBootstrapForm"
+ class="form"
+ #formDir="ngForm"
+ [formGroup]="importBootstrapForm"
+ novalidate>
+ <div class="modal-body">
+ <p>
+ <ng-container i18n>To import a bootstrap token which was created
+ by a peer site cluster, provide the local site's name, select
+ which pools will have mirroring enabled, provide the generated
+ token, and click <kbd>Import</kbd>.</ng-container>
+ </p>
+
+ <div class="form-group">
+ <label class="col-form-label"
+ for="siteName">
+ <span i18n>Site Name</span>
+ <span class="required"></span>
+ </label>
+ <input class="form-control"
+ type="text"
+ placeholder="Name..."
+ i18n-placeholder
+ id="siteName"
+ name="siteName"
+ formControlName="siteName"
+ autofocus>
+ <span *ngIf="importBootstrapForm.showError('siteName', formDir, 'required')"
+ class="invalid-feedback"
+ i18n>This field is required.</span>
+ </div>
+
+ <div class="form-group">
+ <label class="col-form-label"
+ for="direction">
+ <span i18n>Direction</span>
+ </label>
+ <select id="direction"
+ name="direction"
+ class="form-control custom-select"
+ formControlName="direction">
+ <option *ngFor="let direction of directions"
+ [value]="direction.key">{{ direction.desc }}</option>
+ </select>
+ </div>
+
+ <div class="form-group"
+ formGroupName="pools">
+ <label class="col-form-label"
+ for="pools">
+ <span i18n>Pools</span>
+ <span class="required"></span>
+ </label>
+ <div class="custom-control custom-checkbox"
+ *ngFor="let pool of pools">
+ <input type="checkbox"
+ class="custom-control-input"
+ id="{{ pool.name }}"
+ name="{{ pool.name }}"
+ formControlName="{{ pool.name }}">
+ <label class="custom-control-label"
+ for="{{ pool.name }}">{{ pool.name }}</label>
+ </div>
+ <span *ngIf="importBootstrapForm.showError('pools', formDir, 'requirePool')"
+ class="invalid-feedback"
+ i18n>At least one pool is required.</span>
+ </div>
+
+ <div class="form-group">
+ <label class="col-form-label"
+ for="token">
+ <span i18n>Token</span>
+ <span class="required"></span>
+ </label>
+ <textarea class="form-control resize-vertical"
+ placeholder="Generated token..."
+ i18n-placeholder
+ id="token"
+ formControlName="token">
+ </textarea>
+ <span *ngIf="importBootstrapForm.showError('token', formDir, 'required')"
+ class="invalid-feedback"
+ i18n>This field is required.</span>
+ <span *ngIf="importBootstrapForm.showError('token', formDir, 'invalidToken')"
+ class="invalid-feedback"
+ i18n>The token is invalid.</span>
+ </div>
+ </div>
+
+ <div class="modal-footer">
+ <div class="button-group text-right">
+ <cd-submit-button i18n
+ [form]="importBootstrapForm"
+ (submitAction)="import()">Import</cd-submit-button>
+ <cd-back-button [back]="modalRef.hide"
+ name="Close"
+ i18n-name>
+ </cd-back-button>
+ </div>
+ </div>
+ </form>
+ </ng-container>
+</cd-modal>
--- /dev/null
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ReactiveFormsModule } from '@angular/forms';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { BsModalRef, BsModalService } from 'ngx-bootstrap/modal';
+import { ToastrModule } from 'ngx-toastr';
+import { of } from 'rxjs';
+
+import {
+ configureTestBed,
+ FormHelper,
+ i18nProviders
+} from '../../../../../testing/unit-test-helper';
+import { RbdMirroringService } from '../../../../shared/api/rbd-mirroring.service';
+import { NotificationService } from '../../../../shared/services/notification.service';
+import { SharedModule } from '../../../../shared/shared.module';
+import { BootstrapImportModalComponent } from './bootstrap-import-modal.component';
+
+describe('BootstrapImportModalComponent', () => {
+ let component: BootstrapImportModalComponent;
+ let fixture: ComponentFixture<BootstrapImportModalComponent>;
+ let notificationService: NotificationService;
+ let rbdMirroringService: RbdMirroringService;
+ let formHelper: FormHelper;
+
+ configureTestBed({
+ declarations: [BootstrapImportModalComponent],
+ imports: [
+ HttpClientTestingModule,
+ ReactiveFormsModule,
+ RouterTestingModule,
+ SharedModule,
+ ToastrModule.forRoot()
+ ],
+ providers: [BsModalRef, BsModalService, i18nProviders]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(BootstrapImportModalComponent);
+ component = fixture.componentInstance;
+ component.siteName = 'site-A';
+
+ notificationService = TestBed.get(NotificationService);
+ spyOn(notificationService, 'show').and.stub();
+
+ rbdMirroringService = TestBed.get(RbdMirroringService);
+
+ formHelper = new FormHelper(component.importBootstrapForm);
+
+ spyOn(rbdMirroringService, 'getSiteName').and.callFake(() => of({ site_name: 'site-A' }));
+ spyOn(rbdMirroringService, 'subscribeSummary').and.callFake((call) =>
+ of({
+ content_data: {
+ pools: [
+ { name: 'pool1', mirror_mode: 'disabled' },
+ { name: 'pool2', mirror_mode: 'disabled' },
+ { name: 'pool3', mirror_mode: 'disabled' }
+ ]
+ }
+ }).subscribe(call)
+ );
+ });
+
+ it('should import', () => {
+ expect(component).toBeTruthy();
+ });
+
+ describe('import token', () => {
+ beforeEach(() => {
+ spyOn(rbdMirroringService, 'refresh').and.stub();
+ spyOn(component.modalRef, 'hide').and.callThrough();
+ fixture.detectChanges();
+ });
+
+ afterEach(() => {
+ expect(rbdMirroringService.getSiteName).toHaveBeenCalledTimes(1);
+ expect(rbdMirroringService.subscribeSummary).toHaveBeenCalledTimes(1);
+ expect(rbdMirroringService.refresh).toHaveBeenCalledTimes(1);
+ });
+
+ it('should generate a bootstrap token', () => {
+ spyOn(rbdMirroringService, 'setSiteName').and.callFake(() => of({ site_name: 'new-site-A' }));
+ spyOn(rbdMirroringService, 'updatePool').and.callFake(() => of({}));
+ spyOn(rbdMirroringService, 'importBootstrapToken').and.callFake(() => of({ token: 'token' }));
+
+ component.importBootstrapForm.patchValue({
+ siteName: 'new-site-A',
+ pools: { pool1: true, pool3: true },
+ token: 'e30='
+ });
+ component.import();
+ expect(rbdMirroringService.setSiteName).toHaveBeenCalledWith('new-site-A');
+ expect(rbdMirroringService.updatePool).toHaveBeenCalledWith('pool1', {
+ mirror_mode: 'image'
+ });
+ expect(rbdMirroringService.updatePool).toHaveBeenCalledWith('pool3', {
+ mirror_mode: 'image'
+ });
+ expect(rbdMirroringService.importBootstrapToken).toHaveBeenCalledWith(
+ 'pool1',
+ 'rx-tx',
+ 'e30='
+ );
+ expect(rbdMirroringService.importBootstrapToken).toHaveBeenCalledWith(
+ 'pool3',
+ 'rx-tx',
+ 'e30='
+ );
+ });
+ });
+
+ describe('form validation', () => {
+ beforeEach(() => {
+ fixture.detectChanges();
+ });
+
+ it('should require a site name', () => {
+ formHelper.expectErrorChange('siteName', '', 'required');
+ });
+
+ it('should require at least one pool', () => {
+ formHelper.expectError(component.importBootstrapForm.get('pools'), 'requirePool');
+ });
+
+ it('should require a token', () => {
+ formHelper.expectErrorChange('token', '', 'required');
+ });
+
+ it('should verify token is base64-encoded JSON', () => {
+ formHelper.expectErrorChange('token', 'VEVTVA==', 'invalidToken');
+ formHelper.expectErrorChange('token', 'e2RmYXNqZGZrbH0=', 'invalidToken');
+ });
+ });
+});
--- /dev/null
+import { Component, OnDestroy, OnInit } from '@angular/core';
+import { FormControl, FormGroup, ValidatorFn, Validators } from '@angular/forms';
+
+import * as _ from 'lodash';
+import { BsModalRef } from 'ngx-bootstrap/modal';
+import { concat, forkJoin, Observable, Subscription } from 'rxjs';
+import { last } from 'rxjs/operators';
+
+import { RbdMirroringService } from '../../../../shared/api/rbd-mirroring.service';
+import { CdFormGroup } from '../../../../shared/forms/cd-form-group';
+import { FinishedTask } from '../../../../shared/models/finished-task';
+import { TaskWrapperService } from '../../../../shared/services/task-wrapper.service';
+
+@Component({
+ selector: 'cd-bootstrap-import-modal',
+ templateUrl: './bootstrap-import-modal.component.html',
+ styleUrls: ['./bootstrap-import-modal.component.scss']
+})
+export class BootstrapImportModalComponent implements OnInit, OnDestroy {
+ siteName: string;
+ pools: any[] = [];
+ token: string;
+
+ subs: Subscription;
+
+ importBootstrapForm: CdFormGroup;
+
+ directions: Array<any> = [
+ { key: 'rx-tx', desc: 'Bidirectional' },
+ { key: 'rx', desc: 'Unidirectional (receive-only)' }
+ ];
+
+ constructor(
+ public modalRef: BsModalRef,
+ private rbdMirroringService: RbdMirroringService,
+ private taskWrapper: TaskWrapperService
+ ) {
+ this.createForm();
+ }
+
+ createForm() {
+ this.importBootstrapForm = new CdFormGroup({
+ siteName: new FormControl('', {
+ validators: [Validators.required]
+ }),
+ direction: new FormControl('rx-tx', {}),
+ pools: new FormGroup(
+ {},
+ {
+ validators: [this.validatePools()]
+ }
+ ),
+ token: new FormControl('', {
+ validators: [Validators.required, this.validateToken()]
+ })
+ });
+ }
+
+ ngOnInit() {
+ this.rbdMirroringService.getSiteName().subscribe((response: any) => {
+ this.importBootstrapForm.get('siteName').setValue(response.site_name);
+ });
+
+ this.subs = this.rbdMirroringService.subscribeSummary((data: any) => {
+ if (!data) {
+ return;
+ }
+
+ const pools = data.content_data.pools;
+ this.pools = pools.reduce((acc, pool) => {
+ acc.push({
+ name: pool['name'],
+ mirror_mode: pool['mirror_mode']
+ });
+ return acc;
+ }, []);
+
+ const poolsControl = this.importBootstrapForm.get('pools') as FormGroup;
+ _.each(this.pools, (pool) => {
+ const poolName = pool['name'];
+ const mirroring_disabled = pool['mirror_mode'] === 'disabled';
+ const control = poolsControl.controls[poolName];
+ if (control) {
+ if (mirroring_disabled && control.disabled) {
+ control.enable();
+ } else if (!mirroring_disabled && control.enabled) {
+ control.disable();
+ control.setValue(true);
+ }
+ } else {
+ poolsControl.addControl(
+ poolName,
+ new FormControl({ value: !mirroring_disabled, disabled: !mirroring_disabled })
+ );
+ }
+ });
+ });
+ }
+
+ ngOnDestroy() {
+ if (this.subs) {
+ this.subs.unsubscribe();
+ }
+ }
+
+ validatePools(): ValidatorFn {
+ return (poolsControl: FormGroup): { [key: string]: any } => {
+ let checkedCount = 0;
+ _.each(poolsControl.controls, (control) => {
+ if (control.value === true) {
+ ++checkedCount;
+ }
+ });
+
+ if (checkedCount > 0) {
+ return null;
+ }
+
+ return { requirePool: true };
+ };
+ }
+
+ validateToken(): ValidatorFn {
+ return (token: FormControl): { [key: string]: any } => {
+ try {
+ if (JSON.parse(atob(token.value))) {
+ return null;
+ }
+ } catch (error) {}
+ return { invalidToken: true };
+ };
+ }
+
+ import() {
+ const bootstrapPoolNames: string[] = [];
+ const poolNames: string[] = [];
+ const poolsControl = this.importBootstrapForm.get('pools') as FormGroup;
+ _.each(poolsControl.controls, (control, poolName) => {
+ if (control.value === true) {
+ bootstrapPoolNames.push(poolName);
+ if (!control.disabled) {
+ poolNames.push(poolName);
+ }
+ }
+ });
+
+ const poolModeRequest = {
+ mirror_mode: 'image'
+ };
+
+ let apiActionsObs: Observable<any> = concat(
+ this.rbdMirroringService.setSiteName(this.importBootstrapForm.getValue('siteName')),
+ forkJoin(
+ poolNames.map((poolName) => this.rbdMirroringService.updatePool(poolName, poolModeRequest))
+ )
+ );
+
+ apiActionsObs = bootstrapPoolNames
+ .reduce((obs, poolName) => {
+ return concat(
+ obs,
+ this.rbdMirroringService.importBootstrapToken(
+ poolName,
+ this.importBootstrapForm.getValue('direction'),
+ this.importBootstrapForm.getValue('token')
+ )
+ );
+ }, apiActionsObs)
+ .pipe(last());
+
+ const finishHandler = () => {
+ this.rbdMirroringService.refresh();
+ this.importBootstrapForm.setErrors({ cdSubmitButton: true });
+ };
+
+ const taskObs = this.taskWrapper.wrapTaskAroundCall({
+ task: new FinishedTask('rbd/mirroring/bootstrap/import', {}),
+ call: apiActionsObs
+ });
+ taskObs.subscribe(undefined, finishHandler, () => {
+ finishHandler();
+ this.modalRef.hide();
+ });
+ }
+}
import { SharedModule } from '../../../shared/shared.module';
+import { BootstrapCreateModalComponent } from './bootstrap-create-modal/bootstrap-create-modal.component';
+import { BootstrapImportModalComponent } from './bootstrap-import-modal/bootstrap-import-modal.component';
import { DaemonListComponent } from './daemon-list/daemon-list.component';
import { EditSiteNameModalComponent } from './edit-site-name-modal/edit-site-name-modal.component';
import { ImageListComponent } from './image-list/image-list.component';
@NgModule({
entryComponents: [
+ BootstrapCreateModalComponent,
+ BootstrapImportModalComponent,
EditSiteNameModalComponent,
OverviewComponent,
PoolEditModeModalComponent,
NgBootstrapFormValidationModule
],
declarations: [
+ BootstrapCreateModalComponent,
+ BootstrapImportModalComponent,
DaemonListComponent,
EditSiteNameModalComponent,
ImageListComponent,
import { CdTableSelection } from '../../../../shared/models/cd-table-selection';
import { Permission } from '../../../../shared/models/permissions';
import { AuthStorageService } from '../../../../shared/services/auth-storage.service';
+import { BootstrapCreateModalComponent } from '../bootstrap-create-modal/bootstrap-create-modal.component';
+import { BootstrapImportModalComponent } from '../bootstrap-import-modal/bootstrap-import-modal.component';
import { EditSiteNameModalComponent } from '../edit-site-name-modal/edit-site-name-modal.component';
@Component({
canBePrimary: () => true,
disable: () => false
};
- this.tableActions = [editSiteNameAction];
+ const createBootstrapAction: CdTableAction = {
+ permission: 'update',
+ icon: Icons.upload,
+ click: () => this.createBootstrapModal(),
+ name: this.i18n('Create Bootstrap Token'),
+ disable: () => false
+ };
+ const importBootstrapAction: CdTableAction = {
+ permission: 'update',
+ icon: Icons.download,
+ click: () => this.importBootstrapModal(),
+ name: this.i18n('Import Bootstrap Token'),
+ disable: () => this.peersExist
+ };
+ this.tableActions = [editSiteNameAction, createBootstrapAction, importBootstrapAction];
}
ngOnInit() {
};
this.modalRef = this.modalService.show(EditSiteNameModalComponent, { initialState });
}
+
+ createBootstrapModal() {
+ const initialState = {
+ siteName: this.siteName
+ };
+ this.modalRef = this.modalService.show(BootstrapCreateModalComponent, { initialState });
+ }
+
+ importBootstrapModal() {
+ const initialState = {
+ siteName: this.siteName
+ };
+ this.modalRef = this.modalService.show(BootstrapImportModalComponent, { initialState });
+ }
}
rightArrowDouble = 'fa fa-angle-double-right', // Left facing Double angle
flag = 'fa fa-flag', // OSD configuration
clearFilters = 'fa fa-window-close', // Clear filters, solid x
+ download = 'fa fa-download', // Download
+ upload = 'fa fa-upload', // Upload
/* Icons for special effect */
large = 'fa fa-lg', // icon becomes 33% larger
testMessages(new TaskMessageOperation('Deleting', 'delete', 'Deleted'), involves);
};
+ const testImport = (involves: string) => {
+ testMessages(new TaskMessageOperation('Importing', 'import', 'Imported'), involves);
+ };
+
const testErrorCode = (code: number, msg: string) => {
finishedTask.exception = _.assign(new TaskException(), {
code: code
finishedTask.name = 'rbd/mirroring/site_name/edit';
testUpdate('mirroring site name');
});
+ it('tests rbd/mirroring/bootstrap/create messages', () => {
+ finishedTask.name = 'rbd/mirroring/bootstrap/create';
+ testCreate('bootstrap token');
+ });
+ it('tests rbd/mirroring/bootstrap/import messages', () => {
+ finishedTask.name = 'rbd/mirroring/bootstrap/import';
+ testImport('bootstrap token');
+ });
it('tests rbd/mirroring/pool/edit messages', () => {
finishedTask.name = 'rbd/mirroring/pool/edit';
testUpdate(modeMsg);
this.i18n('Removing'),
this.i18n('remove'),
this.i18n('Removed')
+ ),
+ import: new TaskMessageOperation(
+ this.i18n('Importing'),
+ this.i18n('import'),
+ this.i18n('Imported')
)
};
rbd_mirroring = {
site_name: () => this.i18n('mirroring site name'),
+ bootstrap: () => this.i18n('bootstrap token'),
pool: (metadata) =>
this.i18n(`mirror mode for pool '{{id}}'`, {
id: `${metadata.pool_name}`
this.rbd_mirroring.site_name,
() => ({})
),
+ 'rbd/mirroring/bootstrap/create': this.newTaskMessage(
+ this.commonOperations.create,
+ this.rbd_mirroring.bootstrap,
+ () => ({})
+ ),
+ 'rbd/mirroring/bootstrap/import': this.newTaskMessage(
+ this.commonOperations.import,
+ this.rbd_mirroring.bootstrap,
+ () => ({})
+ ),
'rbd/mirroring/pool/edit': this.newTaskMessage(
this.commonOperations.update,
this.rbd_mirroring.pool,