import { Component, OnInit } from '@angular/core';
import { FormArray, FormControl, Validators } from '@angular/forms';
import { ActivatedRoute, 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 { ActionLabelsI18n } from '../../../shared/constants/app.constants';
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 implements OnInit {
targetForm: CdFormGroup;
modalRef: BsModalRef;
minimum_gateways = 1;
target_default_controls: any;
disk_default_controls: any;
backstores: string[];
default_backstore: string;
supported_rbd_features: any;
required_rbd_features: any;
isEdit = false;
target_iqn: string;
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}/;
action: string;
resource: string;
constructor(
private iscsiService: IscsiService,
private modalService: BsModalService,
private rbdService: RbdService,
private router: Router,
private route: ActivatedRoute,
private i18n: I18n,
private taskWrapper: TaskWrapperService,
public actionLabels: ActionLabelsI18n
) {
this.resource = this.i18n('target');
}
ngOnInit() {
const promises: any[] = [
this.iscsiService.listTargets(),
this.rbdService.list(),
this.iscsiService.portals(),
this.iscsiService.settings()
];
if (this.router.url.startsWith('/block/iscsi/targets/edit')) {
this.isEdit = true;
this.route.params.subscribe((params: { target_iqn: string }) => {
this.target_iqn = decodeURIComponent(params.target_iqn);
promises.push(this.iscsiService.getTarget(this.target_iqn));
});
}
this.action = this.isEdit ? this.actionLabels.EDIT : this.actionLabels.CREATE;
forkJoin(promises).subscribe((data: any[]) => {
// iscsiService.listTargets
const usedImages = _(data[0])
.filter((target) => target.target_iqn !== this.target_iqn)
.flatMap((target) => target.disks)
.map((image) => `${image.pool}/${image.image}`)
.value();
// iscsiService.settings()
this.minimum_gateways = data[3].config.minimum_gateways;
this.target_default_controls = data[3].target_default_controls;
this.disk_default_controls = data[3].disk_default_controls;
this.backstores = data[3].backstores;
this.default_backstore = data[3].default_backstore;
this.supported_rbd_features = data[3].supported_rbd_features;
this.required_rbd_features = data[3].required_rbd_features;
// rbdService.list()
this.imagesAll = _(data[1])
.flatMap((pool) => pool.value)
.filter((image) => {
const imageId = `${image.pool_name}/${image.name}`;
if (usedImages.indexOf(imageId) !== -1) {
return false;
}
const validBackstores = this.getValidBackstores(image);
if (validBackstores.length === 0) {
return false;
}
return true;
})
.value();
this.imagesSelections = this.imagesAll.map(
(image) => new SelectOption(false, `${image.pool_name}/${image.name}`, '')
);
// iscsiService.portals()
const portals: SelectOption[] = [];
data[2].forEach((portal) => {
portal.ip_addresses.forEach((ip) => {
portals.push(new SelectOption(false, portal.name + ':' + ip, ''));
});
});
this.portalsSelections = [...portals];
this.createForm();
// iscsiService.getTarget()
if (data[4]) {
this.resolveModel(data[4]);
}
});
}
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([]),
acl_enabled: new FormControl(false)
});
}
resolveModel(res) {
this.targetForm.patchValue({
target_iqn: res.target_iqn,
target_controls: res.target_controls,
acl_enabled: res.acl_enabled
});
const portals = [];
_.forEach(res.portals, (portal) => {
const id = `${portal.host}:${portal.ip}`;
portals.push(id);
});
this.targetForm.patchValue({
portals: portals
});
const disks = [];
_.forEach(res.disks, (disk) => {
const id = `${disk.pool}/${disk.image}`;
disks.push(id);
this.imagesSettings[id] = {
backstore: disk.backstore
};
this.imagesSettings[id][disk.backstore] = disk.controls;
this.onImageSelection({ option: { name: id, selected: true } });
});
this.targetForm.patchValue({
disks: disks
});
_.forEach(res.clients, (client) => {
const initiator = this.addInitiator();
client.luns = _.map(client.luns, (lun) => `${lun.pool}/${lun.image}`);
initiator.patchValue(client);
// updatedInitiatorSelector()
});
_.forEach(res.groups, (group) => {
const fg = this.addGroup();
console.log(group);
group.disks = _.map(group.disks, (disk) => `${disk.pool}/${disk.image}`);
fg.patchValue(group);
_.forEach(group.members, (member) => {
this.onGroupMemberSelection({ option: new SelectOption(true, member, '') });
});
});
}
hasAdvancedSettings(settings: any) {
return Object.values(settings).length > 0;
}
// Portals
get portals() {
return this.targetForm.get('portals') as FormControl;
}
onPortalSelection() {
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);
});
_.forEach(this.imagesInitiatorSelections, (selections, i) => {
this.imagesInitiatorSelections[i] = selections.filter((item: any) => item.name !== name);
});
_.forEach(this.groupDiskSelections, (selections, i) => {
this.groupDiskSelections[i] = selections.filter((item: any) => item.name !== name);
});
}
getDefaultBackstore(imageId) {
let result = this.default_backstore;
const image = this.getImageById(imageId);
if (!this.validFeatures(image, this.default_backstore)) {
this.backstores.forEach((backstore) => {
if (backstore !== this.default_backstore) {
if (this.validFeatures(image, backstore)) {
result = backstore;
}
}
});
}
return result;
}
onImageSelection($event) {
const option = $event.option;
if (option.selected) {
if (!this.imagesSettings[option.name]) {
const defaultBackstore = this.getDefaultBackstore(option.name);
this.imagesSettings[option.name] = {
backstore: defaultBackstore
};
this.imagesSettings[option.name][defaultBackstore] = {};
}
_.forEach(this.imagesInitiatorSelections, (selections, i) => {
selections.push(new SelectOption(false, option.name, ''));
this.imagesInitiatorSelections[i] = [...selections];
});
_.forEach(this.groupDiskSelections, (selections, i) => {
selections.push(new SelectOption(false, option.name, ''));
this.groupDiskSelections[i] = [...selections];
});
} else {
this.removeImageRefs(option.name);
}
}
// 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);
_.forEach(this.groupMembersSelections, (selections, i) => {
selections.push(new SelectOption(false, '', ''));
this.groupMembersSelections[i] = [...selections];
});
const disks = _.map(
this.targetForm.getValue('disks'),
(disk) => new SelectOption(false, disk, '')
);
this.imagesInitiatorSelections.push(disks);
return fg;
}
removeInitiator(index) {
const removed = this.initiators.value[index];
this.initiators.removeAt(index);
_.forEach(this.groupMembersSelections, (selections, i) => {
selections.splice(index, 1);
this.groupMembersSelections[i] = [...selections];
});
this.groups.controls.forEach((element) => {
const newMembers = element.value.members.filter((item) => item !== removed.client_iqn);
element.get('members').setValue(newMembers);
});
this.imagesInitiatorSelections.splice(index, 1);
}
updatedInitiatorSelector() {
// Validate all client_iqn
this.initiators.controls.forEach((control) => {
control.get('client_iqn').updateValueAndValidity({ emitEvent: false });
});
// Update Group Initiator Selector
_.forEach(this.groupMembersSelections, (group, group_index) => {
_.forEach(group, (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[group_index] = [...this.groupMembersSelections[group_index]];
});
}
removeInitiatorImage(initiator: any, lun_index: number, initiator_index: string, image: string) {
const luns = initiator.getValue('luns');
luns.splice(lun_index, 1);
initiator.patchValue({ luns: luns });
this.imagesInitiatorSelections[initiator_index].forEach((value) => {
if (value.name === image) {
value.selected = false;
}
});
return false;
}
// Groups
get groups() {
return this.targetForm.get('groups') as FormArray;
}
addGroup() {
const fg = new CdFormGroup({
group_id: new FormControl('', { validators: [Validators.required] }),
members: new FormControl([]),
disks: new FormControl([])
});
this.groups.push(fg);
const disks = _.map(
this.targetForm.getValue('disks'),
(disk) => new SelectOption(false, disk, '')
);
this.groupDiskSelections.push(disks);
const initiators = _.map(
this.initiators.value,
(initiator) => new SelectOption(false, initiator.client_iqn, '')
);
this.groupMembersSelections.push(initiators);
return fg;
}
removeGroup(index) {
this.groups.removeAt(index);
this.groupDiskSelections.splice(index, 1);
}
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, member_index, group_index) {
const name = group.getValue('members')[member_index];
group.getValue('members').splice(member_index, 1);
this.groupMembersSelections[group_index].forEach((value) => {
if (value.name === name) {
value.selected = false;
}
});
this.groupMembersSelections[group_index] = [...this.groupMembersSelections[group_index]];
this.onGroupMemberSelection({ option: new SelectOption(false, name, '') });
}
removeGroupDisk(group, disk_index, group_index) {
const name = group.getValue('disks')[disk_index];
group.getValue('disks').splice(disk_index, 1);
this.groupDiskSelections[group_index].forEach((value) => {
if (value.name === name) {
value.selected = false;
}
});
this.groupDiskSelections[group_index] = [...this.groupDiskSelections[group_index]];
}
submit() {
const formValue = this.targetForm.value;
const request = {
target_iqn: this.targetForm.getValue('target_iqn'),
target_controls: this.targetForm.getValue('target_controls'),
acl_enabled: this.targetForm.getValue('acl_enabled'),
portals: [],
disks: [],
clients: [],
groups: []
};
// Disks
formValue.disks.forEach((disk) => {
const imageSplit = disk.split('/');
const backstore = this.imagesSettings[disk].backstore;
request.disks.push({
pool: imageSplit[0],
image: imageSplit[1],
backstore: backstore,
controls: this.imagesSettings[disk][backstore]
});
});
// Portals
formValue.portals.forEach((portal) => {
const portalSplit = portal.split(':');
request.portals.push({
host: portalSplit[0],
ip: portalSplit[1]
});
});
// Clients
if (request.acl_enabled) {
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
if (request.acl_enabled) {
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;
}
let wrapTask;
if (this.isEdit) {
request['new_target_iqn'] = request.target_iqn;
request.target_iqn = this.target_iqn;
wrapTask = this.taskWrapper.wrapTaskAroundCall({
task: new FinishedTask('iscsi/target/edit', {
target_iqn: request.target_iqn
}),
call: this.iscsiService.updateTarget(this.target_iqn, request)
});
} else {
wrapTask = this.taskWrapper.wrapTaskAroundCall({
task: new FinishedTask('iscsi/target/create', {
target_iqn: request.target_iqn
}),
call: this.iscsiService.createTarget(request)
});
}
wrapTask.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,
backstores: this.getValidBackstores(this.getImageById(image))
};
this.modalRef = this.modalService.show(IscsiTargetImageSettingsModalComponent, {
initialState
});
}
validFeatures(image, backstore) {
const imageFeatures = image.features;
const requiredFeatures = this.required_rbd_features[backstore];
const supportedFeatures = this.supported_rbd_features[backstore];
// tslint:disable-next-line:no-bitwise
const validRequiredFeatures = (imageFeatures & requiredFeatures) === requiredFeatures;
// tslint:disable-next-line:no-bitwise
const validSupportedFeatures = (imageFeatures & supportedFeatures) === imageFeatures;
return validRequiredFeatures && validSupportedFeatures;
}
getImageById(imageId) {
return this.imagesAll.find((image) => imageId === `${image.pool_name}/${image.name}`);
}
getValidBackstores(image) {
return this.backstores.filter((backstore) => this.validFeatures(image, backstore));
}
}
<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 i18n="form title|Example: Create Pool@@formTitle"
class="panel-title">{{ action | titlecase }} {{ resource | upperFirst }}</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">
<ng-container *ngIf="backstores.length > 1"
i18n>Backstore: {{ imagesSettings[image].backstore }}. </ng-container>
<ng-container *ngIf="hasAdvancedSettings(imagesSettings[image][imagesSettings[image].backstore])"
i18n>This image has modified settings.</ng-container>
</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>
<!-- acl_enabled -->
<div class="form-group">
<div class="col-sm-offset-3 col-sm-9">
<div class="checkbox checkbox-primary">
<input type="checkbox"
formControlName="acl_enabled"
name="acl_enabled"
id="acl_enabled">
<label for="acl_enabled"
i18n>ACL authentication</label>
</div>
<hr />
</div>
</div>
<!-- Initiators -->
<div class="form-group"
*ngIf="targetForm.getValue('acl_enabled')">
<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 ii = index"
[formGroupName]="ii">
<div class="panel-heading">
<ng-container i18n>Initiator</ng-container>: {{ initiator.getValue('client_iqn') }}
<button type="button"
class="close"
(click)="removeInitiator(ii)">
<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="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 li = 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)="removeInitiatorImage(initiator, li, ii, image)">
<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[ii]"
[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(); false"
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"
*ngIf="targetForm.getValue('acl_enabled')"
[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 gi = index"
[formGroupName]="gi">
<div class="panel-heading">
<ng-container i18n>Group</ng-container>: {{ group.getValue('group_id') }}
<button type="button"
class="close"
(click)="groups.removeAt(gi)">
<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, gi)">
<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[gi]"
[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)="removeGroupDisk(group, i, gi)">
<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[gi]"
[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(); false"
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"
(submitAction)="submit()"
i18n="form action button|Example: Create Pool@@formActionButton"
type="button">{{ action | titlecase }} {{ resource | upperFirst }}</cd-submit-button>
<cd-back-button></cd-back-button>
</div>
</div>
</div>
</form>
</div>