Merge pull request #36845 from bk201/wip-44803

mgr/dashboard: allow getting fresh inventory data from the orchestrator

Reviewed-by: Stephan Müller <smueller@suse.com>
Reviewed-by: Volker Theile <vtheile@suse.com>
This commit is contained in:
Lenz Grimmer 2020-09-14 11:48:22 +02:00 committed by GitHub
commit 856d9de394
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 85 additions and 32 deletions

View File

@ -12,7 +12,7 @@ from ..exceptions import DashboardException
from ..security import Scope
from ..services.exception import handle_orchestrator_error
from ..services.orchestrator import OrchClient, OrchFeature
from ..tools import TaskManager
from ..tools import TaskManager, str_to_bool
STATUS_SCHEMA = {
"available": (bool, "Orchestrator status"),
@ -122,10 +122,13 @@ class Orchestrator(RESTController):
class OrchestratorInventory(RESTController):
@raise_if_no_orchestrator([OrchFeature.DEVICE_LIST])
def list(self, hostname=None):
def list(self, hostname=None, refresh=None):
orch = OrchClient.instance()
hosts = [hostname] if hostname else None
inventory_hosts = [host.to_json() for host in orch.inventory.list(hosts)]
do_refresh = False
if refresh is not None:
do_refresh = str_to_bool(refresh)
inventory_hosts = [host.to_json() for host in orch.inventory.list(hosts, do_refresh)]
device_osd_map = get_device_osd_map()
for inventory_host in inventory_hosts:
host_osds = device_osd_map.get(inventory_host['name'])

View File

@ -45,13 +45,17 @@ describe('InventoryComponent', () => {
describe('after ngOnInit', () => {
it('should load devices', () => {
fixture.detectChanges();
expect(orchService.inventoryDeviceList).toHaveBeenCalledWith(undefined);
});
expect(orchService.inventoryDeviceList).toHaveBeenNthCalledWith(1, undefined, false);
component.refresh(); // click refresh button
expect(orchService.inventoryDeviceList).toHaveBeenNthCalledWith(2, undefined, true);
it('should load devices for a host', () => {
component.hostname = 'host0';
const newHost = 'host0';
component.hostname = newHost;
fixture.detectChanges();
expect(orchService.inventoryDeviceList).toHaveBeenCalledWith('host0');
component.ngOnChanges();
expect(orchService.inventoryDeviceList).toHaveBeenNthCalledWith(3, newHost, false);
component.refresh(); // click refresh button
expect(orchService.inventoryDeviceList).toHaveBeenNthCalledWith(4, newHost, true);
});
});
});

View File

@ -1,4 +1,6 @@
import { Component, Input, OnChanges, OnInit } from '@angular/core';
import { Component, Input, NgZone, OnChanges, OnDestroy, OnInit } from '@angular/core';
import { Subscription, timer as observableTimer } from 'rxjs';
import { OrchestratorService } from '../../../shared/api/orchestrator.service';
import { Icons } from '../../../shared/enum/icons.enum';
@ -10,39 +12,59 @@ import { InventoryDevice } from './inventory-devices/inventory-device.model';
templateUrl: './inventory.component.html',
styleUrls: ['./inventory.component.scss']
})
export class InventoryComponent implements OnChanges, OnInit {
export class InventoryComponent implements OnChanges, OnInit, OnDestroy {
// Display inventory page only for this hostname, ignore to display all.
@Input() hostname?: string;
private reloadSubscriber: Subscription;
private reloadInterval = 5000;
private firstRefresh = true;
icons = Icons;
orchStatus: OrchestratorStatus;
devices: Array<InventoryDevice> = [];
constructor(private orchService: OrchestratorService) {}
constructor(private orchService: OrchestratorService, private ngZone: NgZone) {}
ngOnInit() {
this.orchService.status().subscribe((status) => {
this.orchStatus = status;
if (status.available) {
this.getInventory();
// Create a timer to get cached inventory from the orchestrator.
// Do not ask the orchestrator frequently to refresh its cache data because it's expensive.
this.ngZone.runOutsideAngular(() => {
// start after first pass because the embedded table calls refresh at init.
this.reloadSubscriber = observableTimer(
this.reloadInterval,
this.reloadInterval
).subscribe(() => {
this.ngZone.run(() => {
this.getInventory(false);
});
});
});
}
});
}
ngOnDestroy() {
this.reloadSubscriber?.unsubscribe();
}
ngOnChanges() {
if (this.orchStatus) {
if (this.orchStatus?.available) {
this.devices = [];
this.getInventory();
this.getInventory(false);
}
}
getInventory() {
getInventory(refresh: boolean) {
if (this.hostname === '') {
return;
}
this.orchService.inventoryDeviceList(this.hostname).subscribe(
this.orchService.inventoryDeviceList(this.hostname, refresh).subscribe(
(devices: InventoryDevice[]) => {
this.devices = devices;
},
@ -53,6 +75,9 @@ export class InventoryComponent implements OnChanges, OnInit {
}
refresh() {
this.getInventory();
// Make the first reload (triggered by table) use cached data, and
// the remaining reloads (triggered by users) ask orchestrator to refresh inventory.
this.getInventory(!this.firstRefresh);
this.firstRefresh = false;
}
}

View File

@ -33,16 +33,31 @@ describe('OrchestratorService', () => {
expect(req.request.method).toBe('GET');
});
it('should call inventoryList', () => {
service.inventoryList().subscribe();
const req = httpTesting.expectOne(`${apiPath}/inventory`);
expect(req.request.method).toBe('GET');
});
it('should call inventoryList with arguments', () => {
const inventoryPath = `${apiPath}/inventory`;
const tests: { args: any[]; expectedUrl: any }[] = [
{
args: [],
expectedUrl: inventoryPath
},
{
args: ['host0'],
expectedUrl: `${inventoryPath}?hostname=host0`
},
{
args: [undefined, true],
expectedUrl: `${inventoryPath}?refresh=true`
},
{
args: ['host0', true],
expectedUrl: `${inventoryPath}?hostname=host0&refresh=true`
}
];
it('should call inventoryList with a host', () => {
const host = 'host0';
service.inventoryList(host).subscribe();
const req = httpTesting.expectOne(`${apiPath}/inventory?hostname=${host}`);
expect(req.request.method).toBe('GET');
for (const test of tests) {
service.inventoryList(...test.args).subscribe();
const req = httpTesting.expectOne(test.expectedUrl);
expect(req.request.method).toBe('GET');
}
});
});

View File

@ -55,13 +55,19 @@ export class OrchestratorService {
});
}
inventoryList(hostname?: string): Observable<InventoryHost[]> {
const options = hostname ? { params: new HttpParams().set('hostname', hostname) } : {};
return this.http.get<InventoryHost[]>(`${this.url}/inventory`, options);
inventoryList(hostname?: string, refresh?: boolean): Observable<InventoryHost[]> {
let params = new HttpParams();
if (hostname) {
params = params.append('hostname', hostname);
}
if (refresh) {
params = params.append('refresh', _.toString(refresh));
}
return this.http.get<InventoryHost[]>(`${this.url}/inventory`, { params: params });
}
inventoryDeviceList(hostname?: string): Observable<InventoryDevice[]> {
return this.inventoryList(hostname).pipe(
inventoryDeviceList(hostname?: string, refresh?: boolean): Observable<InventoryDevice[]> {
return this.inventoryList(hostname, refresh).pipe(
mergeMap((hosts: InventoryHost[]) => {
const devices = _.flatMap(hosts, (host) => {
return host.devices.map((device) => {