]> 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
6704b41e6480bba162a4365f829da95d3b3e5a72
[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 { 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(() => undefined);
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 }