]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: add single stat cards to rgw overview dashboard
authorAashish Sharma <aasharma@li-e74156cc-2f67-11b2-a85c-e98659a63c5c.ibm.com>
Wed, 5 Jul 2023 14:11:55 +0000 (19:41 +0530)
committerAashish Sharma <aasharma@li-e74156cc-2f67-11b2-a85c-e98659a63c5c.ibm.com>
Thu, 20 Jul 2023 05:07:17 +0000 (10:37 +0530)
Signed-off-by: Aashish Sharma <aasharma@redhat.com>
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-v3.module.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-overview-dashboard/rgw-overview-dashboard.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-overview-dashboard/rgw-overview-dashboard.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-overview-dashboard/rgw-overview-dashboard.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-overview-dashboard/rgw-overview-dashboard.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.html
src/pybind/mgr/dashboard/services/cluster.py

index 2c3b4cc369fc73dd64d3c75c10958ad015170b73..6e55f98531dd1f50965bc07a732fa8a500cb9120 100644 (file)
@@ -42,6 +42,6 @@ import { PgSummaryPipe } from './pg-summary.pipe';
     DashboardTimeSelectorComponent
   ],
 
-  exports: [DashboardV3Component]
+  exports: [DashboardV3Component, CardComponent, CardRowComponent]
 })
 export class DashboardV3Module {}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-overview-dashboard/rgw-overview-dashboard.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-overview-dashboard/rgw-overview-dashboard.component.html
