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>
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:
@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]:
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])
<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>
<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>
+@use './src/styles/vendor/variables' as vv;
+
+.fa-info-circle {
+ color: vv.$info;
+}
+
+.fa-exclamation-triangle {
+ color: vv.$danger;
+}
+
+.ul-margin {
+ margin-left: -30px;
+}
}
];
+ 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;
};
spyOn(cephServiceService, 'getDaemons').and.callFake(() =>
of(getDaemonsByServiceName(component.serviceName))
);
+ spyOn(cephServiceService, 'list').and.returnValue(of(services));
fixture.detectChanges();
});
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();
});
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({
@ViewChild('statusTpl', { static: true })
statusTpl: TemplateRef<any>;
+ @ViewChild('listTpl', { static: true })
+ listTpl: TemplateRef<any>;
+
@ViewChildren('daemonsTable')
daemonsTableTpls: QueryList<TemplateRef<TableComponent>>;
@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,
{
name: $localize`Hostname`,
prop: 'hostname',
- flexGrow: 1,
+ flexGrow: 2,
filterable: true
},
{
{
name: $localize`Container ID`,
prop: 'container_id',
- flexGrow: 3,
+ flexGrow: 2,
filterable: true,
cellTransformation: CellTemplate.truncate,
customTemplateConfig: {
{
name: $localize`Container Image ID`,
prop: 'container_image_id',
- flexGrow: 3,
+ flexGrow: 2,
filterable: true,
cellTransformation: CellTemplate.truncate,
customTemplateConfig: {
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
}
];
if (this.daemonsTableTplsSub) {
this.daemonsTableTplsSub.unsubscribe();
}
+ if (this.serviceSub) {
+ this.serviceSub.unsubscribe();
+ }
}
getStatusClass(row: Daemon): string {
}
);
}
+
+ 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;
+ }
}
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':
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':
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":