From: Kiefer Chang Date: Thu, 20 Feb 2020 09:18:19 +0000 (+0800) Subject: mgr/dashboard: list services and daemons X-Git-Tag: v15.1.1~96^2~2 X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=55adeb93b3f919f202ab79fd93c49fbdf68a7889;p=ceph.git mgr/dashboard: list services and daemons - 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//daemons`: listing daemons for a service. e.g. daemons of OSD. - `/api/host//daemons`: listing daemons of a host. Fixes: https://tracker.ceph.com/issues/44221 Signed-off-by: Kiefer Chang --- diff --git a/qa/tasks/mgr/dashboard/test_orchestrator.py b/qa/tasks/mgr/dashboard/test_orchestrator.py index e8fa0fa93d73..0f0a22431a90 100644 --- a/qa/tasks/mgr/dashboard/test_orchestrator.py +++ b/qa/tasks/mgr/dashboard/test_orchestrator.py @@ -57,7 +57,6 @@ class OrchestratorControllerTest(DashboardTestCase): URL_STATUS = '/api/orchestrator/status' URL_INVENTORY = '/api/orchestrator/inventory' - URL_SERVICE = '/api/orchestrator/service' URL_OSD = '/api/orchestrator/osd' @@ -110,8 +109,6 @@ class OrchestratorControllerTest(DashboardTestCase): 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) diff --git a/src/pybind/mgr/dashboard/controllers/host.py b/src/pybind/mgr/dashboard/controllers/host.py index d75af0507999..c609db0930df 100644 --- a/src/pybind/mgr/dashboard/controllers/host.py +++ b/src/pybind/mgr/dashboard/controllers/host.py @@ -2,10 +2,7 @@ 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 @@ -44,9 +41,9 @@ def merge_hosts_by_hostname(ceph_hosts, orch_hosts): # 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 @@ -119,3 +116,10 @@ class Host(RESTController): 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] diff --git a/src/pybind/mgr/dashboard/controllers/orchestrator.py b/src/pybind/mgr/dashboard/controllers/orchestrator.py index 642a2f6bab21..a1f088ba2c26 100644 --- a/src/pybind/mgr/dashboard/controllers/orchestrator.py +++ b/src/pybind/mgr/dashboard/controllers/orchestrator.py @@ -36,7 +36,7 @@ def get_device_osd_map(): } :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') @@ -123,15 +123,6 @@ class OrchestratorInventory(RESTController): 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): diff --git a/src/pybind/mgr/dashboard/controllers/service.py b/src/pybind/mgr/dashboard/controllers/service.py new file mode 100644 index 000000000000..9f1e70bb94dd --- /dev/null +++ b/src/pybind/mgr/dashboard/controllers/service.py @@ -0,0 +1,31 @@ +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] diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts index 4a11c8e63bc2..53aae8e28d37 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts @@ -47,6 +47,8 @@ import { RulesListComponent } from './prometheus/rules-list/rules-list.component 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({ @@ -116,7 +118,9 @@ import { ServicesComponent } from './services/services.component'; RulesListComponent, ActiveAlertListComponent, MonitoringListComponent, - HostFormComponent + HostFormComponent, + ServiceDetailsComponent, + ServiceDaemonListComponent ] }) export class ClusterModule {} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-details/host-details.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-details/host-details.component.html index 416aa2de3fee..a77f3bcd2226 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-details/host-details.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-details/host-details.component.html @@ -9,12 +9,10 @@ - - + + { 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', () => { @@ -73,7 +67,7 @@ describe('HostDetailsComponent', () => { 'Devices', 'Device health', 'Inventory', - 'Services', + 'Daemons', 'Performance Details' ]); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-daemon-list/service-daemon-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-daemon-list/service-daemon-list.component.html new file mode 100644 index 000000000000..69fdc85413a0 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-daemon-list/service-daemon-list.component.html @@ -0,0 +1,6 @@ + + diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-daemon-list/service-daemon-list.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-daemon-list/service-daemon-list.component.scss new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-daemon-list/service-daemon-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-daemon-list/service-daemon-list.component.spec.ts new file mode 100644 index 000000000000..207a4fb5a344 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-daemon-list/service-daemon-list.component.spec.ts @@ -0,0 +1,114 @@ +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; + + 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); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-daemon-list/service-daemon-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-daemon-list/service-daemon-list.component.ts new file mode 100644 index 000000000000..b18c7c4a2f76 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-daemon-list/service-daemon-list.component.ts @@ -0,0 +1,132 @@ +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; + + @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; + 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(); + } + ); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-details/service-details.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-details/service-details.component.html new file mode 100644 index 000000000000..924fb914947b --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-details/service-details.component.html @@ -0,0 +1,7 @@ + + + + + + diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-details/service-details.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-details/service-details.component.scss new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-details/service-details.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-details/service-details.component.spec.ts new file mode 100644 index 000000000000..d5480c7d6395 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-details/service-details.component.spec.ts @@ -0,0 +1,47 @@ +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; + + 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']); + }); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-details/service-details.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-details/service-details.component.ts new file mode 100644 index 000000000000..2ab5168a8358 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-details/service-details.component.ts @@ -0,0 +1,25 @@ +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() {} +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/services.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/services.component.html index e495bfc6ab2b..ce3e72bceaba 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/services.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/services.component.html @@ -8,10 +8,15 @@ + (updateSelection)="updateSelection($event)"> + + diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/services.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/services.component.spec.ts index adfe93f5c25c..f247dc2b8405 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/services.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/services.component.spec.ts @@ -1,63 +1,62 @@ -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; - 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(); }); @@ -70,15 +69,7 @@ describe('ServicesComponent', () => { }); 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); }); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/services.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/services.component.ts index 6241f3b026ea..226a9df37291 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/services.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/services.component.ts @@ -1,13 +1,17 @@ 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', @@ -23,71 +27,58 @@ export class ServicesComponent implements OnChanges, OnInit { // Do not display these columns @Input() hiddenColumns: string[] = []; + permissions: Permissions; + checkingOrchestrator = true; orchestratorExist = false; docsUrl: string; columns: Array = []; - services: Array = []; + services: Array = []; 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 } ]; @@ -123,18 +114,17 @@ export class ServicesComponent implements OnChanges, OnInit { } } + 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; }, diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/services.model.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/services.model.ts deleted file mode 100644 index 4c8077e397f0..000000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/services.model.ts +++ /dev/null @@ -1,14 +0,0 @@ -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; -} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/ceph-service.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/ceph-service.service.ts new file mode 100644 index 000000000000..8df6cb93c651 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/ceph-service.service.ts @@ -0,0 +1,28 @@ +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 { + const options = serviceName + ? { params: new HttpParams().set('service_name', serviceName) } + : {}; + return this.http.get(this.url, options); + } + + getDaemons(serviceName?: string): Observable { + return this.http.get(`${this.url}/${serviceName}/daemons`); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/host.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/host.service.ts index 01e432990e0e..5fb991c6747a 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/host.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/host.service.ts @@ -4,6 +4,7 @@ import { Injectable } from '@angular/core'; 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'; @@ -38,4 +39,8 @@ export class HostService { getSmartData(hostname: string) { return this.http.get(`${this.baseURL}/${hostname}/smart`); } + + getDaemons(hostname: string): Observable { + return this.http.get(`${this.baseURL}/${hostname}/daemons`); + } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/orchestrator.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/orchestrator.service.spec.ts index 94b0ed8e28f1..b68aefcfba21 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/orchestrator.service.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/orchestrator.service.spec.ts @@ -46,19 +46,6 @@ describe('OrchestratorService', () => { 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: { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/orchestrator.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/orchestrator.service.ts index f26de6d3264f..8b966448cbee 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/orchestrator.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/orchestrator.service.ts @@ -49,11 +49,6 @@ export class OrchestratorService { ); } - 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 diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/daemon.interface.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/daemon.interface.ts new file mode 100644 index 000000000000..c69a27851c68 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/daemon.interface.ts @@ -0,0 +1,12 @@ +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; +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/service.interface.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/service.interface.ts new file mode 100644 index 000000000000..f1acdd4d6d5f --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/service.interface.ts @@ -0,0 +1,8 @@ +export interface CephService { + container_image_id: string; + container_image_name: string; + service_name: string; + size: number; + running: number; + last_refresh: Date; +} diff --git a/src/pybind/mgr/dashboard/services/orchestrator.py b/src/pybind/mgr/dashboard/services/orchestrator.py index b2f282bbc0cb..1d66a9e76830 100644 --- a/src/pybind/mgr/dashboard/services/orchestrator.py +++ b/src/pybind/mgr/dashboard/services/orchestrator.py @@ -1,9 +1,11 @@ # -*- 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 @@ -74,8 +76,18 @@ class InventoryManager(ResourceManager): 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): diff --git a/src/pybind/mgr/dashboard/tests/test_orchestrator.py b/src/pybind/mgr/dashboard/tests/test_orchestrator.py index da1232f8653a..ee6ea44fa60e 100644 --- a/src/pybind/mgr/dashboard/tests/test_orchestrator.py +++ b/src/pybind/mgr/dashboard/tests/test_orchestrator.py @@ -12,13 +12,11 @@ from ..controllers.orchestrator import get_device_osd_map 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 @@ -26,11 +24,9 @@ class OrchestratorControllerTest(ControllerTestCase): # 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')