new file mode 100644 (file)
index 0000000..eaf7f47
--- /dev/null
@@ -0,0 +1,61 @@
+<div class="container-fluid">
+  <div class="row mx-0">
+    <cd-card cardTitle="Daemons"
+             i18n-title
+             link="/rgw/daemons"
+             class="col-sm-2 px-3 d-flex"
+             aria-label="Daemons card">
+      <span class="ms-4 me-4">
+        <h1 class="text-center">{{ rgwDaemonCount }}</h1>
+      </span>
+    </cd-card>
+
+    <cd-card cardTitle="Zoning"
+             i18n-title
+             class="col-sm-2 px-3 d-flex"
+             aria-label="Details card">
+      <span class="ms-4 me-4 text-center">
+        <h3>{{ rgwRealmCount }} Realms</h3>
+        <h3>{{ rgwZonegroupCount }} Zonegroups</h3>
+        <h3>{{ rgwZoneCount }} Zones</h3>
+      </span>
+    </cd-card>
+
+    <cd-card cardTitle="Buckets"
+             i18n-title
+             class="col-sm-2 px-3 d-flex"
+             aria-label="Details card">
+      <span class="ms-4 me-4 text-center">
+        <h2>{{ rgwBucketCount }} Buckets</h2>
+        <h2>{{ objectCount }} Objects</h2>
+      </span>
+    </cd-card>
+
+    <cd-card cardTitle="Users"
+             i18n-title
+             class="col-sm-2 px-3 d-flex"
+             aria-label="Details card">
+      <span class="ms-4 me-4 text-center">
+        <h1>{{ UserCount }}</h1>
+      </span>
+    </cd-card>
+
+    <cd-card cardTitle="Used Capacity"
+             i18n-title
+             class="col-sm-2 px-3 d-flex"
+             aria-label="Details card">
+      <span class="ms-4 me-4 text-center">
+        <h1>{{ totalPoolUsedBytes | dimlessBinary}}</h1>
+      </span>
+    </cd-card>
+
+    <cd-card cardTitle="Avg Object Size"
+             i18n-title
+             class="col-sm-2 px-3 d-flex"
+             aria-label="Details card">
+      <span class="ms-4 me-4 text-center">
+        <h1>{{ averageObjectSize | dimlessBinary}}</h1>
+      </span>
+    </cd-card>
+  </div>
+</div>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-overview-dashboard/rgw-overview-dashboard.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-overview-dashboard/rgw-overview-dashboard.component.scss
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-overview-dashboard/rgw-overview-dashboard.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-overview-dashboard/rgw-overview-dashboard.component.spec.ts
new file mode 100644 (file)
index 0000000..238ac78
--- /dev/null
@@ -0,0 +1,209 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { RgwOverviewDashboardComponent } from './rgw-overview-dashboard.component';
+import { of } from 'rxjs';
+import { RgwDaemonService } from '~/app/shared/api/rgw-daemon.service';
+import { RgwDaemon } from '../models/rgw-daemon';
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { CardComponent } from '../../dashboard-v3/card/card.component';
+import { CardRowComponent } from '../../dashboard-v3/card-row/card-row.component';
+import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe';
+import { RgwRealmService } from '~/app/shared/api/rgw-realm.service';
+import { RgwZonegroupService } from '~/app/shared/api/rgw-zonegroup.service';
+import { RgwZoneService } from '~/app/shared/api/rgw-zone.service';
+import { RgwBucketService } from '~/app/shared/api/rgw-bucket.service';
+import { RgwUserService } from '~/app/shared/api/rgw-user.service';
+import { HealthService } from '~/app/shared/api/health.service';
+
+describe('RgwOverviewDashboardComponent', () => {
+  let component: RgwOverviewDashboardComponent;
+  let fixture: ComponentFixture<RgwOverviewDashboardComponent>;
+  const daemon: RgwDaemon = {
+    id: '8000',
+    service_map_id: '4803',
+    version: 'ceph version',
+    server_hostname: 'ceph',
+    realm_name: 'realm1',
+    zonegroup_name: 'zg1-realm1',
+    zone_name: 'zone1-zg1-realm1',
+    default: true,
+    port: 80
+  };
+
+  const realmList = {
+    default_info: '20f61d29-7e45-4418-8e19-b7e962e4860b',
+    realms: ['realm2', 'realm1']
+  };
+
+  const zonegroupList = {
+    default_info: '20f61d29-7e45-4418-8e19-b7e962e4860b',
+    zonegroups: ['zg-1', 'zg-2', 'zg-3']
+  };
+
+  const zoneList = {
+    default_info: '20f61d29-7e45-4418-8e19-b7e962e4860b',
+    zones: ['zone4', 'zone5', 'zone6', 'zone7']
+  };
+
+  const bucketList = [
+    {
+      bucket: 'bucket',
+      owner: 'testid',
+      usage: {
+        'rgw.main': {
+          size_actual: 4,
+          num_objects: 2
+        },
+        'rgw.none': {
+          size_actual: 6,
+          num_objects: 6
+        }
+      },
+      bucket_quota: {
+        max_size: 20,
+        max_objects: 10,
+        enabled: true
+      }
+    },
+    {
+      bucket: 'bucket2',
+      owner: 'testid',
+      usage: {
+        'rgw.main': {
+          size_actual: 4,
+          num_objects: 2
+        },
+        'rgw.none': {
+          size_actual: 6,
+          num_objects: 6
+        }
+      },
+      bucket_quota: {
+        max_size: 20,
+        max_objects: 10,
+        enabled: true
+      }
+    }
+  ];
+
+  const userList = [
+    {
+      user_id: 'testid',
+      stats: {
+        size_actual: 6,
+        num_objects: 6
+      },
+      user_quota: {
+        max_size: 20,
+        max_objects: 10,
+        enabled: true
+      }
+    },
+    {
+      user_id: 'testid2',
+      stats: {
+        size_actual: 6,
+        num_objects: 6
+      },
+      user_quota: {
+        max_size: 20,
+        max_objects: 10,
+        enabled: true
+      }
+    }
+  ];
+
+  const healthData = {
+    total_objects: '290',
+    total_pool_bytes_used: 9338880
+  };
+
+  let listDaemonsSpy: jest.SpyInstance;
+  let listZonesSpy: jest.SpyInstance;
+  let listZonegroupsSpy: jest.SpyInstance;
+  let listRealmsSpy: jest.SpyInstance;
+  let listBucketsSpy: jest.SpyInstance;
+  let listUsersSpy: jest.SpyInstance;
+  let healthDataSpy: jest.SpyInstance;
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      declarations: [
+        RgwOverviewDashboardComponent,
+        CardComponent,
+        CardRowComponent,
+        DimlessBinaryPipe
+      ],
+      imports: [HttpClientTestingModule]
+    }).compileComponents();
+  });
+
+  beforeEach(() => {
+    listDaemonsSpy = jest
+      .spyOn(TestBed.inject(RgwDaemonService), 'list')
+      .mockReturnValue(of([daemon]));
+    listRealmsSpy = jest
+      .spyOn(TestBed.inject(RgwRealmService), 'list')
+      .mockReturnValue(of(realmList));
+    listZonegroupsSpy = jest
+      .spyOn(TestBed.inject(RgwZonegroupService), 'list')
+      .mockReturnValue(of(zonegroupList));
+    listZonesSpy = jest.spyOn(TestBed.inject(RgwZoneService), 'list').mockReturnValue(of(zoneList));
+    listBucketsSpy = jest
+      .spyOn(TestBed.inject(RgwBucketService), 'list')
+      .mockReturnValue(of(bucketList));
+    listUsersSpy = jest.spyOn(TestBed.inject(RgwUserService), 'list').mockReturnValue(of(userList));
+    healthDataSpy = jest
+      .spyOn(TestBed.inject(HealthService), 'getClusterCapacity')
+      .mockReturnValue(of(healthData));
+    fixture = TestBed.createComponent(RgwOverviewDashboardComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+
+  it('should render all cards', () => {
+    fixture.detectChanges();
+    const dashboardCards = fixture.debugElement.nativeElement.querySelectorAll('cd-card');
+    expect(dashboardCards.length).toBe(6);
+  });
+
+  it('should get corresponding data into Daemons', () => {
+    expect(listDaemonsSpy).toHaveBeenCalled();
+    expect(component.rgwDaemonCount).toEqual(1);
+  });
+
+  it('should get corresponding data into Realms', () => {
+    expect(listRealmsSpy).toHaveBeenCalled();
+    expect(component.rgwRealmCount).toEqual(2);
+  });
+
+  it('should get corresponding data into Zonegroups', () => {
+    expect(listZonegroupsSpy).toHaveBeenCalled();
+    expect(component.rgwZonegroupCount).toEqual(3);
+  });
+
+  it('should get corresponding data into Zones', () => {
+    expect(listZonesSpy).toHaveBeenCalled();
+    expect(component.rgwZoneCount).toEqual(4);
+  });
+
+  it('should get corresponding data into Buckets', () => {
+    expect(listBucketsSpy).toHaveBeenCalled();
+    expect(component.rgwBucketCount).toEqual(2);
+  });
+
+  it('should get corresponding data into Users', () => {
+    expect(listUsersSpy).toHaveBeenCalled();
+    expect(component.UserCount).toEqual(2);
+  });
+
+  it('should get corresponding data into Objects and capacity', () => {
+    expect(healthDataSpy).toHaveBeenCalled();
+    expect(component.objectCount).toEqual('290');
+    expect(component.totalPoolUsedBytes).toEqual(9338880);
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-overview-dashboard/rgw-overview-dashboard.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-overview-dashboard/rgw-overview-dashboard.component.ts
new file mode 100644 (file)
index 0000000..2614a81
--- /dev/null
@@ -0,0 +1,102 @@
+import { Component, OnDestroy, OnInit } from '@angular/core';
+
+import _ from 'lodash';
+import { Subscription } from 'rxjs';
+
+import { HealthService } from '~/app/shared/api/health.service';
+import { Permissions } from '~/app/shared/models/permissions';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import {
+  FeatureTogglesMap$,
+  FeatureTogglesService
+} from '~/app/shared/services/feature-toggles.service';
+import { RefreshIntervalService } from '~/app/shared/services/refresh-interval.service';
+import { RgwDaemonService } from '~/app/shared/api/rgw-daemon.service';
+import { RgwRealmService } from '~/app/shared/api/rgw-realm.service';
+import { RgwZoneService } from '~/app/shared/api/rgw-zone.service';
+import { RgwZonegroupService } from '~/app/shared/api/rgw-zonegroup.service';
+import { RgwBucketService } from '~/app/shared/api/rgw-bucket.service';
+import { RgwUserService } from '~/app/shared/api/rgw-user.service';
+
+@Component({
+  selector: 'cd-rgw-overview-dashboard',
+  templateUrl: './rgw-overview-dashboard.component.html',
+  styleUrls: ['./rgw-overview-dashboard.component.scss']
+})
+export class RgwOverviewDashboardComponent implements OnInit, OnDestroy {
+  interval = new Subscription();
+  permissions: Permissions;
+  enabledFeature$: FeatureTogglesMap$;
+  rgwDaemonCount = 0;
+  rgwRealmCount = 0;
+  rgwZonegroupCount = 0;
+  rgwZoneCount = 0;
+  rgwBucketCount = 0;
+  objectCount = 0;
+  UserCount = 0;
+  totalPoolUsedBytes = 0;
+  averageObjectSize = 0;
+  realmData: any;
+  daemonSub: Subscription;
+  realmSub: Subscription;
+  ZonegroupSub: Subscription;
+  ZoneSUb: Subscription;
+  UserSub: Subscription;
+  HealthSub: Subscription;
+  BucketSub: Subscription;
+
+  constructor(
+    private authStorageService: AuthStorageService,
+    private featureToggles: FeatureTogglesService,
+    private healthService: HealthService,
+    private refreshIntervalService: RefreshIntervalService,
+    private rgwDaemonService: RgwDaemonService,
+    private rgwRealmService: RgwRealmService,
+    private rgwZonegroupService: RgwZonegroupService,
+    private rgwZoneService: RgwZoneService,
+    private rgwBucketService: RgwBucketService,
+    private rgwUserService: RgwUserService
+  ) {
+    this.permissions = this.authStorageService.getPermissions();
+    this.enabledFeature$ = this.featureToggles.get();
+  }
+
+  ngOnInit() {
+    this.interval = this.refreshIntervalService.intervalData$.subscribe(() => {
+      this.daemonSub = this.rgwDaemonService.list().subscribe((data: any) => {
+        this.rgwDaemonCount = data.length;
+      });
+      this.realmSub = this.rgwRealmService.list().subscribe((data: any) => {
+        this.rgwRealmCount = data['realms'].length;
+      });
+      this.ZonegroupSub = this.rgwZonegroupService.list().subscribe((data: any) => {
+        this.rgwZonegroupCount = data['zonegroups'].length;
+      });
+      this.ZoneSUb = this.rgwZoneService.list().subscribe((data: any) => {
+        this.rgwZoneCount = data['zones'].length;
+      });
+      this.BucketSub = this.rgwBucketService.list().subscribe((data: any) => {
+        this.rgwBucketCount = data.length;
+      });
+      this.UserSub = this.rgwUserService.list().subscribe((data: any) => {
+        this.UserCount = data.length;
+      });
+      this.HealthSub = this.healthService.getClusterCapacity().subscribe((data: any) => {
+        this.objectCount = data['total_objects'];
+        this.totalPoolUsedBytes = data['total_pool_bytes_used'];
+        this.averageObjectSize = data['average_object_size'];
+      });
+    });
+  }
+
+  ngOnDestroy() {
+    this.interval.unsubscribe();
+    this.daemonSub.unsubscribe();
+    this.realmSub.unsubscribe();
+    this.ZonegroupSub.unsubscribe();
+    this.ZoneSUb.unsubscribe();
+    this.BucketSub.unsubscribe();
+    this.UserSub.unsubscribe();
+    this.HealthSub.unsubscribe();
+  }
+}
index a0082209b96d1fd52e933b8604afcef3adac4f61..fa0e72584b90019af9f3dccd16187ad7d531890a 100644 (file)
@@ -41,6 +41,8 @@ import { RgwMultisiteMigrateComponent } from './rgw-multisite-migrate/rgw-multis
 import { RgwMultisiteImportComponent } from './rgw-multisite-import/rgw-multisite-import.component';
 import { RgwMultisiteExportComponent } from './rgw-multisite-export/rgw-multisite-export.component';
 import { CreateRgwServiceEntitiesComponent } from './create-rgw-service-entities/create-rgw-service-entities.component';
+import { RgwOverviewDashboardComponent } from './rgw-overview-dashboard/rgw-overview-dashboard.component';
+import { DashboardV3Module } from '../dashboard-v3/dashboard-v3.module';
 
 @NgModule({
   imports: [
@@ -54,7 +56,8 @@ import { CreateRgwServiceEntitiesComponent } from './create-rgw-service-entities
     NgbTooltipModule,
     NgxPipeFunctionModule,
     TreeModule,
-    DataTableModule
+    DataTableModule,
+    DashboardV3Module
   ],
   exports: [
     RgwDaemonListComponent,
@@ -91,7 +94,8 @@ import { CreateRgwServiceEntitiesComponent } from './create-rgw-service-entities
     RgwMultisiteMigrateComponent,
     RgwMultisiteImportComponent,
     RgwMultisiteExportComponent,
-    CreateRgwServiceEntitiesComponent
+    CreateRgwServiceEntitiesComponent,
+    RgwOverviewDashboardComponent
   ]
 })
 export class RgwModule {}
@@ -165,6 +169,11 @@ const routes: Routes = [
       }
     ]
   },
+  {
+    path: 'overview',
+    data: { breadcrumbs: 'Overview' },
+    children: [{ path: '', component: RgwOverviewDashboardComponent }]
+  },
   {
     path: 'multisite',
     canActivate: [FeatureTogglesGuardService, ModuleStatusGuardService],
index 6a05aea981791b9d0749ddc173723279b7216f48..941f8a762e96ca88188d876fb891fd39e2129daf 100644 (file)
         <ul class="list-unstyled"
             id="gateway-nav"
             [ngbCollapse]="displayedSubMenu !== 'rgw'">
+          <li routerLinkActive="active"
+              class="tc_submenuitem tc_submenuitem_rgw_overview">
+            <a i18n
+               routerLink="/rgw/overview">Overview</a>
+          </li>
           <li routerLinkActive="active"
               class="tc_submenuitem tc_submenuitem_rgw_daemons">
             <a i18n
index fbb00bc7370703c5a3095aa0b470e98fc79fd0e9..6cefd532ed823105557b30fe636fa4bf62f5374e 100644 (file)
@@ -9,6 +9,9 @@ class ClusterCapacity(NamedTuple):
     total_avail_bytes: int
     total_bytes: int
     total_used_raw_bytes: int
+    total_objects: int
+    total_pool_bytes_used: int
+    average_object_size: int
 
 
 class ClusterModel:
@@ -44,6 +47,28 @@ class ClusterModel:
     @classmethod
     def get_capacity(cls) -> ClusterCapacity:
         df = mgr.get('df')
+        total_objects = 0
+        total_pool_bytes_used = 0
+        average_object_size = 0
+        rgw_pools = cls.get_rgw_pools()
+        for pool in df['pools']:
+            if pool['name'] in rgw_pools:
+                total_objects = total_objects + pool['stats']['objects']
+                total_pool_bytes_used = total_pool_bytes_used + pool['stats']['bytes_used']
+        if total_objects != 0:
+            average_object_size = total_pool_bytes_used / total_objects
         return ClusterCapacity(total_avail_bytes=df['stats']['total_avail_bytes'],
                                total_bytes=df['stats']['total_bytes'],
-                               total_used_raw_bytes=df['stats']['total_used_raw_bytes'])._asdict()
+                               total_used_raw_bytes=df['stats']['total_used_raw_bytes'],
+                               total_objects=total_objects,
+                               total_pool_bytes_used=total_pool_bytes_used,
+                               average_object_size=average_object_size)._asdict()
+    @classmethod
+    def get_rgw_pools(cls):
+        rgw_pool_names = []
+        osd_map = mgr.get('osd_map')
+        for pool in osd_map['pools']:
+            if 'rgw' in pool.get('application_metadata', {}):
+                rgw_pool_names.append(pool['pool_name'])
+        return rgw_pool_names
+