]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: list services and daemons
authorKiefer Chang <kiefer.chang@suse.com>
Thu, 20 Feb 2020 09:18:19 +0000 (17:18 +0800)
committerKiefer Chang <kiefer.chang@suse.com>
Mon, 2 Mar 2020 02:20:33 +0000 (10:20 +0800)
- 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>
27 files changed:
qa/tasks/mgr/dashboard/test_orchestrator.py
src/pybind/mgr/dashboard/controllers/host.py
src/pybind/mgr/dashboard/controllers/orchestrator.py
src/pybind/mgr/dashboard/controllers/service.py [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-details/host-details.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/host-details/host-details.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-daemon-list/service-daemon-list.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-daemon-list/service-daemon-list.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-daemon-list/service-daemon-list.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-daemon-list/service-daemon-list.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-details/service-details.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-details/service-details.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-details/service-details.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-details/service-details.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/services.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/services.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/services.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/services.model.ts [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/shared/api/ceph-service.service.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/api/host.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/orchestrator.service.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/orchestrator.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/models/daemon.interface.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/models/service.interface.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/services/orchestrator.py
src/pybind/mgr/dashboard/tests/test_orchestrator.py

index e8fa0fa93d732f758a01957bc4c1a9f57462ed8b..0f0a22431a9087cae9764af5f268129608a1f3b4 100644 (file)
@@ -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)
index d75af05079992984c47ec794f66e6509bc8a7f0c..c609db0930df0b06158f97a79ed0a200ec5efd99 100644 (file)
@@ -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]
index 642a2f6bab2121b5981150481544802ef67f8bd3..a1f088ba2c26105088840ead8cb33a29432ce87e 100644 (file)
@@ -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 (file)
index 0000000..9f1e70b
--- /dev/null
@@ -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]
index 4a11c8e63bc29119578a3bab222bb9451496ea7b..53aae8e28d376de50605214aeef8cbcaa9d4e246 100644 (file)
@@ -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 {}
index 416aa2de3fee266435eb8c97153dec37d683b6ca..a77f3bcd222688ef234bf4043120e596c943da24 100644 (file)
@@ -9,12 +9,10 @@
     <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"
index 50759e0a7b4797a7d7e0ee5f394d8606d5e9bb63..b78c15ffc29bdef5544d78316db62708196718be 100644 (file)
@@ -6,11 +6,9 @@ import { NgBootstrapFormValidationModule } from 'ng-bootstrap-form-validation';
 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';
@@ -47,10 +45,6 @@ describe('HostDetailsComponent', () => {
       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 (file)
index 0000000..69fdc85
--- /dev/null
@@ -0,0 +1,6 @@
+<cd-table [data]="daemons"
+          [columns]="columns"
+          columnMode="flex"
+          autoReload="0"
+          (fetchData)="getDaemons($event)">
+</cd-table>
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 (file)
index 0000000..e69de29
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 (file)
index 0000000..207a4fb
--- /dev/null
@@ -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<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);
+  });
+});
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 (file)
index 0000000..b18c7c4
--- /dev/null
@@ -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<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();
+      }
+    );
+  }
+}
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 (file)
index 0000000..924fb91
--- /dev/null
@@ -0,0 +1,7 @@
+<tabset *ngIf="selection.hasSingleSelection">
+  <tab i18n-heading
+       heading="Daemons">
+    <cd-service-daemon-list [serviceName]="selection.first()['service_name']">
+    </cd-service-daemon-list>
+  </tab>
+</tabset>
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 (file)
index 0000000..e69de29
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 (file)
index 0000000..d5480c7
--- /dev/null
@@ -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<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']);
+    });
+  });
+});
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 (file)
index 0000000..2ab5168
--- /dev/null
@@ -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() {}
+}
index e495bfc6ab2bfb105facec88a6bf76bc2515360d..ce3e72bceaba770178eb442052804461e7167440 100644 (file)
@@ -8,10 +8,15 @@
 <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>
index adfe93f5c25c5281984abf798189719892d528da..f247dc2b84059303fda978455e032ab7b74d5f5d 100644 (file)
@@ -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<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();
   });
 
@@ -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);
   });
 });
index 6241f3b026ea3eb57ba521807458b3acc81c2088..226a9df37291749e3a6ae7bbd4825f3fbf9a3f95 100644 (file)
@@ -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<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
       }
     ];
@@ -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 (file)
index 4c8077e..0000000
+++ /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 (file)
index 0000000..8df6cb9
--- /dev/null
@@ -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<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`);
+  }
+}
index 01e432990e0e73de46e7ee732fb3d8c7d843b3b8..5fb991c6747a2ac0408151b05a596e9394ae3817 100644 (file)
@@ -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<SmartDataResponseV1>(`${this.baseURL}/${hostname}/smart`);
   }
+
+  getDaemons(hostname: string): Observable<Daemon[]> {
+    return this.http.get<Daemon[]>(`${this.baseURL}/${hostname}/daemons`);
+  }
 }
index 94b0ed8e28f1824b80fc466b6fed3bcca03edb40..b68aefcfba212706044bf2cf434222a86d1c60c5 100644 (file)
@@ -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: {
index f26de6d3264f7d0abe9fe0ef6c36ae1870e33500..8b966448cbee4d0c9648c945fd18a9349d5cee2f 100644 (file)
@@ -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 (file)
index 0000000..c69a278
--- /dev/null
@@ -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 (file)
index 0000000..f1acdd4
--- /dev/null
@@ -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;
+}
index b2f282bbc0cb3c2bdf586a85b2ab86a0ff909fe5..1d66a9e76830fdef59af650f50d869997f45eeac 100644 (file)
@@ -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):
index da1232f8653a5dbf53ea052212d2acb3b2035c97..ee6ea44fa60ee818dbd4862a4fd3442614c33df4 100644 (file)
@@ -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')