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