import { NgModule } from '@angular/core';
import { ActivatedRouteSnapshot, RouterModule, Routes } from '@angular/router';
+import { IscsiTargetFormComponent } from './ceph/block/iscsi-target-form/iscsi-target-form.component';
import { IscsiTargetListComponent } from './ceph/block/iscsi-target-list/iscsi-target-list.component';
import { IscsiComponent } from './ceph/block/iscsi/iscsi.component';
import { OverviewComponent as RbdMirroringComponent } from './ceph/block/mirroring/overview/overview.component';
path: 'targets',
data: { breadcrumbs: 'Targets' },
children: [
- { path: '', component: IscsiTargetListComponent }
+ { path: '', component: IscsiTargetListComponent },
+ { path: 'add', component: IscsiTargetFormComponent, data: { breadcrumbs: 'Add' } }
]
}
]
import { SharedModule } from '../../shared/shared.module';
import { IscsiTabsComponent } from './iscsi-tabs/iscsi-tabs.component';
+import { IscsiTargetDetailsComponent } from './iscsi-target-details/iscsi-target-details.component';
+import { IscsiTargetFormComponent } from './iscsi-target-form/iscsi-target-form.component';
+import { IscsiTargetImageSettingsModalComponent } from './iscsi-target-image-settings-modal/iscsi-target-image-settings-modal.component';
+import { IscsiTargetIqnSettingsModalComponent } from './iscsi-target-iqn-settings-modal/iscsi-target-iqn-settings-modal.component';
import { IscsiTargetListComponent } from './iscsi-target-list/iscsi-target-list.component';
import { IscsiComponent } from './iscsi/iscsi.component';
import { MirroringModule } from './mirroring/mirroring.module';
import { RbdTrashMoveModalComponent } from './rbd-trash-move-modal/rbd-trash-move-modal.component';
import { RbdTrashPurgeModalComponent } from './rbd-trash-purge-modal/rbd-trash-purge-modal.component';
import { RbdTrashRestoreModalComponent } from './rbd-trash-restore-modal/rbd-trash-restore-modal.component';
-import { IscsiTargetDetailsComponent } from './iscsi-target-details/iscsi-target-details.component';
@NgModule({
entryComponents: [
RbdTrashMoveModalComponent,
RbdTrashRestoreModalComponent,
RbdTrashPurgeModalComponent,
- IscsiTargetDetailsComponent
+ IscsiTargetDetailsComponent,
+ IscsiTargetImageSettingsModalComponent,
+ IscsiTargetIqnSettingsModalComponent
],
imports: [
CommonModule,
RbdImagesComponent,
RbdTrashRestoreModalComponent,
RbdTrashPurgeModalComponent,
- IscsiTargetDetailsComponent
+ IscsiTargetDetailsComponent,
+ IscsiTargetFormComponent,
+ IscsiTargetImageSettingsModalComponent,
+ IscsiTargetIqnSettingsModalComponent
]
})
export class BlockModule {}
--- /dev/null
+<div class="col-sm-12 col-lg-6">
+ <form name="targetForm"
+ class="form-horizontal"
+ #formDir="ngForm"
+ [formGroup]="targetForm"
+ novalidate
+ *ngIf="targetForm">
+ <div class="panel panel-default">
+ <div class="panel-heading">
+ <h3 class="panel-title"
+ i18n>Create target</h3>
+ </div>
+
+ <div class="panel-body">
+ <!-- Target IQN -->
+ <div class="form-group"
+ [ngClass]="{'has-error': targetForm.showError('target_iqn', formDir)}">
+ <label class="control-label col-sm-3"
+ for="target_iqn">
+ <ng-container i18n>Target IQN</ng-container>
+ <span class="required"></span>
+ </label>
+ <div class="col-sm-9">
+ <div class="input-group">
+ <input class="form-control"
+ type="text"
+ id="target_iqn"
+ name="target_iqn"
+ formControlName="target_iqn" />
+ <span class="input-group-btn">
+ <button class="btn btn-default"
+ id="ecp-info-button"
+ type="button"
+ (click)="targetSettingsModal()">
+ <i class="fa fa-cogs fa-fw"
+ aria-hidden="true"></i>
+ </button>
+ </span>
+ </div>
+
+ <span class="help-block"
+ *ngIf="targetForm.showError('target_iqn', formDir, 'required')"
+ i18n>This field is required.</span>
+
+ <span class="help-block"
+ *ngIf="targetForm.showError('target_iqn', formDir, 'pattern')"
+ i18n>IQN has wrong pattern.</span>
+
+ <span class="help-block"
+ *ngIf="targetForm.showError('target_iqn', formDir, 'iqn')">
+ <ng-container i18n>An IQN has the following notation 'iqn.$year-$month.$reversedAddress:$definedName'</ng-container>
+ <br>
+ <ng-container i18n>For example: iqn.2016-06.org.dashboard:storage:disk.sn-a8675309</ng-container>
+ <br>
+ <a target="_blank"
+ href="https://en.wikipedia.org/wiki/ISCSI#Addressing"
+ i18n>More information</a>
+ </span>
+
+ <span class="help-block"
+ *ngIf="hasAdvancedSettings(targetForm.getValue('target_controls'))"
+ i18n>This target has modified advanced settings.</span>
+ <hr />
+ </div>
+ </div>
+
+ <!-- Portals -->
+ <div class="form-group"
+ [ngClass]="{'has-error': targetForm.showError('portals', formDir)}">
+ <label class="control-label col-sm-3"
+ for="portals">
+ <ng-container i18n>Portals</ng-container>
+ <span class="required"></span>
+ </label>
+ <div class="col-sm-9">
+
+ <ng-container *ngFor="let portal of portals.value; let i = index">
+ <div class="input-group cd-mb">
+ <input class="form-control"
+ type="text"
+ [value]="portal"
+ disabled />
+ <span class="input-group-btn">
+ <button class="btn btn-default"
+ type="button"
+ (click)="removePortal(i, portal)">
+ <i class="fa fa-remove fa-fw"
+ aria-hidden="true"></i>
+ </button>
+ </span>
+ </div>
+ </ng-container>
+
+ <span class="help-block"
+ *ngIf="targetForm.showError('portals', formDir, 'minGateways')"
+ i18n>At least {{ minimum_gateways }} gateways are required.</span>
+
+ <div class="row">
+ <div class="col-md-12">
+ <cd-select [data]="portals.value"
+ [options]="portalsSelections"
+ [messages]="messages.portals"
+ (selection)="onPortalSelection($event)"
+ elemClass="btn btn-default pull-right">
+ <i class="fa fa-fw fa-plus"></i>
+ <ng-container i18n>Add portal</ng-container>
+ </cd-select>
+ </div>
+ </div>
+
+ <hr />
+ </div>
+ </div>
+
+ <!-- Images -->
+ <div class="form-group"
+ [ngClass]="{'has-error': targetForm.showError('disks', formDir)}">
+ <label class="control-label col-sm-3"
+ for="disks"
+ i18n>Images</label>
+ <div class="col-sm-9">
+ <ng-container *ngFor="let image of targetForm.getValue('disks'); let i = index">
+ <div class="input-group cd-mb">
+ <input class="form-control"
+ type="text"
+ [value]="image"
+ disabled />
+ <span class="input-group-btn">
+ <button class="btn btn-default"
+ type="button"
+ (click)="imageSettingsModal(image)">
+ <i class="fa fa-cogs fa-fw"
+ aria-hidden="true"></i>
+ </button>
+ <button class="btn btn-default"
+ type="button"
+ (click)="removeImage(i, image)">
+ <i class="fa fa-remove fa-fw"
+ aria-hidden="true"></i>
+ </button>
+ </span>
+
+ </div>
+ <span class="help-block"
+ *ngIf="hasAdvancedSettings(imagesSettings[image])"
+ i18n>This image has modified settings.</span>
+ </ng-container>
+
+ <span class="help-block"
+ *ngIf="targetForm.showError('disks', formDir, 'required')"
+ i18n>At least 1 image is required.</span>
+
+ <div class="row">
+ <div class="col-md-12">
+ <cd-select [data]="disks.value"
+ [options]="imagesSelections"
+ [messages]="messages.images"
+ (selection)="onImageSelection($event)"
+ elemClass="btn btn-default pull-right">
+ <i class="fa fa-fw fa-plus"></i>
+ <ng-container i18n>Add image</ng-container>
+ </cd-select>
+ </div>
+ </div>
+
+ <hr />
+ </div>
+ </div>
+
+ <!-- Initiators -->
+ <div class="form-group">
+ <label class="control-label col-sm-3"
+ for="initiators"
+ i18n>Initiators</label>
+ <div class="col-sm-9"
+ formArrayName="initiators">
+ <div class="panel panel-default"
+ *ngFor="let initiator of initiators.controls; let i = index"
+ [formGroupName]="i">
+ <div class="panel-heading">
+ <ng-container i18n>Initiator</ng-container>: {{ initiator.getValue('client_iqn') }}
+ <button type="button"
+ class="close"
+ (click)="removeInitiator(i)">
+ <i class="fa fa-remove fa-fw"></i>
+ </button>
+ </div>
+ <div class="panel-body">
+ <!-- Initiator: Name -->
+ <div class="form-group"
+ [ngClass]="{'has-error': initiator.showError('client_iqn', formDir)}">
+ <label class="control-label col-sm-3"
+ for="client_iqn">
+ <ng-container i18n>Client IQN</ng-container>
+ <span class="required"></span>
+ </label>
+ <div class="col-sm-9">
+ <input class="form-control"
+ type="text"
+ formControlName="client_iqn"
+ (blur)="updatedInitiatorSelector()">
+
+ <span class="help-block"
+ *ngIf="initiator.showError('client_iqn', formDir, 'notUnique')"
+ i18n>Initiator IQN needs to be unique.</span>
+
+ <span class="help-block"
+ *ngIf="initiator.showError('client_iqn', formDir, 'required')"
+ i18n>This field is required.</span>
+
+ <span class="help-block"
+ *ngIf="initiator.showError('client_iqn', formDir, 'pattern')"
+ i18n>IQN has wrong pattern.</span>
+ </div>
+ </div>
+
+ <ng-container formGroupName="auth">
+ <!-- Initiator: User -->
+ <div class="form-group"
+ [ngClass]="{'has-error': initiator.showError('user', formDir)}">
+ <label class="control-label col-sm-3"
+ for="user"
+ i18n>User</label>
+ <div class="col-sm-9">
+ <input id="user"
+ class="form-control"
+ formControlName="user"
+ type="text">
+ <span class="help-block"
+ *ngIf="initiator.showError('user', formDir, 'required')"
+ i18n>This field is required.</span>
+
+ <span class="help-block"
+ *ngIf="initiator.showError('user', formDir, 'pattern')"
+ i18n>Usernames must have a length of 8 to 64 characters and
+ can only contain letters, '.', '@', '-', '_' or ':'.</span>
+ </div>
+ </div>
+
+ <!-- Initiator: Password -->
+ <div class="form-group"
+ [ngClass]="{'has-error': initiator.showError('password', formDir)}">
+ <label class="control-label col-sm-3"
+ for="password"
+ i18n>Password</label>
+ <div class="col-sm-9">
+ <div class="input-group">
+ <input id="password"
+ class="form-control"
+ formControlName="password"
+ type="password">
+
+ <span class="input-group-btn">
+ <button type="button"
+ class="btn btn-default"
+ cdPasswordButton="password">
+ </button>
+ <button type="button"
+ class="btn btn-default"
+ cdCopy2ClipboardButton="password">
+ </button>
+ </span>
+ </div>
+ <span class="help-block"
+ *ngIf="initiator.showError('password', formDir, 'required')"
+ i18n>This field is required.</span>
+
+ <span class="help-block"
+ *ngIf="initiator.showError('password', formDir, 'pattern')"
+ i18n>Passwords must have a length of 12 to 16 characters
+ and can only contain letters, '@', '-' or '_'.</span>
+ </div>
+ </div>
+
+
+ <!-- Initiator: mutual_user -->
+ <div class="form-group"
+ [ngClass]="{'has-error': initiator.showError('mutual_user', formDir)}">
+ <label class="control-label col-sm-3"
+ for="mutual_user">
+ <ng-container i18n>Mutual User</ng-container>
+ </label>
+ <div class="col-sm-9">
+ <input id="mutual_user"
+ class="form-control"
+ formControlName="mutual_user"
+ type="text">
+
+ <span class="help-block"
+ *ngIf="initiator.showError('mutual_user', formDir, 'required')"
+ i18n>This field is required.</span>
+
+ <span class="help-block"
+ *ngIf="initiator.showError('mutual_user', formDir, 'pattern')"
+ i18n>Usernames must have a length of 8 to 64 characters and
+ can only contain letters, '.', '@', '-', '_' or ':'.</span>
+ </div>
+ </div>
+
+ <!-- Initiator: mutual_password -->
+ <div class="form-group"
+ [ngClass]="{'has-error': initiator.showError('mutual_password', formDir)}">
+ <label class="control-label col-sm-3"
+ for="mutual_password"
+ i18n>Mutual Password</label>
+ <div class="col-sm-9">
+ <div class="input-group">
+ <input id="mutual_password"
+ class="form-control"
+ formControlName="mutual_password"
+ type="mutual_password">
+
+ <span class="input-group-btn">
+ <button type="button"
+ class="btn btn-default"
+ cdPasswordButton="mutual_password">
+ </button>
+ <button type="button"
+ class="btn btn-default"
+ cdCopy2ClipboardButton="mutual_password">
+ </button>
+ </span>
+ </div>
+ <span class="help-block"
+ *ngIf="initiator.showError('mutual_password', formDir, 'required')"
+ i18n>This field is required.</span>
+
+ <span class="help-block"
+ *ngIf="initiator.showError('mutual_password', formDir, 'pattern')"
+ i18n>Passwords must have a length of 12 to 16 characters and
+ can only contain letters, '@', '-' or '_'.</span>
+ </div>
+ </div>
+ </ng-container>
+
+ <!-- Initiator: Images -->
+ <div class="form-group"
+ [ngClass]="{'has-error': initiator.showError('luns', formDir)}">
+ <label class="control-label col-sm-3"
+ for="luns"
+ i18n>Images</label>
+ <div class="col-sm-9">
+ <ng-container *ngFor="let image of initiator.getValue('luns'); let i = index">
+ <div class="input-group cd-mb">
+ <input class="form-control"
+ type="text"
+ [value]="image"
+ disabled />
+ <span class="input-group-btn">
+ <button class="btn btn-default"
+ type="button"
+ (click)="initiator.getValue('luns').splice(i, 1)">
+ <i class="fa fa-remove fa-fw"
+ aria-hidden="true"></i>
+ </button>
+ </span>
+ </div>
+ </ng-container>
+
+ <span *ngIf="initiator.getValue('cdIsInGroup')"
+ i18n>Initiator belongs to a group. Images will be configure in the group.</span>
+
+ <div class="row"
+ *ngIf="!initiator.getValue('cdIsInGroup')">
+ <div class="col-md-12">
+ <cd-select [data]="initiator.getValue('luns')"
+ [options]="imagesInitiatorSelections"
+ [messages]="messages.initiatorImage"
+ elemClass="btn btn-default pull-right">
+ <i class="fa fa-fw fa-plus"></i>
+ <ng-container i18n>Add image</ng-container>
+ </cd-select>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div class="row">
+ <div class="col-md-12">
+ <span class="text-muted"
+ *ngIf="initiators.controls.length === 0"
+ i18n>No items added.</span>
+
+ <button (click)="addInitiator()"
+ class="btn btn-default pull-right">
+ <i class="fa fa-fw fa-plus"></i>
+ <ng-container i18n>Add initiator</ng-container>
+ </button>
+ </div>
+ </div>
+
+ <hr />
+ </div>
+ </div>
+
+ <!-- Groups -->
+ <div class="form-group"
+ [ngClass]="{'has-error': targetForm.showError('groups', formDir)}">
+ <label class="control-label col-sm-3"
+ for="initiators"
+ i18n>Groups</label>
+ <div class="col-sm-9"
+ formArrayName="groups">
+ <div class="panel panel-default"
+ *ngFor="let group of groups.controls; let i = index"
+ [formGroupName]="i">
+ <div class="panel-heading">
+ <ng-container i18n>Group</ng-container>: {{ group.getValue('group_id') }}
+ <button type="button"
+ class="close"
+ (click)="groups.removeAt(i)">
+ <i class="fa fa-remove fa-fw"></i>
+ </button>
+ </div>
+ <div class="panel-body">
+ <!-- Group: group_id -->
+ <div class="form-group">
+ <label class="control-label col-sm-3"
+ for="group_id">
+ <ng-container i18n>Name</ng-container>
+ <span class="required"></span>
+ </label>
+ <div class="col-sm-9">
+ <input class="form-control"
+ type="text"
+ formControlName="group_id">
+ </div>
+ </div>
+
+ <!-- Group: members -->
+ <div class="form-group"
+ [ngClass]="{'has-error': group.showError('members', formDir)}">
+ <label class="control-label col-sm-3"
+ for="members">
+ <ng-container i18n>Initiators</ng-container>
+ </label>
+ <div class="col-sm-9">
+ <ng-container *ngFor="let member of group.getValue('members'); let i = index">
+ <div class="input-group cd-mb">
+ <input class="form-control"
+ type="text"
+ [value]="member"
+ disabled />
+ <span class="input-group-btn">
+ <button class="btn btn-default"
+ type="button"
+ (click)="removeGroupInitiator(group, i)">
+ <i class="fa fa-remove fa-fw"
+ aria-hidden="true"></i>
+ </button>
+ </span>
+ </div>
+ </ng-container>
+
+ <div class="row">
+ <div class="col-md-12">
+ <cd-select [data]="group.getValue('members')"
+ [options]="groupMembersSelections"
+ [messages]="messages.groupInitiator"
+ (selection)="onGroupMemberSelection($event)"
+ elemClass="btn btn-default pull-right">
+ <i class="fa fa-fw fa-plus"></i>
+ <ng-container i18n>Add initiator</ng-container>
+ </cd-select>
+ </div>
+ </div>
+
+ <hr />
+ </div>
+ </div>
+
+ <!-- Group: disks -->
+ <div class="form-group"
+ [ngClass]="{'has-error': group.showError('disks', formDir)}">
+ <label class="control-label col-sm-3"
+ for="disks">
+ <ng-container i18n>Images</ng-container>
+ </label>
+ <div class="col-sm-9">
+ <ng-container *ngFor="let disk of group.getValue('disks'); let i = index">
+ <div class="input-group cd-mb">
+ <input class="form-control"
+ type="text"
+ [value]="disk"
+ disabled />
+ <span class="input-group-btn">
+ <button class="btn btn-default"
+ type="button"
+ (click)="group.getValue('disks').splice(i, 1)">
+ <i class="fa fa-remove fa-fw"
+ aria-hidden="true"></i>
+ </button>
+ </span>
+ </div>
+ </ng-container>
+
+ <div class="row">
+ <div class="col-md-12">
+ <cd-select [data]="group.getValue('disks')"
+ [options]="groupDiskSelections"
+ [messages]="messages.initiatorImage"
+ elemClass="btn btn-default pull-right">
+ <i class="fa fa-fw fa-plus"></i>
+ <ng-container i18n>Add image</ng-container>
+ </cd-select>
+ </div>
+ </div>
+
+ <hr />
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div class="row">
+ <div class="col-md-12">
+ <span class="text-muted"
+ *ngIf="groups.controls.length === 0"
+ i18n>No items added.</span>
+
+ <button (click)="addGroup()"
+ class="btn btn-default pull-right">
+ <i class="fa fa-fw fa-plus"></i>
+ <ng-container i18n>Add group</ng-container>
+ </button>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ </div>
+ <div class="panel-footer">
+ <div class="button-group text-right">
+ <cd-submit-button [form]="formDir"
+ type="button"
+ (submitAction)="submit()"
+ i18n>Create target</cd-submit-button>
+
+ <button type="button"
+ class="btn btn-sm btn-default"
+ routerLink="/block/iscsi/targets"
+ i18n>Back</button>
+ </div>
+ </div>
+ </div>
+ </form>
+</div>
--- /dev/null
+.cd-mb {
+ margin-bottom: 10px;
+}
--- /dev/null
+import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ReactiveFormsModule } from '@angular/forms';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { ToastModule } from 'ng2-toastr';
+
+import { configureTestBed, i18nProviders } from '../../../../testing/unit-test-helper';
+import { SharedModule } from '../../../shared/shared.module';
+import { IscsiTargetFormComponent } from './iscsi-target-form.component';
+
+describe('IscsiTargetFormComponent', () => {
+ let component: IscsiTargetFormComponent;
+ let fixture: ComponentFixture<IscsiTargetFormComponent>;
+ let httpTesting: HttpTestingController;
+
+ const SETTINGS = {
+ config: { minimum_gateways: 2 },
+ disk_default_controls: {
+ hw_max_sectors: 1024,
+ osd_op_timeout: 30,
+ qfull_timeout: 5
+ },
+ target_default_controls: {
+ cmdsn_depth: 128,
+ dataout_timeout: 20,
+ immediate_data: 'Yes'
+ }
+ };
+
+ const LIST_TARGET = [
+ {
+ target_iqn: 'iqn.2003-01.com.redhat.iscsi-gw:iscsi-igw',
+ portals: [{ host: 'node1', ip: '192.168.100.201' }],
+ disks: [{ pool: 'rbd', image: 'disk_1', controls: {} }],
+ clients: [
+ {
+ client_iqn: 'iqn.1994-05.com.redhat:rh7-client',
+ luns: [{ pool: 'rbd', image: 'disk_1' }],
+ auth: {
+ user: 'myiscsiusername',
+ password: 'myiscsipassword',
+ mutual_user: null,
+ mutual_password: null
+ }
+ }
+ ],
+ groups: [],
+ target_controls: {}
+ }
+ ];
+
+ const PORTALS = [
+ { name: 'node1', ip_addresses: ['192.168.100.201', '10.0.2.15'] },
+ { name: 'node2', ip_addresses: ['192.168.100.202'] }
+ ];
+
+ const RBD_LIST = [
+ { status: 0, value: [], pool_name: 'ganesha' },
+ {
+ status: 0,
+ value: [
+ {
+ size: 96636764160,
+ obj_size: 4194304,
+ num_objs: 23040,
+ order: 22,
+ block_name_prefix: 'rbd_data.148162fb31a8',
+ name: 'disk_1',
+ id: '148162fb31a8',
+ pool_name: 'rbd',
+ features: 61,
+ features_name: ['deep-flatten', 'exclusive-lock', 'fast-diff', 'layering', 'object-map'],
+ timestamp: '2019-01-18T10:44:26Z',
+ stripe_count: 1,
+ stripe_unit: 4194304,
+ data_pool: null,
+ parent: null,
+ snapshots: [],
+ total_disk_usage: 0,
+ disk_usage: 0
+ },
+ {
+ size: 119185342464,
+ obj_size: 4194304,
+ num_objs: 28416,
+ order: 22,
+ block_name_prefix: 'rbd_data.14b292cee6cb',
+ name: 'disk_2',
+ id: '14b292cee6cb',
+ pool_name: 'rbd',
+ features: 61,
+ features_name: ['deep-flatten', 'exclusive-lock', 'fast-diff', 'layering', 'object-map'],
+ timestamp: '2019-01-18T10:45:56Z',
+ stripe_count: 1,
+ stripe_unit: 4194304,
+ data_pool: null,
+ parent: null,
+ snapshots: [],
+ total_disk_usage: 0,
+ disk_usage: 0
+ }
+ ],
+ pool_name: 'rbd'
+ }
+ ];
+
+ configureTestBed(
+ {
+ declarations: [IscsiTargetFormComponent],
+ imports: [
+ SharedModule,
+ ReactiveFormsModule,
+ HttpClientTestingModule,
+ RouterTestingModule,
+ ToastModule.forRoot()
+ ],
+ providers: [i18nProviders]
+ },
+ true
+ );
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(IscsiTargetFormComponent);
+ component = fixture.componentInstance;
+ httpTesting = TestBed.get(HttpTestingController);
+ fixture.detectChanges();
+
+ httpTesting.expectOne('ui-api/iscsi/settings').flush(SETTINGS);
+ httpTesting.expectOne('ui-api/iscsi/portals').flush(PORTALS);
+ httpTesting.expectOne('api/summary').flush({});
+ httpTesting.expectOne('api/block/image').flush(RBD_LIST);
+ httpTesting.expectOne('api/iscsi/target').flush(LIST_TARGET);
+ httpTesting.verify();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should only show images not used in other targets', () => {
+ expect(component.imagesAll).toEqual(['rbd/disk_2']);
+ expect(component.imagesSelections).toEqual([
+ { description: '', name: 'rbd/disk_2', selected: false }
+ ]);
+ });
+
+ it('should generate portals selectOptions', () => {
+ expect(component.portalsSelections).toEqual([
+ { description: '', name: 'node1:192.168.100.201', selected: false },
+ { description: '', name: 'node1:10.0.2.15', selected: false },
+ { description: '', name: 'node2:192.168.100.202', selected: false }
+ ]);
+ });
+
+ it('should create the form', () => {
+ expect(component.targetForm.value).toEqual({
+ disks: [],
+ groups: [],
+ initiators: [],
+ portals: [],
+ target_controls: {},
+ target_iqn: component.targetForm.value.target_iqn
+ });
+ });
+
+ it('should prepare data when selecting an image', () => {
+ expect(component.imagesInitiatorSelections).toEqual([]);
+ expect(component.groupDiskSelections).toEqual([]);
+ expect(component.imagesSettings).toEqual({});
+
+ component.onImageSelection({ option: { name: 'rbd/disk_1', selected: true } });
+
+ expect(component.imagesInitiatorSelections).toEqual([
+ { description: '', name: 'rbd/disk_1', selected: false }
+ ]);
+ expect(component.groupDiskSelections).toEqual([
+ { description: '', name: 'rbd/disk_1', selected: false }
+ ]);
+ expect(component.imagesSettings).toEqual({ 'rbd/disk_1': {} });
+ });
+
+ it('should clean data when removing an image', () => {
+ component.onImageSelection({ option: { name: 'rbd/disk_1', selected: true } });
+ component.addGroup();
+ component.groups.controls[0].patchValue({
+ group_id: 'foo',
+ disks: ['rbd/disk_1']
+ });
+
+ expect(component.groups.controls[0].value).toEqual({
+ disks: ['rbd/disk_1'],
+ group_id: 'foo',
+ members: []
+ });
+
+ component.onImageSelection({ option: { name: 'rbd/disk_1', selected: false } });
+
+ expect(component.groups.controls[0].value).toEqual({ disks: [], group_id: 'foo', members: [] });
+ expect(component.imagesInitiatorSelections).toEqual([]);
+ expect(component.groupDiskSelections).toEqual([]);
+ expect(component.imagesSettings).toEqual({ 'rbd/disk_1': {} });
+ });
+
+ describe('should test initiators', () => {
+ beforeEach(() => {
+ component.addInitiator();
+ component.initiators.controls[0].patchValue({
+ client_iqn: 'iqn.initiator'
+ });
+ component.updatedInitiatorSelector();
+ });
+
+ it('should prepare data when creating an initiator', () => {
+ expect(component.initiators.controls.length).toBe(1);
+ expect(component.initiators.controls[0].value).toEqual({
+ auth: { mutual_password: '', mutual_user: '', password: '', user: '' },
+ cdIsInGroup: false,
+ client_iqn: 'iqn.initiator',
+ luns: []
+ });
+ expect(component.groupMembersSelections).toEqual([
+ { description: '', name: 'iqn.initiator', selected: false }
+ ]);
+ });
+
+ it('should update data when changing an initiator name', () => {
+ expect(component.groupMembersSelections).toEqual([
+ { description: '', name: 'iqn.initiator', selected: false }
+ ]);
+
+ component.initiators.controls[0].patchValue({
+ client_iqn: 'iqn.initiator_new'
+ });
+ component.updatedInitiatorSelector();
+
+ expect(component.groupMembersSelections).toEqual([
+ { description: '', name: 'iqn.initiator_new', selected: false }
+ ]);
+ });
+
+ it('should clean data when removing an initiator', () => {
+ component.addGroup();
+ component.groups.controls[0].patchValue({
+ group_id: 'foo',
+ members: ['iqn.initiator']
+ });
+
+ expect(component.groups.controls[0].value).toEqual({
+ disks: [],
+ group_id: 'foo',
+ members: ['iqn.initiator']
+ });
+
+ component.removeInitiator(0);
+
+ expect(component.groups.controls[0].value).toEqual({
+ disks: [],
+ group_id: 'foo',
+ members: []
+ });
+ expect(component.groupMembersSelections).toEqual([]);
+ });
+
+ it('should remove images in the initiator when added in a group', () => {
+ component.initiators.controls[0].patchValue({
+ luns: ['rbd/disk_1']
+ });
+ expect(component.initiators.controls[0].value).toEqual({
+ auth: { mutual_password: '', mutual_user: '', password: '', user: '' },
+ cdIsInGroup: false,
+ client_iqn: 'iqn.initiator',
+ luns: ['rbd/disk_1']
+ });
+
+ component.addGroup();
+ component.groups.controls[0].patchValue({
+ group_id: 'foo',
+ members: ['iqn.initiator']
+ });
+ component.onGroupMemberSelection({
+ option: {
+ name: 'iqn.initiator',
+ selected: true
+ }
+ });
+
+ expect(component.initiators.controls[0].value).toEqual({
+ auth: { mutual_password: '', mutual_user: '', password: '', user: '' },
+ cdIsInGroup: true,
+ client_iqn: 'iqn.initiator',
+ luns: []
+ });
+ });
+ });
+
+ it('should generate the request data', () => {
+ component.onImageSelection({ option: { name: 'rbd/disk_1', selected: true } });
+ component.portals.setValue(['node1:192.168.100.201', 'node2:192.168.100.202']);
+ component.addInitiator();
+ component.initiators.controls[0].patchValue({
+ client_iqn: 'iqn.initiator'
+ });
+ component.addGroup();
+ component.groups.controls[0].patchValue({
+ group_id: 'foo',
+ members: ['iqn.initiator'],
+ disks: ['rbd/disk_1']
+ });
+
+ component.submit();
+
+ const req = httpTesting.expectOne('api/iscsi/target');
+ expect(req.request.method).toBe('POST');
+ expect(req.request.body).toEqual({
+ clients: [
+ {
+ auth: { mutual_password: null, mutual_user: null, password: null, user: null },
+ cdIsInGroup: false,
+ client_iqn: 'iqn.initiator',
+ luns: []
+ }
+ ],
+ disks: [],
+ groups: [
+ { disks: [{ image: 'disk_1', pool: 'rbd' }], group_id: 'foo', members: ['iqn.initiator'] }
+ ],
+ portals: [{ host: 'node1', ip: '192.168.100.201' }, { host: 'node2', ip: '192.168.100.202' }],
+ target_controls: {},
+ target_iqn: component.targetForm.value.target_iqn
+ });
+ });
+});
--- /dev/null
+import { Component } from '@angular/core';
+import { FormArray, FormControl, Validators } from '@angular/forms';
+import { Router } from '@angular/router';
+
+import { I18n } from '@ngx-translate/i18n-polyfill';
+import * as _ from 'lodash';
+import { BsModalRef, BsModalService } from 'ngx-bootstrap/modal';
+import { forkJoin } from 'rxjs';
+
+import { IscsiService } from '../../../shared/api/iscsi.service';
+import { RbdService } from '../../../shared/api/rbd.service';
+import { SelectMessages } from '../../../shared/components/select/select-messages.model';
+import { SelectOption } from '../../../shared/components/select/select-option.model';
+import { CdFormGroup } from '../../../shared/forms/cd-form-group';
+import { CdValidators } from '../../../shared/forms/cd-validators';
+import { FinishedTask } from '../../../shared/models/finished-task';
+import { TaskWrapperService } from '../../../shared/services/task-wrapper.service';
+import { IscsiTargetImageSettingsModalComponent } from '../iscsi-target-image-settings-modal/iscsi-target-image-settings-modal.component';
+import { IscsiTargetIqnSettingsModalComponent } from '../iscsi-target-iqn-settings-modal/iscsi-target-iqn-settings-modal.component';
+
+@Component({
+ selector: 'cd-iscsi-target-form',
+ templateUrl: './iscsi-target-form.component.html',
+ styleUrls: ['./iscsi-target-form.component.scss']
+})
+export class IscsiTargetFormComponent {
+ targetForm: CdFormGroup;
+ modalRef: BsModalRef;
+ minimum_gateways = 1;
+ target_default_controls: any;
+ disk_default_controls: any;
+
+ imagesAll: any[];
+ imagesSelections: SelectOption[];
+ portalsSelections: SelectOption[] = [];
+ imagesInitiatorSelections: SelectOption[] = [];
+ groupDiskSelections: SelectOption[] = [];
+ groupMembersSelections: SelectOption[] = [];
+ imagesSettings: any = {};
+ messages = {
+ portals: new SelectMessages(
+ { noOptions: this.i18n('There are no portals available.') },
+ this.i18n
+ ),
+ images: new SelectMessages(
+ { noOptions: this.i18n('There are no images available.') },
+ this.i18n
+ ),
+ initiatorImage: new SelectMessages(
+ {
+ noOptions: this.i18n(
+ 'There are no images available. Please make sure you add an image to the target.'
+ )
+ },
+ this.i18n
+ ),
+ groupInitiator: new SelectMessages(
+ {
+ noOptions: this.i18n(
+ 'There are no initiators available. Please make sure you add an initiator to the target.'
+ )
+ },
+ this.i18n
+ )
+ };
+
+ IQN_REGEX = /^iqn\.(19|20)\d\d-(0[1-9]|1[0-2])\.\D{2,3}(\.[A-Za-z0-9-]+)+(:[A-Za-z0-9-\.]+)*$/;
+ USER_REGEX = /[\w\.:@_-]{8,64}/;
+ PASSWORD_REGEX = /[\w@\-_]{12,16}/;
+
+ constructor(
+ private iscsiService: IscsiService,
+ private modalService: BsModalService,
+ private rbdService: RbdService,
+ private router: Router,
+ private i18n: I18n,
+ private taskWrapper: TaskWrapperService
+ ) {
+ forkJoin([this.rbdService.list(), this.iscsiService.listTargets()]).subscribe((data: any[]) => {
+ const usedImages = _(data[1])
+ .flatMap((target) => target.disks)
+ .map((image) => `${image.pool}/${image.image}`)
+ .value();
+
+ this.imagesAll = _(data[0])
+ .flatMap((pool) => pool.value)
+ .map((image) => `${image.pool_name}/${image.name}`)
+ .filter((image) => usedImages.indexOf(image) === -1)
+ .value();
+
+ this.imagesSelections = this.imagesAll.map((image) => new SelectOption(false, image, ''));
+ });
+
+ this.iscsiService.portals().subscribe((result: any) => {
+ const portals: SelectOption[] = [];
+ result.forEach((portal) => {
+ portal.ip_addresses.forEach((ip) => {
+ portals.push(new SelectOption(false, portal.name + ':' + ip, ''));
+ });
+ });
+ this.portalsSelections = [...portals];
+ });
+
+ this.iscsiService.settings().subscribe((result: any) => {
+ this.minimum_gateways = result.config.minimum_gateways;
+ this.target_default_controls = result.target_default_controls;
+ this.disk_default_controls = result.disk_default_controls;
+ this.createForm();
+ });
+ }
+
+ createForm() {
+ this.targetForm = new CdFormGroup({
+ target_iqn: new FormControl('iqn.2001-07.com.ceph:' + Date.now(), {
+ validators: [Validators.required, Validators.pattern(this.IQN_REGEX)]
+ }),
+ target_controls: new FormControl({}),
+ portals: new FormControl([], {
+ validators: [
+ CdValidators.custom('minGateways', (value) => {
+ const gateways = _.uniq(value.map((elem) => elem.split(':')[0]));
+ return gateways.length < Math.max(1, this.minimum_gateways);
+ })
+ ]
+ }),
+ disks: new FormControl([]),
+ initiators: new FormArray([]),
+ groups: new FormArray([])
+ });
+ }
+
+ hasAdvancedSettings(settings: any) {
+ return Object.values(settings).length > 0;
+ }
+
+ // Portals
+ get portals() {
+ return this.targetForm.get('portals') as FormControl;
+ }
+
+ onPortalSelection($event) {
+ this.portals.setValue(this.portals.value);
+ }
+
+ removePortal(index: number, portal: string) {
+ this.portalsSelections.forEach((value) => {
+ if (value.name === portal) {
+ value.selected = false;
+ }
+ });
+
+ this.portals.value.splice(index, 1);
+ this.portals.setValue(this.portals.value);
+ return false;
+ }
+
+ // Images
+ get disks() {
+ return this.targetForm.get('disks') as FormControl;
+ }
+
+ removeImage(index: number, image: string) {
+ this.imagesSelections.forEach((value) => {
+ if (value.name === image) {
+ value.selected = false;
+ }
+ });
+ this.disks.value.splice(index, 1);
+ this.removeImageRefs(image);
+ return false;
+ }
+
+ removeImageRefs(name) {
+ this.initiators.controls.forEach((element) => {
+ const newImages = element.value.luns.filter((item) => item !== name);
+ element.get('luns').setValue(newImages);
+ });
+
+ this.groups.controls.forEach((element) => {
+ const newDisks = element.value.disks.filter((item) => item !== name);
+ element.get('disks').setValue(newDisks);
+ });
+
+ this.imagesInitiatorSelections = this.imagesInitiatorSelections.filter(
+ (item) => item.name !== name
+ );
+ this.groupDiskSelections = this.groupDiskSelections.filter((item) => item.name !== name);
+ }
+
+ onImageSelection($event) {
+ const option = $event.option;
+
+ if (option.selected) {
+ if (!this.imagesSettings[option.name]) {
+ this.imagesSettings[option.name] = {};
+ }
+ this.imagesInitiatorSelections.push(new SelectOption(false, option.name, ''));
+ this.groupDiskSelections.push(new SelectOption(false, option.name, ''));
+ } else {
+ this.removeImageRefs(option.name);
+ }
+
+ this.imagesInitiatorSelections = [...this.imagesInitiatorSelections];
+ this.groupDiskSelections = [...this.groupDiskSelections];
+ }
+
+ // Initiators
+ get initiators() {
+ return this.targetForm.get('initiators') as FormArray;
+ }
+
+ addInitiator() {
+ const fg = new CdFormGroup({
+ client_iqn: new FormControl('', {
+ validators: [
+ Validators.required,
+ CdValidators.custom('notUnique', (client_iqn) => {
+ const flattened = this.initiators.controls.reduce(function(accumulator, currentValue) {
+ return accumulator.concat(currentValue.value.client_iqn);
+ }, []);
+
+ return flattened.indexOf(client_iqn) !== flattened.lastIndexOf(client_iqn);
+ }),
+ Validators.pattern(this.IQN_REGEX)
+ ]
+ }),
+ auth: new CdFormGroup({
+ user: new FormControl(''),
+ password: new FormControl(''),
+ mutual_user: new FormControl(''),
+ mutual_password: new FormControl('')
+ }),
+ luns: new FormControl([]),
+ cdIsInGroup: new FormControl(false)
+ });
+
+ CdValidators.validateIf(
+ fg.get('user'),
+ () => fg.getValue('password') || fg.getValue('mutual_user') || fg.getValue('mutual_password'),
+ [Validators.required],
+ [Validators.pattern(this.USER_REGEX)],
+ [fg.get('password'), fg.get('mutual_user'), fg.get('mutual_password')]
+ );
+
+ CdValidators.validateIf(
+ fg.get('password'),
+ () => fg.getValue('user') || fg.getValue('mutual_user') || fg.getValue('mutual_password'),
+ [Validators.required],
+ [Validators.pattern(this.PASSWORD_REGEX)],
+ [fg.get('user'), fg.get('mutual_user'), fg.get('mutual_password')]
+ );
+
+ CdValidators.validateIf(
+ fg.get('mutual_user'),
+ () => fg.getValue('mutual_password'),
+ [Validators.required],
+ [Validators.pattern(this.USER_REGEX)],
+ [fg.get('user'), fg.get('password'), fg.get('mutual_password')]
+ );
+
+ CdValidators.validateIf(
+ fg.get('mutual_password'),
+ () => fg.getValue('mutual_user'),
+ [Validators.required],
+ [Validators.pattern(this.PASSWORD_REGEX)],
+ [fg.get('user'), fg.get('password'), fg.get('mutual_user')]
+ );
+
+ this.initiators.push(fg);
+
+ this.groupMembersSelections.push(new SelectOption(false, '', ''));
+ this.groupMembersSelections = [...this.groupMembersSelections];
+
+ return false;
+ }
+
+ removeInitiator(index) {
+ this.initiators.removeAt(index);
+
+ const removed: SelectOption[] = this.groupMembersSelections.splice(index, 1);
+ this.groupMembersSelections = [...this.groupMembersSelections];
+
+ this.groups.controls.forEach((element) => {
+ const newMembers = element.value.members.filter((item) => item !== removed[0].name);
+ element.get('members').setValue(newMembers);
+ });
+ }
+
+ updatedInitiatorSelector() {
+ // Validate all client_iqn
+ this.initiators.controls.forEach((control) => {
+ control.get('client_iqn').updateValueAndValidity({ emitEvent: false });
+ });
+
+ // Update Group Initiator Selector
+ this.groupMembersSelections.forEach((elem, index) => {
+ const oldName = elem.name;
+ elem.name = this.initiators.controls[index].value.client_iqn;
+
+ this.groups.controls.forEach((element) => {
+ const members = element.value.members;
+ const i = members.indexOf(oldName);
+
+ if (i !== -1) {
+ members[i] = elem.name;
+ }
+ element.get('members').setValue(members);
+ });
+ });
+ this.groupMembersSelections = [...this.groupMembersSelections];
+ }
+
+ // Groups
+ get groups() {
+ return this.targetForm.get('groups') as FormArray;
+ }
+
+ addGroup() {
+ this.groups.push(
+ new CdFormGroup({
+ group_id: new FormControl('', { validators: [Validators.required] }),
+ members: new FormControl([]),
+ disks: new FormControl([])
+ })
+ );
+ return false;
+ }
+
+ onGroupMemberSelection($event) {
+ const option = $event.option;
+
+ this.initiators.controls.forEach((element) => {
+ if (element.value.client_iqn === option.name) {
+ element.patchValue({ luns: [] });
+ element.get('cdIsInGroup').setValue(option.selected);
+ }
+ });
+ }
+ removeGroupInitiator(group, i) {
+ const name = group.getValue('members')[i];
+ group.getValue('members').splice(i, 1);
+
+ this.groupMembersSelections.forEach((value) => {
+ if (value.name === name) {
+ value.selected = false;
+ }
+ });
+ this.groupMembersSelections = [...this.groupMembersSelections];
+
+ this.onGroupMemberSelection({ option: new SelectOption(false, name, '') });
+ }
+
+ submit() {
+ const formValue = this.targetForm.value;
+
+ const request = {
+ target_iqn: this.targetForm.getValue('target_iqn'),
+ target_controls: this.targetForm.getValue('target_controls'),
+ portals: [],
+ disks: [],
+ clients: [],
+ groups: []
+ };
+
+ // Disks
+ formValue.disks.forEach((disk) => {
+ const imageSplit = disk.split('/');
+ request.disks.push({
+ pool: imageSplit[0],
+ image: imageSplit[1],
+ controls: this.imagesSettings[disk]
+ });
+ });
+
+ // Portals
+ formValue.portals.forEach((portal) => {
+ const portalSplit = portal.split(':');
+ request.portals.push({
+ host: portalSplit[0],
+ ip: portalSplit[1]
+ });
+ });
+
+ // Clients
+ formValue.initiators.forEach((initiator) => {
+ if (!initiator.auth.user) {
+ initiator.auth.user = null;
+ }
+ if (!initiator.auth.password) {
+ initiator.auth.password = null;
+ }
+ if (!initiator.auth.mutual_user) {
+ initiator.auth.mutual_user = null;
+ }
+ if (!initiator.auth.mutual_password) {
+ initiator.auth.mutual_password = null;
+ }
+
+ const newLuns = [];
+ initiator.luns.forEach((lun) => {
+ const imageSplit = lun.split('/');
+ newLuns.push({
+ pool: imageSplit[0],
+ image: imageSplit[1]
+ });
+ });
+
+ initiator.luns = newLuns;
+ });
+ request.clients = formValue.initiators;
+
+ // Groups
+ formValue.groups.forEach((group) => {
+ const newDisks = [];
+ group.disks.forEach((disk) => {
+ const imageSplit = disk.split('/');
+ newDisks.push({
+ pool: imageSplit[0],
+ image: imageSplit[1]
+ });
+ });
+
+ group.disks = newDisks;
+ });
+ request.groups = formValue.groups;
+
+ this.taskWrapper
+ .wrapTaskAroundCall({
+ task: new FinishedTask('iscsi/target/create', {
+ target_iqn: request.target_iqn
+ }),
+ call: this.iscsiService.createTarget(request)
+ })
+ .subscribe(
+ undefined,
+ () => {
+ this.targetForm.setErrors({ cdSubmitButton: true });
+ },
+ () => this.router.navigate(['/block/iscsi/targets'])
+ );
+ }
+
+ targetSettingsModal() {
+ const initialState = {
+ target_controls: this.targetForm.get('target_controls'),
+ target_default_controls: this.target_default_controls
+ };
+
+ this.modalRef = this.modalService.show(IscsiTargetIqnSettingsModalComponent, { initialState });
+ }
+
+ imageSettingsModal(image) {
+ const initialState = {
+ imagesSettings: this.imagesSettings,
+ image: image,
+ disk_default_controls: this.disk_default_controls
+ };
+
+ this.modalRef = this.modalService.show(IscsiTargetImageSettingsModalComponent, {
+ initialState
+ });
+ }
+}
--- /dev/null
+<cd-modal>
+ <ng-container class="modal-title">
+ <ng-container i18n>Settings</ng-container>
+ <small>{{ image }}</small>
+ </ng-container>
+
+ <ng-container class="modal-content">
+ <form name="settingsForm"
+ class="form"
+ #formDir="ngForm"
+ [formGroup]="settingsForm"
+ novalidate>
+ <div class="modal-body">
+ <p class="alert-warning"
+ i18n>Changing these parameters from their default values is usually not necessary.</p>
+
+ <div class="form-group row"
+ *ngFor="let setting of disk_default_controls | keyvalue"
+ [ngClass]="{'has-error': settingsForm.showError(setting.key, formDir)}">
+ <div class="col-sm-12">
+ <label class="control-label"
+ for="{{ setting.key }}">{{ setting.key }}</label>
+ <input type="number"
+ class="form-control"
+ [formControlName]="setting.key">
+ <span class="help-block">{{ helpText[setting.key]?.help }}</span>
+ </div>
+ </div>
+ </div>
+
+ <div class="modal-footer">
+ <div class="button-group text-right">
+ <cd-submit-button i18n
+ [form]="settingsForm"
+ (submitAction)="save()">Confirm</cd-submit-button>
+ <button i18n
+ type="button"
+ class="btn btn-sm btn-default"
+ (click)="modalRef.hide()">Cancel</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 { BsModalRef } from 'ngx-bootstrap/modal';
+
+import { configureTestBed, i18nProviders } from '../../../../testing/unit-test-helper';
+import { SharedModule } from '../../../shared/shared.module';
+import { IscsiTargetImageSettingsModalComponent } from './iscsi-target-image-settings-modal.component';
+
+describe('IscsiTargetImageSettingsModalComponent', () => {
+ let component: IscsiTargetImageSettingsModalComponent;
+ let fixture: ComponentFixture<IscsiTargetImageSettingsModalComponent>;
+
+ configureTestBed({
+ declarations: [IscsiTargetImageSettingsModalComponent],
+ imports: [SharedModule, ReactiveFormsModule, HttpClientTestingModule],
+ providers: [BsModalRef, i18nProviders]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(IscsiTargetImageSettingsModalComponent);
+ component = fixture.componentInstance;
+
+ component.imagesSettings = { 'rbd/disk_1': {} };
+ component.image = 'rbd/disk_1';
+ component.disk_default_controls = {
+ foo: 1,
+ bar: 2
+ };
+ component.ngOnInit();
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should fill the settingsForm', () => {
+ expect(component.settingsForm.value).toEqual({
+ foo: null,
+ bar: null
+ });
+ });
+
+ it('should save changes to imagesSettings', () => {
+ component.settingsForm.patchValue({ foo: 1234 });
+ expect(component.imagesSettings).toEqual({ 'rbd/disk_1': {} });
+ component.save();
+ expect(component.imagesSettings).toEqual({
+ 'rbd/disk_1': {
+ foo: 1234
+ }
+ });
+ });
+});
--- /dev/null
+import { Component, OnInit } from '@angular/core';
+import { FormControl } from '@angular/forms';
+
+import * as _ from 'lodash';
+import { BsModalRef } from 'ngx-bootstrap/modal';
+
+import { IscsiService } from '../../../shared/api/iscsi.service';
+import { CdFormGroup } from '../../../shared/forms/cd-form-group';
+
+@Component({
+ selector: 'cd-iscsi-target-image-settings-modal',
+ templateUrl: './iscsi-target-image-settings-modal.component.html',
+ styleUrls: ['./iscsi-target-image-settings-modal.component.scss']
+})
+export class IscsiTargetImageSettingsModalComponent implements OnInit {
+ image: string;
+ imagesSettings: any;
+ disk_default_controls: any;
+
+ settingsForm: CdFormGroup;
+ helpText: any;
+
+ constructor(public modalRef: BsModalRef, public iscsiService: IscsiService) {}
+
+ ngOnInit() {
+ const fg = {};
+ const currentSettings = this.imagesSettings[this.image];
+ this.helpText = this.iscsiService.imageAdvancedSettings;
+
+ _.forIn(this.disk_default_controls, (value, key) => {
+ fg[key] = new FormControl(currentSettings[key]);
+ });
+
+ this.settingsForm = new CdFormGroup(fg);
+ }
+
+ save() {
+ const settings = {};
+ _.forIn(this.settingsForm.value, (value, key) => {
+ if (!(value === '' || value === null)) {
+ settings[key] = value;
+ }
+ });
+
+ this.imagesSettings[this.image] = settings;
+ this.imagesSettings = { ...this.imagesSettings };
+ this.modalRef.hide();
+ }
+}
--- /dev/null
+<cd-modal>
+ <ng-container class="modal-title"
+ i18n>Advanced Settings</ng-container>
+
+ <ng-container class="modal-content">
+ <form name="settingsForm"
+ class="form"
+ #formDir="ngForm"
+ [formGroup]="settingsForm"
+ novalidate>
+ <div class="modal-body">
+ <p class="alert-warning"
+ i18n>Changing these parameters from their default values is usually not necessary.</p>
+
+ <div class="form-group row"
+ *ngFor="let setting of settingsForm.controls | keyvalue"
+ [ngClass]="{'has-error': settingsForm.showError(setting.key, formDir)}">
+ <div class="col-sm-12">
+ <label class="control-label"
+ for="{{ setting.key }}">{{ setting.key }}</label>
+ <input class="form-control"
+ *ngIf="!isRadio(setting.key)"
+ type="number"
+ [formControlName]="setting.key">
+
+ <ng-container *ngIf="isRadio(setting.key)">
+ <br>
+ <div class="radio radio-inline">
+ <input type="radio"
+ [id]="setting.key + 'Yes'"
+ value="Yes"
+ [formControlName]="setting.key">
+ <label [for]="setting.key + 'Yes'">Yes</label>
+ </div>
+ <div class="radio radio-inline">
+ <input type="radio"
+ [id]="setting.key + 'No'"
+ value="No"
+ [formControlName]="setting.key">
+ <label [for]="setting.key + 'No'">No</label>
+ </div>
+ </ng-container>
+
+ <span class="help-block">{{ helpText[setting.key]?.help }}</span>
+ </div>
+ </div>
+ </div>
+
+ <div class="modal-footer">
+ <div class="button-group text-right">
+ <cd-submit-button i18n
+ [form]="settingsForm"
+ (submitAction)="save()">Confirm</cd-submit-button>
+ <button i18n
+ type="button"
+ class="btn btn-sm btn-default"
+ (click)="modalRef.hide()">Cancel</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 { FormControl, ReactiveFormsModule } from '@angular/forms';
+
+import { BsModalRef } from 'ngx-bootstrap/modal';
+
+import { configureTestBed, i18nProviders } from '../../../../testing/unit-test-helper';
+import { SharedModule } from '../../../shared/shared.module';
+import { IscsiTargetIqnSettingsModalComponent } from './iscsi-target-iqn-settings-modal.component';
+
+describe('IscsiTargetIqnSettingsModalComponent', () => {
+ let component: IscsiTargetIqnSettingsModalComponent;
+ let fixture: ComponentFixture<IscsiTargetIqnSettingsModalComponent>;
+
+ configureTestBed({
+ declarations: [IscsiTargetIqnSettingsModalComponent],
+ imports: [SharedModule, ReactiveFormsModule, HttpClientTestingModule],
+ providers: [BsModalRef, i18nProviders]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(IscsiTargetIqnSettingsModalComponent);
+ component = fixture.componentInstance;
+ component.target_controls = new FormControl({});
+ component.target_default_controls = {
+ cmdsn_depth: 1,
+ dataout_timeout: 2,
+ first_burst_length: 'Yes'
+ };
+ component.ngOnInit();
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should fill the settingsForm', () => {
+ expect(component.settingsForm.value).toEqual({
+ cmdsn_depth: null,
+ dataout_timeout: null,
+ first_burst_length: null
+ });
+ });
+
+ it('should save changes to target_controls', () => {
+ component.settingsForm.patchValue({ dataout_timeout: 1234 });
+ expect(component.target_controls.value).toEqual({});
+ component.save();
+ expect(component.target_controls.value).toEqual({ dataout_timeout: 1234 });
+ });
+});
--- /dev/null
+import { Component, OnInit } from '@angular/core';
+import { FormControl } from '@angular/forms';
+
+import * as _ from 'lodash';
+import { BsModalRef } from 'ngx-bootstrap/modal';
+
+import { IscsiService } from '../../../shared/api/iscsi.service';
+import { CdFormGroup } from '../../../shared/forms/cd-form-group';
+
+@Component({
+ selector: 'cd-iscsi-target-iqn-settings-modal',
+ templateUrl: './iscsi-target-iqn-settings-modal.component.html',
+ styleUrls: ['./iscsi-target-iqn-settings-modal.component.scss']
+})
+export class IscsiTargetIqnSettingsModalComponent implements OnInit {
+ target_controls: FormControl;
+ target_default_controls: any;
+
+ settingsForm: CdFormGroup;
+ helpText: any;
+
+ constructor(public modalRef: BsModalRef, public iscsiService: IscsiService) {}
+
+ ngOnInit() {
+ const fg = {};
+ this.helpText = this.iscsiService.targetAdvancedSettings;
+
+ _.forIn(this.target_default_controls, (value, key) => {
+ fg[key] = new FormControl(this.target_controls.value[key]);
+ });
+
+ this.settingsForm = new CdFormGroup(fg);
+ }
+
+ save() {
+ const settings = {};
+ _.forIn(this.settingsForm.controls, (control, key) => {
+ if (!(control.value === '' || control.value === null)) {
+ settings[key] = control.value;
+ }
+ });
+
+ this.target_controls.setValue(settings);
+ this.modalRef.hide();
+ }
+
+ isRadio(control) {
+ return ['Yes', 'No'].indexOf(this.target_default_controls[control]) !== -1;
+ }
+}
forceIdentifier="true"
selectionType="single"
(updateSelection)="updateSelection($event)">
+ <div class="table-actions btn-toolbar">
+ <cd-table-actions class="btn-group"
+ [permission]="permissions.iscsi"
+ [selection]="selection"
+ [tableActions]="tableActions">
+ </cd-table-actions>
+ </div>
+
<cd-iscsi-target-details cdTableDetail
*ngIf="selection.hasSingleSelection"
[selection]="selection"
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { By } from '@angular/platform-browser';
import { RouterTestingModule } from '@angular/router/testing';
import { ToastModule } from 'ng2-toastr';
import { TreeModule } from 'ng2-tree';
import { TabsModule } from 'ngx-bootstrap/tabs';
+import { BehaviorSubject, of } from 'rxjs';
-import { configureTestBed, i18nProviders } from '../../../../testing/unit-test-helper';
+import {
+ configureTestBed,
+ i18nProviders,
+ PermissionHelper
+} from '../../../../testing/unit-test-helper';
+import { IscsiService } from '../../../shared/api/iscsi.service';
+import { TableActionsComponent } from '../../../shared/datatable/table-actions/table-actions.component';
+import { ExecutingTask } from '../../../shared/models/executing-task';
+import { SummaryService } from '../../../shared/services/summary.service';
import { TaskListService } from '../../../shared/services/task-list.service';
import { SharedModule } from '../../../shared/shared.module';
import { IscsiTabsComponent } from '../iscsi-tabs/iscsi-tabs.component';
describe('IscsiTargetListComponent', () => {
let component: IscsiTargetListComponent;
let fixture: ComponentFixture<IscsiTargetListComponent>;
+ let summaryService: SummaryService;
+ let iscsiService: IscsiService;
+
+ const refresh = (data) => {
+ summaryService['summaryDataSource'].next(data);
+ };
configureTestBed({
imports: [
beforeEach(() => {
fixture = TestBed.createComponent(IscsiTargetListComponent);
component = fixture.componentInstance;
- fixture.detectChanges();
+ summaryService = TestBed.get(SummaryService);
+ iscsiService = TestBed.get(IscsiService);
+
+ // this is needed because summaryService isn't being reset after each test.
+ summaryService['summaryDataSource'] = new BehaviorSubject(null);
+ summaryService['summaryData$'] = summaryService['summaryDataSource'].asObservable();
+
+ spyOn(iscsiService, 'status').and.returnValue(of({ available: true }));
});
it('should create', () => {
expect(component).toBeTruthy();
});
+
+ describe('after ngOnInit', () => {
+ beforeEach(() => {
+ spyOn(iscsiService, 'listTargets').and.callThrough();
+ fixture.detectChanges();
+ });
+
+ it('should load targets on init', () => {
+ refresh({});
+ expect(iscsiService.status).toHaveBeenCalled();
+ expect(iscsiService.listTargets).toHaveBeenCalled();
+ });
+
+ it('should not load targets on init because no data', () => {
+ refresh(undefined);
+ expect(iscsiService.listTargets).not.toHaveBeenCalled();
+ });
+
+ it('should call error function on init when summary service fails', () => {
+ spyOn(component.table, 'reset');
+ summaryService['summaryDataSource'].error(undefined);
+ expect(component.table.reset).toHaveBeenCalled();
+ });
+ });
+
+ describe('handling of executing tasks', () => {
+ let targets: any[];
+
+ const addTarget = (name) => {
+ const model: any = {
+ target_iqn: name,
+ portals: [{ host: 'node1', ip: '192.168.100.201' }],
+ disks: [{ pool: 'rbd', image: 'disk_1', controls: {} }],
+ clients: [
+ {
+ client_iqn: 'iqn.1994-05.com.redhat:rh7-client',
+ luns: [{ pool: 'rbd', image: 'disk_1' }],
+ auth: {
+ user: 'myiscsiusername',
+ password: 'myiscsipassword',
+ mutual_user: null,
+ mutual_password: null
+ }
+ }
+ ],
+ groups: [],
+ target_controls: {}
+ };
+ targets.push(model);
+ };
+
+ const addTask = (name: string, target_iqn: string) => {
+ const task = new ExecutingTask();
+ task.name = name;
+ switch (task.name) {
+ case 'iscsi/target/create':
+ task.metadata = {
+ target_iqn: target_iqn
+ };
+ break;
+ case 'iscsi/target/delete':
+ task.metadata = {
+ target_iqn: target_iqn
+ };
+ break;
+ default:
+ task.metadata = {
+ target_iqn: target_iqn
+ };
+ break;
+ }
+ summaryService.addRunningTask(task);
+ };
+
+ const expectTargetTasks = (target: any, executing: string) => {
+ expect(target.cdExecuting).toEqual(executing);
+ };
+
+ beforeEach(() => {
+ targets = [];
+ addTarget('iqn.a');
+ addTarget('iqn.b');
+ addTarget('iqn.c');
+
+ component.targets = targets;
+ refresh({ executing_tasks: [], finished_tasks: [] });
+ spyOn(iscsiService, 'listTargets').and.callFake(() => of(targets));
+ fixture.detectChanges();
+ });
+
+ it('should gets all targets without tasks', () => {
+ expect(component.targets.length).toBe(3);
+ expect(component.targets.every((target) => !target.cdExecuting)).toBeTruthy();
+ });
+
+ it('should add a new target from a task', () => {
+ addTask('iscsi/target/create', 'iqn.d');
+ expect(component.targets.length).toBe(4);
+ expectTargetTasks(component.targets[0], undefined);
+ expectTargetTasks(component.targets[1], undefined);
+ expectTargetTasks(component.targets[2], undefined);
+ expectTargetTasks(component.targets[3], 'Creating');
+ });
+
+ it('should show when an existing target is being modified', () => {
+ addTask('iscsi/target/delete', 'iqn.b');
+ expect(component.targets.length).toBe(3);
+ expectTargetTasks(component.targets[1], 'Deleting');
+ });
+ });
+
+ describe('show action buttons and drop down actions depending on permissions', () => {
+ let tableActions: TableActionsComponent;
+ let scenario: { fn; empty; single };
+ let permissionHelper: PermissionHelper;
+
+ const getTableActionComponent = (): TableActionsComponent => {
+ fixture.detectChanges();
+ return fixture.debugElement.query(By.directive(TableActionsComponent)).componentInstance;
+ };
+
+ beforeEach(() => {
+ permissionHelper = new PermissionHelper(component.permissions.iscsi, () =>
+ getTableActionComponent()
+ );
+ scenario = {
+ fn: () => tableActions.getCurrentButton().name,
+ single: 'Delete',
+ empty: 'Add'
+ };
+ });
+
+ describe('with all', () => {
+ beforeEach(() => {
+ tableActions = permissionHelper.setPermissionsAndGetActions(1, 1, 1);
+ });
+
+ it(`shows 'Delete' for single selection else 'Add' as main action`, () =>
+ permissionHelper.testScenarios(scenario));
+
+ it('shows all actions', () => {
+ expect(tableActions.tableActions.length).toBe(2);
+ expect(tableActions.tableActions).toEqual(component.tableActions);
+ });
+ });
+
+ describe('with read, create and update', () => {
+ beforeEach(() => {
+ tableActions = permissionHelper.setPermissionsAndGetActions(1, 1, 0);
+ scenario.single = 'Add';
+ });
+
+ it(`should always show 'Add'`, () => {
+ permissionHelper.testScenarios(scenario);
+ });
+
+ it(`shows all actions except for 'Delete'`, () => {
+ expect(tableActions.tableActions.length).toBe(1);
+ component.tableActions.pop();
+ expect(tableActions.tableActions).toEqual(component.tableActions);
+ });
+ });
+
+ describe('with read, create and delete', () => {
+ beforeEach(() => {
+ tableActions = permissionHelper.setPermissionsAndGetActions(1, 0, 1);
+ });
+
+ it(`shows 'Delete' for single selection else 'Add' as main action`, () => {
+ scenario.single = 'Delete';
+ permissionHelper.testScenarios(scenario);
+ });
+
+ it(`shows 'Add' and 'Delete' actions`, () => {
+ expect(tableActions.tableActions.length).toBe(2);
+ expect(tableActions.tableActions).toEqual([
+ component.tableActions[0],
+ component.tableActions[1]
+ ]);
+ });
+ });
+
+ describe('with read, edit and delete', () => {
+ beforeEach(() => {
+ tableActions = permissionHelper.setPermissionsAndGetActions(0, 1, 1);
+ });
+
+ it(`shows always 'Delete' as main action`, () => {
+ scenario.empty = 'Delete';
+ permissionHelper.testScenarios(scenario);
+ });
+
+ it(`shows 'Delete' action`, () => {
+ expect(tableActions.tableActions.length).toBe(1);
+ expect(tableActions.tableActions).toEqual([component.tableActions[1]]);
+ });
+ });
+
+ describe('with read and create', () => {
+ beforeEach(() => {
+ tableActions = permissionHelper.setPermissionsAndGetActions(1, 0, 0);
+ });
+
+ it(`shows 'Add' for single selection and 'Add' as main action`, () => {
+ scenario.single = 'Add';
+ permissionHelper.testScenarios(scenario);
+ });
+
+ it(`shows 'Add' actions`, () => {
+ expect(tableActions.tableActions.length).toBe(1);
+ expect(tableActions.tableActions).toEqual([component.tableActions[0]]);
+ });
+ });
+
+ describe('with read and edit', () => {
+ beforeEach(() => {
+ tableActions = permissionHelper.setPermissionsAndGetActions(0, 1, 0);
+ });
+
+ it(`shows no actions`, () => {
+ expect(tableActions.tableActions.length).toBe(0);
+ expect(tableActions.tableActions).toEqual([]);
+ });
+ });
+
+ describe('with read and delete', () => {
+ beforeEach(() => {
+ tableActions = permissionHelper.setPermissionsAndGetActions(0, 0, 1);
+ });
+
+ it(`shows always 'Delete' as main action`, () => {
+ scenario.single = 'Delete';
+ scenario.empty = 'Delete';
+ permissionHelper.testScenarios(scenario);
+ });
+
+ it(`shows 'Delete' actions`, () => {
+ expect(tableActions.tableActions.length).toBe(1);
+ expect(tableActions.tableActions).toEqual([component.tableActions[1]]);
+ });
+ });
+
+ describe('with only read', () => {
+ beforeEach(() => {
+ tableActions = permissionHelper.setPermissionsAndGetActions(0, 0, 0);
+ });
+
+ it('shows no main action', () => {
+ permissionHelper.testScenarios({
+ fn: () => tableActions.getCurrentButton(),
+ single: undefined,
+ empty: undefined
+ });
+ });
+
+ it('shows no actions', () => {
+ expect(tableActions.tableActions.length).toBe(0);
+ expect(tableActions.tableActions).toEqual([]);
+ });
+ });
+ });
});
import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { I18n } from '@ngx-translate/i18n-polyfill';
+import { BsModalRef, BsModalService } from 'ngx-bootstrap/modal';
import { Subscription } from 'rxjs';
import { IscsiService } from '../../../shared/api/iscsi.service';
+import { CriticalConfirmationModalComponent } from '../../../shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
import { TableComponent } from '../../../shared/datatable/table/table.component';
import { CellTemplate } from '../../../shared/enum/cell-template.enum';
+import { CdTableAction } from '../../../shared/models/cd-table-action';
import { CdTableColumn } from '../../../shared/models/cd-table-column';
import { CdTableSelection } from '../../../shared/models/cd-table-selection';
+import { FinishedTask } from '../../../shared/models/finished-task';
import { Permissions } from '../../../shared/models/permissions';
import { CephReleaseNamePipe } from '../../../shared/pipes/ceph-release-name.pipe';
import { AuthStorageService } from '../../../shared/services/auth-storage.service';
import { SummaryService } from '../../../shared/services/summary.service';
import { TaskListService } from '../../../shared/services/task-list.service';
+import { TaskWrapperService } from '../../../shared/services/task-wrapper.service';
@Component({
selector: 'cd-iscsi-target-list',
tableActions: CdTableAction[];
targets = [];
+ builders = {
+ 'iscsi/target/create': (metadata) => {
+ return {
+ target_iqn: metadata['target_iqn']
+ };
+ }
+ };
+
constructor(
private authStorageService: AuthStorageService,
private i18n: I18n,
private iscsiService: IscsiService,
private taskListService: TaskListService,
private cephReleaseNamePipe: CephReleaseNamePipe,
- private summaryservice: SummaryService
+ private summaryservice: SummaryService,
+ private modalService: BsModalService,
+ private taskWrapper: TaskWrapperService
) {
this.permissions = this.authStorageService.getPermissions();
+
+ this.tableActions = [
+ {
+ permission: 'create',
+ icon: 'fa-plus',
+ routerLink: () => '/block/iscsi/targets/add',
+ name: this.i18n('Add')
+ },
+ {
+ permission: 'delete',
+ icon: 'fa-times',
+ click: () => this.deleteIscsiTargetModal(),
+ name: this.i18n('Delete')
+ }
+ ];
}
ngOnInit() {
(resp) => this.prepareResponse(resp),
(targets) => (this.targets = targets),
() => this.onFetchError(),
- () => false,
- () => false,
- undefined
+ this.taskFilter,
+ this.itemFilter,
+ this.builders
);
this.iscsiService.settings().subscribe((settings: any) => {
this.table.reset(); // Disable loading indicator.
}
+ itemFilter(entry, task) {
+ return entry.target_iqn === task.metadata['target_iqn'];
+ }
+
+ taskFilter(task) {
+ return ['iscsi/target/create', 'iscsi/target/delete'].includes(task.name);
+ }
+
updateSelection(selection: CdTableSelection) {
this.selection = selection;
}
+
+ deleteIscsiTargetModal() {
+ const target_iqn = this.selection.first().target_iqn;
+
+ this.modalRef = this.modalService.show(CriticalConfirmationModalComponent, {
+ initialState: {
+ itemDescription: this.i18n('iSCSI'),
+ submitActionObservable: () =>
+ this.taskWrapper.wrapTaskAroundCall({
+ task: new FinishedTask('iscsi/target/delete', {
+ target_iqn: target_iqn
+ }),
+ call: this.iscsiService.deleteTarget(target_iqn)
+ })
+ }
+ });
+ }
}
<context context-type="sourcefile">app/core/navigation/navigation/navigation.component.html</context>
<context context-type="linenumber">128</context>
</context-group>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/block/iscsi-target-form/iscsi-target-form.component.html</context>
+ <context context-type="linenumber">120</context>
+ </context-group>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/block/iscsi-target-form/iscsi-target-form.component.html</context>
+ <context context-type="linenumber">341</context>
+ </context-group>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/block/iscsi-target-form/iscsi-target-form.component.html</context>
+ <context context-type="linenumber">479</context>
+ </context-group>
<context-group purpose="location">
<context context-type="sourcefile">app/ceph/block/iscsi/iscsi.component.html</context>
- <context context-type="linenumber">7</context>
+ <context context-type="linenumber">9</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">app/ceph/block/mirroring/overview/overview.component.html</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">app/ceph/block/iscsi/iscsi.component.html</context>
- <context context-type="linenumber">1</context>
+ <context context-type="linenumber">3</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">app/ceph/block/mirroring/overview/overview.component.html</context>
<context context-type="sourcefile">app/shared/components/error-panel/error-panel.component.html</context>
<context context-type="linenumber">27</context>
</context-group>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/block/iscsi-target-form/iscsi-target-form.component.html</context>
+ <context context-type="linenumber">544</context>
+ </context-group>
<context-group purpose="location">
<context context-type="sourcefile">app/ceph/block/rbd-form/rbd-form.component.html</context>
<context context-type="linenumber">303</context>
<context context-type="linenumber">118</context>
</context-group>
<note priority="1" from="description">X total</note>
- </trans-unit><trans-unit id="beb1391a148bd032914f49a2665dcc414a5d56d0" datatype="html">
- <source>{VAR_SELECT, select, editing {Edit} cloning {Clone} copying {Copy} other {Add} }</source>
+ </trans-unit><trans-unit id="121cc5391cd2a5115bc2b3160379ee5b36cd7716" datatype="html">
+ <source>Settings</source>
<context-group purpose="location">
- <context context-type="sourcefile">app/ceph/block/rbd-form/rbd-form.component.html</context>
- <context context-type="linenumber">10</context>
+ <context context-type="sourcefile">app/ceph/block/iscsi-target-image-settings-modal/iscsi-target-image-settings-modal.component.html</context>
+ <context context-type="linenumber">3</context>
</context-group>
- </trans-unit><trans-unit id="52dd89f49fc440660cbbb3665b88d80f5baa7437" datatype="html">
- <source>{VAR_SELECT, select, cloning {Clone from} copying {Copy from} other {Parent} }</source>
+ </trans-unit><trans-unit id="9e515f954730279c31d5301f02479666d6264e8b" datatype="html">
+ <source>Changing these parameters from their default values is usually not necessary.</source>
<context-group purpose="location">
- <context context-type="sourcefile">app/ceph/block/rbd-form/rbd-form.component.html</context>
- <context context-type="linenumber">20</context>
+ <context context-type="sourcefile">app/ceph/block/iscsi-target-image-settings-modal/iscsi-target-image-settings-modal.component.html</context>
+ <context context-type="linenumber">15</context>
</context-group>
- </trans-unit><trans-unit id="cff1428d10d59d14e45edec3c735a27b5482db59" datatype="html">
- <source>Name</source>
<context-group purpose="location">
- <context context-type="sourcefile">app/ceph/block/rbd-form/rbd-form.component.html</context>
- <context context-type="linenumber">36</context>
+ <context context-type="sourcefile">app/ceph/block/iscsi-target-iqn-settings-modal/iscsi-target-iqn-settings-modal.component.html</context>
+ <context context-type="linenumber">13</context>
</context-group>
+ </trans-unit><trans-unit id="68e710782ccb5398b3acb8844caf0b199da2c3da" datatype="html">
+ <source>Confirm</source>
<context-group purpose="location">
- <context context-type="sourcefile">app/ceph/cluster/configuration/configuration-form/configuration-form.component.html</context>
- <context context-type="linenumber">18</context>
+ <context context-type="sourcefile">app/ceph/block/iscsi-target-image-settings-modal/iscsi-target-image-settings-modal.component.html</context>
+ <context context-type="linenumber">35</context>
</context-group>
<context-group purpose="location">
- <context context-type="sourcefile">app/ceph/pool/erasure-code-profile-form/erasure-code-profile-form.component.html</context>
+ <context context-type="sourcefile">app/ceph/block/iscsi-target-iqn-settings-modal/iscsi-target-iqn-settings-modal.component.html</context>
+ <context context-type="linenumber">53</context>
+ </context-group>
+ </trans-unit><trans-unit id="d7b35c384aecd25a516200d6921836374613dfe7" datatype="html">
+ <source>Cancel</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/block/iscsi-target-image-settings-modal/iscsi-target-image-settings-modal.component.html</context>
+ <context context-type="linenumber">39</context>
+ </context-group>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/block/iscsi-target-iqn-settings-modal/iscsi-target-iqn-settings-modal.component.html</context>
+ <context context-type="linenumber">57</context>
+ </context-group>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component.html</context>
+ <context context-type="linenumber">38</context>
+ </context-group>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/shared/components/confirmation-modal/confirmation-modal.component.html</context>
<context context-type="linenumber">21</context>
</context-group>
<context-group purpose="location">
- <context context-type="sourcefile">app/ceph/pool/pool-form/pool-form.component.html</context>
- <context context-type="linenumber">26</context>
+ <context context-type="sourcefile">app/ceph/cluster/osd/osd-flags-modal/osd-flags-modal.component.html</context>
+ <context context-type="linenumber">38</context>
</context-group>
<context-group purpose="location">
- <context context-type="sourcefile">app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.html</context>
- <context context-type="linenumber">42</context>
+ <context context-type="sourcefile">app/ceph/cluster/osd/osd-recv-speed-modal/osd-recv-speed-modal.component.html</context>
+ <context context-type="linenumber">91</context>
</context-group>
<context-group purpose="location">
- <context context-type="sourcefile">app/core/auth/role-form/role-form.component.html</context>
- <context context-type="linenumber">19</context>
+ <context context-type="sourcefile">app/ceph/cluster/osd/osd-reweight-modal/osd-reweight-modal.component.html</context>
+ <context context-type="linenumber">34</context>
</context-group>
<context-group purpose="location">
- <context context-type="sourcefile">app/ceph/rgw/rgw-bucket-details/rgw-bucket-details.component.html</context>
- <context context-type="linenumber">8</context>
+ <context context-type="sourcefile">app/ceph/cluster/osd/osd-scrub-modal/osd-scrub-modal.component.html</context>
+ <context context-type="linenumber">25</context>
</context-group>
<context-group purpose="location">
- <context context-type="sourcefile">app/ceph/cluster/configuration/configuration-details/configuration-details.component.html</context>
- <context context-type="linenumber">8</context>
+ <context context-type="sourcefile">app/ceph/block/rbd-trash-move-modal/rbd-trash-move-modal.component.html</context>
+ <context context-type="linenumber">44</context>
</context-group>
<context-group purpose="location">
- <context context-type="sourcefile">app/ceph/block/rbd-details/rbd-details.component.html</context>
- <context context-type="linenumber">13</context>
+ <context context-type="sourcefile">app/ceph/block/rbd-trash-purge-modal/rbd-trash-purge-modal.component.html</context>
+ <context context-type="linenumber">45</context>
</context-group>
<context-group purpose="location">
- <context context-type="sourcefile">app/ceph/block/rbd-snapshot-form/rbd-snapshot-form.component.html</context>
- <context context-type="linenumber">23</context>
+ <context context-type="sourcefile">app/ceph/block/rbd-trash-restore-modal/rbd-trash-restore-modal.component.html</context>
+ <context context-type="linenumber">44</context>
+ </context-group>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/block/mirroring/pool-edit-mode-modal/pool-edit-mode-modal.component.html</context>
+ <context context-type="linenumber">45</context>
+ </context-group>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/block/mirroring/pool-edit-peer-modal/pool-edit-peer-modal.component.html</context>
+ <context context-type="linenumber">110</context>
+ </context-group>
+ </trans-unit><trans-unit id="339878da255ab55447c43afef8d9b2f9753bf5f6" datatype="html">
+ <source>Advanced Settings</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/block/iscsi-target-iqn-settings-modal/iscsi-target-iqn-settings-modal.component.html</context>
+ <context context-type="linenumber">3</context>
+ </context-group>
+ </trans-unit><trans-unit id="a01e6937a5d1ee040a02416eed34544c4ea61e38" datatype="html">
+ <source>Create target</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/block/iscsi-target-form/iscsi-target-form.component.html</context>
+ <context context-type="linenumber">11</context>
+ </context-group>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/block/iscsi-target-form/iscsi-target-form.component.html</context>
+ <context context-type="linenumber">539</context>
+ </context-group>
+ </trans-unit><trans-unit id="1406c2fb12a20c1528b19bcc5e24a6a2386167f3" datatype="html">
+ <source>Target IQN</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/block/iscsi-target-form/iscsi-target-form.component.html</context>
+ <context context-type="linenumber">20</context>
</context-group>
</trans-unit><trans-unit id="7cbdabcece469fab89cfa687ab152bca18b97498" datatype="html">
<source>This field is required.</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/block/iscsi-target-form/iscsi-target-form.component.html</context>
+ <context context-type="linenumber">43</context>
+ </context-group>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/block/iscsi-target-form/iscsi-target-form.component.html</context>
+ <context context-type="linenumber">209</context>
+ </context-group>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/block/iscsi-target-form/iscsi-target-form.component.html</context>
+ <context context-type="linenumber">231</context>
+ </context-group>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/block/iscsi-target-form/iscsi-target-form.component.html</context>
+ <context context-type="linenumber">266</context>
+ </context-group>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/block/iscsi-target-form/iscsi-target-form.component.html</context>
+ <context context-type="linenumber">291</context>
+ </context-group>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/block/iscsi-target-form/iscsi-target-form.component.html</context>
+ <context context-type="linenumber">326</context>
+ </context-group>
<context-group purpose="location">
<context context-type="sourcefile">app/ceph/block/rbd-form/rbd-form.component.html</context>
<context context-type="linenumber">49</context>
<context context-type="sourcefile">app/ceph/block/mirroring/pool-edit-peer-modal/pool-edit-peer-modal.component.html</context>
<context context-type="linenumber">58</context>
</context-group>
+ </trans-unit><trans-unit id="5fe42339be910372fa689f559155631862d218e8" datatype="html">
+ <source>IQN has wrong pattern.</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/block/iscsi-target-form/iscsi-target-form.component.html</context>
+ <context context-type="linenumber">47</context>
+ </context-group>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/block/iscsi-target-form/iscsi-target-form.component.html</context>
+ <context context-type="linenumber">213</context>
+ </context-group>
+ </trans-unit><trans-unit id="47d1bfe4f5b3f292e1202dfe691195b10cb99500" datatype="html">
+ <source>An IQN has the following notation 'iqn.$year-$month.$reversedAddress:$definedName'</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/block/iscsi-target-form/iscsi-target-form.component.html</context>
+ <context context-type="linenumber">51</context>
+ </context-group>
+ </trans-unit><trans-unit id="c8ada4b53396d8366db00a435acc61d53d857047" datatype="html">
+ <source>For example: iqn.2016-06.org.dashboard:storage:disk.sn-a8675309</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/block/iscsi-target-form/iscsi-target-form.component.html</context>
+ <context context-type="linenumber">53</context>
+ </context-group>
+ </trans-unit><trans-unit id="e60c11e1b1dfbbeda577364b8de39ded2d796c5e" datatype="html">
+ <source>More information</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/block/iscsi-target-form/iscsi-target-form.component.html</context>
+ <context context-type="linenumber">57</context>
+ </context-group>
+ </trans-unit><trans-unit id="9b1aa85dfc6849196e64060db02c5410de69b7a1" datatype="html">
+ <source>This target has modified advanced settings.</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/block/iscsi-target-form/iscsi-target-form.component.html</context>
+ <context context-type="linenumber">62</context>
+ </context-group>
+ </trans-unit><trans-unit id="6990ad8d6182662e864495ac31c3758cda1c7a28" datatype="html">
+ <source>Portals</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/block/iscsi-target-form/iscsi-target-form.component.html</context>
+ <context context-type="linenumber">72</context>
+ </context-group>
+ </trans-unit><trans-unit id="c3638c01b6c34066438909713ec96087c813fc7e" datatype="html">
+ <source>At least <x id="INTERPOLATION" equiv-text="{{ minimum_gateways }}"/> gateways are required.</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/block/iscsi-target-form/iscsi-target-form.component.html</context>
+ <context context-type="linenumber">96</context>
+ </context-group>
+ </trans-unit><trans-unit id="6a3ac2b4137d723fd9878cd357c2012ff6c07973" datatype="html">
+ <source>Add portal</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/block/iscsi-target-form/iscsi-target-form.component.html</context>
+ <context context-type="linenumber">106</context>
+ </context-group>
+ </trans-unit><trans-unit id="e3484cae8b118c576ca2815bf9c9406c2eb2cae3" datatype="html">
+ <source>This image has modified settings.</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/block/iscsi-target-form/iscsi-target-form.component.html</context>
+ <context context-type="linenumber">146</context>
+ </context-group>
+ </trans-unit><trans-unit id="107c84e820909b44fe258673938a68ced1bbff72" datatype="html">
+ <source>At least 1 image is required.</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/block/iscsi-target-form/iscsi-target-form.component.html</context>
+ <context context-type="linenumber">151</context>
+ </context-group>
+ </trans-unit><trans-unit id="808038f912fdc7f0e03f82d4afd3bf9178527fc8" datatype="html">
+ <source>Add image</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/block/iscsi-target-form/iscsi-target-form.component.html</context>
+ <context context-type="linenumber">161</context>
+ </context-group>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/block/iscsi-target-form/iscsi-target-form.component.html</context>
+ <context context-type="linenumber">371</context>
+ </context-group>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/block/iscsi-target-form/iscsi-target-form.component.html</context>
+ <context context-type="linenumber">506</context>
+ </context-group>
+ </trans-unit><trans-unit id="f494bd31f095f6dcc656ce87ec2dcf07a2e9b30c" datatype="html">
+ <source>Initiators</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/block/iscsi-target-form/iscsi-target-form.component.html</context>
+ <context context-type="linenumber">174</context>
+ </context-group>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/block/iscsi-target-form/iscsi-target-form.component.html</context>
+ <context context-type="linenumber">437</context>
+ </context-group>
+ </trans-unit><trans-unit id="e98239d8a6be1100119ff4b5630c822b82786740" datatype="html">
+ <source>Initiator</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/block/iscsi-target-form/iscsi-target-form.component.html</context>
+ <context context-type="linenumber">181</context>
+ </context-group>
+ </trans-unit><trans-unit id="f2c5059d8cda15d8d03e2cce30f2d139623d9a91" datatype="html">
+ <source>Client IQN</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/block/iscsi-target-form/iscsi-target-form.component.html</context>
+ <context context-type="linenumber">194</context>
+ </context-group>
+ </trans-unit><trans-unit id="107d5aabce23d900f0a80e6ddc1c10e29aa0bed8" datatype="html">
+ <source>Initiator IQN needs to be unique.</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/block/iscsi-target-form/iscsi-target-form.component.html</context>
+ <context context-type="linenumber">205</context>
+ </context-group>
+ </trans-unit><trans-unit id="e08a77594f3d89311cdf6da5090044270909c194" datatype="html">
+ <source>User</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/block/iscsi-target-form/iscsi-target-form.component.html</context>
+ <context context-type="linenumber">223</context>
+ </context-group>
+ </trans-unit><trans-unit id="bbf0b34a3fcc80800fcb44b9e1e86931a530dfe3" datatype="html">
+ <source>Usernames must have a length of 8 to 64 characters and
+ can only contain letters, '.', '@', '-', '_' or ':'.</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/block/iscsi-target-form/iscsi-target-form.component.html</context>
+ <context context-type="linenumber">235</context>
+ </context-group>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/block/iscsi-target-form/iscsi-target-form.component.html</context>
+ <context context-type="linenumber">295</context>
+ </context-group>
+ </trans-unit><trans-unit id="c32ef07f8803a223a83ed17024b38e8d82292407" datatype="html">
+ <source>Password</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/block/iscsi-target-form/iscsi-target-form.component.html</context>
+ <context context-type="linenumber">245</context>
+ </context-group>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/core/auth/user-form/user-form.component.html</context>
+ <context context-type="linenumber">42</context>
+ </context-group>
+ </trans-unit><trans-unit id="4b2dd8635fba00476da25977e0884969821e62da" datatype="html">
+ <source>Passwords must have a length of 12 to 16 characters
+ and can only contain letters, '@', '-' or '_'.</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/block/iscsi-target-form/iscsi-target-form.component.html</context>
+ <context context-type="linenumber">270</context>
+ </context-group>
+ </trans-unit><trans-unit id="ff40391de7a1944ea95091e4045cc34c4979b736" datatype="html">
+ <source>Mutual User</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/block/iscsi-target-form/iscsi-target-form.component.html</context>
+ <context context-type="linenumber">281</context>
+ </context-group>
+ </trans-unit><trans-unit id="0cf73dbebe99b737c4d288788182fc356e3c93d3" datatype="html">
+ <source>Mutual Password</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/block/iscsi-target-form/iscsi-target-form.component.html</context>
+ <context context-type="linenumber">305</context>
+ </context-group>
+ </trans-unit><trans-unit id="c58e136a292acf8ebccfa6d777fdff9f392b6ee2" datatype="html">
+ <source>Passwords must have a length of 12 to 16 characters and
+ can only contain letters, '@', '-' or '_'.</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/block/iscsi-target-form/iscsi-target-form.component.html</context>
+ <context context-type="linenumber">330</context>
+ </context-group>
+ </trans-unit><trans-unit id="5d1878d5fc761cbe9614bfd87047a740c82a6951" datatype="html">
+ <source>Initiator belongs to a group. Images will be configure in the group.</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/block/iscsi-target-form/iscsi-target-form.component.html</context>
+ <context context-type="linenumber">361</context>
+ </context-group>
+ </trans-unit><trans-unit id="c0de67b9d97fafbf200f9451e8388ee8128a56ac" datatype="html">
+ <source>No items added.</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/block/iscsi-target-form/iscsi-target-form.component.html</context>
+ <context context-type="linenumber">384</context>
+ </context-group>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/block/iscsi-target-form/iscsi-target-form.component.html</context>
+ <context context-type="linenumber">521</context>
+ </context-group>
+ </trans-unit><trans-unit id="d565e47726158e428ecdc952fc9233b9b7d7f049" datatype="html">
+ <source>Add initiator</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/block/iscsi-target-form/iscsi-target-form.component.html</context>
+ <context context-type="linenumber">389</context>
+ </context-group>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/block/iscsi-target-form/iscsi-target-form.component.html</context>
+ <context context-type="linenumber">465</context>
+ </context-group>
+ </trans-unit><trans-unit id="c22ba03540aa3217da059f45e7eab138b51a96e2" datatype="html">
+ <source>Groups</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/block/iscsi-target-form/iscsi-target-form.component.html</context>
+ <context context-type="linenumber">403</context>
+ </context-group>
+ </trans-unit><trans-unit id="4c90059afafb7e160384d9f512797c95bb95c6dc" datatype="html">
+ <source>Group</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/block/iscsi-target-form/iscsi-target-form.component.html</context>
+ <context context-type="linenumber">410</context>
+ </context-group>
+ </trans-unit><trans-unit id="cff1428d10d59d14e45edec3c735a27b5482db59" datatype="html">
+ <source>Name</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/block/iscsi-target-form/iscsi-target-form.component.html</context>
+ <context context-type="linenumber">422</context>
+ </context-group>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/block/rbd-form/rbd-form.component.html</context>
+ <context context-type="linenumber">36</context>
+ </context-group>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/cluster/configuration/configuration-form/configuration-form.component.html</context>
+ <context context-type="linenumber">18</context>
+ </context-group>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/pool/erasure-code-profile-form/erasure-code-profile-form.component.html</context>
+ <context context-type="linenumber">21</context>
+ </context-group>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/pool/pool-form/pool-form.component.html</context>
+ <context context-type="linenumber">26</context>
+ </context-group>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.html</context>
+ <context context-type="linenumber">42</context>
+ </context-group>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/core/auth/role-form/role-form.component.html</context>
+ <context context-type="linenumber">19</context>
+ </context-group>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/rgw/rgw-bucket-details/rgw-bucket-details.component.html</context>
+ <context context-type="linenumber">8</context>
+ </context-group>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/cluster/configuration/configuration-details/configuration-details.component.html</context>
+ <context context-type="linenumber">8</context>
+ </context-group>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/block/rbd-details/rbd-details.component.html</context>
+ <context context-type="linenumber">13</context>
+ </context-group>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/block/rbd-snapshot-form/rbd-snapshot-form.component.html</context>
+ <context context-type="linenumber">23</context>
+ </context-group>
+ </trans-unit><trans-unit id="3084948274cff4f56d0f431af47240e9cf02fcc7" datatype="html">
+ <source>Add group</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/block/iscsi-target-form/iscsi-target-form.component.html</context>
+ <context context-type="linenumber">526</context>
+ </context-group>
+ </trans-unit><trans-unit id="54839ebc827b73c9dc4e1df731c6d36a85036af7" datatype="html">
+ <source>Are you sure that you want to <x id="INTERPOLATION" equiv-text="{{ actionDescription | lowercase }}"/> the selected <x id="INTERPOLATION_1" equiv-text="{{ itemDescription }}"/>?</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component.html</context>
+ <context context-type="linenumber">15</context>
+ </context-group>
+ </trans-unit><trans-unit id="2be8b7f04f0104d3fad90d079d8202b74f758b9a" datatype="html">
+ <source>Yes, I am sure.</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component.html</context>
+ <context context-type="linenumber">25</context>
+ </context-group>
+ </trans-unit><trans-unit id="53a583cd5f15059cc958b7d547f72cc78f68e123" datatype="html">
+ <source>Please consult the <x id="START_LINK" ctype="x-a" equiv-text="<a>"/>documentation<x id="CLOSE_LINK" ctype="x-a" equiv-text="</a>"/>
+ on how to configure and enable the iSCSI Targets management functionality.</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/block/iscsi-target-list/iscsi-target-list.component.html</context>
+ <context context-type="linenumber">6</context>
+ </context-group>
+ </trans-unit><trans-unit id="3b301d0044f62c92af0da53d7aaca52a436a547d" datatype="html">
+ <source>Available information:</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/block/iscsi-target-list/iscsi-target-list.component.html</context>
+ <context context-type="linenumber">12</context>
+ </context-group>
+ </trans-unit><trans-unit id="332227f088a4877b3c11f5fb3ae8bc812c470fae" datatype="html">
+ <source>iSCSI Targets not available</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/block/iscsi-target-list/iscsi-target-list.component.html</context>
+ <context context-type="linenumber">4</context>
+ </context-group>
+ </trans-unit><trans-unit id="beb1391a148bd032914f49a2665dcc414a5d56d0" datatype="html">
+ <source>{VAR_SELECT, select, editing {Edit} cloning {Clone} copying {Copy} other {Add} }</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/block/rbd-form/rbd-form.component.html</context>
+ <context context-type="linenumber">10</context>
+ </context-group>
+ </trans-unit><trans-unit id="52dd89f49fc440660cbbb3665b88d80f5baa7437" datatype="html">
+ <source>{VAR_SELECT, select, cloning {Clone from} copying {Copy from} other {Parent} }</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/block/rbd-form/rbd-form.component.html</context>
+ <context context-type="linenumber">20</context>
+ </context-group>
</trans-unit><trans-unit id="94516fa213706c67ce5a5b5765681d7fb032033a" datatype="html">
<source>Loading...</source>
<context-group purpose="location">
<context context-type="sourcefile">app/ceph/cluster/monitor/monitor.component.html</context>
<context context-type="linenumber">54</context>
</context-group>
- </trans-unit><trans-unit id="d7b35c384aecd25a516200d6921836374613dfe7" datatype="html">
- <source>Cancel</source>
- <context-group purpose="location">
- <context context-type="sourcefile">app/shared/components/confirmation-modal/confirmation-modal.component.html</context>
- <context context-type="linenumber">21</context>
- </context-group>
- <context-group purpose="location">
- <context context-type="sourcefile">app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component.html</context>
- <context context-type="linenumber">38</context>
- </context-group>
- <context-group purpose="location">
- <context context-type="sourcefile">app/ceph/cluster/osd/osd-flags-modal/osd-flags-modal.component.html</context>
- <context context-type="linenumber">38</context>
- </context-group>
- <context-group purpose="location">
- <context context-type="sourcefile">app/ceph/cluster/osd/osd-recv-speed-modal/osd-recv-speed-modal.component.html</context>
- <context context-type="linenumber">91</context>
- </context-group>
- <context-group purpose="location">
- <context context-type="sourcefile">app/ceph/cluster/osd/osd-reweight-modal/osd-reweight-modal.component.html</context>
- <context context-type="linenumber">34</context>
- </context-group>
- <context-group purpose="location">
- <context context-type="sourcefile">app/ceph/cluster/osd/osd-scrub-modal/osd-scrub-modal.component.html</context>
- <context context-type="linenumber">25</context>
- </context-group>
- <context-group purpose="location">
- <context context-type="sourcefile">app/ceph/block/rbd-trash-move-modal/rbd-trash-move-modal.component.html</context>
- <context context-type="linenumber">44</context>
- </context-group>
- <context-group purpose="location">
- <context context-type="sourcefile">app/ceph/block/rbd-trash-purge-modal/rbd-trash-purge-modal.component.html</context>
- <context context-type="linenumber">45</context>
- </context-group>
- <context-group purpose="location">
- <context context-type="sourcefile">app/ceph/block/rbd-trash-restore-modal/rbd-trash-restore-modal.component.html</context>
- <context context-type="linenumber">44</context>
- </context-group>
- <context-group purpose="location">
- <context context-type="sourcefile">app/ceph/block/mirroring/pool-edit-mode-modal/pool-edit-mode-modal.component.html</context>
- <context context-type="linenumber">45</context>
- </context-group>
- <context-group purpose="location">
- <context context-type="sourcefile">app/ceph/block/mirroring/pool-edit-peer-modal/pool-edit-peer-modal.component.html</context>
- <context context-type="linenumber">110</context>
- </context-group>
- </trans-unit><trans-unit id="54839ebc827b73c9dc4e1df731c6d36a85036af7" datatype="html">
- <source>Are you sure that you want to <x id="INTERPOLATION" equiv-text="{{ actionDescription | lowercase }}"/> the selected <x id="INTERPOLATION_1" equiv-text="{{ itemDescription }}"/>?</source>
- <context-group purpose="location">
- <context context-type="sourcefile">app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component.html</context>
- <context context-type="linenumber">15</context>
- </context-group>
- </trans-unit><trans-unit id="2be8b7f04f0104d3fad90d079d8202b74f758b9a" datatype="html">
- <source>Yes, I am sure.</source>
- <context-group purpose="location">
- <context context-type="sourcefile">app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component.html</context>
- <context context-type="linenumber">25</context>
- </context-group>
</trans-unit><trans-unit id="5ef50ba2514414f799d4c8fc36067a251904ba81" datatype="html">
<source>Cluster-wide OSD Flags</source>
<context-group purpose="location">
<context context-type="sourcefile">app/core/auth/user-form/user-form.component.html</context>
<context context-type="linenumber">10</context>
</context-group>
- </trans-unit><trans-unit id="c32ef07f8803a223a83ed17024b38e8d82292407" datatype="html">
- <source>Password</source>
- <context-group purpose="location">
- <context context-type="sourcefile">app/core/auth/user-form/user-form.component.html</context>
- <context context-type="linenumber">42</context>
- </context-group>
</trans-unit><trans-unit id="7f3bdcce4b2e8c37cd7f0f6c92ef8cff34b039b8" datatype="html">
<source>Confirm password</source>
<context-group purpose="location">
<context context-type="sourcefile">app/ceph/block/rbd-trash-move-modal/rbd-trash-move-modal.component.html</context>
<context context-type="linenumber">40</context>
</context-group>
+ </trans-unit><trans-unit id="e95ae009d0bdb45fcc656e8b65248cf7396080d5" datatype="html">
+ <source>Overview</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/block/iscsi-tabs/iscsi-tabs.component.html</context>
+ <context context-type="linenumber">2</context>
+ </context-group>
+ </trans-unit><trans-unit id="bbd2045d5c37e4bb39a3c0fb62ea1ddf70a12838" datatype="html">
+ <source>Targets</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/block/iscsi-tabs/iscsi-tabs.component.html</context>
+ <context context-type="linenumber">7</context>
+ </context-group>
</trans-unit><trans-unit id="0f6e8f6094b180eaf1f11bc0ffe383f1cdcd059e" datatype="html">
<source>Only available for RBD images with <x id="START_TAG_STRONG" ctype="x-strong" equiv-text="<strong>"/>fast-diff<x id="CLOSE_TAG_STRONG" ctype="x-strong" equiv-text="</strong>"/> enabled</source>
<context-group purpose="location">
<context context-type="sourcefile">app/ceph/block/rbd-trash-list/rbd-trash-list.component.html</context>
<context context-type="linenumber">47</context>
</context-group>
+ </trans-unit><trans-unit id="a674ab267d1934bf395f87ca1503fd474296893f" datatype="html">
+ <source>iSCSI Topology</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">app/ceph/block/iscsi-target-details/iscsi-target-details.component.html</context>
+ <context context-type="linenumber">2</context>
+ </context-group>
</trans-unit><trans-unit id="66db799d67958d4b0765181d072df62e2d1c16f5" datatype="html">
<source>Issues</source>
<context-group purpose="location">
<context context-type="linenumber">36</context>
</context-group>
</trans-unit>
+ <trans-unit id="bd5a3b1c5a3c185c7bbb0e09a061d4cdc88ce5ad" datatype="html">
+ <source>Current</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">src/app/ceph/block/iscsi-target-details/iscsi-target-details.component.ts</context>
+ <context context-type="linenumber">1</context>
+ </context-group>
+ </trans-unit>
+ <trans-unit id="dd4ab758afd5fd5a6c6a25b2b30ff99d0c00e9ad" datatype="html">
+ <source>There are no portals available.</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">src/app/ceph/block/iscsi-target-form/iscsi-target-form.component.ts</context>
+ <context context-type="linenumber">1</context>
+ </context-group>
+ </trans-unit>
+ <trans-unit id="83021e45778a4a230a14ca0c6d6ccdf05500ad93" datatype="html">
+ <source>There are no images available.</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">src/app/ceph/block/iscsi-target-form/iscsi-target-form.component.ts</context>
+ <context context-type="linenumber">1</context>
+ </context-group>
+ </trans-unit>
+ <trans-unit id="86684eb21f8a768e1dbc659e3d5da6861db544a0" datatype="html">
+ <source>There are no images available. Please make sure you add an image to the target.</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">src/app/ceph/block/iscsi-target-form/iscsi-target-form.component.ts</context>
+ <context context-type="linenumber">1</context>
+ </context-group>
+ </trans-unit>
+ <trans-unit id="fffda6e440078f57eba93944ce051c593cc6ed7f" datatype="html">
+ <source>There are no initiators available. Please make sure you add an initiator to the target.</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">src/app/ceph/block/iscsi-target-form/iscsi-target-form.component.ts</context>
+ <context context-type="linenumber">1</context>
+ </context-group>
+ </trans-unit>
+ <trans-unit id="38baeb215c17af9d9e295e371a57f4a48ab4c191" datatype="html">
+ <source>Target</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">src/app/ceph/block/iscsi-target-list/iscsi-target-list.component.ts</context>
+ <context context-type="linenumber">1</context>
+ </context-group>
+ </trans-unit>
<trans-unit id="9a541ec1a4319fffc16ad3b3ab2c2b6d251a829d" datatype="html">
<source>Hostname</source>
<context-group purpose="location">
<context context-type="linenumber">1</context>
</context-group>
</trans-unit>
- <trans-unit id="5c2114c69efd7a972c62db45f99b8b7550a5b673" datatype="html">
- <source>There are no items.</source>
+ <trans-unit id="37391297bb077a6f84484930261b01a3ce38327b" datatype="html">
+ <source>No items selected.</source>
<context-group purpose="location">
- <context context-type="sourcefile">src/app/shared/components/select-badges/select-badges-messages.model.ts</context>
+ <context context-type="sourcefile">src/app/shared/components/select/select-messages.model.ts</context>
<context context-type="linenumber">1</context>
</context-group>
</trans-unit>
<trans-unit id="2c1e52ee832661b4a0f570877d24661736b16af1" datatype="html">
<source>Deselect item to select again</source>
<context-group purpose="location">
- <context context-type="sourcefile">src/app/shared/components/select-badges/select-badges-messages.model.ts</context>
+ <context context-type="sourcefile">src/app/shared/components/select/select-messages.model.ts</context>
<context context-type="linenumber">1</context>
</context-group>
</trans-unit>
<trans-unit id="c8c9c6e5918659336824bbdda3501c66eaa79a4c" datatype="html">
<source>Selection limit reached</source>
<context-group purpose="location">
- <context context-type="sourcefile">src/app/shared/components/select-badges/select-badges-messages.model.ts</context>
+ <context context-type="sourcefile">src/app/shared/components/select/select-messages.model.ts</context>
<context context-type="linenumber">1</context>
</context-group>
</trans-unit>
<trans-unit id="02d184c288f567825a1fcbf83bcd3099a10853d5" datatype="html">
<source>Filter tags</source>
<context-group purpose="location">
- <context context-type="sourcefile">src/app/shared/components/select-badges/select-badges-messages.model.ts</context>
+ <context context-type="sourcefile">src/app/shared/components/select/select-messages.model.ts</context>
<context context-type="linenumber">1</context>
</context-group>
</trans-unit>
<trans-unit id="aa00748e49c269956837d6f3acdd8d218796a8d8" datatype="html">
<source>Add badge</source>
<context-group purpose="location">
- <context context-type="sourcefile">src/app/shared/components/select-badges/select-badges-messages.model.ts</context>
+ <context context-type="sourcefile">src/app/shared/components/select/select-messages.model.ts</context>
+ <context context-type="linenumber">1</context>
+ </context-group>
+ </trans-unit>
+ <trans-unit id="4078a92d8121abdce7d8f346a88914923ec835fc" datatype="html">
+ <source>There are no items available.</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">src/app/shared/components/select/select-messages.model.ts</context>
<context context-type="linenumber">1</context>
</context-group>
</trans-unit>