@Endpoint(path='/sync-policy')
@EndpointDoc("Get the sync policy")
@ReadPermission
- def get_sync_policy(self, bucket_name='', zonegroup_name=''):
+ def get_sync_policy(self, bucket_name='', zonegroup_name='', all_policy=None):
multisite_instance = RgwMultisite()
+ all_policy = str_to_bool(all_policy)
+ if all_policy:
+ sync_policy_list = []
+ buckets = json.loads(RgwBucket().list(stats=False))
+ for bucket in buckets:
+ sync_policy = multisite_instance.get_sync_policy(bucket, zonegroup_name)
+ for policy in sync_policy['groups']:
+ policy['bucketName'] = bucket
+ sync_policy_list.append(policy)
+ other_sync_policy = multisite_instance.get_sync_policy(bucket_name, zonegroup_name)
+ for policy in other_sync_policy['groups']:
+ sync_policy_list.append(policy)
+ return sync_policy_list
return multisite_instance.get_sync_policy(bucket_name, zonegroup_name)
@Endpoint(path='/sync-policy-group')
--- /dev/null
+import { MultisitePageHelper } from './multisite.po';
+
+describe('Multisite page', () => {
+ const multisite = new MultisitePageHelper();
+
+ beforeEach(() => {
+ cy.login();
+ multisite.navigateTo();
+ });
+
+ describe('tabs and table tests', () => {
+ it('should show two tabs', () => {
+ multisite.getTabsCount().should('eq', 2);
+ });
+
+ it('should show Configuration tab as a first tab', () => {
+ multisite.getTabText(0).should('eq', 'Configuration');
+ });
+
+ it('should show sync policy tab as a second tab', () => {
+ multisite.getTabText(1).should('eq', 'Sync Policy');
+ });
+
+ it('should show empty table in Sync Policy page', () => {
+ multisite.getTab('Sync Policy').click();
+ multisite.getDataTables().should('exist');
+ multisite.getTableCount('total').should('eq', 0);
+ });
+ });
+});
--- /dev/null
+import { PageHelper } from '../page-helper.po';
+
+const pages = {
+ index: { url: '#/rgw/multisite', id: 'cd-rgw-multisite-details' }
+};
+export class MultisitePageHelper extends PageHelper {
+ pages = pages;
+}
},
setupFiles: ['jest-canvas-mock'],
coverageReporters: ['cobertura', 'html'],
- modulePathIgnorePatterns: ['<rootDir>/coverage/', '<rootDir>/node_modules/simplebar-angular'],
+ modulePathIgnorePatterns: ['<rootDir>/coverage/', '<rootDir>/node_modules/simplebar-angular', '<rootDir>/cypress'],
testMatch: ['**/*.spec.ts'],
testRunner: 'jest-jasmine2'
};
-<div class="row">
- <div class="col-sm-12 col-lg-12">
- <div>
- <cd-alert-panel *ngIf="!rgwModuleStatus"
- type="info"
- spacingClass="mb-3"
- class="d-flex align-items-center"
- i18n>In order to access the import/export feature, the rgw module must be enabled
+<nav ngbNav
+ #nav="ngbNav"
+ class="nav-tabs"
+ (navChange)="onNavChange($event)">
+ <ng-container ngbNavItem="configuration">
+ <a ngbNavLink
+ i18n>Configuration</a>
+ <ng-template ngbNavContent>
+ <div>
+ <cd-alert-panel
+ *ngIf="!rgwModuleStatus"
+ type="info"
+ spacingClass="mb-3"
+ class="d-flex align-items-center"
+ i18n
+ >In order to access the import/export feature, the rgw module must be enabled
- <button class="btn btn-light mx-2"
- type="button"
- (click)="enableRgwModule()">Enable</button>
- </cd-alert-panel>
- <cd-alert-panel *ngIf="restartGatewayMessage"
- type="warning"
- spacingClass="mb-3"
- i18n>Please restart all Ceph Object Gateway instances in all zones to ensure consistent multisite configuration updates.
- <a class="text-decoration-underline"
- routerLink="/services">
- Cluster->Services</a>
- </cd-alert-panel>
- <cd-table-actions class="btn-group mb-4 me-2"
- [permission]="permission"
- [selection]="selection"
- [tableActions]="createTableActions">
- </cd-table-actions>
- <span *ngIf="showMigrateAction">
- <cd-table-actions class="btn-group mb-4 me-2 secondary"
- [permission]="permission"
- [btnColor]="'light'"
- [selection]="selection"
- [tableActions]="migrateTableAction">
+ <button class="btn btn-light mx-2"
+ type="button"
+ (click)="enableRgwModule()">
+ Enable
+ </button>
+ </cd-alert-panel>
+ <cd-alert-panel
+ *ngIf="restartGatewayMessage"
+ type="warning"
+ spacingClass="mb-3"
+ i18n>Please restart all Ceph Object Gateway instances in all zones to ensure consistent
+ multisite configuration updates.
+ <a class="text-decoration-underline"
+ routerLink="/services"> Cluster->Services</a>
+ </cd-alert-panel>
+ <cd-table-actions
+ class="btn-group mb-4 me-2"
+ [permission]="permission"
+ [selection]="selection"
+ [tableActions]="createTableActions"
+ >
</cd-table-actions>
- </span>
- <cd-table-actions class="btn-group mb-4 me-2"
- [permission]="permission"
- [btnColor]="'light'"
- [selection]="selection"
- [tableActions]="importAction">
- </cd-table-actions>
- <cd-table-actions class="btn-group mb-4 me-2"
- [permission]="permission"
- [btnColor]="'light'"
- [selection]="selection"
- [tableActions]="exportAction">
- </cd-table-actions>
- </div>
- <div class="card">
- <div class="card-header"
- i18n>Topology Viewer</div>
- <div class="card-body">
- <div class="row">
- <div class="col-sm-6 col-lg-6 tree-container">
- <i *ngIf="loadingIndicator"
- [ngClass]="[icons.large, icons.spinner, icons.spin]"></i>
- <tree-root #tree
- [nodes]="nodes"
- [options]="treeOptions"
- (updateData)="onUpdateData()">
- <ng-template #treeNodeTemplate
- let-node>
- <span *ngIf="node.data.name"
- class="me-3">
- <span *ngIf="(node.data.show_warning)">
- <i class="text-danger"
+ <span *ngIf="showMigrateAction">
+ <cd-table-actions
+ class="btn-group mb-4 me-2 secondary"
+ [permission]="permission"
+ [btnColor]="'light'"
+ [selection]="selection"
+ [tableActions]="migrateTableAction"
+ >
+ </cd-table-actions>
+ </span>
+ <cd-table-actions
+ class="btn-group mb-4 me-2"
+ [permission]="permission"
+ [btnColor]="'light'"
+ [selection]="selection"
+ [tableActions]="importAction"
+ >
+ </cd-table-actions>
+ <cd-table-actions
+ class="btn-group mb-4 me-2"
+ [permission]="permission"
+ [btnColor]="'light'"
+ [selection]="selection"
+ [tableActions]="exportAction">
+ </cd-table-actions>
+ </div>
+ <div class="card">
+ <div class="card-header"
+ i18n>Topology Viewer</div>
+ <div class="card-body">
+ <div class="row">
+ <div class="col-sm-6 col-lg-6 tree-container">
+ <i *ngIf="loadingIndicator"
+ [ngClass]="[icons.large, icons.spinner, icons.spin]"></i>
+ <tree-root
+ #tree
+ [nodes]="nodes"
+ [options]="treeOptions"
+ (updateData)="onUpdateData()">
+ <ng-template
+ #treeNodeTemplate
+ let-node>
+ <span *ngIf="node.data.name"
+ class="me-3">
+ <span *ngIf="node.data.show_warning">
+ <i
+ class="text-danger"
i18n-title
[title]="node.data.warning_message"
- [ngClass]="icons.danger"></i>
- </span>
- <i [ngClass]="node.data.icon"></i>
+ [ngClass]="icons.danger"
+ ></i>
+ </span>
+ <i [ngClass]="node.data.icon"></i>
{{ node.data.name }}
- </span>
- <span class="badge badge-success me-2"
- *ngIf="node.data.is_default">
- default
- </span>
- <span class="badge badge-warning me-2"
- *ngIf="node.data.is_master">
- master
- </span>
- <span class="badge badge-warning me-2"
- *ngIf="node.data.secondary_zone">
- secondary-zone
- </span>
- <div class="btn-group align-inline-btns"
- *ngIf="node.isFocused"
- role="group">
- <div [title]="editTitle"
- i18n-title>
- <button type="button"
- class="btn btn-light dropdown-toggle-split ms-1"
- (click)="openModal(node, true)"
- [disabled]="getDisable() || node.data.secondary_zone">
- <i [ngClass]="[icons.edit]"></i>
- </button>
- </div>
- <div [title]="deleteTitle"
- i18n-title>
- <button type="button"
- class="btn btn-light ms-1"
- [disabled]="isDeleteDisabled(node) || node.data.secondary_zone"
- (click)="delete(node)">
- <i [ngClass]="[icons.destroy]"></i>
- </button>
+ </span>
+ <span class="badge badge-success me-2"
+ *ngIf="node.data.is_default">
+ default
+ </span>
+ <span class="badge badge-warning me-2"
+ *ngIf="node.data.is_master"> master </span>
+ <span class="badge badge-warning me-2"
+ *ngIf="node.data.secondary_zone">
+ secondary-zone
+ </span>
+ <div class="btn-group align-inline-btns"
+ *ngIf="node.isFocused"
+ role="group">
+ <div [title]="editTitle"
+ i18n-title>
+ <button
+ type="button"
+ class="btn btn-light dropdown-toggle-split ms-1"
+ (click)="openModal(node, true)"
+ [disabled]="getDisable() || node.data.secondary_zone">
+ <i [ngClass]="[icons.edit]"></i>
+ </button>
+ </div>
+ <div [title]="deleteTitle"
+ i18n-title>
+ <button
+ type="button"
+ class="btn btn-light ms-1"
+ [disabled]="isDeleteDisabled(node) || node.data.secondary_zone"
+ (click)="delete(node)">
+ <i [ngClass]="[icons.destroy]"></i>
+ </button>
+ </div>
</div>
- </div>
- </ng-template>
- </tree-root>
- </div>
- <div class="col-sm-6 col-lg-6 metadata"
- *ngIf="metadata">
- <legend>{{ metadataTitle }}</legend>
- <div>
- <cd-table-key-value cdTableDetail
- [data]="metadata">
- </cd-table-key-value>
+ </ng-template>
+ </tree-root>
+ </div>
+ <div class="col-sm-6 col-lg-6 metadata"
+ *ngIf="metadata">
+ <legend>{{ metadataTitle }}</legend>
+ <div>
+ <cd-table-key-value
+ cdTableDetail
+ [data]="metadata"></cd-table-key-value>
+ </div>
</div>
</div>
</div>
</div>
- </div>
- </div>
-</div>
+ </ng-template>
+ </ng-container>
+ <ng-container ngbNavItem="syncPolicy">
+ <a ngbNavLink
+ i18n>Sync Policy</a>
+ <ng-template ngbNavContent>
+ <cd-rgw-multisite-sync-policy></cd-rgw-multisite-sync-policy>
+ </ng-template>
+ </ng-container>
+</nav>
+
+<div [ngbNavOutlet]="nav"></div>
import { RgwMultisiteDetailsComponent } from './rgw-multisite-details.component';
import { RouterTestingModule } from '@angular/router/testing';
import { configureTestBed } from '~/testing/unit-test-helper';
+import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap';
describe('RgwMultisiteDetailsComponent', () => {
let component: RgwMultisiteDetailsComponent;
TreeModule,
SharedModule,
ToastrModule.forRoot(),
- RouterTestingModule
+ RouterTestingModule,
+ NgbNavModule
]
});
}
});
}
-
/* setConfigValues() {
this.rgwDaemonService
.setMultisiteConfig(
}
);
}
+
+ onNavChange(event: any) {
+ if (event.nextId == 'configuration') {
+ this.metadata = null;
+ /*
+ It is a known issue with angular2-tree package when tree is hidden (for example inside tab or modal),
+ it is not rendered when it becomes visible. Solution is to call this.tree.sizeChanged() which recalculates
+ the rendered nodes according to the actual viewport size. (https://angular2-tree.readme.io/docs/common-issues)
+ */
+ setTimeout(() => {
+ this.tree.sizeChanged();
+ this.onUpdateData();
+ }, 200);
+ }
+ }
}
--- /dev/null
+ <legend i18n>
+ Multisite Sync Policy
+ <cd-help-text>
+ Multisite bucket-granularity sync policy provides fine grained control of data movement between buckets in different zones.
+ </cd-help-text>
+ </legend>
+ <cd-table #table
+ [data]="syncPolicyData"
+ [columns]="columns"
+ columnMode="flex"
+ selectionType="single"
+ [searchableObjects]="true"
+ [hasDetails]="false"
+ [serverSide]="false"
+ [count]="0"
+ [maxLimit]="25"
+ [toolHeader]="true">
+ </cd-table>
--- /dev/null
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { RgwMultisiteSyncPolicyComponent } from './rgw-multisite-sync-policy.component';
+import { HttpClientModule } from '@angular/common/http';
+import { TitleCasePipe } from '@angular/common';
+
+describe('RgwMultisiteSyncPolicyComponent', () => {
+ let component: RgwMultisiteSyncPolicyComponent;
+ let fixture: ComponentFixture<RgwMultisiteSyncPolicyComponent>;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [RgwMultisiteSyncPolicyComponent],
+ imports: [HttpClientModule],
+ providers: [TitleCasePipe]
+ }).compileComponents();
+
+ fixture = TestBed.createComponent(RgwMultisiteSyncPolicyComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
--- /dev/null
+import { TitleCasePipe } from '@angular/common';
+import { Component, OnInit } from '@angular/core';
+import { RgwMultisiteService } from '~/app/shared/api/rgw-multisite.service';
+import { CellTemplate } from '~/app/shared/enum/cell-template.enum';
+import { CdTableColumn } from '~/app/shared/models/cd-table-column';
+
+@Component({
+ selector: 'cd-rgw-multisite-sync-policy',
+ templateUrl: './rgw-multisite-sync-policy.component.html',
+ styleUrls: ['./rgw-multisite-sync-policy.component.scss']
+})
+export class RgwMultisiteSyncPolicyComponent implements OnInit {
+ columns: Array<CdTableColumn> = [];
+ syncPolicyData: any = [];
+
+ constructor(
+ private rgwMultisiteService: RgwMultisiteService,
+ private titleCasePipe: TitleCasePipe
+ ) {}
+
+ ngOnInit(): void {
+ this.columns = [
+ {
+ name: $localize`Group Name`,
+ prop: 'groupName',
+ flexGrow: 1
+ },
+ {
+ name: $localize`Status`,
+ prop: 'status',
+ flexGrow: 1,
+ cellTransformation: CellTemplate.tooltip,
+ customTemplateConfig: {
+ map: {
+ Enabled: { class: 'badge-success', tooltip: 'sync is allowed and enabled' },
+ Allowed: { class: 'badge-info', tooltip: 'sync is allowed' },
+ Forbidden: {
+ class: 'badge-warning',
+ tooltip:
+ 'sync (as defined by this group) is not allowed and can override other groups'
+ }
+ }
+ },
+ pipe: this.titleCasePipe
+ },
+ {
+ name: $localize`Zonegroup`,
+ prop: 'zonegroup',
+ flexGrow: 1
+ },
+ {
+ name: $localize`Bucket`,
+ prop: 'bucket',
+ flexGrow: 1
+ }
+ ];
+
+ this.rgwMultisiteService
+ .getSyncPolicy('', '', true)
+ .subscribe((allSyncPolicyData: Array<Object>) => {
+ if (allSyncPolicyData && allSyncPolicyData.length > 0) {
+ allSyncPolicyData.forEach((policy) => {
+ this.syncPolicyData.push({
+ groupName: policy['id'],
+ status: policy['status'],
+ bucket: policy['bucketName'],
+ zonegroup: ''
+ });
+ });
+ this.syncPolicyData = [...this.syncPolicyData];
+ }
+ });
+ }
+}
-import { CommonModule } from '@angular/common';
+import { CommonModule, TitleCasePipe } from '@angular/common';
import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { RouterModule, Routes } from '@angular/router';
import { BucketTagModalComponent } from './bucket-tag-modal/bucket-tag-modal.component';
import { NfsListComponent } from '../nfs/nfs-list/nfs-list.component';
import { NfsFormComponent } from '../nfs/nfs-form/nfs-form.component';
+import { RgwMultisiteSyncPolicyComponent } from './rgw-multisite-sync-policy/rgw-multisite-sync-policy.component';
@NgModule({
imports: [
RgwSyncPrimaryZoneComponent,
RgwSyncMetadataInfoComponent,
RgwSyncDataInfoComponent,
- BucketTagModalComponent
- ]
+ BucketTagModalComponent,
+ RgwMultisiteSyncPolicyComponent
+ ],
+ providers: [TitleCasePipe]
})
export class RgwModule {}
--- /dev/null
+import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
+import { TestBed } from '@angular/core/testing';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { RgwMultisiteService } from './rgw-multisite.service';
+
+const mockSyncPolicyData: any = [
+ {
+ id: 'test',
+ data_flow: {},
+ pipes: [],
+ status: 'enabled',
+ bucketName: 'test'
+ },
+ {
+ id: 'test',
+ data_flow: {},
+ pipes: [],
+ status: 'enabled'
+ }
+];
+
+describe('RgwMultisiteService', () => {
+ let service: RgwMultisiteService;
+ let httpTesting: HttpTestingController;
+
+ configureTestBed({
+ providers: [RgwMultisiteService],
+ imports: [HttpClientTestingModule]
+ });
+
+ beforeEach(() => {
+ service = TestBed.inject(RgwMultisiteService);
+ httpTesting = TestBed.inject(HttpTestingController);
+ });
+
+ afterEach(() => {
+ httpTesting.verify();
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+
+ it('should fetch all the sync policy related or un-related to a bucket', () => {
+ service.getSyncPolicy('', '', true).subscribe();
+ const req = httpTesting.expectOne('api/rgw/multisite/sync-policy?all_policy=true');
+ expect(req.request.method).toBe('GET');
+ req.flush(mockSyncPolicyData);
+ });
+});
status() {
return this.http.get(`${this.uiUrl}/status`);
}
+
+ getSyncPolicy(bucketName?: string, zonegroup?: string, fetchAllPolicy = false) {
+ let params = new HttpParams();
+ if (bucketName) {
+ params = params.append('bucket_name', bucketName);
+ }
+ if (zonegroup) {
+ params = params.append('zonegroup_name', zonegroup);
+ }
+ // fetchAllPolicy - if true, will fetch all the policy either linked or not linked with the buckets
+ params = params.append('all_policy', fetchAllPolicy);
+ return this.http.get(`${this.url}/sync-policy`, { params });
+ }
}
<span>{{ value | map:column?.customTemplateConfig }}</span>
</ng-template>
+<ng-template #tooltipTpl
+ let-column="column"
+ let-value="value">
+ <span *ngFor="let item of (value | array);">
+ <span
+ i18n
+ i18n-ngbTooltip
+ class="{{(column?.customTemplateConfig?.map && column?.customTemplateConfig?.map[item]?.class) ? column.customTemplateConfig.map[item].class : ''}}"
+ ngbTooltip="{{(column?.customTemplateConfig?.map && column?.customTemplateConfig?.map[item]?.tooltip) ? column.customTemplateConfig.map[item].tooltip : ''}}">
+ {{value}}
+ </span>
+ </span>
+</ng-template>
+
<ng-template #truncateTpl
let-column="column"
let-value="value">
rowSelectionTpl: TemplateRef<any>;
@ViewChild('pathTpl', { static: true })
pathTpl: TemplateRef<any>;
+ @ViewChild('tooltipTpl', { static: true })
+ tooltipTpl: TemplateRef<any>;
// This is the array with the items to be shown.
@Input()
this.cellTemplates.truncate = this.truncateTpl;
this.cellTemplates.timeAgo = this.timeAgoTpl;
this.cellTemplates.path = this.pathTpl;
+ this.cellTemplates.tooltip = this.tooltipTpl;
}
useCustomClass(value: any): string {
This template truncates a path to a shorter format and shows the whole path in a tooltip
eg: /var/lib/ceph/osd/ceph-0 -> /var/.../ceph-0
*/
- path = 'path'
+ path = 'path',
+ /*
+ This template is used to attach tooltip to the given column value
+ // {
+ // ...
+ // cellTransformation: CellTemplate.tooltip,
+ // customTemplateConfig: {
+ // map?: {
+ // [key: any]: { class?: string, tooltip: string }
+ // }
+ // }
+ */
+ tooltip = 'tooltip'
}
name: zonegroup_name
schema:
type: string
+ - allowEmptyValue: true
+ in: query
+ name: all_policy
+ schema:
+ type: string
responses:
'200':
content: