From 21537ab60aec73883ee061aa79a1febffe9606f6 Mon Sep 17 00:00:00 2001 From: Aashish Sharma Date: Thu, 18 Mar 2021 15:10:43 +0530 Subject: [PATCH] mgr/dashboard: provide the service events when showing a service in the UI When service deployment failures occur, events are generated and associated with the service. This PR intends to add these events as Daemon Logs and Service Logs to the UI for troubleshooting and doing diagnostics for the same. Fixes: https://tracker.ceph.com/issues/49262 Signed-off-by: Aashish Sharma (cherry picked from commit 00ce7c90ef19f02beea9431ab360d23744efde2d) --- src/pybind/mgr/dashboard/controllers/host.py | 2 +- .../mgr/dashboard/controllers/service.py | 4 +- .../host-details/host-details.component.html | 3 +- .../service-daemon-list.component.html | 78 +++++++++++++++++-- .../service-daemon-list.component.scss | 13 ++++ .../service-daemon-list.component.spec.ts | 33 ++++++++ .../service-daemon-list.component.ts | 67 +++++++++++++++- src/pybind/mgr/orchestrator/_interface.py | 66 ++++++++++++++++ 8 files changed, 250 insertions(+), 16 deletions(-) diff --git a/src/pybind/mgr/dashboard/controllers/host.py b/src/pybind/mgr/dashboard/controllers/host.py index c822cb503136d..590925ca58bf9 100644 --- a/src/pybind/mgr/dashboard/controllers/host.py +++ b/src/pybind/mgr/dashboard/controllers/host.py @@ -364,7 +364,7 @@ class Host(RESTController): def daemons(self, hostname: str) -> List[dict]: orch = OrchClient.instance() daemons = orch.services.list_daemons(hostname=hostname) - return [d.to_json() for d in daemons] + return [d.to_dict() for d in daemons] @handle_orchestrator_error('host') def get(self, hostname: str) -> Dict: diff --git a/src/pybind/mgr/dashboard/controllers/service.py b/src/pybind/mgr/dashboard/controllers/service.py index 4470f8f831e02..b3c3ab94798bf 100644 --- a/src/pybind/mgr/dashboard/controllers/service.py +++ b/src/pybind/mgr/dashboard/controllers/service.py @@ -32,7 +32,7 @@ class Service(RESTController): @raise_if_no_orchestrator([OrchFeature.SERVICE_LIST]) 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=service_name)] + return [service.to_dict() for service in orch.services.list(service_name=service_name)] @raise_if_no_orchestrator([OrchFeature.SERVICE_LIST]) def get(self, service_name: str) -> List[dict]: @@ -47,7 +47,7 @@ class Service(RESTController): def daemons(self, service_name: str) -> List[dict]: orch = OrchClient.instance() daemons = orch.services.list_daemons(service_name=service_name) - return [d.to_json() for d in daemons] + return [d.to_dict() for d in daemons] @CreatePermission @raise_if_no_orchestrator([OrchFeature.SERVICE_CREATE]) 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 79dd6daf66838..55966e9ed2099 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 @@ -23,7 +23,8 @@ Daemons - + 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 index 8e70b94a4ca89..5a8116d5a288a 100644 --- 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 @@ -1,12 +1,51 @@ - - + +
+ + +
+ + + + +
+
+
@@ -15,3 +54,26 @@ {{ row.status_desc }} + + +
+ No data available +
+
+
    +
  • {{ event.created | relativeDate }} - + {{ event.subject }}
    + + + + + + + {{ event.message }}
  • +
