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