]> git.apps.os.sepia.ceph.com Git - ceph.git/blob
bf9e73ccbe5c5ea017c9e094de2fb39f27635664
[ceph.git] /
1 import { Component, OnInit } from '@angular/core';
2 import { FormArray, FormControl, Validators } from '@angular/forms';
3 import { ActivatedRoute, Router } from '@angular/router';
4
5 import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
6 import _ from 'lodash';
7 import { forkJoin } from 'rxjs';
8
9 import { IscsiService } from '~/app/shared/api/iscsi.service';
10 import { RbdService } from '~/app/shared/api/rbd.service';
11 import { SelectMessages } from '~/app/shared/components/select/select-messages.model';
12 import { SelectOption } from '~/app/shared/components/select/select-option.model';
13 import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
14 import { Icons } from '~/app/shared/enum/icons.enum';
15 import { CdForm } from '~/app/shared/forms/cd-form';
16 import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
17 import { CdValidators } from '~/app/shared/forms/cd-validators';
18 import { CdTableFetchDataContext } from '~/app/shared/models/cd-table-fetch-data-context';
19 import { FinishedTask } from '~/app/shared/models/finished-task';
20 import { ModalService } from '~/app/shared/services/modal.service';
21 import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
22 import { IscsiTargetImageSettingsModalComponent } from '../iscsi-target-image-settings-modal/iscsi-target-image-settings-modal.component';
23 import { IscsiTargetIqnSettingsModalComponent } from '../iscsi-target-iqn-settings-modal/iscsi-target-iqn-settings-modal.component';
24
25 @Component({
26   selector: 'cd-iscsi-target-form',
27   templateUrl: './iscsi-target-form.component.html',
28   styleUrls: ['./iscsi-target-form.component.scss']
29 })
30 export class IscsiTargetFormComponent extends CdForm implements OnInit {
31   cephIscsiConfigVersion: number;
32   targetForm: CdFormGroup;
33   modalRef: NgbModalRef;
34   api_version = 0;
35   minimum_gateways = 1;
36   target_default_controls: any;
37   target_controls_limits: any;
38   disk_default_controls: any;
39   disk_controls_limits: any;
40   backstores: string[];
41   default_backstore: string;
42   unsupported_rbd_features: any;
43   required_rbd_features: any;
44
45   icons = Icons;
46
47   isEdit = false;
48   target_iqn: string;
49
50   imagesAll: any[];
51   imagesSelections: SelectOption[];
52   portalsSelections: SelectOption[] = [];
53
54   imagesInitiatorSelections: SelectOption[][] = [];
55   groupDiskSelections: SelectOption[][] = [];
56   groupMembersSelections: SelectOption[][] = [];
57
58   imagesSettings: any = {};
59   messages = {
60     portals: new SelectMessages({ noOptions: $localize`There are no portals available.` }),
61     images: new SelectMessages({ noOptions: $localize`There are no images available.` }),
62     initiatorImage: new SelectMessages({
63       noOptions: $localize`There are no images available. Please make sure you add an image to the target.`
64     }),
65     groupInitiator: new SelectMessages({
66       noOptions: $localize`There are no initiators available. Please make sure you add an initiator to the target.`
67     })
68   };
69
70   IQN_REGEX = /^iqn\.(19|20)\d\d-(0[1-9]|1[0-2])\.\D{2,3}(\.[A-Za-z0-9-]+)+(:[A-Za-z0-9-\.]+)*$/;
71   USER_REGEX = /^[\w\.:@_-]{8,64}$/;
72   PASSWORD_REGEX = /^[\w@\-_\/]{12,16}$/;
73   action: string;
74   resource: string;
75
76   constructor(
77     private iscsiService: IscsiService,
78     private modalService: ModalService,
79     private rbdService: RbdService,
80     private router: Router,
81     private route: ActivatedRoute,
82     private taskWrapper: TaskWrapperService,
83     public actionLabels: ActionLabelsI18n
84   ) {
85     super();
86     this.resource = $localize`target`;
87   }
88
89   ngOnInit() {
90     const rbdListContext = new CdTableFetchDataContext(() => {});
91     /* limit -1 to specify all images */
92     rbdListContext.pageInfo.limit = -1;
93     const promises: any[] = [
94       this.iscsiService.listTargets(),
95       /* tslint:disable:no-empty */
96       this.rbdService.list(rbdListContext.toParams()),
97       this.iscsiService.portals(),
98       this.iscsiService.settings(),
99       this.iscsiService.version()
100     ];
101
102     if (this.router.url.startsWith('/block/iscsi/targets/edit')) {
103       this.isEdit = true;
104       this.route.params.subscribe((params: { target_iqn: string }) => {
105         this.target_iqn = decodeURIComponent(params.target_iqn);
106         promises.push(this.iscsiService.getTarget(this.target_iqn));
107       });
108     }
109     this.action = this.isEdit ? this.actionLabels.EDIT : this.actionLabels.CREATE;
110
111     forkJoin(promises).subscribe((data: any[]) => {
112       // iscsiService.listTargets
113       const usedImages = _(data[0])
114         .filter((target) => target.target_iqn !== this.target_iqn)
115         .flatMap((target) => target.disks)
116         .map((image) => `${image.pool}/${image.image}`)
117         .value();
118
119       // iscsiService.settings()
120       if ('api_version' in data[3]) {
121         this.api_version = data[3].api_version;
122       }
123       this.minimum_gateways = data[3].config.minimum_gateways;
124       this.target_default_controls = data[3].target_default_controls;
125       this.target_controls_limits = data[3].target_controls_limits;
126       this.disk_default_controls = data[3].disk_default_controls;
127       this.disk_controls_limits = data[3].disk_controls_limits;
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;
132
133       // rbdService.list()
134       this.imagesAll = _(data[1])
135         .flatMap((pool) => pool.value)
136         .filter((image) => {
137           // Namespaces are not supported by ceph-iscsi
138           if (image.namespace) {
139             return false;
140           }
141           const imageId = `${image.pool_name}/${image.name}`;
142           if (usedImages.indexOf(imageId) !== -1) {
143             return false;
144           }
145           const validBackstores = this.getValidBackstores(image);
146           if (validBackstores.length === 0) {
147             return false;
148           }
149           return true;
150         })
151         .value();
152
153       this.imagesSelections = this.imagesAll.map(
154         (image) => new SelectOption(false, `${image.pool_name}/${image.name}`, '')
155       );
156
157       // iscsiService.portals()
158       const portals: SelectOption[] = [];
159       data[2].forEach((portal: Record<string, any>) => {
160         portal.ip_addresses.forEach((ip: string) => {
161           portals.push(new SelectOption(false, portal.name + ':' + ip, ''));
162         });
163       });
164       this.portalsSelections = [...portals];
165
166       // iscsiService.version()
167       this.cephIscsiConfigVersion = data[4]['ceph_iscsi_config_version'];
168
169       this.createForm();
170
171       // iscsiService.getTarget()
172       if (data[5]) {
173         this.resolveModel(data[5]);
174       }
175
176       this.loadingReady();
177     });
178   }
179
180   createForm() {
181     this.targetForm = new CdFormGroup({
182       target_iqn: new FormControl('iqn.2001-07.com.ceph:' + Date.now(), {
183         validators: [Validators.required, Validators.pattern(this.IQN_REGEX)]
184       }),
185       target_controls: new FormControl({}),
186       portals: new FormControl([], {
187         validators: [
188           CdValidators.custom('minGateways', (value: any[]) => {
189             const gateways = _.uniq(value.map((elem) => elem.split(':')[0]));
190             return gateways.length < Math.max(1, this.minimum_gateways);
191           })
192         ]
193       }),
194       disks: new FormControl([], {
195         validators: [
196           CdValidators.custom('dupLunId', (value: any[]) => {
197             const lunIds = this.getLunIds(value);
198             return lunIds.length !== _.uniq(lunIds).length;
199           }),
200           CdValidators.custom('dupWwn', (value: any[]) => {
201             const wwns = this.getWwns(value);
202             return wwns.length !== _.uniq(wwns).length;
203           })
204         ]
205       }),
206       initiators: new FormArray([]),
207       groups: new FormArray([]),
208       acl_enabled: new FormControl(false)
209     });
210     // Target level authentication was introduced in ceph-iscsi config v11
211     if (this.cephIscsiConfigVersion > 10) {
212       const authFormGroup = new CdFormGroup({
213         user: new FormControl(''),
214         password: new FormControl(''),
215         mutual_user: new FormControl(''),
216         mutual_password: new FormControl('')
217       });
218       this.setAuthValidator(authFormGroup);
219       this.targetForm.addControl('auth', authFormGroup);
220     }
221   }
222
223   resolveModel(res: Record<string, any>) {
224     this.targetForm.patchValue({
225       target_iqn: res.target_iqn,
226       target_controls: res.target_controls,
227       acl_enabled: res.acl_enabled
228     });
229     // Target level authentication was introduced in ceph-iscsi config v11
230     if (this.cephIscsiConfigVersion > 10) {
231       this.targetForm.patchValue({
232         auth: res.auth
233       });
234     }
235     const portals: any[] = [];
236     _.forEach(res.portals, (portal) => {
237       const id = `${portal.host}:${portal.ip}`;
238       portals.push(id);
239     });
240     this.targetForm.patchValue({
241       portals: portals
242     });
243
244     const disks: any[] = [];
245     _.forEach(res.disks, (disk) => {
246       const id = `${disk.pool}/${disk.image}`;
247       disks.push(id);
248       this.imagesSettings[id] = {
249         backstore: disk.backstore
250       };
251       this.imagesSettings[id][disk.backstore] = disk.controls;
252       if ('lun' in disk) {
253         this.imagesSettings[id]['lun'] = disk.lun;
254       }
255       if ('wwn' in disk) {
256         this.imagesSettings[id]['wwn'] = disk.wwn;
257       }
258
259       this.onImageSelection({ option: { name: id, selected: true } });
260     });
261     this.targetForm.patchValue({
262       disks: disks
263     });
264
265     _.forEach(res.clients, (client) => {
266       const initiator = this.addInitiator();
267       client.luns = _.map(client.luns, (lun) => `${lun.pool}/${lun.image}`);
268       initiator.patchValue(client);
269       // updatedInitiatorSelector()
270     });
271
272     (res.groups as any[]).forEach((group: any, group_index: number) => {
273       const fg = this.addGroup();
274       group.disks = _.map(group.disks, (disk) => `${disk.pool}/${disk.image}`);
275       fg.patchValue(group);
276       _.forEach(group.members, (member) => {
277         this.onGroupMemberSelection({ option: new SelectOption(true, member, '') }, group_index);
278       });
279     });
280   }
281
282   hasAdvancedSettings(settings: any) {
283     return Object.values(settings).length > 0;
284   }
285
286   // Portals
287   get portals() {
288     return this.targetForm.get('portals') as FormControl;
289   }
290
291   onPortalSelection() {
292     this.portals.setValue(this.portals.value);
293   }
294
295   removePortal(index: number, portal: string) {
296     this.portalsSelections.forEach((value) => {
297       if (value.name === portal) {
298         value.selected = false;
299       }
300     });
301
302     this.portals.value.splice(index, 1);
303     this.portals.setValue(this.portals.value);
304     return false;
305   }
306
307   // Images
308   get disks() {
309     return this.targetForm.get('disks') as FormControl;
310   }
311
312   removeImage(index: number, image: string) {
313     this.imagesSelections.forEach((value) => {
314       if (value.name === image) {
315         value.selected = false;
316       }
317     });
318     this.disks.value.splice(index, 1);
319     this.removeImageRefs(image);
320     this.targetForm.get('disks').updateValueAndValidity({ emitEvent: false });
321     return false;
322   }
323
324   removeImageRefs(name: string) {
325     this.initiators.controls.forEach((element) => {
326       const newImages = element.value.luns.filter((item: string) => item !== name);
327       element.get('luns').setValue(newImages);
328     });
329
330     this.groups.controls.forEach((element) => {
331       const newDisks = element.value.disks.filter((item: string) => item !== name);
332       element.get('disks').setValue(newDisks);
333     });
334
335     _.forEach(this.imagesInitiatorSelections, (selections, i) => {
336       this.imagesInitiatorSelections[i] = selections.filter((item: any) => item.name !== name);
337     });
338     _.forEach(this.groupDiskSelections, (selections, i) => {
339       this.groupDiskSelections[i] = selections.filter((item: any) => item.name !== name);
340     });
341   }
342
343   getDefaultBackstore(imageId: string) {
344     let result = this.default_backstore;
345     const image = this.getImageById(imageId);
346     if (!this.validFeatures(image, this.default_backstore)) {
347       this.backstores.forEach((backstore) => {
348         if (backstore !== this.default_backstore) {
349           if (this.validFeatures(image, backstore)) {
350             result = backstore;
351           }
352         }
353       });
354     }
355     return result;
356   }
357
358   isLunIdInUse(lunId: string, imageId: string) {
359     const images = this.disks.value.filter((currentImageId: string) => currentImageId !== imageId);
360     return this.getLunIds(images).includes(lunId);
361   }
362
363   getLunIds(images: object) {
364     return _.map(images, (image) => this.imagesSettings[image]['lun']);
365   }
366
367   nextLunId(imageId: string) {
368     const images = this.disks.value.filter((currentImageId: string) => currentImageId !== imageId);
369     const lunIdsInUse = this.getLunIds(images);
370     let lunIdCandidate = 0;
371     while (lunIdsInUse.includes(lunIdCandidate)) {
372       lunIdCandidate++;
373     }
374     return lunIdCandidate;
375   }
376
377   getWwns(images: object) {
378     const wwns = _.map(images, (image) => this.imagesSettings[image]['wwn']);
379     return wwns.filter((wwn) => _.isString(wwn) && wwn !== '');
380   }
381
382   onImageSelection($event: any) {
383     const option = $event.option;
384
385     if (option.selected) {
386       if (!this.imagesSettings[option.name]) {
387         const defaultBackstore = this.getDefaultBackstore(option.name);
388         this.imagesSettings[option.name] = {
389           backstore: defaultBackstore,
390           lun: this.nextLunId(option.name)
391         };
392         this.imagesSettings[option.name][defaultBackstore] = {};
393       } else if (this.isLunIdInUse(this.imagesSettings[option.name]['lun'], option.name)) {
394         // If the lun id is now in use, we have to generate a new one
395         this.imagesSettings[option.name]['lun'] = this.nextLunId(option.name);
396       }
397
398       _.forEach(this.imagesInitiatorSelections, (selections, i) => {
399         selections.push(new SelectOption(false, option.name, ''));
400         this.imagesInitiatorSelections[i] = [...selections];
401       });
402
403       _.forEach(this.groupDiskSelections, (selections, i) => {
404         selections.push(new SelectOption(false, option.name, ''));
405         this.groupDiskSelections[i] = [...selections];
406       });
407     } else {
408       this.removeImageRefs(option.name);
409     }
410     this.targetForm.get('disks').updateValueAndValidity({ emitEvent: false });
411   }
412
413   // Initiators
414   get initiators() {
415     return this.targetForm.get('initiators') as FormArray;
416   }
417
418   addInitiator() {
419     const fg = new CdFormGroup({
420       client_iqn: new FormControl('', {
421         validators: [
422           Validators.required,
423           CdValidators.custom('notUnique', (client_iqn: string) => {
424             const flattened = this.initiators.controls.reduce(function (accumulator, currentValue) {
425               return accumulator.concat(currentValue.value.client_iqn);
426             }, []);
427
428             return flattened.indexOf(client_iqn) !== flattened.lastIndexOf(client_iqn);
429           }),
430           Validators.pattern(this.IQN_REGEX)
431         ]
432       }),
433       auth: new CdFormGroup({
434         user: new FormControl(''),
435         password: new FormControl(''),
436         mutual_user: new FormControl(''),
437         mutual_password: new FormControl('')
438       }),
439       luns: new FormControl([]),
440       cdIsInGroup: new FormControl(false)
441     });
442
443     this.setAuthValidator(fg);
444
445     this.initiators.push(fg);
446
447     _.forEach(this.groupMembersSelections, (selections, i) => {
448       selections.push(new SelectOption(false, '', ''));
449       this.groupMembersSelections[i] = [...selections];
450     });
451
452     const disks = _.map(
453       this.targetForm.getValue('disks'),
454       (disk) => new SelectOption(false, disk, '')
455     );
456     this.imagesInitiatorSelections.push(disks);
457
458     return fg;
459   }
460
461   setAuthValidator(fg: CdFormGroup) {
462     CdValidators.validateIf(
463       fg.get('user'),
464       () => fg.getValue('password') || fg.getValue('mutual_user') || fg.getValue('mutual_password'),
465       [Validators.required],
466       [Validators.pattern(this.USER_REGEX)],
467       [fg.get('password'), fg.get('mutual_user'), fg.get('mutual_password')]
468     );
469
470     CdValidators.validateIf(
471       fg.get('password'),
472       () => fg.getValue('user') || fg.getValue('mutual_user') || fg.getValue('mutual_password'),
473       [Validators.required],
474       [Validators.pattern(this.PASSWORD_REGEX)],
475       [fg.get('user'), fg.get('mutual_user'), fg.get('mutual_password')]
476     );
477
478     CdValidators.validateIf(
479       fg.get('mutual_user'),
480       () => fg.getValue('mutual_password'),
481       [Validators.required],
482       [Validators.pattern(this.USER_REGEX)],
483       [fg.get('user'), fg.get('password'), fg.get('mutual_password')]
484     );
485
486     CdValidators.validateIf(
487       fg.get('mutual_password'),
488       () => fg.getValue('mutual_user'),
489       [Validators.required],
490       [Validators.pattern(this.PASSWORD_REGEX)],
491       [fg.get('user'), fg.get('password'), fg.get('mutual_user')]
492     );
493   }
494
495   removeInitiator(index: number) {
496     const removed = this.initiators.value[index];
497
498     this.initiators.removeAt(index);
499
500     _.forEach(this.groupMembersSelections, (selections, i) => {
501       selections.splice(index, 1);
502       this.groupMembersSelections[i] = [...selections];
503     });
504
505     this.groups.controls.forEach((element) => {
506       const newMembers = element.value.members.filter(
507         (item: string) => item !== removed.client_iqn
508       );
509       element.get('members').setValue(newMembers);
510     });
511
512     this.imagesInitiatorSelections.splice(index, 1);
513   }
514
515   updatedInitiatorSelector() {
516     // Validate all client_iqn
517     this.initiators.controls.forEach((control) => {
518       control.get('client_iqn').updateValueAndValidity({ emitEvent: false });
519     });
520
521     // Update Group Initiator Selector
522     _.forEach(this.groupMembersSelections, (group, group_index) => {
523       _.forEach(group, (elem, index) => {
524         const oldName = elem.name;
525         elem.name = this.initiators.controls[index].value.client_iqn;
526
527         this.groups.controls.forEach((element) => {
528           const members = element.value.members;
529           const i = members.indexOf(oldName);
530
531           if (i !== -1) {
532             members[i] = elem.name;
533           }
534           element.get('members').setValue(members);
535         });
536       });
537       this.groupMembersSelections[group_index] = [...this.groupMembersSelections[group_index]];
538     });
539   }
540
541   removeInitiatorImage(initiator: any, lun_index: number, initiator_index: number, image: string) {
542     const luns = initiator.getValue('luns');
543     luns.splice(lun_index, 1);
544     initiator.patchValue({ luns: luns });
545
546     this.imagesInitiatorSelections[initiator_index].forEach((value: Record<string, any>) => {
547       if (value.name === image) {
548         value.selected = false;
549       }
550     });
551
552     return false;
553   }
554
555   // Groups
556   get groups() {
557     return this.targetForm.get('groups') as FormArray;
558   }
559
560   addGroup() {
561     const fg = new CdFormGroup({
562       group_id: new FormControl('', { validators: [Validators.required] }),
563       members: new FormControl([]),
564       disks: new FormControl([])
565     });
566
567     this.groups.push(fg);
568
569     const disks = _.map(
570       this.targetForm.getValue('disks'),
571       (disk) => new SelectOption(false, disk, '')
572     );
573     this.groupDiskSelections.push(disks);
574
575     const initiators = _.map(
576       this.initiators.value,
577       (initiator) => new SelectOption(false, initiator.client_iqn, '', !initiator.cdIsInGroup)
578     );
579     this.groupMembersSelections.push(initiators);
580
581     return fg;
582   }
583
584   removeGroup(index: number) {
585     // Remove group and disk selections
586     this.groups.removeAt(index);
587
588     // Free initiator from group
589     const selectedMembers = this.groupMembersSelections[index].filter((value) => value.selected);
590     selectedMembers.forEach((selection) => {
591       selection.selected = false;
592       this.onGroupMemberSelection({ option: selection }, index);
593     });
594
595     this.groupMembersSelections.splice(index, 1);
596     this.groupDiskSelections.splice(index, 1);
597   }
598
599   onGroupMemberSelection($event: any, group_index: number) {
600     const option = $event.option;
601
602     let luns: string[] = [];
603     if (!option.selected) {
604       const selectedDisks = this.groupDiskSelections[group_index].filter((value) => value.selected);
605       luns = selectedDisks.map((value) => value.name);
606     }
607
608     this.initiators.controls.forEach((element, index) => {
609       if (element.value.client_iqn === option.name) {
610         element.patchValue({ luns: luns });
611         element.get('cdIsInGroup').setValue(option.selected);
612
613         // Members can only be at one group at a time, so when a member is selected
614         // in one group we need to disable its selection in other groups
615         _.forEach(this.groupMembersSelections, (group) => {
616           group[index].enabled = !option.selected;
617         });
618
619         this.imagesInitiatorSelections[index].forEach((image) => {
620           image.selected = luns.includes(image.name);
621         });
622       }
623     });
624   }
625
626   removeGroupInitiator(group: CdFormGroup, member_index: number, group_index: number) {
627     const name = group.getValue('members')[member_index];
628     group.getValue('members').splice(member_index, 1);
629
630     this.onGroupMemberSelection({ option: new SelectOption(false, name, '') }, group_index);
631   }
632
633   removeGroupDisk(group: CdFormGroup, disk_index: number, group_index: number) {
634     const name = group.getValue('disks')[disk_index];
635     group.getValue('disks').splice(disk_index, 1);
636
637     this.groupDiskSelections[group_index].forEach((value) => {
638       if (value.name === name) {
639         value.selected = false;
640       }
641     });
642     this.groupDiskSelections[group_index] = [...this.groupDiskSelections[group_index]];
643   }
644
645   submit() {
646     const formValue = _.cloneDeep(this.targetForm.value);
647
648     const request: Record<string, any> = {
649       target_iqn: this.targetForm.getValue('target_iqn'),
650       target_controls: this.targetForm.getValue('target_controls'),
651       acl_enabled: this.targetForm.getValue('acl_enabled'),
652       portals: [],
653       disks: [],
654       clients: [],
655       groups: []
656     };
657
658     // Target level authentication was introduced in ceph-iscsi config v11
659     if (this.cephIscsiConfigVersion > 10) {
660       const targetAuth: CdFormGroup = this.targetForm.get('auth') as CdFormGroup;
661       if (!targetAuth.getValue('user')) {
662         targetAuth.get('user').setValue('');
663       }
664       if (!targetAuth.getValue('password')) {
665         targetAuth.get('password').setValue('');
666       }
667       if (!targetAuth.getValue('mutual_user')) {
668         targetAuth.get('mutual_user').setValue('');
669       }
670       if (!targetAuth.getValue('mutual_password')) {
671         targetAuth.get('mutual_password').setValue('');
672       }
673       const acl_enabled = this.targetForm.getValue('acl_enabled');
674       request['auth'] = {
675         user: acl_enabled ? '' : targetAuth.getValue('user'),
676         password: acl_enabled ? '' : targetAuth.getValue('password'),
677         mutual_user: acl_enabled ? '' : targetAuth.getValue('mutual_user'),
678         mutual_password: acl_enabled ? '' : targetAuth.getValue('mutual_password')
679       };
680     }
681
682     // Disks
683     formValue.disks.forEach((disk: string) => {
684       const imageSplit = disk.split('/');
685       const backstore = this.imagesSettings[disk].backstore;
686       request.disks.push({
687         pool: imageSplit[0],
688         image: imageSplit[1],
689         backstore: backstore,
690         controls: this.imagesSettings[disk][backstore],
691         lun: this.imagesSettings[disk]['lun'],
692         wwn: this.imagesSettings[disk]['wwn']
693       });
694     });
695
696     // Portals
697     formValue.portals.forEach((portal: string) => {
698       const index = portal.indexOf(':');
699       request.portals.push({
700         host: portal.substring(0, index),
701         ip: portal.substring(index + 1)
702       });
703     });
704
705     // Clients
706     if (request.acl_enabled) {
707       formValue.initiators.forEach((initiator: Record<string, any>) => {
708         if (!initiator.auth.user) {
709           initiator.auth.user = '';
710         }
711         if (!initiator.auth.password) {
712           initiator.auth.password = '';
713         }
714         if (!initiator.auth.mutual_user) {
715           initiator.auth.mutual_user = '';
716         }
717         if (!initiator.auth.mutual_password) {
718           initiator.auth.mutual_password = '';
719         }
720         delete initiator.cdIsInGroup;
721
722         const newLuns: any[] = [];
723         initiator.luns.forEach((lun: string) => {
724           const imageSplit = lun.split('/');
725           newLuns.push({
726             pool: imageSplit[0],
727             image: imageSplit[1]
728           });
729         });
730
731         initiator.luns = newLuns;
732       });
733       request.clients = formValue.initiators;
734     }
735
736     // Groups
737     if (request.acl_enabled) {
738       formValue.groups.forEach((group: Record<string, any>) => {
739         const newDisks: any[] = [];
740         group.disks.forEach((disk: string) => {
741           const imageSplit = disk.split('/');
742           newDisks.push({
743             pool: imageSplit[0],
744             image: imageSplit[1]
745           });
746         });
747
748         group.disks = newDisks;
749       });
750       request.groups = formValue.groups;
751     }
752
753     let wrapTask;
754     if (this.isEdit) {
755       request['new_target_iqn'] = request.target_iqn;
756       request.target_iqn = this.target_iqn;
757       wrapTask = this.taskWrapper.wrapTaskAroundCall({
758         task: new FinishedTask('iscsi/target/edit', {
759           target_iqn: request.target_iqn
760         }),
761         call: this.iscsiService.updateTarget(this.target_iqn, request)
762       });
763     } else {
764       wrapTask = this.taskWrapper.wrapTaskAroundCall({
765         task: new FinishedTask('iscsi/target/create', {
766           target_iqn: request.target_iqn
767         }),
768         call: this.iscsiService.createTarget(request)
769       });
770     }
771
772     wrapTask.subscribe({
773       error: () => {
774         this.targetForm.setErrors({ cdSubmitButton: true });
775       },
776       complete: () => this.router.navigate(['/block/iscsi/targets'])
777     });
778   }
779
780   targetSettingsModal() {
781     const initialState = {
782       target_controls: this.targetForm.get('target_controls'),
783       target_default_controls: this.target_default_controls,
784       target_controls_limits: this.target_controls_limits
785     };
786
787     this.modalRef = this.modalService.show(IscsiTargetIqnSettingsModalComponent, initialState);
788   }
789
790   imageSettingsModal(image: string) {
791     const initialState = {
792       imagesSettings: this.imagesSettings,
793       image: image,
794       api_version: this.api_version,
795       disk_default_controls: this.disk_default_controls,
796       disk_controls_limits: this.disk_controls_limits,
797       backstores: this.getValidBackstores(this.getImageById(image)),
798       control: this.targetForm.get('disks')
799     };
800
801     this.modalRef = this.modalService.show(IscsiTargetImageSettingsModalComponent, initialState);
802   }
803
804   validFeatures(image: Record<string, any>, backstore: string) {
805     const imageFeatures = image.features;
806     const requiredFeatures = this.required_rbd_features[backstore];
807     const unsupportedFeatures = this.unsupported_rbd_features[backstore];
808     // tslint:disable-next-line:no-bitwise
809     const validRequiredFeatures = (imageFeatures & requiredFeatures) === requiredFeatures;
810     // tslint:disable-next-line:no-bitwise
811     const validSupportedFeatures = (imageFeatures & unsupportedFeatures) === 0;
812     return validRequiredFeatures && validSupportedFeatures;
813   }
814
815   getImageById(imageId: string) {
816     return this.imagesAll.find((image) => imageId === `${image.pool_name}/${image.name}`);
817   }
818
819   getValidBackstores(image: object) {
820     return this.backstores.filter((backstore) => this.validFeatures(image, backstore));
821   }
822 }