]> git.apps.os.sepia.ceph.com Git - ceph.git/blob
4b65b7e37bd4573cb38112ffaf05585269949e3e
[ceph.git] /
1 import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
2 import {
3   TreeComponent,
4   ITreeOptions,
5   TreeModel,
6   TreeNode,
7   TREE_ACTIONS
8 } from '@circlon/angular-tree-component';
9 import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
10 import _ from 'lodash';
11
12 import { forkJoin, Subscription, timer as observableTimer } from 'rxjs';
13 import { RgwRealmService } from '~/app/shared/api/rgw-realm.service';
14 import { RgwZoneService } from '~/app/shared/api/rgw-zone.service';
15 import { RgwZonegroupService } from '~/app/shared/api/rgw-zonegroup.service';
16 import { CriticalConfirmationModalComponent } from '~/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
17 import { ActionLabelsI18n, TimerServiceInterval } from '~/app/shared/constants/app.constants';
18 import { Icons } from '~/app/shared/enum/icons.enum';
19 import { NotificationType } from '~/app/shared/enum/notification-type.enum';
20 import { CdTableAction } from '~/app/shared/models/cd-table-action';
21 import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
22 import { Permission } from '~/app/shared/models/permissions';
23 import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
24 import { ModalService } from '~/app/shared/services/modal.service';
25 import { NotificationService } from '~/app/shared/services/notification.service';
26 import { TimerService } from '~/app/shared/services/timer.service';
27 import { RgwRealm, RgwZone, RgwZonegroup } from '../models/rgw-multisite';
28 import { RgwMultisiteMigrateComponent } from '../rgw-multisite-migrate/rgw-multisite-migrate.component';
29 import { RgwMultisiteZoneDeletionFormComponent } from '../models/rgw-multisite-zone-deletion-form/rgw-multisite-zone-deletion-form.component';
30 import { RgwMultisiteZonegroupDeletionFormComponent } from '../models/rgw-multisite-zonegroup-deletion-form/rgw-multisite-zonegroup-deletion-form.component';
31 import { RgwMultisiteExportComponent } from '../rgw-multisite-export/rgw-multisite-export.component';
32 import { RgwMultisiteImportComponent } from '../rgw-multisite-import/rgw-multisite-import.component';
33 import { RgwMultisiteRealmFormComponent } from '../rgw-multisite-realm-form/rgw-multisite-realm-form.component';
34 import { RgwMultisiteZoneFormComponent } from '../rgw-multisite-zone-form/rgw-multisite-zone-form.component';
35 import { RgwMultisiteZonegroupFormComponent } from '../rgw-multisite-zonegroup-form/rgw-multisite-zonegroup-form.component';
36 import { RgwDaemonService } from '~/app/shared/api/rgw-daemon.service';
37 import { MgrModuleService } from '~/app/shared/api/mgr-module.service';
38 import { BlockUI, NgBlockUI } from 'ng-block-ui';
39 import { Router } from '@angular/router';
40
41 @Component({
42   selector: 'cd-rgw-multisite-details',
43   templateUrl: './rgw-multisite-details.component.html',
44   styleUrls: ['./rgw-multisite-details.component.scss']
45 })
46 export class RgwMultisiteDetailsComponent implements OnDestroy, OnInit {
47   private sub = new Subscription();
48
49   @ViewChild('tree') tree: TreeComponent;
50
51   messages = {
52     noDefaultRealm: $localize`Please create a default realm first to enable this feature`,
53     noMasterZone: $localize`Please create a master zone for each zone group to enable this feature`,
54     noRealmExists: $localize`No realm exists`,
55     disableExport: $localize`Please create master zone group and master zone for each of the realms`
56   };
57
58   @BlockUI()
59   blockUI: NgBlockUI;
60
61   icons = Icons;
62   permission: Permission;
63   selection = new CdTableSelection();
64   createTableActions: CdTableAction[];
65   migrateTableAction: CdTableAction[];
66   importAction: CdTableAction[];
67   exportAction: CdTableAction[];
68   loadingIndicator = true;
69   nodes: object[] = [];
70   treeOptions: ITreeOptions = {
71     useVirtualScroll: true,
72     nodeHeight: 22,
73     levelPadding: 20,
74     actionMapping: {
75       mouse: {
76         click: this.onNodeSelected.bind(this)
77       }
78     }
79   };
80   modalRef: NgbModalRef;
81
82   realms: RgwRealm[] = [];
83   zonegroups: RgwZonegroup[] = [];
84   zones: RgwZone[] = [];
85   metadata: any;
86   metadataTitle: string;
87   bsModalRef: NgbModalRef;
88   realmIds: string[] = [];
89   zoneIds: string[] = [];
90   defaultRealmId = '';
91   defaultZonegroupId = '';
92   defaultZoneId = '';
93   multisiteInfo: object[] = [];
94   defaultsInfo: string[] = [];
95   showMigrateAction: boolean = false;
96   editTitle: string = 'Edit';
97   deleteTitle: string = 'Delete';
98   disableExport = true;
99   rgwModuleStatus: boolean;
100   restartGatewayMessage = false;
101   rgwModuleData: string | any[] = [];
102   activeId: string;
103
104   constructor(
105     private modalService: ModalService,
106     private timerService: TimerService,
107     private authStorageService: AuthStorageService,
108     public actionLabels: ActionLabelsI18n,
109     public timerServiceVariable: TimerServiceInterval,
110     public router: Router,
111     public rgwRealmService: RgwRealmService,
112     public rgwZonegroupService: RgwZonegroupService,
113     public rgwZoneService: RgwZoneService,
114     public rgwDaemonService: RgwDaemonService,
115     public mgrModuleService: MgrModuleService,
116     private notificationService: NotificationService
117   ) {
118     this.permission = this.authStorageService.getPermissions().rgw;
119     const activeId = this.router.getCurrentNavigation()?.extras?.state?.activeId;
120     if (activeId) {
121       this.activeId = activeId;
122     }
123   }
124
125   openModal(entity: any, edit = false) {
126     const entityName = edit ? entity.data.type : entity;
127     const action = edit ? 'edit' : 'create';
128     const initialState = {
129       resource: entityName,
130       action: action,
131       info: entity,
132       defaultsInfo: this.defaultsInfo,
133       multisiteInfo: this.multisiteInfo
134     };
135     if (entityName === 'realm') {
136       this.bsModalRef = this.modalService.show(RgwMultisiteRealmFormComponent, initialState, {
137         size: 'lg'
138       });
139     } else if (entityName === 'zonegroup') {
140       this.bsModalRef = this.modalService.show(RgwMultisiteZonegroupFormComponent, initialState, {
141         size: 'lg'
142       });
143     } else {
144       this.bsModalRef = this.modalService.show(RgwMultisiteZoneFormComponent, initialState, {
145         size: 'lg'
146       });
147     }
148   }
149
150   openMigrateModal() {
151     const initialState = {
152       multisiteInfo: this.multisiteInfo
153     };
154     this.bsModalRef = this.modalService.show(RgwMultisiteMigrateComponent, initialState, {
155       size: 'lg'
156     });
157   }
158
159   openImportModal() {
160     const initialState = {
161       multisiteInfo: this.multisiteInfo
162     };
163     this.bsModalRef = this.modalService.show(RgwMultisiteImportComponent, initialState, {
164       size: 'lg'
165     });
166   }
167
168   openExportModal() {
169     const initialState = {
170       defaultsInfo: this.defaultsInfo,
171       multisiteInfo: this.multisiteInfo
172     };
173     this.bsModalRef = this.modalService.show(RgwMultisiteExportComponent, initialState, {
174       size: 'lg'
175     });
176   }
177
178   getDisableExport() {
179     this.realms.forEach((realm: any) => {
180       this.zonegroups.forEach((zonegroup) => {
181         if (realm.id === zonegroup.realm_id) {
182           if (zonegroup.is_master && zonegroup.master_zone !== '') {
183             this.disableExport = false;
184           }
185         }
186       });
187     });
188     if (!this.rgwModuleStatus) {
189       return true;
190     }
191     if (this.realms.length < 1) {
192       return this.messages.noRealmExists;
193     } else if (this.disableExport) {
194       return this.messages.disableExport;
195     } else {
196       return false;
197     }
198   }
199
200   getDisableImport() {
201     if (!this.rgwModuleStatus) {
202       return true;
203     } else {
204       return false;
205     }
206   }
207
208   ngOnInit() {
209     const createRealmAction: CdTableAction = {
210       permission: 'create',
211       icon: Icons.add,
212       name: this.actionLabels.CREATE + ' Realm',
213       click: () => this.openModal('realm')
214     };
215     const createZonegroupAction: CdTableAction = {
216       permission: 'create',
217       icon: Icons.add,
218       name: this.actionLabels.CREATE + ' Zone Group',
219       click: () => this.openModal('zonegroup'),
220       disable: () => this.getDisable()
221     };
222     const createZoneAction: CdTableAction = {
223       permission: 'create',
224       icon: Icons.add,
225       name: this.actionLabels.CREATE + ' Zone',
226       click: () => this.openModal('zone')
227     };
228     const migrateMultsiteAction: CdTableAction = {
229       permission: 'read',
230       icon: Icons.exchange,
231       name: this.actionLabels.MIGRATE,
232       click: () => this.openMigrateModal()
233     };
234     const importMultsiteAction: CdTableAction = {
235       permission: 'read',
236       icon: Icons.download,
237       name: this.actionLabels.IMPORT,
238       click: () => this.openImportModal(),
239       disable: () => this.getDisableImport()
240     };
241     const exportMultsiteAction: CdTableAction = {
242       permission: 'read',
243       icon: Icons.upload,
244       name: this.actionLabels.EXPORT,
245       click: () => this.openExportModal(),
246       disable: () => this.getDisableExport()
247     };
248     this.createTableActions = [createRealmAction, createZonegroupAction, createZoneAction];
249     this.migrateTableAction = [migrateMultsiteAction];
250     this.importAction = [importMultsiteAction];
251     this.exportAction = [exportMultsiteAction];
252
253     const observables = [
254       this.rgwRealmService.getAllRealmsInfo(),
255       this.rgwZonegroupService.getAllZonegroupsInfo(),
256       this.rgwZoneService.getAllZonesInfo()
257     ];
258     this.sub = this.timerService
259       .get(() => forkJoin(observables), this.timerServiceVariable.TIMER_SERVICE_PERIOD * 2)
260       .subscribe(
261         (multisiteInfo: [object, object, object]) => {
262           this.multisiteInfo = multisiteInfo;
263           this.loadingIndicator = false;
264           this.nodes = this.abstractTreeData(multisiteInfo);
265         },
266         (_error) => {}
267       );
268     this.mgrModuleService.list().subscribe((moduleData: any) => {
269       this.rgwModuleData = moduleData.filter((module: object) => module['name'] === 'rgw');
270       if (this.rgwModuleData.length > 0) {
271         this.rgwModuleStatus = this.rgwModuleData[0].enabled;
272       }
273     });
274   }
275   /* setConfigValues() {
276     this.rgwDaemonService
277       .setMultisiteConfig(
278         this.defaultsInfo['defaultRealmName'],
279         this.defaultsInfo['defaultZonegroupName'],
280         this.defaultsInfo['defaultZoneName']
281       )
282       .subscribe(() => {});
283   }*/
284
285   ngOnDestroy() {
286     this.sub.unsubscribe();
287   }
288
289   private abstractTreeData(multisiteInfo: [object, object, object]): any[] {
290     let allNodes: object[] = [];
291     let rootNodes = {};
292     let firstChildNodes = {};
293     let allFirstChildNodes = [];
294     let secondChildNodes = {};
295     let allSecondChildNodes: {}[] = [];
296     this.realms = multisiteInfo[0]['realms'];
297     this.zonegroups = multisiteInfo[1]['zonegroups'];
298     this.zones = multisiteInfo[2]['zones'];
299     this.defaultRealmId = multisiteInfo[0]['default_realm'];
300     this.defaultZonegroupId = multisiteInfo[1]['default_zonegroup'];
301     this.defaultZoneId = multisiteInfo[2]['default_zone'];
302     this.defaultsInfo = this.getDefaultsEntities(
303       this.defaultRealmId,
304       this.defaultZonegroupId,
305       this.defaultZoneId
306     );
307     if (this.realms.length > 0) {
308       // get tree for realm -> zonegroup -> zone
309       for (const realm of this.realms) {
310         const result = this.rgwRealmService.getRealmTree(realm, this.defaultRealmId);
311         rootNodes = result['nodes'];
312         this.realmIds = this.realmIds.concat(result['realmIds']);
313         for (const zonegroup of this.zonegroups) {
314           if (zonegroup.realm_id === realm.id) {
315             firstChildNodes = this.rgwZonegroupService.getZonegroupTree(
316               zonegroup,
317               this.defaultZonegroupId,
318               realm
319             );
320             for (const zone of zonegroup.zones) {
321               const zoneResult = this.rgwZoneService.getZoneTree(
322                 zone,
323                 this.defaultZoneId,
324                 this.zones,
325                 zonegroup,
326                 realm
327               );
328               secondChildNodes = zoneResult['nodes'];
329               this.zoneIds = this.zoneIds.concat(zoneResult['zoneIds']);
330               allSecondChildNodes.push(secondChildNodes);
331               secondChildNodes = {};
332             }
333             firstChildNodes['children'] = allSecondChildNodes;
334             allSecondChildNodes = [];
335             allFirstChildNodes.push(firstChildNodes);
336             firstChildNodes = {};
337           }
338         }
339         rootNodes['children'] = allFirstChildNodes;
340         allNodes.push(rootNodes);
341         firstChildNodes = {};
342         secondChildNodes = {};
343         rootNodes = {};
344         allFirstChildNodes = [];
345         allSecondChildNodes = [];
346       }
347     }
348     if (this.zonegroups.length > 0) {
349       // get tree for zonegroup -> zone (standalone zonegroups that don't match a realm eg(initial default))
350       for (const zonegroup of this.zonegroups) {
351         if (!this.realmIds.includes(zonegroup.realm_id)) {
352           rootNodes = this.rgwZonegroupService.getZonegroupTree(zonegroup, this.defaultZonegroupId);
353           for (const zone of zonegroup.zones) {
354             const zoneResult = this.rgwZoneService.getZoneTree(
355               zone,
356               this.defaultZoneId,
357               this.zones,
358               zonegroup
359             );
360             firstChildNodes = zoneResult['nodes'];
361             this.zoneIds = this.zoneIds.concat(zoneResult['zoneIds']);
362             allFirstChildNodes.push(firstChildNodes);
363             firstChildNodes = {};
364           }
365           rootNodes['children'] = allFirstChildNodes;
366           allNodes.push(rootNodes);
367           firstChildNodes = {};
368           rootNodes = {};
369           allFirstChildNodes = [];
370         }
371       }
372     }
373     if (this.zones.length > 0) {
374       // get tree for standalone zones(zones that do not belong to a zonegroup)
375       for (const zone of this.zones) {
376         if (this.zoneIds.length > 0 && !this.zoneIds.includes(zone.id)) {
377           const zoneResult = this.rgwZoneService.getZoneTree(zone, this.defaultZoneId, this.zones);
378           rootNodes = zoneResult['nodes'];
379           allNodes.push(rootNodes);
380           rootNodes = {};
381         }
382       }
383     }
384     if (this.realms.length < 1 && this.zonegroups.length < 1 && this.zones.length < 1) {
385       return [
386         {
387           name: 'No nodes!'
388         }
389       ];
390     }
391     this.realmIds = [];
392     this.zoneIds = [];
393     this.getDisableMigrate();
394     this.rgwDaemonService.list().subscribe((data: any) => {
395       const realmName = data.map((item: { [x: string]: any }) => item['realm_name']);
396       if (
397         this.defaultRealmId != '' &&
398         this.defaultZonegroupId != '' &&
399         this.defaultZoneId != '' &&
400         realmName.includes('')
401       ) {
402         this.restartGatewayMessage = true;
403       }
404     });
405     return allNodes;
406   }
407
408   getDefaultsEntities(
409     defaultRealmId: string,
410     defaultZonegroupId: string,
411     defaultZoneId: string
412   ): any {
413     const defaultRealm = this.realms.find((x: { id: string }) => x.id === defaultRealmId);
414     const defaultZonegroup = this.zonegroups.find(
415       (x: { id: string }) => x.id === defaultZonegroupId
416     );
417     const defaultZone = this.zones.find((x: { id: string }) => x.id === defaultZoneId);
418     const defaultRealmName = defaultRealm !== undefined ? defaultRealm.name : null;
419     const defaultZonegroupName = defaultZonegroup !== undefined ? defaultZonegroup.name : null;
420     const defaultZoneName = defaultZone !== undefined ? defaultZone.name : null;
421     return {
422       defaultRealmName: defaultRealmName,
423       defaultZonegroupName: defaultZonegroupName,
424       defaultZoneName: defaultZoneName
425     };
426   }
427
428   onNodeSelected(tree: TreeModel, node: TreeNode) {
429     TREE_ACTIONS.ACTIVATE(tree, node, true);
430     this.metadataTitle = node.data.name;
431     this.metadata = node.data.info;
432     node.data.show = true;
433   }
434
435   onUpdateData() {
436     this.tree.treeModel.expandAll();
437   }
438
439   getDisable() {
440     let isMasterZone = true;
441     if (this.defaultRealmId === '') {
442       return this.messages.noDefaultRealm;
443     } else {
444       this.zonegroups.forEach((zgp: any) => {
445         if (_.isEmpty(zgp.master_zone)) {
446           isMasterZone = false;
447         }
448       });
449       if (!isMasterZone) {
450         this.editTitle =
451           'Please create a master zone for each existing zonegroup to enable this feature';
452         return this.messages.noMasterZone;
453       } else {
454         this.editTitle = 'Edit';
455         return false;
456       }
457     }
458   }
459
460   getDisableMigrate() {
461     if (
462       this.realms.length === 0 &&
463       this.zonegroups.length === 1 &&
464       this.zonegroups[0].name === 'default' &&
465       this.zones.length === 1 &&
466       this.zones[0].name === 'default'
467     ) {
468       this.showMigrateAction = true;
469     } else {
470       this.showMigrateAction = false;
471     }
472     return this.showMigrateAction;
473   }
474
475   isDeleteDisabled(node: TreeNode): boolean {
476     let disable: boolean = false;
477     let masterZonegroupCount: number = 0;
478     if (node.data.type === 'realm' && node.data.is_default && this.realms.length < 2) {
479       disable = true;
480     }
481
482     if (node.data.type === 'zonegroup') {
483       if (this.zonegroups.length < 2) {
484         this.deleteTitle = 'You can not delete the only zonegroup available';
485         disable = true;
486       } else if (node.data.is_default) {
487         this.deleteTitle = 'You can not delete the default zonegroup';
488         disable = true;
489       } else if (node.data.is_master) {
490         for (let zonegroup of this.zonegroups) {
491           if (zonegroup.is_master === true) {
492             masterZonegroupCount++;
493             if (masterZonegroupCount > 1) break;
494           }
495         }
496         if (masterZonegroupCount < 2) {
497           this.deleteTitle = 'You can not delete the only master zonegroup available';
498           disable = true;
499         }
500       }
501     }
502
503     if (node.data.type === 'zone') {
504       if (this.zones.length < 2) {
505         this.deleteTitle = 'You can not delete the only zone available';
506         disable = true;
507       } else if (node.data.is_default) {
508         this.deleteTitle = 'You can not delete the default zone';
509         disable = true;
510       } else if (node.data.is_master && node.data.zone_zonegroup.zones.length < 2) {
511         this.deleteTitle =
512           'You can not delete the master zone as there are no more zones in this zonegroup';
513         disable = true;
514       }
515     }
516
517     if (!disable) {
518       this.deleteTitle = 'Delete';
519     }
520
521     return disable;
522   }
523
524   delete(node: TreeNode) {
525     if (node.data.type === 'realm') {
526       this.modalRef = this.modalService.show(CriticalConfirmationModalComponent, {
527         itemDescription: $localize`${node.data.type} ${node.data.name}`,
528         itemNames: [`${node.data.name}`],
529         submitAction: () => {
530           this.rgwRealmService.delete(node.data.name).subscribe(
531             () => {
532               this.modalRef.close();
533               this.notificationService.show(
534                 NotificationType.success,
535                 $localize`Realm: '${node.data.name}' deleted successfully`
536               );
537             },
538             () => {
539               this.modalRef.componentInstance.stopLoadingSpinner();
540             }
541           );
542         }
543       });
544     } else if (node.data.type === 'zonegroup') {
545       this.modalRef = this.modalService.show(RgwMultisiteZonegroupDeletionFormComponent, {
546         zonegroup: node.data
547       });
548     } else if (node.data.type === 'zone') {
549       this.modalRef = this.modalService.show(RgwMultisiteZoneDeletionFormComponent, {
550         zone: node.data
551       });
552     }
553   }
554
555   enableRgwModule() {
556     let $obs;
557     const fnWaitUntilReconnected = () => {
558       observableTimer(2000).subscribe(() => {
559         // Trigger an API request to check if the connection is
560         // re-established.
561         this.mgrModuleService.list().subscribe(
562           () => {
563             // Resume showing the notification toasties.
564             this.notificationService.suspendToasties(false);
565             // Unblock the whole UI.
566             this.blockUI.stop();
567             // Reload the data table content.
568             this.notificationService.show(NotificationType.success, $localize`Enabled RGW Module`);
569             this.router.navigateByUrl('/', { skipLocationChange: true }).then(() => {
570               this.router.navigate(['/rgw/multisite']);
571             });
572             // Reload the data table content.
573           },
574           () => {
575             fnWaitUntilReconnected();
576           }
577         );
578       });
579     };
580
581     if (!this.rgwModuleStatus) {
582       $obs = this.mgrModuleService.enable('rgw');
583     }
584     $obs.subscribe(
585       () => undefined,
586       () => {
587         // Suspend showing the notification toasties.
588         this.notificationService.suspendToasties(true);
589         // Block the whole UI to prevent user interactions until
590         // the connection to the backend is reestablished
591         this.blockUI.start($localize`Reconnecting, please wait ...`);
592         fnWaitUntilReconnected();
593       }
594     );
595   }
596
597   onNavChange(event: any) {
598     if (event.nextId == 'configuration') {
599       this.metadata = null;
600       /*
601         It is a known issue with angular2-tree package when tree is hidden (for example inside tab or modal),
602         it is not rendered when it becomes visible. Solution is to call this.tree.sizeChanged() which recalculates
603         the rendered nodes according to the actual viewport size. (https://angular2-tree.readme.io/docs/common-issues)
604       */
605       setTimeout(() => {
606         this.tree.sizeChanged();
607         this.onUpdateData();
608       }, 200);
609     }
610   }
611 }