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