class OsdTest(DashboardTestCase):
AUTH_ROLES = ['cluster-manager']
+ _VERSION = '1.1'
@classmethod
def setUpClass(cls):
@DashboardTestCase.RunAs('test', 'test', ['block-manager'])
def test_access_permissions(self):
- self._get('/api/osd')
+ self._get('/api/osd', version=self._VERSION)
self.assertStatus(403)
self._get('/api/osd/0')
self.assertStatus(403)
self.assertSchema(data, JObj({p: JAny(none=False) for p in properties}, allow_unknown=True))
def test_list(self):
- data = self._get('/api/osd')
+ data = self._get('/api/osd', version=self._VERSION)
self.assertStatus(200)
self.assertGreaterEqual(len(data), 1)
import time
from typing import Any, Dict, List, Optional, Union
+import cherrypy
from ceph.deployment.drive_group import DriveGroupSpec, DriveGroupValidationError # type: ignore
from mgr_util import get_most_recent_rate
from .. import mgr
from ..exceptions import DashboardException
from ..security import Scope
+from ..services._paginate import ListPaginator
from ..services.ceph_service import CephService, SendCommandError
from ..services.exception import handle_orchestrator_error, handle_send_command_error
from ..services.orchestrator import OrchClient, OrchFeature
@APIRouter('/osd', Scope.OSD)
@APIDoc('OSD management API', 'OSD')
class Osd(RESTController):
- def list(self):
- osds = self.get_osd_map()
+ @RESTController.MethodMap(version=APIVersion(1, 1))
+ def list(self, offset: int = 0, limit: int = 10,
+ search: str = '', sort: str = ''):
+ all_osds = self.get_osd_map()
+
+ paginator = ListPaginator(int(offset), int(limit), sort, search,
+ input_list=all_osds.values(),
+ searchable_params=['id'],
+ sortable_params=['id'],
+ default_sort='+id')
+
+ cherrypy.response.headers['X-Total-Count'] = paginator.get_count()
+
+ paginated_osds_list = list(paginator.list())
+ # creating a dictionary to have faster lookups
+ paginated_osds_by_id = {osd['id']: osd for osd in paginated_osds_list}
+ try:
+ osds = {
+ key: paginated_osds_by_id[int(key)]
+ for key in all_osds.keys()
+ if int(key) in paginated_osds_by_id
+ }
+ except ValueError as e:
+ raise DashboardException(e, component='osd', http_status_code=400)
# Extending by osd stats information
for stat in mgr.get('osd_stats')['osd_stats']:
i18n>OSDs List</a>
<ng-template ngbNavContent>
<cd-table [data]="osds"
- (fetchData)="getOsdList()"
+ (fetchData)="getOsdList($event)"
[columns]="columns"
selectionType="multiClick"
[hasDetails]="true"
(setExpandedRow)="setExpandedRow($event)"
(updateSelection)="updateSelection($event)"
- [updateSelectionOnRefresh]="'never'">
+ [updateSelectionOnRefresh]="'never'"
+ [serverSide]="true"
+ [count]="count">
<div class="table-actions btn-toolbar">
<cd-table-actions [permission]="permissions.osd"
import { OsdReweightModalComponent } from '../osd-reweight-modal/osd-reweight-modal.component';
import { OsdListComponent } from './osd-list.component';
import { ResizeObserver as ResizeObserverPolyfill } from '@juggle/resize-observer';
+import { PaginateObservable } from '~/app/shared/api/paginate.model';
+import { Osd } from '~/app/shared/models/osd.model';
describe('OsdListComponent', () => {
let component: OsdListComponent;
});
describe('getOsdList', () => {
- let osds: any[];
+ let osds: Osd[];
let flagsSpy: jasmine.Spy;
- const createOsd = (n: number) =>
- <Record<string, any>>{
- in: 'in',
- up: 'up',
- tree: {
- device_class: 'ssd'
- },
- stats_history: {
- op_out_bytes: [
- [n, n],
- [n * 2, n * 2]
- ],
- op_in_bytes: [
- [n * 3, n * 3],
- [n * 4, n * 4]
- ]
- },
- stats: {
- stat_bytes_used: n * n,
- stat_bytes: n * n * n
- },
- state: []
- };
+ const createOsd = (n: number): Osd => ({
+ id: n,
+ host: {
+ id: 0,
+ name: 'test_host'
+ },
+ in: 1,
+ up: 1,
+ tree: {
+ device_class: 'ssd'
+ },
+ stats_history: {
+ op_out_bytes: [
+ [n, n],
+ [n * 2, n * 2]
+ ],
+ op_in_bytes: [
+ [n * 3, n * 3],
+ [n * 4, n * 4]
+ ]
+ },
+ stats: {
+ stat_bytes_used: n * n,
+ stat_bytes: n * n * n
+ },
+ state: []
+ });
const expectAttributeOnEveryOsd = (attr: string) =>
expect(component.osds.every((osd) => Boolean(_.get(osd, attr)))).toBeTruthy();
beforeEach(() => {
- spyOn(osdService, 'getList').and.callFake(() => of(osds));
+ spyOn(osdService, 'getList').and.callFake(() => new PaginateObservable<Osd[]>(of(osds)));
flagsSpy = spyOn(osdService, 'getFlags').and.callFake(() => of([]));
osds = [createOsd(1), createOsd(2), createOsd(3)];
component.getOsdList();
beforeEach(() => {
component.permissions = fakeAuthStorageService.getPermissions();
- spyOn(osdService, 'getList').and.callFake(() => of(fakeOsds));
+ spyOn(osdService, 'getList').and.callFake(() => new PaginateObservable<Osd[]>(of(fakeOsds)));
spyOn(osdService, 'getFlags').and.callFake(() => of([]));
+ component.getOsdList();
});
const testTableActions = async (
import { OsdRecvSpeedModalComponent } from '../osd-recv-speed-modal/osd-recv-speed-modal.component';
import { OsdReweightModalComponent } from '../osd-reweight-modal/osd-reweight-modal.component';
import { OsdScrubModalComponent } from '../osd-scrub-modal/osd-scrub-modal.component';
+import { CdTableFetchDataContext } from '~/app/shared/models/cd-table-fetch-data-context';
+import { Osd } from '~/app/shared/models/osd.model';
const BASE_URL = 'osd';
clusterWideActions: CdTableAction[];
icons = Icons;
osdSettings = new OsdSettings();
+ count = 0;
selection = new CdTableSelection();
osds: any[] = [];
}
}
- getOsdList() {
- const observables = [this.osdService.getList(), this.osdService.getFlags()];
- observableForkJoin(observables).subscribe((resp: [any[], string[]]) => {
- this.osds = resp[0].map((osd) => {
+ getOsdList(context?: CdTableFetchDataContext) {
+ if (!context) context = new CdTableFetchDataContext();
+ const pagination_obs = this.osdService.getList(context.toParams());
+ const observables = [pagination_obs.observable, this.osdService.getFlags()];
+ observableForkJoin(observables).subscribe((resp: any) => {
+ this.osds = resp[0].map((osd: Osd) => {
+ this.count = pagination_obs.count;
osd.collectedStates = OsdListComponent.collectStates(osd);
osd.stats_history.out_bytes = osd.stats_history.op_out_bytes.map((i: string) => i[1]);
osd.stats_history.in_bytes = osd.stats_history.op_in_bytes.map((i: string) => i[1]);
import { configureTestBed } from '~/testing/unit-test-helper';
import { OsdService } from './osd.service';
+import { CdTableFetchDataContext } from '../models/cd-table-fetch-data-context';
describe('OsdService', () => {
let service: OsdService;
});
it('should call getList', () => {
- service.getList().subscribe();
- const req = httpTesting.expectOne('api/osd');
+ const context = new CdTableFetchDataContext(() => {});
+ service.getList(context.toParams()).observable.subscribe();
+ const req = httpTesting.expectOne('api/osd?offset=0&limit=10&search=&sort=%2Bname');
expect(req.request.method).toBe('GET');
});
-import { HttpClient } from '@angular/common/http';
+import { HttpClient, HttpParams } from '@angular/common/http';
import { Injectable } from '@angular/core';
import _ from 'lodash';
import { OsdSettings } from '../models/osd-settings';
import { SmartDataResponseV1 } from '../models/smart';
import { DeviceService } from '../services/device.service';
+import { PaginateObservable } from './paginate.model';
+import { PaginateParams } from '../classes/paginate-params.class';
+import { Osd } from '../models/osd.model';
@Injectable({
providedIn: 'root'
return this.http.post(this.path, request, { observe: 'response' });
}
- getList() {
- return this.http.get(`${this.path}`);
+ getList(params: HttpParams): PaginateObservable<Osd[]> {
+ return new PaginateObservable<Osd[]>(
+ this.http.get<Osd[]>(this.path, new PaginateParams(params, 1, 1))
+ );
}
getOsdSettings(): Observable<OsdSettings> {
this.observable = obs.pipe(
map((response: any) => {
this.count = Number(response.headers?.get('X-Total-Count'));
- return response['body'];
+ return response['body'] || response;
})
);
}
--- /dev/null
+import { HttpParams } from '@angular/common/http';
+
+export class PaginateParams {
+ constructor(params: HttpParams, majorVersion = 1, minorVersion = 0) {
+ const options = {
+ params: params,
+ headers: {
+ Accept: `application/vnd.ceph.api.v${majorVersion}.${minorVersion}+json`
+ }
+ };
+
+ options['observe'] = 'response';
+ return options;
+ }
+}
search = '';
sort = '+name';
- constructor(error: () => void) {
+ constructor(error?: () => void) {
this.error = error;
}
--- /dev/null
+/* We will need to check what are all the value that the
+ UI need and only make them the mandatory parameters here.
+ For now based on what I saw in the unit test file;
+ osd-list.component.spec.ts, I've made the decision to make
+ things optional and non-optional. This should be re-evaluated. */
+
+export interface Osd {
+ id: number;
+ host: Host;
+ stats_history: StatsHistory;
+ state: string[];
+ stats: Stats;
+ collectedStates?: string[];
+ in?: number;
+ out?: number;
+ up?: number;
+ down?: number;
+ destroyed?: number;
+ cdIsBinary?: boolean;
+ cdIndivFlags?: string[];
+ cdClusterFlags?: string[];
+ cdExecuting?: any;
+ tree?: Tree;
+ operational_status?: string;
+}
+
+interface Tree {
+ device_class: string;
+}
+
+interface Host {
+ id: number;
+ name: string;
+}
+
+interface StatsHistory {
+ op_out_bytes: any[];
+ op_in_bytes: any[];
+ out_bytes?: any[];
+ in_bytes?: any[];
+}
+
+interface Stats {
+ stat_bytes_used: number;
+ stat_bytes: number;
+ op_w?: number;
+ op_r?: number;
+ usage?: number;
+}
- NFS-Ganesha
/api/osd:
get:
- parameters: []
+ parameters:
+ - default: 0
+ in: query
+ name: offset
+ schema:
+ type: integer
+ - default: 10
+ in: query
+ name: limit
+ schema:
+ type: integer
+ - default: ''
+ in: query
+ name: search
+ schema:
+ type: string
+ - default: ''
+ in: query
+ name: sort
+ schema:
+ type: string
responses:
'200':
content:
- application/vnd.ceph.api.v1.0+json:
+ application/vnd.ceph.api.v1.1+json:
type: object
description: OK
'400':
from ceph.deployment.service_spec import PlacementSpec
from .. import mgr
+from ..controllers._version import APIVersion
from ..controllers.osd import Osd, OsdUi
from ..services.osd import OsdDeploymentOptions
from ..tests import ControllerTestCase
osds_leftover = [0, 1, 2]
with self._mock_osd_list(osd_stat_ids=osds_actual, osdmap_tree_node_ids=osds_leftover,
osdmap_ids=osds_actual):
- self._get('/api/osd')
+ self._get('/api/osd', version=APIVersion(1, 1))
self.assertEqual(len(self.json_body()), 2, 'It should display two OSDs without failure')
self.assertStatus(200)