vsphere-infra
Version:
361 lines (337 loc) • 14.5 kB
text/typescript
/*
* Copyright 2014-2021 Firestack, all rights reserved.
*/
import * as soap from 'soap';
import { getMoTypeByName, iidParse } from './managed.objects';
import { VsphereDatacenter } from './vsphere';
import { ref, xsi, paramFilter, parseObjects } from './soap.parser';
import { ix, promise } from 'ts-basis';
export class VsphereCaller extends ix.Entity {
$dc: VsphereDatacenter;
topError: any = null;
topErrorBody: string = '';
topErrorMessage: string = '';
topErrorDetail: string = '';
errors: {data: any, t: number}[] = [];
client: soap.Client;
serviceContent: any;
containerView: any;
inventoryData: any;
inventoryRaw: any;
inventoryStubsData: any;
inventoryStubsRaw: any;
options = { forever: true, timeout: 30000 };
constructor($dc: VsphereDatacenter, client) {
super('vcenter-api');
this.$dc = $dc;
this.client = client;
$dc.lifecycle.manage(this);
}
resetData() {
this.serviceContent = null;
this.containerView = null;
this.inventoryData = null;
this.inventoryRaw = null;
this.inventoryStubsData = null;
this.inventoryStubsRaw = null;
}
errorCodeHandle(message: string) {
const dc = this.$dc;
const has = part => message.indexOf(part) >= 0;
if (has('ManagedObjectNotFoundFault')) {
if (has('vim.view.ContainerView:session') &&
has('has already been deleted or has not been completely created')) {
// refetch default container view
this.getDefaultContainerView(true);
dc.emitEvent(dc.sessionNotValid$, { type: 'not_found' });
return;
} else {
const moValue = message.split('</obj>')[0].split('>').pop();
const iid = iidParse(moValue);
dc.emitEvent(dc.entryNotFound$, { iid, value: moValue });
return;
}
} else if (has('Unknown mo value session[')) {
this.getDefaultContainerView(true);
dc.emitEvent(dc.sessionNotValid$, { type: 'unknown_mo' });
return;
}
return 'YES_DEBUG_OUTPUT';
}
resolveError(error, resolve) {
this.topError = error;
if (error.body) {
this.topErrorBody = error.body;
try {
if (error.body.indexOf('<faultstring>') >= 0) {
this.topErrorMessage = error.body.split('<faultstring>')[1].split('</faultstring>')[0];
this.topErrorDetail = error.body.split('<detail>')[1].split('</detail>')[0];
let doOutput = 'YES_DEBUG_OUTPUT';
if (this.topErrorDetail) { doOutput = this.errorCodeHandle(this.topErrorDetail); }
if (doOutput) { this.$dc.debugOutput(new Error(this.topErrorDetail)); }
} else {
let handled = false;
if (error instanceof Error) {
if (error.message === 'ESOCKETTIMEDOUT') {
handled = true;
this.$dc.debugOutput('request.js error code: ESOCKETTIMEDOUT');
}
} else {
}
if (!handled) {
this.$dc.ix.pushError(new Error(`Unknown error: ${JSON.stringify(error)}`), 2);
}
}
} catch (e) { this.$dc.ix.pushError(e); }
} else {
if (error instanceof Error) {
this.$dc.ix.pushError(error, 2);
} else {
this.$dc.ix.pushError(new Error(`Unknown error: ${JSON.stringify(error)}`), 2);
}
}
resolve(null);
}
async getServiceContent(): Promise<any> {
if (!this.serviceContent) {
this.serviceContent = promise<any>(async resolve => {
this.client.VimService.VimPort.RetrieveServiceContent(paramFilter({
_this: 'ServiceInstance'
}), async (e, r) => {
if (e) { return this.resolveError(e, resolve); }
if (!r) { return this.resolveError(new Error('Emtpy response'), resolve); }
this.serviceContent = r.returnval;
this.$dc.debugOutput(`fetched service content`);
return resolve(r.returnval);
}, this.options);
});
}
return await Promise.resolve(this.serviceContent);
}
async getDefaultContainerView(noCache = false) {
if (!this.containerView || noCache) {
this.containerView = promise(async resolve => {
if (!await this.getServiceContent()) { return resolve(null); }
this.client.VimService.VimPort.CreateContainerView(paramFilter({
_this: ref(this.serviceContent.viewManager),
container: ref(this.serviceContent.rootFolder),
recursive: true
}), async (e, r) => {
if (e) { return this.resolveError(e, resolve); }
if (!r) { return this.resolveError(new Error('Emtpy response'), resolve); }
this.containerView = r.returnval;
this.$dc.debugOutput(`fetched default container view (${this.containerView.$value})`);
return resolve(r.returnval);
}, this.options);
});
}
return await Promise.resolve(this.containerView);
}
async getInventoryStubs(noCache = false) {
if (!this.inventoryStubsData || noCache) {
this.inventoryStubsData = promise(async resolve => {
if (!await this.getDefaultContainerView()) { return resolve(null); }
this.client.VimService.VimPort.RetrievePropertiesEx(paramFilter({
_this: ref(this.serviceContent.propertyCollector),
specSet: [
xsi('PropertyFilterSpec', {
propSet: [
xsi('PropertySpec', { type: 'Folder', all: false, pathSet: ['value'] }),
xsi('PropertySpec', { type: 'HostSystem', all: false, pathSet: ['value'] }),
xsi('PropertySpec', { type: 'Datacenter', all: false, pathSet: ['value'] }),
xsi('PropertySpec', { type: 'ComputeResource', all: false, pathSet: ['value'] }),
xsi('PropertySpec', { type: 'Network', all: false, pathSet: ['value'] }),
xsi('PropertySpec', { type: 'Datastore', all: false, pathSet: ['value'] }),
xsi('PropertySpec', { type: 'VirtualMachine', all: false, pathSet: ['value'] }),
xsi('PropertySpec', { type: 'ResourcePool', all: false, pathSet: ['value'] }),
xsi('PropertySpec', { type: 'VirtualApp', all: false, pathSet: ['value'] }),
xsi('PropertySpec', { type: 'DistributedVirtualSwitch', all: false, pathSet: ['value'] }),
],
objectSet: [
xsi('ObjectSpec', {
obj: ref(this.containerView), skip: true,
selectSet: [ xsi('TraversalSpec', { type: 'ContainerView', path: 'view', skip: false }) ]
})
],
})
],
options: { maxObjects: 1000000 },
}), async (e, r) => {
if (e) { return this.resolveError(e, resolve); }
if (!r) { return this.resolveError(new Error('Emtpy response'), resolve); }
const pages = await this.getAllPages(r.returnval);
if (!pages) { return resolve(null); }
this.inventoryStubsRaw = pages.rawData;
this.inventoryStubsData = pages.data;
return resolve(this.inventoryStubsData);
}, this.options);
});
}
return await Promise.resolve(this.inventoryStubsData);
}
async getInventoryData(noCache = false) {
if (!this.inventoryData || noCache) {
this.inventoryData = promise(async resolve => {
if (!await this.getDefaultContainerView()) { return resolve(null); }
this.client.VimService.VimPort.RetrievePropertiesEx(paramFilter({
_this: ref(this.serviceContent.propertyCollector),
specSet: [
xsi('PropertyFilterSpec', {
propSet: [
xsi('PropertySpec', { type: 'Folder', all: true }),
xsi('PropertySpec', { type: 'HostSystem', all: true }),
xsi('PropertySpec', { type: 'Datacenter', all: true }),
xsi('PropertySpec', { type: 'ComputeResource', all: true }),
xsi('PropertySpec', { type: 'Network', all: true }),
xsi('PropertySpec', { type: 'Datastore', all: true }),
xsi('PropertySpec', { type: 'VirtualMachine', all: true }),
xsi('PropertySpec', { type: 'ResourcePool', all: true }),
xsi('PropertySpec', { type: 'VirtualApp', all: true }),
xsi('PropertySpec', { type: 'DistributedVirtualSwitch', all: true }),
],
objectSet: [
xsi('ObjectSpec', {
obj: ref(this.containerView), skip: true,
selectSet: [ xsi('TraversalSpec', { type: 'ContainerView', path: 'view', skip: false }) ]
})
],
})
],
options: { maxObjects: 1000000 },
}), async (e, r) => {
if (e) { return this.resolveError(e, resolve); }
if (!r) { return this.resolveError(new Error('Emtpy response'), resolve); }
const pages = await this.getAllPages(r.returnval);
if (!pages) { return resolve(null); }
this.inventoryRaw = pages.rawData;
this.inventoryData = pages.data;
this.$dc.lockStubs();
const allProms = [];
for (const moData of this.inventoryData) {
const moTypeName = moData.iid.split(':')[1];
const moClass = getMoTypeByName(moTypeName);
const mo = new moClass(this.$dc, moData);
if (mo) { allProms.push(mo.$initPending); }
}
this.$dc.unlockStubs();
this.$dc.inventoryPending = promise<boolean>(async invResolve => {
await Promise.all(allProms);
await this.$dc.postInventoryDataFetch();
this.$dc.inventoryPending = false;
invResolve(false);
});
return resolve(r.returnval.objects);
}, this.options);
});
}
return await Promise.resolve(this.inventoryData);
}
async getInventory(noCache = false) {
this.$dc.debugOutput(`mapping inventory...`);
await this.getInventoryData(noCache);
await Promise.resolve(this.$dc.inventoryPending);
const hostCount = Object.keys(this.$dc.inventory.hostSystem).length;
const foldersCount = Object.keys(this.$dc.inventory.folder).length;
const vmCount = Object.keys(this.$dc.inventory.virtualMachine).length;
this.$dc.debugOutput(`finished mapping inventory (VMs=${vmCount}, Folders=${foldersCount}, Hosts=${hostCount})`);
this.$dc.inventoryChangedLast = Date.now();
this.$dc.preserializeInventory();
return this.$dc;
}
async getManagedObjectData(target: any) {
const stub = ref(target);
const moType = stub.attributes.type;
return await promise(async resolve => {
if (!await this.getDefaultContainerView()) { return resolve(null); }
this.client.VimService.VimPort.RetrievePropertiesEx(paramFilter({
_this: ref(this.serviceContent.propertyCollector),
specSet: [
xsi('PropertyFilterSpec', {
propSet: [ xsi('PropertySpec', { type: moType, all: true }), ],
objectSet: [ xsi('ObjectSpec', { obj: stub }) ],
})
],
options: { maxObjects: 1 },
}), async (e, r) => {
if (e) { return this.resolveError(e, resolve); }
if (!r) { return this.resolveError(new Error('Emtpy response'), resolve); }
const targetData = (await parseObjects(r.returnval.objects))[0];
return resolve(targetData);
}, this.options);
});
}
async prop(target, prop) {
const targetStub = ref(target);
let targetType = targetStub.attributes.type;
if (!targetType) { targetType = target._type; }
if (!targetType) {
this.$dc.debugOutput(`unknown target type: ${JSON.stringify(targetStub)}`);
return null;
}
return await promise(async resolve => {
if (!await this.getDefaultContainerView()) { return resolve(null); }
this.client.VimService.VimPort.RetrievePropertiesEx(paramFilter({
_this: ref(this.serviceContent.propertyCollector),
specSet: [
xsi('PropertyFilterSpec', {
propSet: [ xsi('PropertySpec', { type: targetType, all: [ prop ] }), ],
objectSet: [ xsi('ObjectSpec', { obj: targetStub }), ],
})
],
options: { maxObjects: 1 },
}), async (e, r) => {
if (e) { return this.resolveError(e, resolve); }
if (!r) { return this.resolveError(new Error('Emtpy response'), resolve); }
const list = r.returnval.objects;
if (!list || list.length === 0) { return null; }
return resolve(r.returnval.objects);
}, this.options);
});
}
async propChain(target, ...props: string[]) {
let prevTarget = target;
for (const prop of props) {
target = await this.prop(target, prop);
if (target === null) {
this.$dc.debugOutput(`Cannot fetch '${prop}' of prop chain '${props.join('.')}' on target ${JSON.stringify(ref(prevTarget))}`);
return null;
}
prevTarget = target;
}
return target;
}
async getAllPages(returnVal: any, stepDelayMs: number = 100) {
const objectsPagesRaw = [returnVal.objects];
const objectsPages = [await parseObjects(returnVal.objects)];
let token = returnVal.token;
while (token) {
await sleepMs(stepDelayMs);
const page = await this.furtherRetrievePages(token);
if (!page) { return null; }
objectsPagesRaw.push(page.objects);
objectsPages.push(await parseObjects(page.objects));
if (page.token === token) { return null; }
token = page.token;
}
const result = {
rawData: [].concat(...objectsPagesRaw),
data: [].concat(...objectsPages),
};
return result;
}
async furtherRetrievePages(furtherToken: string): Promise<{objects: any[]; token?: string}> {
return await promise(async resolve => {
if (!await this.getDefaultContainerView()) { return resolve(null); }
this.client.VimService.VimPort.ContinueRetrievePropertiesEx(paramFilter({
_this: ref(this.serviceContent.propertyCollector), token: furtherToken
}), async (e, r) => {
if (e) { return this.resolveError(e, resolve); }
if (!r) { return this.resolveError(new Error('Emtpy response'), resolve); }
resolve({ objects: r.returnval.objects as any[], token: r.returnval.token });
}, this.options);
});
}
}
async function sleepMs(ms: number) {
return promise<void>(resolve => setTimeout(resolve, ms));
}