- Display services and daemons in the cluster/services page.
- Display daemons in the cluster/hosts/host-detail page (Daemons tab).
This PR also partially addresses https://tracker.ceph.com/issues/43165:
The endpoint `/api/orchestrator/service` is removed.
Create new endpoints:
- `/api/service`: listing all services in the Ceph cluster.
- `/api/service/<service_name>/daemons`: listing daemons for a
service. e.g. daemons of OSD.
- `/api/host/<hostname>/daemons`: listing daemons of a host.
Fixes: https://tracker.ceph.com/issues/44221
Signed-off-by: Kiefer Chang <kiefer.chang@suse.com>
URL_STATUS = '/api/orchestrator/status'
URL_INVENTORY = '/api/orchestrator/inventory'
- URL_SERVICE = '/api/orchestrator/service'
URL_OSD = '/api/orchestrator/osd'
self.assertStatus(200)
self._get(self.URL_INVENTORY)
self.assertStatus(403)
- self._get(self.URL_SERVICE)
- self.assertStatus(403)
def test_status_get(self):
data = self._get(self.URL_STATUS)
from __future__ import absolute_import
import copy
-try:
- from typing import List
-except ImportError:
- pass
+from typing import List
from mgr_util import merge_dicts
from orchestrator import HostSpec
# Hosts only in Orchestrator
orch_sources = {'ceph': False, 'orchestrator': True}
- orch_hosts = [dict(hostname=hostname, ceph_version='', services=[], sources=orch_sources)
- for hostname in orch_hostnames]
- _ceph_hosts.extend(orch_hosts)
+ _orch_hosts = [dict(hostname=hostname, ceph_version='', services=[], sources=orch_sources)
+ for hostname in orch_hostnames]
+ _ceph_hosts.extend(_orch_hosts)
return _ceph_hosts
def smart(self, hostname):
# type: (str) -> dict
return CephService.get_smart_data_by_host(hostname)
+
+ @RESTController.Resource('GET')
+ @raise_if_no_orchestrator
+ def daemons(self, hostname: str) -> List[dict]:
+ orch = OrchClient.instance()
+ daemons = orch.services.list_daemons(None, hostname)
+ return [d.to_json() for d in daemons]
}
:rtype: dict
"""
- result = {}
+ result: dict = {}
for osd_id, osd_metadata in mgr.get('osd_metadata').items():
hostname = osd_metadata.get('hostname')
devices = osd_metadata.get('devices')
return inventory_hosts
-@ApiController('/orchestrator/service', Scope.HOSTS)
-class OrchestratorService(RESTController):
-
- @raise_if_no_orchestrator
- def list(self, hostname=None):
- orch = OrchClient.instance()
- return [service.to_json() for service in orch.services.list(None, None, hostname)]
-
-
@ApiController('/orchestrator/osd', Scope.OSD)
class OrchestratorOsd(RESTController):
--- /dev/null
+from typing import List, Optional
+import cherrypy
+
+from . import ApiController, RESTController
+from .orchestrator import raise_if_no_orchestrator
+from ..security import Scope
+from ..services.orchestrator import OrchClient
+
+
+@ApiController('/service', Scope.HOSTS)
+class Service(RESTController):
+
+ @raise_if_no_orchestrator
+ def list(self, service_name: Optional[str] = None) -> List[dict]:
+ orch = OrchClient.instance()
+ return [service.to_json() for service in orch.services.list(service_name)]
+
+ @raise_if_no_orchestrator
+ def get(self, service_name: str) -> List[dict]:
+ orch = OrchClient.instance()
+ services = orch.services.get(service_name)
+ if not services:
+ raise cherrypy.HTTPError(404, 'Service {} not found'.format(service_name))
+ return services[0].to_json()
+
+ @RESTController.Resource('GET')
+ @raise_if_no_orchestrator
+ def daemons(self, service_name: str) -> List[dict]:
+ orch = OrchClient.instance()
+ daemons = orch.services.list_daemons(service_name)
+ return [d.to_json() for d in daemons]
import { SilenceFormComponent } from './prometheus/silence-form/silence-form.component';
import { SilenceListComponent } from './prometheus/silence-list/silence-list.component';
import { SilenceMatcherModalComponent } from './prometheus/silence-matcher-modal/silence-matcher-modal.component';
+import { ServiceDaemonListComponent } from './services/service-daemon-list/service-daemon-list.component';
+import { ServiceDetailsComponent } from './services/service-details/service-details.component';
import { ServicesComponent } from './services/services.component';
@NgModule({
RulesListComponent,
ActiveAlertListComponent,
MonitoringListComponent,
- HostFormComponent
+ HostFormComponent,
+ ServiceDetailsComponent,
+ ServiceDaemonListComponent
]
})
export class ClusterModule {}
<cd-inventory [hostname]="selectedHostname"></cd-inventory>
</tab>
<tab i18n-heading
- heading="Services"
+ heading="Daemons"
*ngIf="permissions.hosts.read">
- <cd-services
- [hostname]="selectedHostname"
- [hiddenColumns]="['nodename']">
- </cd-services>
+ <cd-service-daemon-list [hostname]="selectedHostname">
+ </cd-service-daemon-list>
</tab>
<tab i18n-heading
heading="Performance Details"
import { BsDropdownModule } from 'ngx-bootstrap/dropdown';
import { TabsModule } from 'ngx-bootstrap/tabs';
import { ToastrModule } from 'ngx-toastr';
-import { of } from 'rxjs';
import { configureTestBed, i18nProviders } from '../../../../../testing/unit-test-helper';
import { CoreModule } from '../../../../core/core.module';
-import { OrchestratorService } from '../../../../shared/api/orchestrator.service';
import { CdTableSelection } from '../../../../shared/models/cd-table-selection';
import { Permissions } from '../../../../shared/models/permissions';
import { SharedModule } from '../../../../shared/shared.module';
hosts: ['read'],
grafana: ['read']
});
- const orchService = TestBed.get(OrchestratorService);
- spyOn(orchService, 'status').and.returnValue(of({ available: true }));
- spyOn(orchService, 'inventoryDeviceList').and.returnValue(of([]));
- spyOn(orchService, 'serviceList').and.returnValue(of([]));
});
it('should create', () => {
'Devices',
'Device health',
'Inventory',
- 'Services',
+ 'Daemons',
'Performance Details'
]);
});
--- /dev/null
+<cd-table [data]="daemons"
+ [columns]="columns"
+ columnMode="flex"
+ autoReload="0"
+ (fetchData)="getDaemons($event)">
+</cd-table>
--- /dev/null
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import * as _ from 'lodash';
+import { of } from 'rxjs';
+
+import { configureTestBed, i18nProviders } from '../../../../../testing/unit-test-helper';
+import { CoreModule } from '../../../../core/core.module';
+import { CephServiceService } from '../../../../shared/api/ceph-service.service';
+import { HostService } from '../../../../shared/api/host.service';
+import { CdTableFetchDataContext } from '../../../../shared/models/cd-table-fetch-data-context';
+import { SharedModule } from '../../../../shared/shared.module';
+import { CephModule } from '../../../ceph.module';
+import { ServiceDaemonListComponent } from './service-daemon-list.component';
+
+describe('ServiceDaemonListComponent', () => {
+ let component: ServiceDaemonListComponent;
+ let fixture: ComponentFixture<ServiceDaemonListComponent>;
+
+ const daemons = [
+ {
+ hostname: 'osd0',
+ container_id: '003c10beafc8c27b635bcdfed1ed832e4c1005be89bb1bb05ad4cc6c2b98e41b',
+ container_image_id: 'e70344c77bcbf3ee389b9bf5128f635cf95f3d59e005c5d8e67fc19bcc74ed23',
+ container_image_name: 'docker.io/ceph/daemon-base:latest-master-devel',
+ daemon_id: '3',
+ daemon_type: 'osd',
+ version: '15.1.0-1174-g16a11f7',
+ status: 1,
+ status_desc: 'running',
+ last_refresh: '2020-02-25T04:33:26.465699'
+ },
+ {
+ hostname: 'osd0',
+ container_id: 'baeec41a01374b3ed41016d542d19aef4a70d69c27274f271e26381a0cc58e7a',
+ container_image_id: 'e70344c77bcbf3ee389b9bf5128f635cf95f3d59e005c5d8e67fc19bcc74ed23',
+ container_image_name: 'docker.io/ceph/daemon-base:latest-master-devel',
+ daemon_id: '4',
+ daemon_type: 'osd',
+ version: '15.1.0-1174-g16a11f7',
+ status: 1,
+ status_desc: 'running',
+ last_refresh: '2020-02-25T04:33:26.465822'
+ },
+ {
+ hostname: 'osd0',
+ container_id: '8483de277e365bea4365cee9e1f26606be85c471e4da5d51f57e4b85a42c616e',
+ container_image_id: 'e70344c77bcbf3ee389b9bf5128f635cf95f3d59e005c5d8e67fc19bcc74ed23',
+ container_image_name: 'docker.io/ceph/daemon-base:latest-master-devel',
+ daemon_id: '5',
+ daemon_type: 'osd',
+ version: '15.1.0-1174-g16a11f7',
+ status: 1,
+ status_desc: 'running',
+ last_refresh: '2020-02-25T04:33:26.465886'
+ },
+ {
+ hostname: 'mon0',
+ container_id: '6ca0574f47e300a6979eaf4e7c283a8c4325c2235ae60358482fc4cd58844a21',
+ container_image_id: 'e70344c77bcbf3ee389b9bf5128f635cf95f3d59e005c5d8e67fc19bcc74ed23',
+ container_image_name: 'docker.io/ceph/daemon-base:latest-master-devel',
+ daemon_id: 'a',
+ daemon_type: 'mon',
+ version: '15.1.0-1174-g16a11f7',
+ status: 1,
+ status_desc: 'running',
+ last_refresh: '2020-02-25T04:33:26.465886'
+ }
+ ];
+
+ const getDaemonsByHostname = (hostname?: string) => {
+ return hostname ? _.filter(daemons, { hostname: hostname }) : daemons;
+ };
+
+ const getDaemonsByServiceName = (serviceName?: string) => {
+ return serviceName ? _.filter(daemons, { daemon_type: serviceName }) : daemons;
+ };
+
+ configureTestBed({
+ imports: [HttpClientTestingModule, CephModule, CoreModule, SharedModule],
+ declarations: [],
+ providers: [i18nProviders]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(ServiceDaemonListComponent);
+ component = fixture.componentInstance;
+ const hostService = TestBed.get(HostService);
+ const cephServiceService = TestBed.get(CephServiceService);
+ spyOn(hostService, 'getDaemons').and.callFake(() =>
+ of(getDaemonsByHostname(component.hostname))
+ );
+ spyOn(cephServiceService, 'getDaemons').and.callFake(() =>
+ of(getDaemonsByServiceName(component.serviceName))
+ );
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should list daemons by host', () => {
+ component.hostname = 'mon0';
+ component.getDaemons(new CdTableFetchDataContext(() => {}));
+ expect(component.daemons.length).toBe(1);
+ });
+
+ it('should list daemons by service', () => {
+ component.serviceName = 'osd';
+ component.getDaemons(new CdTableFetchDataContext(() => {}));
+ expect(component.daemons.length).toBe(3);
+ });
+});
--- /dev/null
+import { Component, Input, OnChanges, OnInit, TemplateRef, ViewChild } from '@angular/core';
+
+import { I18n } from '@ngx-translate/i18n-polyfill';
+import { Observable } from 'rxjs';
+
+import { CephServiceService } from '../../../../shared/api/ceph-service.service';
+import { HostService } from '../../../../shared/api/host.service';
+import { TableComponent } from '../../../../shared/datatable/table/table.component';
+import { CdTableColumn } from '../../../../shared/models/cd-table-column';
+import { CdTableFetchDataContext } from '../../../../shared/models/cd-table-fetch-data-context';
+import { Daemon } from '../../../../shared/models/daemon.interface';
+
+@Component({
+ selector: 'cd-service-daemon-list',
+ templateUrl: './service-daemon-list.component.html',
+ styleUrls: ['./service-daemon-list.component.scss']
+})
+export class ServiceDaemonListComponent implements OnInit, OnChanges {
+ @ViewChild(TableComponent, { static: true })
+ table: TableComponent;
+ @ViewChild('lastSeenTpl', { static: true })
+ lastSeenTpl: TemplateRef<any>;
+
+ @Input()
+ serviceName?: string;
+
+ @Input()
+ hostname?: string;
+
+ daemons: Daemon[] = [];
+ columns: CdTableColumn[] = [];
+
+ constructor(
+ private i18n: I18n,
+ private hostService: HostService,
+ private cephServiceService: CephServiceService
+ ) {}
+
+ ngOnInit() {
+ this.columns = [
+ {
+ name: this.i18n('Hostname'),
+ prop: 'hostname',
+ flexGrow: 1,
+ filterable: true
+ },
+ {
+ name: this.i18n('Daemon type'),
+ prop: 'daemon_type',
+ flexGrow: 1,
+ filterable: true
+ },
+ {
+ name: this.i18n('Daemon ID'),
+ prop: 'daemon_id',
+ flexGrow: 1,
+ filterable: true
+ },
+ {
+ name: this.i18n('Container ID'),
+ prop: 'container_id',
+ flexGrow: 3,
+ filterable: true
+ },
+ {
+ name: this.i18n('Container Image name'),
+ prop: 'container_image_name',
+ flexGrow: 3,
+ filterable: true
+ },
+ {
+ name: this.i18n('Container Image ID'),
+ prop: 'container_image_id',
+ flexGrow: 3,
+ filterable: true
+ },
+ {
+ name: this.i18n('Version'),
+ prop: 'version',
+ flexGrow: 1,
+ filterable: true
+ },
+ {
+ name: this.i18n('Status'),
+ prop: 'status',
+ flexGrow: 1,
+ filterable: true
+ },
+ {
+ name: this.i18n('Status Description'),
+ prop: 'status_desc',
+ flexGrow: 1,
+ filterable: true
+ },
+ {
+ name: this.i18n('Last Refreshed'),
+ prop: 'last_refresh',
+ flexGrow: 2
+ }
+ ];
+ }
+
+ ngOnChanges() {
+ this.daemons = [];
+ this.table.reloadData();
+ }
+
+ updateData(daemons: Daemon[]) {
+ this.daemons = daemons;
+ }
+
+ getDaemons(context: CdTableFetchDataContext) {
+ let observable: Observable<Daemon[]>;
+ if (this.hostname) {
+ observable = this.hostService.getDaemons(this.hostname);
+ } else if (this.serviceName) {
+ observable = this.cephServiceService.getDaemons(this.serviceName);
+ } else {
+ this.daemons = [];
+ return;
+ }
+ observable.subscribe(
+ (daemons: Daemon[]) => {
+ this.daemons = daemons;
+ },
+ () => {
+ this.daemons = [];
+ context.error();
+ }
+ );
+ }
+}
--- /dev/null
+<tabset *ngIf="selection.hasSingleSelection">
+ <tab i18n-heading
+ heading="Daemons">
+ <cd-service-daemon-list [serviceName]="selection.first()['service_name']">
+ </cd-service-daemon-list>
+ </tab>
+</tabset>
--- /dev/null
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { configureTestBed, i18nProviders } from '../../../../../testing/unit-test-helper';
+import { CoreModule } from '../../../../core/core.module';
+import { CdTableSelection } from '../../../../shared/models/cd-table-selection';
+import { SharedModule } from '../../../../shared/shared.module';
+import { CephModule } from '../../../ceph.module';
+import { ServiceDetailsComponent } from './service-details.component';
+
+describe('ServiceDetailsComponent', () => {
+ let component: ServiceDetailsComponent;
+ let fixture: ComponentFixture<ServiceDetailsComponent>;
+
+ configureTestBed({
+ imports: [HttpClientTestingModule, CephModule, CoreModule, SharedModule],
+ declarations: [],
+ providers: [i18nProviders]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(ServiceDetailsComponent);
+ component = fixture.componentInstance;
+ component.selection = new CdTableSelection();
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ describe('Service details tabset', () => {
+ beforeEach(() => {
+ component.selection.selected = [{ serviceName: 'osd' }];
+ fixture.detectChanges();
+ });
+
+ it('should recognize a tabset child', () => {
+ const tabsetChild = component.tabsetChild;
+ expect(tabsetChild).toBeDefined();
+ });
+
+ it('should show tabs', () => {
+ expect(component.tabsetChild.tabs.map((t) => t.heading)).toEqual(['Daemons']);
+ });
+ });
+});
--- /dev/null
+import { Component, Input, OnInit, ViewChild } from '@angular/core';
+import { TabsetComponent } from 'ngx-bootstrap/tabs';
+
+import { CdTableSelection } from '../../../../shared/models/cd-table-selection';
+import { Permissions } from '../../../../shared/models/permissions';
+
+@Component({
+ selector: 'cd-service-details',
+ templateUrl: './service-details.component.html',
+ styleUrls: ['./service-details.component.scss']
+})
+export class ServiceDetailsComponent implements OnInit {
+ @ViewChild(TabsetComponent, { static: false })
+ tabsetChild: TabsetComponent;
+
+ @Input()
+ permissions: Permissions;
+
+ @Input()
+ selection: CdTableSelection;
+
+ constructor() {}
+
+ ngOnInit() {}
+}
<ng-container *ngIf="orchestratorExist">
<cd-table [data]="services"
[columns]="columns"
- identifier="uid"
+ identifier="service_name"
forceIdentifier="true"
columnMode="flex"
+ selectionType="single"
(fetchData)="getServices($event)"
- selectionType="single">
+ (updateSelection)="updateSelection($event)">
+ <cd-service-details cdTableDetail
+ [permissions]="permissions"
+ [selection]="selection">
+ </cd-service-details>
</cd-table>
</ng-container>
-import { ComponentFixture, TestBed } from '@angular/core/testing';
-
import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
+
import { of } from 'rxjs';
+
import { configureTestBed, i18nProviders } from '../../../../testing/unit-test-helper';
+import { CoreModule } from '../../../core/core.module';
+import { CephServiceService } from '../../../shared/api/ceph-service.service';
import { OrchestratorService } from '../../../shared/api/orchestrator.service';
import { CdTableFetchDataContext } from '../../../shared/models/cd-table-fetch-data-context';
+import { Permissions } from '../../../shared/models/permissions';
+import { AuthStorageService } from '../../../shared/services/auth-storage.service';
import { SharedModule } from '../../../shared/shared.module';
+import { CephModule } from '../../ceph.module';
import { ServicesComponent } from './services.component';
describe('ServicesComponent', () => {
let component: ServicesComponent;
let fixture: ComponentFixture<ServicesComponent>;
- let reqHostname: string;
+
+ const fakeAuthStorageService = {
+ getPermissions: () => {
+ return new Permissions({ hosts: ['read'] });
+ }
+ };
const services = [
{
- hostname: 'host0',
- service: '',
- service_instance: 'x',
- service_type: 'mon'
+ container_image_id: 'e70344c77bcbf3ee389b9bf5128f635cf95f3d59e005c5d8e67fc19bcc74ed23',
+ container_image_name: 'docker.io/ceph/daemon-base:latest-master-devel',
+ service_name: 'osd',
+ size: 3,
+ running: 3,
+ last_refresh: '2020-02-25T04:33:26.465699'
},
{
- hostname: 'host0',
- service: '',
- service_instance: '0',
- service_type: 'osd'
- },
- {
- hostname: 'host1',
- service: '',
- service_instance: 'y',
- service_type: 'mon'
- },
- {
- hostname: 'host1',
- service: '',
- service_instance: '1',
- service_type: 'osd'
+ container_image_id: 'e70344c77bcbf3ee389b9bf5128f635cf95f3d59e005c5d8e67fc19bcc74ed23',
+ container_image_name: 'docker.io/ceph/daemon-base:latest-master-devel',
+ service_name: 'crash',
+ size: 1,
+ running: 1,
+ last_refresh: '2020-02-25T04:33:26.465766'
}
];
- const getServiceList = (hostname: String) => {
- return hostname ? services.filter((service) => service.hostname === hostname) : services;
- };
-
configureTestBed({
- imports: [SharedModule, HttpClientTestingModule, RouterTestingModule],
- providers: [i18nProviders],
- declarations: [ServicesComponent]
+ imports: [CephModule, CoreModule, SharedModule, HttpClientTestingModule, RouterTestingModule],
+ providers: [{ provide: AuthStorageService, useValue: fakeAuthStorageService }, i18nProviders],
+ declarations: []
});
beforeEach(() => {
fixture = TestBed.createComponent(ServicesComponent);
component = fixture.componentInstance;
const orchService = TestBed.get(OrchestratorService);
+ const cephServiceService = TestBed.get(CephServiceService);
spyOn(orchService, 'status').and.returnValue(of({ available: true }));
- reqHostname = '';
- spyOn(orchService, 'serviceList').and.callFake(() => of(getServiceList(reqHostname)));
+ spyOn(cephServiceService, 'list').and.returnValue(of(services));
fixture.detectChanges();
});
});
it('should return all services', () => {
- component.getServices(new CdTableFetchDataContext(() => {}));
- expect(component.services.length).toBe(4);
- });
-
- it('should return services on a host', () => {
- reqHostname = 'host0';
component.getServices(new CdTableFetchDataContext(() => {}));
expect(component.services.length).toBe(2);
- expect(component.services[0].hostname).toBe(reqHostname);
- expect(component.services[1].hostname).toBe(reqHostname);
});
});
import { Component, Input, OnChanges, OnInit, ViewChild } from '@angular/core';
import { I18n } from '@ngx-translate/i18n-polyfill';
+import { CephServiceService } from '../../../shared/api/ceph-service.service';
import { OrchestratorService } from '../../../shared/api/orchestrator.service';
import { TableComponent } from '../../../shared/datatable/table/table.component';
import { CdTableColumn } from '../../../shared/models/cd-table-column';
import { CdTableFetchDataContext } from '../../../shared/models/cd-table-fetch-data-context';
+import { CdTableSelection } from '../../../shared/models/cd-table-selection';
+import { Permissions } from '../../../shared/models/permissions';
+import { CephService } from '../../../shared/models/service.interface';
import { CephReleaseNamePipe } from '../../../shared/pipes/ceph-release-name.pipe';
+import { AuthStorageService } from '../../../shared/services/auth-storage.service';
import { SummaryService } from '../../../shared/services/summary.service';
-import { Service } from './services.model';
@Component({
selector: 'cd-services',
// Do not display these columns
@Input() hiddenColumns: string[] = [];
+ permissions: Permissions;
+
checkingOrchestrator = true;
orchestratorExist = false;
docsUrl: string;
columns: Array<CdTableColumn> = [];
- services: Array<Service> = [];
+ services: Array<CephService> = [];
isLoadingServices = false;
+ selection = new CdTableSelection();
constructor(
+ private authStorageService: AuthStorageService,
private cephReleaseNamePipe: CephReleaseNamePipe,
private i18n: I18n,
private orchService: OrchestratorService,
+ private cephServiceService: CephServiceService,
private summaryService: SummaryService
- ) {}
+ ) {
+ this.permissions = this.authStorageService.getPermissions();
+ }
ngOnInit() {
const columns = [
- {
- name: this.i18n('Hostname'),
- prop: 'hostname',
- flexGrow: 2
- },
- {
- name: this.i18n('Service type'),
- prop: 'service_type',
- flexGrow: 1
- },
{
name: this.i18n('Service'),
- prop: 'service',
- flexGrow: 1
- },
- {
- name: this.i18n('Service instance'),
- prop: 'service_instance',
+ prop: 'service_name',
flexGrow: 1
},
{
- name: this.i18n('Container id'),
- prop: 'container_id',
+ name: this.i18n('Container image name'),
+ prop: 'container_image_name',
flexGrow: 3
},
{
- name: this.i18n('Version'),
- prop: 'version',
- flexGrow: 1
+ name: this.i18n('Container image ID'),
+ prop: 'container_image_id',
+ flexGrow: 3
},
{
- name: this.i18n('Rados config location'),
- prop: 'rados_config_location',
+ name: this.i18n('Running'),
+ prop: 'running',
flexGrow: 1
},
{
- name: this.i18n('Service URL'),
- prop: 'service_url',
- flexGrow: 2
- },
- {
- name: this.i18n('Status'),
- prop: 'status',
+ name: this.i18n('Size'),
+ prop: 'size',
flexGrow: 1
},
{
- name: this.i18n('Status Description'),
- prop: 'status_desc',
+ name: this.i18n('Last Refreshed'),
+ prop: 'last_refresh',
flexGrow: 1
}
];
}
}
+ updateSelection(selection: CdTableSelection) {
+ this.selection = selection;
+ }
+
getServices(context: CdTableFetchDataContext) {
if (this.isLoadingServices) {
return;
}
this.isLoadingServices = true;
- this.orchService.serviceList(this.hostname).subscribe(
- (data: Service[]) => {
- const services: Service[] = [];
- data.forEach((service: Service) => {
- service.uid = `${service.hostname}-${service.service_type}-${service.service}-${service.service_instance}`;
- services.push(service);
- });
+ this.cephServiceService.list().subscribe(
+ (services: CephService[]) => {
this.services = services;
this.isLoadingServices = false;
},
+++ /dev/null
-export class Service {
- uid: string;
-
- hostname: string;
- container_id: string;
- service: string;
- service_instance: string;
- service_type: string;
- version: string;
- rados_config_location: string;
- service_url: string;
- status: string;
- status_desc: string;
-}
--- /dev/null
+import { HttpClient, HttpParams } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+
+import { Observable } from 'rxjs';
+
+import { Daemon } from '../models/daemon.interface';
+import { CephService } from '../models/service.interface';
+import { ApiModule } from './api.module';
+
+@Injectable({
+ providedIn: ApiModule
+})
+export class CephServiceService {
+ private url = 'api/service';
+
+ constructor(private http: HttpClient) {}
+
+ list(serviceName?: string): Observable<CephService[]> {
+ const options = serviceName
+ ? { params: new HttpParams().set('service_name', serviceName) }
+ : {};
+ return this.http.get<CephService[]>(this.url, options);
+ }
+
+ getDaemons(serviceName?: string): Observable<Daemon[]> {
+ return this.http.get<Daemon[]>(`${this.url}/${serviceName}/daemons`);
+ }
+}
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
+import { Daemon } from '../models/daemon.interface';
import { CdDevice } from '../models/devices';
import { SmartDataResponseV1 } from '../models/smart';
import { DeviceService } from '../services/device.service';
getSmartData(hostname: string) {
return this.http.get<SmartDataResponseV1>(`${this.baseURL}/${hostname}/smart`);
}
+
+ getDaemons(hostname: string): Observable<Daemon[]> {
+ return this.http.get<Daemon[]>(`${this.baseURL}/${hostname}/daemons`);
+ }
}
expect(req.request.method).toBe('GET');
});
- it('should call serviceList', () => {
- service.serviceList().subscribe();
- const req = httpTesting.expectOne(`${apiPath}/service`);
- expect(req.request.method).toBe('GET');
- });
-
- it('should call serviceList with a host', () => {
- const host = 'host0';
- service.serviceList(host).subscribe();
- const req = httpTesting.expectOne(`${apiPath}/service?hostname=${host}`);
- expect(req.request.method).toBe('GET');
- });
-
it('should call osdCreate', () => {
const data = {
drive_group: {
);
}
- serviceList(hostname?: string) {
- const options = hostname ? { params: new HttpParams().set('hostname', hostname) } : {};
- return this.http.get(`${this.url}/service`, options);
- }
-
osdCreate(driveGroup: {}) {
const request = {
drive_group: driveGroup
--- /dev/null
+export interface Daemon {
+ nodename: string;
+ container_id: string;
+ container_image_id: string;
+ container_image_name: string;
+ daemon_id: string;
+ daemon_type: string;
+ version: string;
+ status: number;
+ status_desc: string;
+ last_refresh: Date;
+}
--- /dev/null
+export interface CephService {
+ container_image_id: string;
+ container_image_name: string;
+ service_name: string;
+ size: number;
+ running: number;
+ last_refresh: Date;
+}
# -*- coding: utf-8 -*-
from __future__ import absolute_import
-
import logging
+from typing import List, Optional
+
from orchestrator import InventoryFilter, DeviceLightLoc, Completion
+from orchestrator import ServiceDescription, DaemonDescription
from orchestrator import OrchestratorClientMixin, raise_if_exception, OrchestratorError
from .. import mgr
from ..tools import wraps
class ServiceManager(ResourceManager):
@wait_api_result
- def list(self, service_type=None, service_id=None, host_name=None):
- return self.api.list_daemons(service_type, service_id, host_name)
+ def list(self, service_name: Optional[str] = None) -> List[ServiceDescription]:
+ return self.api.describe_service(None, service_name)
+
+ @wait_api_result
+ def get(self, service_name: str) -> ServiceDescription:
+ return self.api.describe_service(None, service_name)
+
+ @wait_api_result
+ def list_daemons(self,
+ service_name: Optional[str] = None,
+ hostname: Optional[str] = None) -> List[DaemonDescription]:
+ return self.api.list_daemons(service_name, host=hostname)
def reload(self, service_type, service_ids):
if not isinstance(service_ids, list):
from ..controllers.orchestrator import Orchestrator
from ..controllers.orchestrator import OrchestratorInventory
from ..controllers.orchestrator import OrchestratorOsd
-from ..controllers.orchestrator import OrchestratorService
class OrchestratorControllerTest(ControllerTestCase):
URL_STATUS = '/api/orchestrator/status'
URL_INVENTORY = '/api/orchestrator/inventory'
- URL_SERVICE = '/api/orchestrator/service'
URL_OSD = '/api/orchestrator/osd'
@classmethod
# pylint: disable=protected-access
Orchestrator._cp_config['tools.authenticate.on'] = False
OrchestratorInventory._cp_config['tools.authenticate.on'] = False
- OrchestratorService._cp_config['tools.authenticate.on'] = False
OrchestratorOsd._cp_config['tools.authenticate.on'] = False
cls.setup_controllers([Orchestrator,
OrchestratorInventory,
- OrchestratorService,
OrchestratorOsd])
@mock.patch('dashboard.controllers.orchestrator.OrchClient.instance')