]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: provide the service events when showing a service in the UI 41494/head
authorAashish Sharma <aashishsharma@localhost.localdomain>
Thu, 18 Mar 2021 09:40:43 +0000 (15:10 +0530)
committerAashish Sharma <aashishsharma@localhost.localdomain>
Mon, 24 May 2021 04:49:52 +0000 (10:19 +0530)
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 <aasharma@redhat.com>
(cherry picked from commit 00ce7c90ef19f02beea9431ab360d23744efde2d)

src/pybind/mgr/dashboard/controllers/host.py
src/pybind/mgr/dashboard/controllers/service.py
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/services/service-daemon-list/service-daemon-list.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-daemon-list/service-daemon-list.component.scss
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-daemon-list/service-daemon-list.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-daemon-list/service-daemon-list.component.ts
src/pybind/mgr/orchestrator/_interface.py

index c822cb503136d5a22f63e2ea52ca142b06ada1d5..590925ca58bf9cdef6c748f8dc3ff6c551d7553a 100644 (file)
@@ -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:
index 4470f8f831e02ff5231f6c139f8123007435e22e..b3c3ab94798bf4c4788ec29cb68057c7823fb74b 100644 (file)
@@ -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])
index 79dd6daf668382e68ab5e924c3621fd0fe80eef6..55966e9ed20995ee91bcd654b7f719bb3492d63c 100644 (file)
@@ -23,7 +23,8 @@
       <a ngbNavLink
          i18n>Daemons</a>
       <ng-template ngbNavContent>
-        <cd-service-daemon-list [hostname]="selectedHostname">
+        <cd-service-daemon-list [hostname]="selectedHostname"
+                                flag="hostDetails">
         </cd-service-daemon-list>
       </ng-template>
     </li>
index 8e70b94a4ca89dac8e4bd6c74236033619dd3cd7..5a8116d5a288ac3ed3701c8af682999e6bf1896f 100644 (file)
@@ -1,12 +1,51 @@
 <cd-orchestrator-doc-panel *ngIf="showDocPanel"></cd-orchestrator-doc-panel>
-<cd-table *ngIf="hasOrchestrator"
-          #daemonsTable
-          [data]="daemons"
-          [columns]="columns"
-          columnMode="flex"
-          [autoReload]="5000"
-          (fetchData)="getDaemons($event)">
-</cd-table>
+
+<div *ngIf="flag === 'hostDetails'; else serviceDetailsTpl">
+  <cd-table *ngIf="hasOrchestrator"
+            #daemonsTable
+            [data]="daemons"
+            [columns]="columns"
+            columnMode="flex"
+            (fetchData)="getDaemons($event)">
+  </cd-table>
+</div>
+
+<ng-template #serviceDetailsTpl>
+  <ng-container>
+    <ul ngbNav
+        #nav="ngbNav"
+        class="nav-tabs"
+        cdStatefulTab="service-details">
+      <li ngbNavItem="details">
+        <a ngbNavLink
+           i18n>Details</a>
+        <ng-template ngbNavContent>
+          <cd-table *ngIf="hasOrchestrator"
+                    #daemonsTable
+                    [data]="daemons"
+                    [columns]="columns"
+                    columnMode="flex"
+                    (fetchData)="getDaemons($event)">
+          </cd-table>
+        </ng-template>
+      </li>
+      <li ngbNavItem="service_events">
+        <a ngbNavLink
+           i18n>Service Events</a>
+        <ng-template ngbNavContent>
+          <cd-table *ngIf="hasOrchestrator"
+                    #serviceTable
+                    [data]="services"
+                    [columns]="serviceColumns"
+                    columnMode="flex"
+                    (fetchData)="getServices($event)">
+          </cd-table>
+        </ng-template>
+      </li>
+    </ul>
+    <div [ngbNavOutlet]="nav"></div>
+  </ng-container>
+</ng-template>
 
 <ng-template #statusTpl
              let-row="row">
     {{ row.status_desc }}
   </span>
 </ng-template>
+
+<ng-template #listTpl
+             let-events="value">
+  <div *ngIf="events.length == 0 || events == undefined">
+    <span>No data available</span>
+  </div>
+  <div *ngIf="events.length != 0 && events != undefined"
+       class="ul-margin">
+    <ul *ngFor="let event of events; trackBy:trackByFn">
+      <li><b>{{ event.created | relativeDate }} - </b>
+      <span class="badge badge-info">{{ event.subject }}</span><br>
+      <span *ngIf="event.level == 'INFO'">
+      <i [ngClass]="[icons.infoCircle]"
+         aria-hidden="true"></i>
+      </span>
+      <span *ngIf="event.level == 'ERROR'">
+      <i [ngClass]="[icons.warning]"
+         aria-hidden="true"></i>
+      </span>
+      {{ event.message }}</li>
+    </ul>
+  </div>
+</ng-template>
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..c9a361d329614441f463d194f5fd046e5843e262 100644 (file)
@@ -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;
+}
index 42c06228d867f273549147603ebfad525e0e2943..e9b49ce80ea02d8498483d51af1aa689ae55a398 100644 (file)
@@ -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();
   });
index c6d0a0a0561a82bc71846a8cbe163fcd34191924..1a17fdb61ccddd2d07187f528a5cc7f952029239 100644 (file)
@@ -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<any>;
 
+  @ViewChild('listTpl', { static: true })
+  listTpl: TemplateRef<any>;
+
   @ViewChildren('daemonsTable')
   daemonsTableTpls: QueryList<TemplateRef<TableComponent>>;
 
@@ -42,14 +47,22 @@ export class ServiceDaemonListComponent implements OnInit, OnChanges, AfterViewI
   @Input()
   hostname?: string;
 
+  @Input()
+  flag?: string;
+
+  icons = Icons;
+
   daemons: Daemon[] = [];
+  services: Array<CephServiceSpec> = [];
   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;
+  }
 }
index 6f14a78cde3f504ae6026b5ad405687b3673b39e..f805a685e72e1b093c1c9ef8f86b5b320da548a8 100644 (file)
@@ -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":