+
+
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 index e69de29bb2d1d..c9a361d329614 100644 --- 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 @@ -0,0 +1,13 @@ +@use './src/styles/vendor/variables' as vv; + +.fa-info-circle { + color: vv.$info; +} + +.fa-exclamation-triangle { + color: vv.$danger; +} + +.ul-margin { + margin-left: -30px; +} 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 index 42c06228d867f..e9b49ce80ea02 100644 --- 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 @@ -69,6 +69,33 @@ describe('ServiceDaemonListComponent', () => { } ]; + const services = [ + { + service_type: 'osd', + service_name: 'osd', + status: { + container_image_id: 'e70344c77bcbf3ee389b9bf5128f635cf95f3d59e005c5d8e67fc19bcc74ed23', + container_image_name: 'docker.io/ceph/daemon-base:latest-master-devel', + size: 3, + running: 3, + last_refresh: '2020-02-25T04:33:26.465699' + }, + events: '2021-03-22T07:34:48.582163Z service:osd [INFO] "service was created"' + }, + { + service_type: 'crash', + service_name: 'crash', + status: { + container_image_id: 'e70344c77bcbf3ee389b9bf5128f635cf95f3d59e005c5d8e67fc19bcc74ed23', + container_image_name: 'docker.io/ceph/daemon-base:latest-master-devel', + size: 1, + running: 1, + last_refresh: '2020-02-25T04:33:26.465766' + }, + events: '2021-03-22T07:34:48.582163Z service:osd [INFO] "service was created"' + } + ]; + const getDaemonsByHostname = (hostname?: string) => { return hostname ? _.filter(daemons, { hostname: hostname }) : daemons; }; @@ -92,6 +119,7 @@ describe('ServiceDaemonListComponent', () => { spyOn(cephServiceService, 'getDaemons').and.callFake(() => of(getDaemonsByServiceName(component.serviceName)) ); + spyOn(cephServiceService, 'list').and.returnValue(of(services)); fixture.detectChanges(); }); @@ -111,6 +139,11 @@ describe('ServiceDaemonListComponent', () => { expect(component.daemons.length).toBe(3); }); + it('should list services', () => { + component.getServices(new CdTableFetchDataContext(() => undefined)); + expect(component.services.length).toBe(2); + }); + it('should not display doc panel if orchestrator is available', () => { expect(component.showDocPanel).toBeFalsy(); }); 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 index c6d0a0a0561a8..1a17fdb61ccdd 100644 --- 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 @@ -19,9 +19,11 @@ import { HostService } from '~/app/shared/api/host.service'; import { OrchestratorService } from '~/app/shared/api/orchestrator.service'; import { TableComponent } from '~/app/shared/datatable/table/table.component'; import { CellTemplate } from '~/app/shared/enum/cell-template.enum'; +import { Icons } from '~/app/shared/enum/icons.enum'; import { CdTableColumn } from '~/app/shared/models/cd-table-column'; import { CdTableFetchDataContext } from '~/app/shared/models/cd-table-fetch-data-context'; import { Daemon } from '~/app/shared/models/daemon.interface'; +import { CephServiceSpec } from '~/app/shared/models/service.interface'; import { RelativeDatePipe } from '~/app/shared/pipes/relative-date.pipe'; @Component({ @@ -33,6 +35,9 @@ export class ServiceDaemonListComponent implements OnInit, OnChanges, AfterViewI @ViewChild('statusTpl', { static: true }) statusTpl: TemplateRef; + @ViewChild('listTpl', { static: true }) + listTpl: TemplateRef; + @ViewChildren('daemonsTable') daemonsTableTpls: QueryList>; @@ -42,14 +47,22 @@ export class ServiceDaemonListComponent implements OnInit, OnChanges, AfterViewI @Input() hostname?: string; + @Input() + flag?: string; + + icons = Icons; + daemons: Daemon[] = []; + services: Array = []; columns: CdTableColumn[] = []; + serviceColumns: CdTableColumn[] = []; hasOrchestrator = false; showDocPanel = false; private daemonsTable: TableComponent; private daemonsTableTplsSub: Subscription; + private serviceSub: Subscription; constructor( private hostService: HostService, @@ -63,7 +76,7 @@ export class ServiceDaemonListComponent implements OnInit, OnChanges, AfterViewI { name: $localize`Hostname`, prop: 'hostname', - flexGrow: 1, + flexGrow: 2, filterable: true }, { @@ -81,7 +94,7 @@ export class ServiceDaemonListComponent implements OnInit, OnChanges, AfterViewI { name: $localize`Container ID`, prop: 'container_id', - flexGrow: 3, + flexGrow: 2, filterable: true, cellTransformation: CellTemplate.truncate, customTemplateConfig: { @@ -97,7 +110,7 @@ export class ServiceDaemonListComponent implements OnInit, OnChanges, AfterViewI { name: $localize`Container Image ID`, prop: 'container_image_id', - flexGrow: 3, + flexGrow: 2, filterable: true, cellTransformation: CellTemplate.truncate, customTemplateConfig: { @@ -121,7 +134,34 @@ export class ServiceDaemonListComponent implements OnInit, OnChanges, AfterViewI name: $localize`Last Refreshed`, prop: 'last_refresh', pipe: this.relativeDatePipe, - flexGrow: 2 + flexGrow: 1 + }, + { + name: $localize`Daemon Events`, + prop: 'events', + flexGrow: 5, + cellTemplate: this.listTpl + } + ]; + + this.serviceColumns = [ + { + name: $localize`Service Name`, + prop: 'service_name', + flexGrow: 2, + filterable: true + }, + { + name: $localize`Service Type`, + prop: 'service_type', + flexGrow: 1, + filterable: true + }, + { + name: $localize`Service Events`, + prop: 'events', + flexGrow: 5, + cellTemplate: this.listTpl } ]; @@ -149,6 +189,9 @@ export class ServiceDaemonListComponent implements OnInit, OnChanges, AfterViewI if (this.daemonsTableTplsSub) { this.daemonsTableTplsSub.unsubscribe(); } + if (this.serviceSub) { + this.serviceSub.unsubscribe(); + } } getStatusClass(row: Daemon): string { @@ -183,4 +226,20 @@ export class ServiceDaemonListComponent implements OnInit, OnChanges, AfterViewI } ); } + + getServices(context: CdTableFetchDataContext) { + this.serviceSub = this.cephServiceService.list(this.serviceName).subscribe( + (services: CephServiceSpec[]) => { + this.services = services; + }, + () => { + this.services = []; + context.error(); + } + ); + } + + trackByFn(_index: any, item: any) { + return item.created; + } } diff --git a/src/pybind/mgr/orchestrator/_interface.py b/src/pybind/mgr/orchestrator/_interface.py index 6f14a78cde3f5..f805a685e72e1 100644 --- a/src/pybind/mgr/orchestrator/_interface.py +++ b/src/pybind/mgr/orchestrator/_interface.py @@ -973,6 +973,40 @@ class DaemonDescription(object): del out[e] return out + def to_dict(self) -> dict: + out: Dict[str, Any] = OrderedDict() + out['daemon_type'] = self.daemon_type + out['daemon_id'] = self.daemon_id + out['hostname'] = self.hostname + out['container_id'] = self.container_id + out['container_image_id'] = self.container_image_id + out['container_image_name'] = self.container_image_name + out['container_image_digests'] = self.container_image_digests + out['memory_usage'] = self.memory_usage + out['memory_request'] = self.memory_request + out['memory_limit'] = self.memory_limit + out['version'] = self.version + out['status'] = self.status.value if self.status is not None else None + out['status_desc'] = self.status_desc + if self.daemon_type == 'osd': + out['osdspec_affinity'] = self.osdspec_affinity + out['is_active'] = self.is_active + out['ports'] = self.ports + out['ip'] = self.ip + + for k in ['last_refresh', 'created', 'started', 'last_deployed', + 'last_configured']: + if getattr(self, k): + out[k] = datetime_to_str(getattr(self, k)) + + if self.events: + out['events'] = [e.to_dict() for e in self.events] + + empty = [k for k, v in out.items() if v is None] + for e in empty: + del out[e] + return out + @classmethod @handle_type_error def from_json(cls, data: dict) -> 'DaemonDescription': @@ -1092,6 +1126,29 @@ class ServiceDescription(object): out['events'] = [e.to_json() for e in self.events] return out + def to_dict(self) -> OrderedDict: + out = self.spec.to_json() + status = { + 'container_image_id': self.container_image_id, + 'container_image_name': self.container_image_name, + 'rados_config_location': self.rados_config_location, + 'service_url': self.service_url, + 'size': self.size, + 'running': self.running, + 'last_refresh': self.last_refresh, + 'created': self.created, + 'virtual_ip': self.virtual_ip, + 'ports': self.ports if self.ports else None, + } + for k in ['last_refresh', 'created']: + if getattr(self, k): + status[k] = datetime_to_str(getattr(self, k)) + status = {k: v for (k, v) in status.items() if v is not None} + out['status'] = status + if self.events: + out['events'] = [e.to_dict() for e in self.events] + return out + @classmethod @handle_type_error def from_json(cls, data: dict) -> 'ServiceDescription': @@ -1249,6 +1306,15 @@ class OrchestratorEvent: created = datetime_to_str(self.created) return f'{created} {self.kind_subject()} [{self.level}] "{self.message}"' + def to_dict(self) -> dict: + # Convert events data to dict. + return { + 'created': datetime_to_str(self.created), + 'subject': self.kind_subject(), + 'level': self.level, + 'message': self.message + } + @classmethod @handle_type_error def from_json(cls, data: str) -> "OrchestratorEvent": -- 2.39.5