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