from ..services.orchestrator import OrchClient, OrchFeature
from . import APIDoc, APIRouter, CreatePermission, DeletePermission, Endpoint, \
ReadPermission, RESTController, Task, UpdatePermission
+from ._version import APIVersion
from .orchestrator import raise_if_no_orchestrator
return ServiceSpec.KNOWN_SERVICE_TYPES
@raise_if_no_orchestrator([OrchFeature.SERVICE_LIST])
- def list(self, service_name: Optional[str] = None) -> List[dict]:
+ @RESTController.MethodMap(version=APIVersion(2, 0)) # type: ignore
+ def list(self, service_name: Optional[str] = None, offset: int = 0, limit: int = 5,
+ search: str = '', sort: str = '+service_name') -> List[dict]:
orch = OrchClient.instance()
- return [service.to_dict() for service in orch.services.list(service_name=service_name)]
+ services, count = orch.services.list(service_name=service_name, offset=int(offset),
+ limit=int(limit), search=search, sort=sort)
+ cherrypy.response.headers['X-Total-Count'] = count
+ return services
@raise_if_no_orchestrator([OrchFeature.SERVICE_LIST])
def get(self, service_name: str) -> List[dict]:
import { CoreModule } from '~/app/core/core.module';
import { CephServiceService } from '~/app/shared/api/ceph-service.service';
import { HostService } from '~/app/shared/api/host.service';
+import { PaginateObservable } from '~/app/shared/api/paginate.model';
import { CdTableFetchDataContext } from '~/app/shared/models/cd-table-fetch-data-context';
import { SharedModule } from '~/app/shared/shared.module';
import { configureTestBed } from '~/testing/unit-test-helper';
}
];
+ const context = new CdTableFetchDataContext(() => undefined);
+
const getDaemonsByHostname = (hostname?: string) => {
return hostname ? _.filter(daemons, { hostname: hostname }) : daemons;
};
spyOn(cephServiceService, 'getDaemons').and.callFake(() =>
of(getDaemonsByServiceName(component.serviceName))
);
- spyOn(cephServiceService, 'list').and.returnValue(of(services));
+
+ const paginate_obs = new PaginateObservable<any>(of(services));
+ spyOn(cephServiceService, 'list').and.returnValue(paginate_obs);
+ context.pageInfo.offset = 0;
+ context.pageInfo.limit = -1;
+
fixture.detectChanges();
});
it('should list daemons by host', () => {
component.hostname = 'mon0';
- component.getDaemons(new CdTableFetchDataContext(() => undefined));
+ component.getDaemons(context);
expect(component.daemons.length).toBe(1);
});
it('should list daemons by service', () => {
component.serviceName = 'osd';
- component.getDaemons(new CdTableFetchDataContext(() => undefined));
+ component.getDaemons(context);
expect(component.daemons.length).toBe(3);
});
it('should list services', () => {
- component.getServices(new CdTableFetchDataContext(() => undefined));
+ component.getServices(context);
expect(component.services.length).toBe(2);
});
+import { HttpParams } from '@angular/common/http';
import {
AfterViewInit,
Component,
});
}
getServices(context: CdTableFetchDataContext) {
- this.serviceSub = this.cephServiceService.list(this.serviceName).subscribe(
- (services: CephServiceSpec[]) => {
- this.services = services;
- },
- () => {
- this.services = [];
- context.error();
- }
- );
+ this.serviceSub = this.cephServiceService
+ .list(new HttpParams({ fromObject: { limit: -1, offset: 0 } }), this.serviceName)
+ .observable.subscribe(
+ (services: CephServiceSpec[]) => {
+ this.services = services;
+ },
+ () => {
+ this.services = [];
+ context.error();
+ }
+ );
}
trackByFn(_index: any, item: any) {
import { NgbActiveModal, NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstrap';
import _ from 'lodash';
import { ToastrModule } from 'ngx-toastr';
+import { of } from 'rxjs';
import { CephServiceService } from '~/app/shared/api/ceph-service.service';
+import { PaginateObservable } from '~/app/shared/api/paginate.model';
import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
import { SharedModule } from '~/app/shared/shared.module';
import { configureTestBed, FormHelper } from '~/testing/unit-test-helper';
describe('should test service mds', () => {
beforeEach(() => {
formHelper.setValue('service_type', 'mds');
+ const paginate_obs = new PaginateObservable<any>(of({}));
+ spyOn(cephServiceService, 'list').and.returnValue(paginate_obs);
});
it('should test mds valid service id', () => {
});
it('should check whether edit field is correctly loaded', () => {
- const cephServiceSpy = spyOn(cephServiceService, 'list').and.callThrough();
+ const paginate_obs = new PaginateObservable<any>(of({}));
+ const cephServiceSpy = spyOn(cephServiceService, 'list').and.returnValue(paginate_obs);
component.ngOnInit();
expect(cephServiceSpy).toBeCalledTimes(2);
expect(component.action).toBe('Edit');
+import { HttpParams } from '@angular/common/http';
import { Component, Input, OnInit, ViewChild } from '@angular/core';
import { AbstractControl, Validators } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
});
}
- this.cephServiceService.list().subscribe((services: CephServiceSpec[]) => {
- this.serviceList = services;
- this.services = services.filter((service: any) =>
- this.INGRESS_SUPPORTED_SERVICE_TYPES.includes(service.service_type)
- );
- });
+ this.cephServiceService
+ .list(new HttpParams({ fromObject: { limit: -1, offset: 0 } }))
+ .observable.subscribe((services: CephServiceSpec[]) => {
+ this.serviceList = services;
+ this.services = services.filter((service: any) =>
+ this.INGRESS_SUPPORTED_SERVICE_TYPES.includes(service.service_type)
+ );
+ });
this.cephServiceService.getKnownTypes().subscribe((resp: Array<string>) => {
// Remove service types:
if (this.editing) {
this.action = this.actionLabels.EDIT;
this.disableForEditing(this.serviceType);
- this.cephServiceService.list(this.serviceName).subscribe((response: CephServiceSpec[]) => {
- const formKeys = ['service_type', 'service_id', 'unmanaged'];
- formKeys.forEach((keys) => {
- this.serviceForm.get(keys).setValue(response[0][keys]);
- });
- if (!response[0]['unmanaged']) {
- const placementKey = Object.keys(response[0]['placement'])[0];
- let placementValue: string;
- ['hosts', 'label'].indexOf(placementKey) >= 0
- ? (placementValue = placementKey)
- : (placementValue = 'hosts');
- this.serviceForm.get('placement').setValue(placementValue);
- this.serviceForm.get('count').setValue(response[0]['placement']['count']);
- if (response[0]?.placement[placementValue]) {
- this.serviceForm.get(placementValue).setValue(response[0]?.placement[placementValue]);
- }
- }
- switch (this.serviceType) {
- case 'iscsi':
- const specKeys = ['pool', 'api_password', 'api_user', 'trusted_ip_list', 'api_port'];
- specKeys.forEach((key) => {
- this.serviceForm.get(key).setValue(response[0].spec[key]);
- });
- this.serviceForm.get('ssl').setValue(response[0].spec?.api_secure);
- if (response[0].spec?.api_secure) {
- this.serviceForm.get('ssl_cert').setValue(response[0].spec?.ssl_cert);
- this.serviceForm.get('ssl_key').setValue(response[0].spec?.ssl_key);
+ this.cephServiceService
+ .list(new HttpParams({ fromObject: { limit: -1, offset: 0 } }), this.serviceName)
+ .observable.subscribe((response: CephServiceSpec[]) => {
+ const formKeys = ['service_type', 'service_id', 'unmanaged'];
+ formKeys.forEach((keys) => {
+ this.serviceForm.get(keys).setValue(response[0][keys]);
+ });
+ if (!response[0]['unmanaged']) {
+ const placementKey = Object.keys(response[0]['placement'])[0];
+ let placementValue: string;
+ ['hosts', 'label'].indexOf(placementKey) >= 0
+ ? (placementValue = placementKey)
+ : (placementValue = 'hosts');
+ this.serviceForm.get('placement').setValue(placementValue);
+ this.serviceForm.get('count').setValue(response[0]['placement']['count']);
+ if (response[0]?.placement[placementValue]) {
+ this.serviceForm.get(placementValue).setValue(response[0]?.placement[placementValue]);
}
- break;
- case 'rgw':
- this.serviceForm.get('rgw_frontend_port').setValue(response[0].spec?.rgw_frontend_port);
- this.serviceForm.get('ssl').setValue(response[0].spec?.ssl);
- if (response[0].spec?.ssl) {
+ }
+ switch (this.serviceType) {
+ case 'iscsi':
+ const specKeys = ['pool', 'api_password', 'api_user', 'trusted_ip_list', 'api_port'];
+ specKeys.forEach((key) => {
+ this.serviceForm.get(key).setValue(response[0].spec[key]);
+ });
+ this.serviceForm.get('ssl').setValue(response[0].spec?.api_secure);
+ if (response[0].spec?.api_secure) {
+ this.serviceForm.get('ssl_cert').setValue(response[0].spec?.ssl_cert);
+ this.serviceForm.get('ssl_key').setValue(response[0].spec?.ssl_key);
+ }
+ break;
+ case 'rgw':
this.serviceForm
- .get('ssl_cert')
- .setValue(response[0].spec?.rgw_frontend_ssl_certificate);
- }
- break;
- case 'ingress':
- const ingressSpecKeys = [
- 'backend_service',
- 'virtual_ip',
- 'frontend_port',
- 'monitor_port',
- 'virtual_interface_networks',
- 'ssl'
- ];
- ingressSpecKeys.forEach((key) => {
- this.serviceForm.get(key).setValue(response[0].spec[key]);
- });
- if (response[0].spec?.ssl) {
- this.serviceForm.get('ssl_cert').setValue(response[0].spec?.ssl_cert);
- this.serviceForm.get('ssl_key').setValue(response[0].spec?.ssl_key);
- }
- break;
- case 'snmp-gateway':
- const snmpCommonSpecKeys = ['snmp_version', 'snmp_destination'];
- snmpCommonSpecKeys.forEach((key) => {
- this.serviceForm.get(key).setValue(response[0].spec[key]);
- });
- if (this.serviceForm.getValue('snmp_version') === 'V3') {
- const snmpV3SpecKeys = [
- 'engine_id',
- 'auth_protocol',
- 'privacy_protocol',
- 'snmp_v3_auth_username',
- 'snmp_v3_auth_password',
- 'snmp_v3_priv_password'
+ .get('rgw_frontend_port')
+ .setValue(response[0].spec?.rgw_frontend_port);
+ this.serviceForm.get('ssl').setValue(response[0].spec?.ssl);
+ if (response[0].spec?.ssl) {
+ this.serviceForm
+ .get('ssl_cert')
+ .setValue(response[0].spec?.rgw_frontend_ssl_certificate);
+ }
+ break;
+ case 'ingress':
+ const ingressSpecKeys = [
+ 'backend_service',
+ 'virtual_ip',
+ 'frontend_port',
+ 'monitor_port',
+ 'virtual_interface_networks',
+ 'ssl'
];
- snmpV3SpecKeys.forEach((key) => {
- if (key !== null) {
- if (
- key === 'snmp_v3_auth_username' ||
- key === 'snmp_v3_auth_password' ||
- key === 'snmp_v3_priv_password'
- ) {
- this.serviceForm.get(key).setValue(response[0].spec['credentials'][key]);
- } else {
- this.serviceForm.get(key).setValue(response[0].spec[key]);
- }
- }
+ ingressSpecKeys.forEach((key) => {
+ this.serviceForm.get(key).setValue(response[0].spec[key]);
});
- } else {
- this.serviceForm
- .get('snmp_community')
- .setValue(response[0].spec['credentials']['snmp_community']);
- }
- break;
- }
- });
+ if (response[0].spec?.ssl) {
+ this.serviceForm.get('ssl_cert').setValue(response[0].spec?.ssl_cert);
+ this.serviceForm.get('ssl_key').setValue(response[0].spec?.ssl_key);
+ }
+ break;
+ case 'snmp-gateway':
+ const snmpCommonSpecKeys = ['snmp_version', 'snmp_destination'];
+ snmpCommonSpecKeys.forEach((key) => {
+ this.serviceForm.get(key).setValue(response[0].spec[key]);
+ });
+ if (this.serviceForm.getValue('snmp_version') === 'V3') {
+ const snmpV3SpecKeys = [
+ 'engine_id',
+ 'auth_protocol',
+ 'privacy_protocol',
+ 'snmp_v3_auth_username',
+ 'snmp_v3_auth_password',
+ 'snmp_v3_priv_password'
+ ];
+ snmpV3SpecKeys.forEach((key) => {
+ if (key !== null) {
+ if (
+ key === 'snmp_v3_auth_username' ||
+ key === 'snmp_v3_auth_password' ||
+ key === 'snmp_v3_priv_password'
+ ) {
+ this.serviceForm.get(key).setValue(response[0].spec['credentials'][key]);
+ } else {
+ this.serviceForm.get(key).setValue(response[0].spec[key]);
+ }
+ }
+ });
+ } else {
+ this.serviceForm
+ .get('snmp_community')
+ .setValue(response[0].spec['credentials']['snmp_community']);
+ }
+ break;
+ }
+ });
}
}
[autoReload]="5000"
(fetchData)="getServices($event)"
[hasDetails]="hasDetails"
+ [serverSide]="true"
+ [count]="count"
(setExpandedRow)="setExpandedRow($event)"
(updateSelection)="updateSelection($event)">
<cd-table-actions class="table-actions"
import { CoreModule } from '~/app/core/core.module';
import { CephServiceService } from '~/app/shared/api/ceph-service.service';
import { OrchestratorService } from '~/app/shared/api/orchestrator.service';
+import { PaginateObservable } from '~/app/shared/api/paginate.model';
import { CdTableFetchDataContext } from '~/app/shared/models/cd-table-fetch-data-context';
import { Permissions } from '~/app/shared/models/permissions';
import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
component = fixture.componentInstance;
const orchService = TestBed.inject(OrchestratorService);
const cephServiceService = TestBed.inject(CephServiceService);
- spyOn(orchService, 'status').and.returnValue(of({ available: true }));
+ const paginate_obs = new PaginateObservable<any>(of({ available: true }));
+ spyOn(orchService, 'status').and.returnValue(paginate_obs);
spyOn(cephServiceService, 'list').and.returnValue(of(services));
fixture.detectChanges();
});
});
it('should return all services', () => {
- component.getServices(new CdTableFetchDataContext(() => undefined));
+ const context = new CdTableFetchDataContext(() => undefined)
+ context.pageInfo.offset = 0;
+ context.pageInfo.limit = -1
+ component.getServices(context);
expect(component.services.length).toBe(2);
});
permissions: Permissions;
tableActions: CdTableAction[];
showDocPanel = false;
+ count = 0;
bsModalRef: NgbModalRef;
orchStatus: OrchestratorStatus;
return;
}
this.isLoadingServices = true;
- this.cephServiceService.list().subscribe(
+ const pagination_obs = this.cephServiceService.list(context.toParams());
+ pagination_obs.observable.subscribe(
(services: CephServiceSpec[]) => {
this.services = services;
+ this.count = pagination_obs.count;
this.services = this.services.filter((col: any) => {
return !this.hiddenServices.includes(col.service_name);
});
import { Observable } from 'rxjs';
+import { ApiClient } from '~/app/shared/api/api-client';
import { Daemon } from '../models/daemon.interface';
import { CephServiceSpec } from '../models/service.interface';
+import { PaginateObservable } from './paginate.model';
@Injectable({
providedIn: 'root'
})
-export class CephServiceService {
+export class CephServiceService extends ApiClient {
private url = 'api/service';
- constructor(private http: HttpClient) {}
+ constructor(private http: HttpClient) {
+ super();
+ }
- list(serviceName?: string): Observable<CephServiceSpec[]> {
- const options = serviceName
- ? { params: new HttpParams().set('service_name', serviceName) }
- : {};
- return this.http.get<CephServiceSpec[]>(this.url, options);
+ list(httpParams: HttpParams, serviceName?: string): PaginateObservable<CephServiceSpec[]> {
+ const options = {
+ headers: { Accept: this.getVersionHeaderValue(2, 0) },
+ params: httpParams
+ };
+ options['observe'] = 'response';
+ if (serviceName) {
+ options.params = options.params.append('service_name', serviceName);
+ }
+ return new PaginateObservable<CephServiceSpec[]>(
+ this.http.get<CephServiceSpec[]>(this.url, options)
+ );
}
getDaemons(serviceName?: string): Observable<Daemon[]> {
--- /dev/null
+import { Observable } from 'rxjs';
+import { map } from 'rxjs/operators';
+
+export class PaginateObservable<Type> {
+ observable: Observable<Type>;
+ count: number;
+
+ subscribe: any;
+ constructor(obs: Observable<Type>) {
+ this.observable = obs.pipe(
+ map((response: any) => {
+ this.count = Number(response.headers?.get('X-Total-Count'));
+ return response['body'];
+ })
+ );
+ }
+}
}
toParams(): HttpParams {
+ if (this.pageInfo.offset == NaN) {
+ this.pageInfo.offset = 0;
+ }
+
if (this.pageInfo.limit === null) {
this.pageInfo.limit = 0;
}
+
if (!this.search) {
this.search = '';
}
+
if (!this.sort || this.sort.length < 2) {
this.sort = '+name';
}
+
return new HttpParams({
fromObject: {
offset: String(this.pageInfo.offset * this.pageInfo.limit),
--- /dev/null
+from typing import Any, Dict, List
+
+from ..exceptions import DashboardException
+
+
+class ListPaginator:
+ def __init__(self, offset: int, limit: int, sort: str, search: str,
+ input_list: List[Any], default_sort: str,
+ searchable_params: List[str] = [], sortable_params: List[str] = []):
+ self.offset = offset
+ if limit < -1:
+ raise DashboardException(msg=f'Wrong limit value {limit}', code=400)
+ self.limit = limit
+ self.sort = sort
+ self.search = search
+ self.input_list = input_list
+ self.default_sort = default_sort
+ self.searchable_params = searchable_params
+ self.sortable_params = sortable_params
+
+ def get_count(self):
+ return len(self.input_list)
+
+ def find_value(self, item: Dict[str, Any], key: str):
+ keys = key.split('.')
+ value = item
+ for key in keys:
+ if key in value:
+ value = value[key]
+ else:
+ return ''
+ return value
+
+ def list(self):
+ end = self.offset + self.limit
+ # '-1' is a special number to refer to all items in list
+ if self.limit == -1:
+ end = len(self.input_list)
+
+ desc = self.sort[0] == '-'
+ sort_by = self.sort[1:]
+
+ # trim down by search
+ trimmed_list = []
+ if self.search:
+ for item in self.input_list:
+ for searchable_param in self.searchable_params:
+ value = self.find_value(item, searchable_param)
+ if isinstance(value, str):
+ if self.search in str(value):
+ trimmed_list.append(item)
+
+ else:
+ trimmed_list = self.input_list
+
+ if sort_by not in self.sortable_params:
+ sort_by = self.default_sort
+
+ def sort(item):
+ return self.find_value(item, sort_by)
+
+ for item in sorted(trimmed_list, key=sort, reverse=desc)[self.offset:end]:
+ yield item
ServiceDescription, raise_if_exception
from .. import mgr
+from ._paginate import ListPaginator
logger = logging.getLogger('orchestrator')
class ServiceManager(ResourceManager):
- @wait_api_result
def list(self,
service_type: Optional[str] = None,
- service_name: Optional[str] = None) -> List[ServiceDescription]:
- return self.api.describe_service(service_type, service_name)
+ service_name: Optional[str] = None,
+ offset: int = 0, limit: int = -1,
+ sort: str = '+service_name', search: str = '') -> List[ServiceDescription]:
+ services = self.api.describe_service(service_type, service_name)
+ services = [service.to_dict() for service in services.result]
+ paginator = ListPaginator(offset, limit, sort, search,
+ input_list=services,
+ searchable_params=['service_name', 'status.running',
+ 'status.last_refreshed', 'status.size'],
+ sortable_params=['service_name', 'status.running',
+ 'status.last_refreshed', 'status.size'],
+ default_sort='service_name')
+ return list(paginator.list()), paginator.get_count()
@wait_api_result
def get(self, service_name: str) -> ServiceDescription: