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