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