]> git.apps.os.sepia.ceph.com Git - ceph.git/blob
3d502663ca5a70320fdd35c341b6848ca60d2194
[ceph.git] /
1 import { Component, OnInit } from '@angular/core';
2 import { FormArray, FormControl, Validators } from '@angular/forms';
3 import { ActivatedRoute, Router } from '@angular/router';
4
5 import { I18n } from '@ngx-translate/i18n-polyfill';
6 import * as _ from 'lodash';
7 import { BsModalRef, BsModalService } from 'ngx-bootstrap/modal';
8 import { forkJoin } from 'rxjs';
9
10 import { IscsiService } from '../../../shared/api/iscsi.service';
11 import { RbdService } from '../../../shared/api/rbd.service';
12 import { SelectMessages } from '../../../shared/components/select/select-messages.model';
13 import { SelectOption } from '../../../shared/components/select/select-option.model';
14 import { ActionLabelsI18n } from '../../../shared/constants/app.constants';
15 import { CdFormGroup } from '../../../shared/forms/cd-form-group';
16 import { CdValidators } from '../../../shared/forms/cd-validators';
17 import { FinishedTask } from '../../../shared/models/finished-task';
18 import { TaskWrapperService } from '../../../shared/services/task-wrapper.service';
19 import { IscsiTargetImageSettingsModalComponent } from '../iscsi-target-image-settings-modal/iscsi-target-image-settings-modal.component';
20 import { IscsiTargetIqnSettingsModalComponent } from '../iscsi-target-iqn-settings-modal/iscsi-target-iqn-settings-modal.component';
21
22 @Component({
23   selector: 'cd-iscsi-target-form',
24   templateUrl: './iscsi-target-form.component.html',
25   styleUrls: ['./iscsi-target-form.component.scss']
26 })
27 export class IscsiTargetFormComponent implements OnInit {
28   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       console.log(group);
230       group.disks = _.map(group.disks, (disk) => `${disk.pool}/${disk.image}`);
231       fg.patchValue(group);
232       _.forEach(group.members, (member) => {
233         this.onGroupMemberSelection({ option: new SelectOption(true, member, '') });
234       });
235     });
236   }
237
238   hasAdvancedSettings(settings: any) {
239     return Object.values(settings).length > 0;
240   }
241
242   // Portals
243   get portals() {
244     return this.targetForm.get('portals') as FormControl;
245   }
246
247   onPortalSelection() {
248     this.portals.setValue(this.portals.value);
249   }
250
251   removePortal(index: number, portal: string) {
252     this.portalsSelections.forEach((value) => {
253       if (value.name === portal) {
254         value.selected = false;
255       }
256     });
257
258     this.portals.value.splice(index, 1);
259     this.portals.setValue(this.portals.value);
260     return false;
261   }
262
263   // Images
264   get disks() {
265     return this.targetForm.get('disks') as FormControl;
266   }
267
268   removeImage(index: number, image: string) {
269     this.imagesSelections.forEach((value) => {
270       if (value.name === image) {
271         value.selected = false;
272       }
273     });
274     this.disks.value.splice(index, 1);
275     this.removeImageRefs(image);
276     return false;
277   }
278
279   removeImageRefs(name) {
280     this.initiators.controls.forEach((element) => {
281       const newImages = element.value.luns.filter((item) => item !== name);
282       element.get('luns').setValue(newImages);
283     });
284
285     this.groups.controls.forEach((element) => {
286       const newDisks = element.value.disks.filter((item) => item !== name);
287       element.get('disks').setValue(newDisks);
288     });
289
290     _.forEach(this.imagesInitiatorSelections, (selections, i) => {
291       this.imagesInitiatorSelections[i] = selections.filter((item: any) => item.name !== name);
292     });
293     _.forEach(this.groupDiskSelections, (selections, i) => {
294       this.groupDiskSelections[i] = selections.filter((item: any) => item.name !== name);
295     });
296   }
297
298   getDefaultBackstore(imageId) {
299     let result = this.default_backstore;
300     const image = this.getImageById(imageId);
301     if (!this.validFeatures(image, this.default_backstore)) {
302       this.backstores.forEach((backstore) => {
303         if (backstore !== this.default_backstore) {
304           if (this.validFeatures(image, backstore)) {
305             result = backstore;
306           }
307         }
308       });
309     }
310     return result;
311   }
312
313   onImageSelection($event) {
314     const option = $event.option;
315
316     if (option.selected) {
317       if (!this.imagesSettings[option.name]) {
318         const defaultBackstore = this.getDefaultBackstore(option.name);
319         this.imagesSettings[option.name] = {
320           backstore: defaultBackstore
321         };
322         this.imagesSettings[option.name][defaultBackstore] = {};
323       }
324
325       _.forEach(this.imagesInitiatorSelections, (selections, i) => {
326         selections.push(new SelectOption(false, option.name, ''));
327         this.imagesInitiatorSelections[i] = [...selections];
328       });
329
330       _.forEach(this.groupDiskSelections, (selections, i) => {
331         selections.push(new SelectOption(false, option.name, ''));
332         this.groupDiskSelections[i] = [...selections];
333       });
334     } else {
335       this.removeImageRefs(option.name);
336     }
337   }
338
339   // Initiators
340   get initiators() {
341     return this.targetForm.get('initiators') as FormArray;
342   }
343
344   addInitiator() {
345     const fg = new CdFormGroup({
346       client_iqn: new FormControl('', {
347         validators: [
348           Validators.required,
349           CdValidators.custom('notUnique', (client_iqn) => {
350             const flattened = this.initiators.controls.reduce(function(accumulator, currentValue) {
351               return accumulator.concat(currentValue.value.client_iqn);
352             }, []);
353
354             return flattened.indexOf(client_iqn) !== flattened.lastIndexOf(client_iqn);
355           }),
356           Validators.pattern(this.IQN_REGEX)
357         ]
358       }),
359       auth: new CdFormGroup({
360         user: new FormControl(''),
361         password: new FormControl(''),
362         mutual_user: new FormControl(''),
363         mutual_password: new FormControl('')
364       }),
365       luns: new FormControl([]),
366       cdIsInGroup: new FormControl(false)
367     });
368
369     CdValidators.validateIf(
370       fg.get('user'),
371       () => fg.getValue('password') || fg.getValue('mutual_user') || fg.getValue('mutual_password'),
372       [Validators.required],
373       [Validators.pattern(this.USER_REGEX)],
374       [fg.get('password'), fg.get('mutual_user'), fg.get('mutual_password')]
375     );
376
377     CdValidators.validateIf(
378       fg.get('password'),
379       () => fg.getValue('user') || fg.getValue('mutual_user') || fg.getValue('mutual_password'),
380       [Validators.required],
381       [Validators.pattern(this.PASSWORD_REGEX)],
382       [fg.get('user'), fg.get('mutual_user'), fg.get('mutual_password')]
383     );
384
385     CdValidators.validateIf(
386       fg.get('mutual_user'),
387       () => fg.getValue('mutual_password'),
388       [Validators.required],
389       [Validators.pattern(this.USER_REGEX)],
390       [fg.get('user'), fg.get('password'), fg.get('mutual_password')]
391     );
392
393     CdValidators.validateIf(
394       fg.get('mutual_password'),
395       () => fg.getValue('mutual_user'),
396       [Validators.required],
397       [Validators.pattern(this.PASSWORD_REGEX)],
398       [fg.get('user'), fg.get('password'), fg.get('mutual_user')]
399     );
400
401     this.initiators.push(fg);
402
403     _.forEach(this.groupMembersSelections, (selections, i) => {
404       selections.push(new SelectOption(false, '', ''));
405       this.groupMembersSelections[i] = [...selections];
406     });
407
408     const disks = _.map(
409       this.targetForm.getValue('disks'),
410       (disk) => new SelectOption(false, disk, '')
411     );
412     this.imagesInitiatorSelections.push(disks);
413
414     return fg;
415   }
416
417   removeInitiator(index) {
418     const removed = this.initiators.value[index];
419
420     this.initiators.removeAt(index);
421
422     _.forEach(this.groupMembersSelections, (selections, i) => {
423       selections.splice(index, 1);
424       this.groupMembersSelections[i] = [...selections];
425     });
426
427     this.groups.controls.forEach((element) => {
428       const newMembers = element.value.members.filter((item) => item !== removed.client_iqn);
429       element.get('members').setValue(newMembers);
430     });
431
432     this.imagesInitiatorSelections.splice(index, 1);
433   }
434
435   updatedInitiatorSelector() {
436     // Validate all client_iqn
437     this.initiators.controls.forEach((control) => {
438       control.get('client_iqn').updateValueAndValidity({ emitEvent: false });
439     });
440
441     // Update Group Initiator Selector
442     _.forEach(this.groupMembersSelections, (group, group_index) => {
443       _.forEach(group, (elem, index) => {
444         const oldName = elem.name;
445         elem.name = this.initiators.controls[index].value.client_iqn;
446
447         this.groups.controls.forEach((element) => {
448           const members = element.value.members;
449           const i = members.indexOf(oldName);
450
451           if (i !== -1) {
452             members[i] = elem.name;
453           }
454           element.get('members').setValue(members);
455         });
456       });
457       this.groupMembersSelections[group_index] = [...this.groupMembersSelections[group_index]];
458     });
459   }
460
461   removeInitiatorImage(initiator: any, lun_index: number, initiator_index: string, image: string) {
462     const luns = initiator.getValue('luns');
463     luns.splice(lun_index, 1);
464     initiator.patchValue({ luns: luns });
465
466     this.imagesInitiatorSelections[initiator_index].forEach((value) => {
467       if (value.name === image) {
468         value.selected = false;
469       }
470     });
471
472     return false;
473   }
474
475   // Groups
476   get groups() {
477     return this.targetForm.get('groups') as FormArray;
478   }
479
480   addGroup() {
481     const fg = new CdFormGroup({
482       group_id: new FormControl('', { validators: [Validators.required] }),
483       members: new FormControl([]),
484       disks: new FormControl([])
485     });
486
487     this.groups.push(fg);
488
489     const disks = _.map(
490       this.targetForm.getValue('disks'),
491       (disk) => new SelectOption(false, disk, '')
492     );
493     this.groupDiskSelections.push(disks);
494
495     const initiators = _.map(
496       this.initiators.value,
497       (initiator) => new SelectOption(false, initiator.client_iqn, '')
498     );
499     this.groupMembersSelections.push(initiators);
500
501     return fg;
502   }
503
504   removeGroup(index) {
505     this.groups.removeAt(index);
506     this.groupDiskSelections.splice(index, 1);
507   }
508
509   onGroupMemberSelection($event) {
510     const option = $event.option;
511
512     this.initiators.controls.forEach((element) => {
513       if (element.value.client_iqn === option.name) {
514         element.patchValue({ luns: [] });
515         element.get('cdIsInGroup').setValue(option.selected);
516       }
517     });
518   }
519
520   removeGroupInitiator(group, member_index, group_index) {
521     const name = group.getValue('members')[member_index];
522     group.getValue('members').splice(member_index, 1);
523
524     this.groupMembersSelections[group_index].forEach((value) => {
525       if (value.name === name) {
526         value.selected = false;
527       }
528     });
529     this.groupMembersSelections[group_index] = [...this.groupMembersSelections[group_index]];
530
531     this.onGroupMemberSelection({ option: new SelectOption(false, name, '') });
532   }
533
534   removeGroupDisk(group, disk_index, group_index) {
535     const name = group.getValue('disks')[disk_index];
536     group.getValue('disks').splice(disk_index, 1);
537
538     this.groupDiskSelections[group_index].forEach((value) => {
539       if (value.name === name) {
540         value.selected = false;
541       }
542     });
543     this.groupDiskSelections[group_index] = [...this.groupDiskSelections[group_index]];
544   }
545
546   submit() {
547     const formValue = _.cloneDeep(this.targetForm.value);
548
549     const request = {
550       target_iqn: this.targetForm.getValue('target_iqn'),
551       target_controls: this.targetForm.getValue('target_controls'),
552       acl_enabled: this.targetForm.getValue('acl_enabled'),
553       portals: [],
554       disks: [],
555       clients: [],
556       groups: []
557     };
558
559     // Disks
560     formValue.disks.forEach((disk) => {
561       const imageSplit = disk.split('/');
562       const backstore = this.imagesSettings[disk].backstore;
563       request.disks.push({
564         pool: imageSplit[0],
565         image: imageSplit[1],
566         backstore: backstore,
567         controls: this.imagesSettings[disk][backstore]
568       });
569     });
570
571     // Portals
572     formValue.portals.forEach((portal) => {
573       const portalSplit = portal.split(':');
574       request.portals.push({
575         host: portalSplit[0],
576         ip: portalSplit[1]
577       });
578     });
579
580     // Clients
581     if (request.acl_enabled) {
582       formValue.initiators.forEach((initiator) => {
583         if (!initiator.auth.user) {
584           initiator.auth.user = '';
585         }
586         if (!initiator.auth.password) {
587           initiator.auth.password = '';
588         }
589         if (!initiator.auth.mutual_user) {
590           initiator.auth.mutual_user = '';
591         }
592         if (!initiator.auth.mutual_password) {
593           initiator.auth.mutual_password = '';
594         }
595         delete initiator.cdIsInGroup;
596
597         const newLuns = [];
598         initiator.luns.forEach((lun) => {
599           const imageSplit = lun.split('/');
600           newLuns.push({
601             pool: imageSplit[0],
602             image: imageSplit[1]
603           });
604         });
605
606         initiator.luns = newLuns;
607       });
608       request.clients = formValue.initiators;
609     }
610
611     // Groups
612     if (request.acl_enabled) {
613       formValue.groups.forEach((group) => {
614         const newDisks = [];
615         group.disks.forEach((disk) => {
616           const imageSplit = disk.split('/');
617           newDisks.push({
618             pool: imageSplit[0],
619             image: imageSplit[1]
620           });
621         });
622
623         group.disks = newDisks;
624       });
625       request.groups = formValue.groups;
626     }
627
628     let wrapTask;
629     if (this.isEdit) {
630       request['new_target_iqn'] = request.target_iqn;
631       request.target_iqn = this.target_iqn;
632       wrapTask = this.taskWrapper.wrapTaskAroundCall({
633         task: new FinishedTask('iscsi/target/edit', {
634           target_iqn: request.target_iqn
635         }),
636         call: this.iscsiService.updateTarget(this.target_iqn, request)
637       });
638     } else {
639       wrapTask = this.taskWrapper.wrapTaskAroundCall({
640         task: new FinishedTask('iscsi/target/create', {
641           target_iqn: request.target_iqn
642         }),
643         call: this.iscsiService.createTarget(request)
644       });
645     }
646
647     wrapTask.subscribe(
648       undefined,
649       () => {
650         this.targetForm.setErrors({ cdSubmitButton: true });
651       },
652       () => this.router.navigate(['/block/iscsi/targets'])
653     );
654   }
655
656   targetSettingsModal() {
657     const initialState = {
658       target_controls: this.targetForm.get('target_controls'),
659       target_default_controls: this.target_default_controls
660     };
661
662     this.modalRef = this.modalService.show(IscsiTargetIqnSettingsModalComponent, { initialState });
663   }
664
665   imageSettingsModal(image) {
666     const initialState = {
667       imagesSettings: this.imagesSettings,
668       image: image,
669       disk_default_controls: this.disk_default_controls,
670       backstores: this.getValidBackstores(this.getImageById(image))
671     };
672
673     this.modalRef = this.modalService.show(IscsiTargetImageSettingsModalComponent, {
674       initialState
675     });
676   }
677
678   validFeatures(image, backstore) {
679     const imageFeatures = image.features;
680     const requiredFeatures = this.required_rbd_features[backstore];
681     const supportedFeatures = this.supported_rbd_features[backstore];
682     // tslint:disable-next-line:no-bitwise
683     const validRequiredFeatures = (imageFeatures & requiredFeatures) === requiredFeatures;
684     // tslint:disable-next-line:no-bitwise
685     const validSupportedFeatures = (imageFeatures & supportedFeatures) === imageFeatures;
686     return validRequiredFeatures && validSupportedFeatures;
687   }
688
689   getImageById(imageId) {
690     return this.imagesAll.find((image) => imageId === `${image.pool_name}/${image.name}`);
691   }
692
693   getValidBackstores(image) {
694     return this.backstores.filter((backstore) => this.validFeatures(image, backstore));
695   }
696 }