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