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