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