From: Aashish Sharma Date: Thu, 18 Mar 2021 09:40:43 +0000 (+0530) Subject: mgr/dashboard: provide the service events when showing a service in the UI X-Git-Tag: v16.2.5~114^2 X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=21537ab60aec73883ee061aa79a1febffe9606f6;p=ceph.git 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) --- diff --git a/src/pybind/mgr/dashboard/controllers/host.py b/src/pybind/mgr/dashboard/controllers/host.py index c822cb50313..590925ca58b 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 4470f8f831e..b3c3ab94798 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 79dd6daf668..55966e9ed20 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 8e70b94a4ca..5a8116d5a28 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 e69de29bb2d..c9a361d3296 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 42c06228d86..e9b49ce80ea 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 c6d0a0a0561..1a17fdb61cc 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 6f14a78cde3..f805a685e72 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":