1 import { Component, OnInit } from '@angular/core';
2 import { FormArray, FormControl, Validators } from '@angular/forms';
3 import { ActivatedRoute, Router } from '@angular/router';
5 import { I18n } from '@ngx-translate/i18n-polyfill';
6 import * as _ from 'lodash';
7 import { BsModalRef, BsModalService } from 'ngx-bootstrap/modal';
8 import { forkJoin } from 'rxjs';
10 import { IscsiService } from '../../../shared/api/iscsi.service';
11 import { RbdService } from '../../../shared/api/rbd.service';
12 import { SelectMessages } from '../../../shared/components/select/select-messages.model';
13 import { SelectOption } from '../../../shared/components/select/select-option.model';
14 import { ActionLabelsI18n } from '../../../shared/constants/app.constants';
15 import { Icons } from '../../../shared/enum/icons.enum';
16 import { CdFormGroup } from '../../../shared/forms/cd-form-group';
17 import { CdValidators } from '../../../shared/forms/cd-validators';
18 import { FinishedTask } from '../../../shared/models/finished-task';
19 import { TaskWrapperService } from '../../../shared/services/task-wrapper.service';
20 import { IscsiTargetImageSettingsModalComponent } from '../iscsi-target-image-settings-modal/iscsi-target-image-settings-modal.component';
21 import { IscsiTargetIqnSettingsModalComponent } from '../iscsi-target-iqn-settings-modal/iscsi-target-iqn-settings-modal.component';
24 selector: 'cd-iscsi-target-form',
25 templateUrl: './iscsi-target-form.component.html',
26 styleUrls: ['./iscsi-target-form.component.scss']
28 export class IscsiTargetFormComponent implements OnInit {
29 targetForm: CdFormGroup;
32 target_default_controls: any;
33 disk_default_controls: any;
35 default_backstore: string;
36 unsupported_rbd_features: any;
37 required_rbd_features: any;
45 imagesSelections: SelectOption[];
46 portalsSelections: SelectOption[] = [];
48 imagesInitiatorSelections: SelectOption[][] = [];
49 groupDiskSelections: SelectOption[][] = [];
50 groupMembersSelections: SelectOption[][] = [];
52 imagesSettings: any = {};
54 portals: new SelectMessages(
55 { noOptions: this.i18n('There are no portals available.') },
58 images: new SelectMessages(
59 { noOptions: this.i18n('There are no images available.') },
62 initiatorImage: new SelectMessages(
65 'There are no images available. Please make sure you add an image to the target.'
70 groupInitiator: new SelectMessages(
73 'There are no initiators available. Please make sure you add an initiator to the target.'
80 IQN_REGEX = /^iqn\.(19|20)\d\d-(0[1-9]|1[0-2])\.\D{2,3}(\.[A-Za-z0-9-]+)+(:[A-Za-z0-9-\.]+)*$/;
81 USER_REGEX = /[\w\.:@_-]{8,64}/;
82 PASSWORD_REGEX = /[\w@\-_\/]{12,16}/;
87 private iscsiService: IscsiService,
88 private modalService: BsModalService,
89 private rbdService: RbdService,
90 private router: Router,
91 private route: ActivatedRoute,
93 private taskWrapper: TaskWrapperService,
94 public actionLabels: ActionLabelsI18n
96 this.resource = this.i18n('target');
100 const promises: any[] = [
101 this.iscsiService.listTargets(),
102 this.rbdService.list(),
103 this.iscsiService.portals(),
104 this.iscsiService.settings()
107 if (this.router.url.startsWith('/block/iscsi/targets/edit')) {
109 this.route.params.subscribe((params: { target_iqn: string }) => {
110 this.target_iqn = decodeURIComponent(params.target_iqn);
111 promises.push(this.iscsiService.getTarget(this.target_iqn));
114 this.action = this.isEdit ? this.actionLabels.EDIT : this.actionLabels.CREATE;
116 forkJoin(promises).subscribe((data: any[]) => {
117 // iscsiService.listTargets
118 const usedImages = _(data[0])
119 .filter((target) => target.target_iqn !== this.target_iqn)
120 .flatMap((target) => target.disks)
121 .map((image) => `${image.pool}/${image.image}`)
124 // iscsiService.settings()
125 this.minimum_gateways = data[3].config.minimum_gateways;
126 this.target_default_controls = data[3].target_default_controls;
127 this.disk_default_controls = data[3].disk_default_controls;
128 this.backstores = data[3].backstores;
129 this.default_backstore = data[3].default_backstore;
130 this.unsupported_rbd_features = data[3].unsupported_rbd_features;
131 this.required_rbd_features = data[3].required_rbd_features;
134 this.imagesAll = _(data[1])
135 .flatMap((pool) => pool.value)
137 const imageId = `${image.pool_name}/${image.name}`;
138 if (usedImages.indexOf(imageId) !== -1) {
141 const validBackstores = this.getValidBackstores(image);
142 if (validBackstores.length === 0) {
149 this.imagesSelections = this.imagesAll.map(
150 (image) => new SelectOption(false, `${image.pool_name}/${image.name}`, '')
153 // iscsiService.portals()
154 const portals: SelectOption[] = [];
155 data[2].forEach((portal) => {
156 portal.ip_addresses.forEach((ip) => {
157 portals.push(new SelectOption(false, portal.name + ':' + ip, ''));
160 this.portalsSelections = [...portals];
164 // iscsiService.getTarget()
166 this.resolveModel(data[4]);
172 this.targetForm = new CdFormGroup({
173 target_iqn: new FormControl('iqn.2001-07.com.ceph:' + Date.now(), {
174 validators: [Validators.required, Validators.pattern(this.IQN_REGEX)]
176 target_controls: new FormControl({}),
177 portals: new FormControl([], {
179 CdValidators.custom('minGateways', (value) => {
180 const gateways = _.uniq(value.map((elem) => elem.split(':')[0]));
181 return gateways.length < Math.max(1, this.minimum_gateways);
185 disks: new FormControl([]),
186 initiators: new FormArray([]),
187 groups: new FormArray([]),
188 acl_enabled: new FormControl(false)
193 this.targetForm.patchValue({
194 target_iqn: res.target_iqn,
195 target_controls: res.target_controls,
196 acl_enabled: res.acl_enabled
200 _.forEach(res.portals, (portal) => {
201 const id = `${portal.host}:${portal.ip}`;
204 this.targetForm.patchValue({
209 _.forEach(res.disks, (disk) => {
210 const id = `${disk.pool}/${disk.image}`;
212 this.imagesSettings[id] = {
213 backstore: disk.backstore
215 this.imagesSettings[id][disk.backstore] = disk.controls;
217 this.onImageSelection({ option: { name: id, selected: true } });
219 this.targetForm.patchValue({
223 _.forEach(res.clients, (client) => {
224 const initiator = this.addInitiator();
225 client.luns = _.map(client.luns, (lun) => `${lun.pool}/${lun.image}`);
226 initiator.patchValue(client);
227 // updatedInitiatorSelector()
230 _.forEach(res.groups, (group) => {
231 const fg = this.addGroup();
232 group.disks = _.map(group.disks, (disk) => `${disk.pool}/${disk.image}`);
233 fg.patchValue(group);
234 _.forEach(group.members, (member) => {
235 this.onGroupMemberSelection({ option: new SelectOption(true, member, '') });
240 hasAdvancedSettings(settings: any) {
241 return Object.values(settings).length > 0;
246 return this.targetForm.get('portals') as FormControl;
249 onPortalSelection() {
250 this.portals.setValue(this.portals.value);
253 removePortal(index: number, portal: string) {
254 this.portalsSelections.forEach((value) => {
255 if (value.name === portal) {
256 value.selected = false;
260 this.portals.value.splice(index, 1);
261 this.portals.setValue(this.portals.value);
267 return this.targetForm.get('disks') as FormControl;
270 removeImage(index: number, image: string) {
271 this.imagesSelections.forEach((value) => {
272 if (value.name === image) {
273 value.selected = false;
276 this.disks.value.splice(index, 1);
277 this.removeImageRefs(image);
281 removeImageRefs(name) {
282 this.initiators.controls.forEach((element) => {
283 const newImages = element.value.luns.filter((item) => item !== name);
284 element.get('luns').setValue(newImages);
287 this.groups.controls.forEach((element) => {
288 const newDisks = element.value.disks.filter((item) => item !== name);
289 element.get('disks').setValue(newDisks);
292 _.forEach(this.imagesInitiatorSelections, (selections, i) => {
293 this.imagesInitiatorSelections[i] = selections.filter((item: any) => item.name !== name);
295 _.forEach(this.groupDiskSelections, (selections, i) => {
296 this.groupDiskSelections[i] = selections.filter((item: any) => item.name !== name);
300 getDefaultBackstore(imageId) {
301 let result = this.default_backstore;
302 const image = this.getImageById(imageId);
303 if (!this.validFeatures(image, this.default_backstore)) {
304 this.backstores.forEach((backstore) => {
305 if (backstore !== this.default_backstore) {
306 if (this.validFeatures(image, backstore)) {
315 onImageSelection($event) {
316 const option = $event.option;
318 if (option.selected) {
319 if (!this.imagesSettings[option.name]) {
320 const defaultBackstore = this.getDefaultBackstore(option.name);
321 this.imagesSettings[option.name] = {
322 backstore: defaultBackstore
324 this.imagesSettings[option.name][defaultBackstore] = {};
327 _.forEach(this.imagesInitiatorSelections, (selections, i) => {
328 selections.push(new SelectOption(false, option.name, ''));
329 this.imagesInitiatorSelections[i] = [...selections];
332 _.forEach(this.groupDiskSelections, (selections, i) => {
333 selections.push(new SelectOption(false, option.name, ''));
334 this.groupDiskSelections[i] = [...selections];
337 this.removeImageRefs(option.name);
343 return this.targetForm.get('initiators') as FormArray;
347 const fg = new CdFormGroup({
348 client_iqn: new FormControl('', {
351 CdValidators.custom('notUnique', (client_iqn) => {
352 const flattened = this.initiators.controls.reduce(function(accumulator, currentValue) {
353 return accumulator.concat(currentValue.value.client_iqn);
356 return flattened.indexOf(client_iqn) !== flattened.lastIndexOf(client_iqn);
358 Validators.pattern(this.IQN_REGEX)
361 auth: new CdFormGroup({
362 user: new FormControl(''),
363 password: new FormControl(''),
364 mutual_user: new FormControl(''),
365 mutual_password: new FormControl('')
367 luns: new FormControl([]),
368 cdIsInGroup: new FormControl(false)
371 CdValidators.validateIf(
373 () => fg.getValue('password') || fg.getValue('mutual_user') || fg.getValue('mutual_password'),
374 [Validators.required],
375 [Validators.pattern(this.USER_REGEX)],
376 [fg.get('password'), fg.get('mutual_user'), fg.get('mutual_password')]
379 CdValidators.validateIf(
381 () => fg.getValue('user') || fg.getValue('mutual_user') || fg.getValue('mutual_password'),
382 [Validators.required],
383 [Validators.pattern(this.PASSWORD_REGEX)],
384 [fg.get('user'), fg.get('mutual_user'), fg.get('mutual_password')]
387 CdValidators.validateIf(
388 fg.get('mutual_user'),
389 () => fg.getValue('mutual_password'),
390 [Validators.required],
391 [Validators.pattern(this.USER_REGEX)],
392 [fg.get('user'), fg.get('password'), fg.get('mutual_password')]
395 CdValidators.validateIf(
396 fg.get('mutual_password'),
397 () => fg.getValue('mutual_user'),
398 [Validators.required],
399 [Validators.pattern(this.PASSWORD_REGEX)],
400 [fg.get('user'), fg.get('password'), fg.get('mutual_user')]
403 this.initiators.push(fg);
405 _.forEach(this.groupMembersSelections, (selections, i) => {
406 selections.push(new SelectOption(false, '', ''));
407 this.groupMembersSelections[i] = [...selections];
411 this.targetForm.getValue('disks'),
412 (disk) => new SelectOption(false, disk, '')
414 this.imagesInitiatorSelections.push(disks);
419 removeInitiator(index) {
420 const removed = this.initiators.value[index];
422 this.initiators.removeAt(index);
424 _.forEach(this.groupMembersSelections, (selections, i) => {
425 selections.splice(index, 1);
426 this.groupMembersSelections[i] = [...selections];
429 this.groups.controls.forEach((element) => {
430 const newMembers = element.value.members.filter((item) => item !== removed.client_iqn);
431 element.get('members').setValue(newMembers);
434 this.imagesInitiatorSelections.splice(index, 1);
437 updatedInitiatorSelector() {
438 // Validate all client_iqn
439 this.initiators.controls.forEach((control) => {
440 control.get('client_iqn').updateValueAndValidity({ emitEvent: false });
443 // Update Group Initiator Selector
444 _.forEach(this.groupMembersSelections, (group, group_index) => {
445 _.forEach(group, (elem, index) => {
446 const oldName = elem.name;
447 elem.name = this.initiators.controls[index].value.client_iqn;
449 this.groups.controls.forEach((element) => {
450 const members = element.value.members;
451 const i = members.indexOf(oldName);
454 members[i] = elem.name;
456 element.get('members').setValue(members);
459 this.groupMembersSelections[group_index] = [...this.groupMembersSelections[group_index]];
463 removeInitiatorImage(initiator: any, lun_index: number, initiator_index: string, image: string) {
464 const luns = initiator.getValue('luns');
465 luns.splice(lun_index, 1);
466 initiator.patchValue({ luns: luns });
468 this.imagesInitiatorSelections[initiator_index].forEach((value) => {
469 if (value.name === image) {
470 value.selected = false;
479 return this.targetForm.get('groups') as FormArray;
483 const fg = new CdFormGroup({
484 group_id: new FormControl('', { validators: [Validators.required] }),
485 members: new FormControl([]),
486 disks: new FormControl([])
489 this.groups.push(fg);
492 this.targetForm.getValue('disks'),
493 (disk) => new SelectOption(false, disk, '')
495 this.groupDiskSelections.push(disks);
497 const initiators = _.map(
498 this.initiators.value,
499 (initiator) => new SelectOption(false, initiator.client_iqn, '', !initiator.cdIsInGroup)
501 this.groupMembersSelections.push(initiators);
507 this.groups.removeAt(index);
508 this.groupDiskSelections.splice(index, 1);
511 onGroupMemberSelection($event) {
512 const option = $event.option;
514 let initiator_index: number;
515 this.initiators.controls.forEach((element, index) => {
516 if (element.value.client_iqn === option.name) {
517 element.patchValue({ luns: [] });
518 element.get('cdIsInGroup').setValue(option.selected);
519 initiator_index = index;
523 // Members can only be at one group at a time, so when a member is selected
524 // in one group we need to disable its selection in other groups
525 _.forEach(this.groupMembersSelections, (group) => {
526 group[initiator_index].enabled = !option.selected;
530 removeGroupInitiator(group, member_index, group_index) {
531 const name = group.getValue('members')[member_index];
532 group.getValue('members').splice(member_index, 1);
534 this.groupMembersSelections[group_index].forEach((value) => {
535 if (value.name === name) {
536 value.selected = false;
539 this.groupMembersSelections[group_index] = [...this.groupMembersSelections[group_index]];
541 this.onGroupMemberSelection({ option: new SelectOption(false, name, '') });
544 removeGroupDisk(group, disk_index, group_index) {
545 const name = group.getValue('disks')[disk_index];
546 group.getValue('disks').splice(disk_index, 1);
548 this.groupDiskSelections[group_index].forEach((value) => {
549 if (value.name === name) {
550 value.selected = false;
553 this.groupDiskSelections[group_index] = [...this.groupDiskSelections[group_index]];
557 const formValue = _.cloneDeep(this.targetForm.value);
560 target_iqn: this.targetForm.getValue('target_iqn'),
561 target_controls: this.targetForm.getValue('target_controls'),
562 acl_enabled: this.targetForm.getValue('acl_enabled'),
570 formValue.disks.forEach((disk) => {
571 const imageSplit = disk.split('/');
572 const backstore = this.imagesSettings[disk].backstore;
575 image: imageSplit[1],
576 backstore: backstore,
577 controls: this.imagesSettings[disk][backstore]
582 formValue.portals.forEach((portal) => {
583 const index = portal.indexOf(':');
584 request.portals.push({
585 host: portal.substring(0, index),
586 ip: portal.substring(index + 1)
591 if (request.acl_enabled) {
592 formValue.initiators.forEach((initiator) => {
593 if (!initiator.auth.user) {
594 initiator.auth.user = '';
596 if (!initiator.auth.password) {
597 initiator.auth.password = '';
599 if (!initiator.auth.mutual_user) {
600 initiator.auth.mutual_user = '';
602 if (!initiator.auth.mutual_password) {
603 initiator.auth.mutual_password = '';
605 delete initiator.cdIsInGroup;
608 initiator.luns.forEach((lun) => {
609 const imageSplit = lun.split('/');
616 initiator.luns = newLuns;
618 request.clients = formValue.initiators;
622 if (request.acl_enabled) {
623 formValue.groups.forEach((group) => {
625 group.disks.forEach((disk) => {
626 const imageSplit = disk.split('/');
633 group.disks = newDisks;
635 request.groups = formValue.groups;
640 request['new_target_iqn'] = request.target_iqn;
641 request.target_iqn = this.target_iqn;
642 wrapTask = this.taskWrapper.wrapTaskAroundCall({
643 task: new FinishedTask('iscsi/target/edit', {
644 target_iqn: request.target_iqn
646 call: this.iscsiService.updateTarget(this.target_iqn, request)
649 wrapTask = this.taskWrapper.wrapTaskAroundCall({
650 task: new FinishedTask('iscsi/target/create', {
651 target_iqn: request.target_iqn
653 call: this.iscsiService.createTarget(request)
660 this.targetForm.setErrors({ cdSubmitButton: true });
662 () => this.router.navigate(['/block/iscsi/targets'])
666 targetSettingsModal() {
667 const initialState = {
668 target_controls: this.targetForm.get('target_controls'),
669 target_default_controls: this.target_default_controls
672 this.modalRef = this.modalService.show(IscsiTargetIqnSettingsModalComponent, { initialState });
675 imageSettingsModal(image) {
676 const initialState = {
677 imagesSettings: this.imagesSettings,
679 disk_default_controls: this.disk_default_controls,
680 backstores: this.getValidBackstores(this.getImageById(image))
683 this.modalRef = this.modalService.show(IscsiTargetImageSettingsModalComponent, {
688 validFeatures(image, backstore) {
689 const imageFeatures = image.features;
690 const requiredFeatures = this.required_rbd_features[backstore];
691 const unsupportedFeatures = this.unsupported_rbd_features[backstore];
692 // tslint:disable-next-line:no-bitwise
693 const validRequiredFeatures = (imageFeatures & requiredFeatures) === requiredFeatures;
694 // tslint:disable-next-line:no-bitwise
695 const validSupportedFeatures = (imageFeatures & unsupportedFeatures) === 0;
696 return validRequiredFeatures && validSupportedFeatures;
699 getImageById(imageId) {
700 return this.imagesAll.find((image) => imageId === `${image.pool_name}/${image.name}`);
703 getValidBackstores(image) {
704 return this.backstores.filter((backstore) => this.validFeatures(image, backstore));