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":