Initial
This commit is contained in:
@@ -1,38 +1,49 @@
|
||||
import './setup.mjs';
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { load } from './setup.mjs';
|
||||
import http from 'node:http';
|
||||
import { load, makeUser } from './setup.mjs';
|
||||
|
||||
const {
|
||||
buildVmListPath,
|
||||
buildHostPayloadFromVm,
|
||||
buildStandaloneHostCandidate,
|
||||
createCredential,
|
||||
createVCenterConnection,
|
||||
filterSummaryVms,
|
||||
getVCenterConnection,
|
||||
getVCenterVmPowerState,
|
||||
normalizeBaseUrl,
|
||||
normalizeVCenterVm,
|
||||
previewVCenterHosts,
|
||||
settingsFromVCenterConnection,
|
||||
vmMatchesFilter
|
||||
} = await load('../services/vcenterService.js');
|
||||
} = {
|
||||
...(await load('../services/vcenterService.js')),
|
||||
...(await load('../models/credentialModel.js')),
|
||||
...(await load('../models/vcenterConnectionModel.js'))
|
||||
};
|
||||
|
||||
const originalFetch = globalThis.fetch;
|
||||
|
||||
function soapResponse(body) {
|
||||
function soapResponse(body, init = {}) {
|
||||
return new Response(`<?xml version="1.0" encoding="UTF-8"?><soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"><soapenv:Body>${body}</soapenv:Body></soapenv:Envelope>`, {
|
||||
status: 200,
|
||||
headers: { 'content-type': 'text/xml' }
|
||||
headers: { 'content-type': 'text/xml', ...(init.headers || {}) }
|
||||
});
|
||||
}
|
||||
|
||||
function installStandaloneEsxiSoapStub() {
|
||||
globalThis.fetch = async (_url, options = {}) => {
|
||||
function installStandaloneEsxiSoapStub({ sdk404 = false } = {}) {
|
||||
const calls = [];
|
||||
globalThis.fetch = async (url, options = {}) => {
|
||||
calls.push({ url: String(url), method: options.method || 'GET', body: String(options.body || ''), headers: options.headers || {} });
|
||||
const body = String(options.body || '');
|
||||
if (options.method === 'GET') return new Response('<definitions>vimService</definitions>', { status: 200, headers: { 'content-type': 'text/xml' } });
|
||||
if (sdk404 && String(url).endsWith('/sdk')) return new Response('not found', { status: 404, statusText: 'Not Found', headers: { 'content-type': 'text/plain' } });
|
||||
if (body.includes('<RetrieveServiceContent')) {
|
||||
return soapResponse(`<RetrieveServiceContentResponse xmlns="urn:vim25"><returnval><rootFolder type="Folder">ha-folder-root</rootFolder><propertyCollector type="PropertyCollector">ha-property-collector</propertyCollector><viewManager type="ViewManager">ViewManager</viewManager><sessionManager type="SessionManager">ha-sessionmgr</sessionManager></returnval></RetrieveServiceContentResponse>`);
|
||||
}
|
||||
if (body.includes('<Login')) {
|
||||
return soapResponse(`<LoginResponse xmlns="urn:vim25"><returnval type="UserSession">session-1</returnval></LoginResponse>`);
|
||||
return soapResponse(`<LoginResponse xmlns="urn:vim25"><returnval type="UserSession">session-1</returnval></LoginResponse>`, { headers: { 'set-cookie': 'vmware_soap_session=session-1; Path=/sdk; Secure' } });
|
||||
}
|
||||
if (body.includes('<CreateContainerView')) {
|
||||
return soapResponse(`<CreateContainerViewResponse xmlns="urn:vim25"><returnval type="ContainerView">session[52b6]-vm-view</returnval></CreateContainerViewResponse>`);
|
||||
@@ -42,15 +53,79 @@ function installStandaloneEsxiSoapStub() {
|
||||
}
|
||||
return soapResponse('');
|
||||
};
|
||||
return () => { globalThis.fetch = originalFetch; };
|
||||
return { calls, restore: () => { globalThis.fetch = originalFetch; } };
|
||||
}
|
||||
|
||||
function startStandaloneEsxiSoapServer() {
|
||||
const requests = [];
|
||||
const server = http.createServer((req, res) => {
|
||||
let body = '';
|
||||
req.on('data', (chunk) => { body += chunk; });
|
||||
req.on('end', () => {
|
||||
requests.push({ url: req.url, method: req.method, body, cookie: req.headers.cookie || '' });
|
||||
res.setHeader('content-type', 'text/xml');
|
||||
if (req.method === 'GET') {
|
||||
res.end('<definitions>vimService</definitions>');
|
||||
} else if (body.includes('<Login')) {
|
||||
res.setHeader('set-cookie', 'vmware_soap_session=session-http; Path=/sdk');
|
||||
res.end(`<?xml version="1.0"?><Envelope><Body><LoginResponse><returnval type="UserSession">session-http</returnval></LoginResponse></Body></Envelope>`);
|
||||
} else if (body.includes('<RetrieveServiceContent')) {
|
||||
res.end(`<?xml version="1.0"?><Envelope><Body><RetrieveServiceContentResponse><returnval><rootFolder type="Folder">ha-folder-root</rootFolder><propertyCollector type="PropertyCollector">ha-property-collector</propertyCollector><viewManager type="ViewManager">ViewManager</viewManager><sessionManager type="SessionManager">ha-sessionmgr</sessionManager></returnval></RetrieveServiceContentResponse></Body></Envelope>`);
|
||||
} else if (body.includes('<CreateContainerView')) {
|
||||
res.end(`<?xml version="1.0"?><Envelope><Body><CreateContainerViewResponse><returnval type="ContainerView">session-http-vm-view</returnval></CreateContainerViewResponse></Body></Envelope>`);
|
||||
} else if (body.includes('<RetrievePropertiesEx')) {
|
||||
res.end(`<?xml version="1.0"?><Envelope><Body><RetrievePropertiesExResponse><returnval><objects><obj type="VirtualMachine">vim.VirtualMachine:201</obj><propSet><name>name</name><val>HTTP-ESXI-VM01</val></propSet><propSet><name>guest.hostName</name><val>http-esxi-vm01.lab.local</val></propSet><propSet><name>guest.ipAddress</name><val>10.42.9.21</val></propSet><propSet><name>guest.guestFullName</name><val>Microsoft Windows Server 2022</val></propSet><propSet><name>runtime.powerState</name><val>poweredOn</val></propSet></objects></returnval></RetrievePropertiesExResponse></Body></Envelope>`);
|
||||
} else {
|
||||
res.end(`<?xml version="1.0"?><Envelope><Body /></Envelope>`);
|
||||
}
|
||||
});
|
||||
});
|
||||
return new Promise((resolve) => {
|
||||
server.listen(0, '127.0.0.1', () => {
|
||||
resolve({
|
||||
baseUrl: `http://127.0.0.1:${server.address().port}`,
|
||||
requests,
|
||||
close: () => new Promise((done) => server.close(done))
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
test('normalizeBaseUrl trims trailing slashes', () => {
|
||||
assert.equal(normalizeBaseUrl('https://vcenter.lab.local///'), 'https://vcenter.lab.local');
|
||||
});
|
||||
|
||||
test('VMware connections resolve API credentials from Credential Vault', () => {
|
||||
const owner = makeUser({ email: 'vmware-vault@test.local' });
|
||||
const credential = createCredential({
|
||||
name: 'vCenter API',
|
||||
kind: 'username_password',
|
||||
username: 'administrator@vsphere.local',
|
||||
secret: 'vault-secret',
|
||||
visibility: 'personal'
|
||||
}, owner);
|
||||
const connection = createVCenterConnection({
|
||||
name: 'Vault Backed vCenter',
|
||||
targetType: 'vcenter',
|
||||
baseUrl: 'https://vcenter.lab.local',
|
||||
credentialId: credential.id,
|
||||
apiMode: 'auto',
|
||||
requestTimeoutMs: 20000,
|
||||
allowUntrustedTls: true,
|
||||
enabled: true,
|
||||
visibility: 'personal'
|
||||
}, owner);
|
||||
const secretConnection = getVCenterConnection(connection.id, owner, true);
|
||||
|
||||
assert.equal(connection.credentialId, credential.id);
|
||||
assert.equal(connection.credentialName, 'vCenter API');
|
||||
assert.equal(connection.username, 'administrator@vsphere.local');
|
||||
assert.equal(secretConnection.username, 'administrator@vsphere.local');
|
||||
assert.equal(secretConnection.password, 'vault-secret');
|
||||
});
|
||||
|
||||
test('standalone VMware host connections use the Web Services profile', async () => {
|
||||
const restoreFetch = installStandaloneEsxiSoapStub();
|
||||
const { calls, restore } = installStandaloneEsxiSoapStub();
|
||||
const connection = {
|
||||
id: 'vc_host',
|
||||
name: 'ESXi 01',
|
||||
@@ -68,9 +143,11 @@ test('standalone VMware host connections use the Web Services profile', async ()
|
||||
assert.equal(settings.baseUrl, 'https://esxi01.lab.local/sdk');
|
||||
|
||||
const preview = await previewVCenterHosts({ connection, filter: { field: 'hostname', operator: 'contains', value: 'ENG-ENT' } });
|
||||
restoreFetch();
|
||||
restore();
|
||||
|
||||
assert.equal(preview.count, 1);
|
||||
assert.ok(calls.some((call) => call.url.endsWith('/sdk/vimService.wsdl')));
|
||||
assert.ok(calls.some((call) => call.headers.Cookie?.includes('vmware_soap_session')));
|
||||
assert.equal(preview.diagnostics.standaloneHost, true);
|
||||
assert.equal(preview.candidates[0].source, 'standalone-vm');
|
||||
assert.equal(preview.candidates[0].fqdn, 'eng-ent-app01.contoso.local');
|
||||
@@ -79,7 +156,7 @@ test('standalone VMware host connections use the Web Services profile', async ()
|
||||
});
|
||||
|
||||
test('standalone VMware VM candidates build managed host payloads', async () => {
|
||||
const restoreFetch = installStandaloneEsxiSoapStub();
|
||||
const { restore } = installStandaloneEsxiSoapStub();
|
||||
const connection = {
|
||||
id: 'vc_host',
|
||||
name: 'ESXi 01',
|
||||
@@ -98,7 +175,7 @@ test('standalone VMware VM candidates build managed host payloads', async () =>
|
||||
visibility: 'shared'
|
||||
});
|
||||
const power = await getVCenterVmPowerState(connection, preview.candidates[0].id);
|
||||
restoreFetch();
|
||||
restore();
|
||||
|
||||
assert.equal(payload.name, 'ENG-ENT-APP01');
|
||||
assert.equal(payload.address, 'eng-ent-app01.contoso.local');
|
||||
@@ -112,6 +189,50 @@ test('standalone VMware VM candidates build managed host payloads', async () =>
|
||||
assert.equal(power.state, 'POWERED_ON');
|
||||
});
|
||||
|
||||
test('standalone VMware SOAP retries alternate endpoint paths after /sdk 404', async () => {
|
||||
const { calls, restore } = installStandaloneEsxiSoapStub({ sdk404: true });
|
||||
const connection = {
|
||||
id: 'vc_host_retry',
|
||||
name: 'ESXi Retry',
|
||||
target_type: 'host',
|
||||
base_url: 'https://esxi01.lab.local/sdk/vimService.wsdl',
|
||||
username: 'root',
|
||||
password: 'secret',
|
||||
enabled: 1
|
||||
};
|
||||
const preview = await previewVCenterHosts({ connection, filter: { field: 'hostname', operator: 'contains', value: 'eng-ent' } });
|
||||
restore();
|
||||
|
||||
assert.equal(preview.count, 1);
|
||||
assert.ok(calls.some((call) => call.url === 'https://esxi01.lab.local/sdk'));
|
||||
assert.ok(calls.some((call) => call.url === 'https://esxi01.lab.local/sdk/'));
|
||||
assert.ok(preview.trace.some((entry) => String(entry.message || '').includes('Retrying VMware SOAP action RetrieveServiceContent')));
|
||||
});
|
||||
|
||||
test('standalone VMware custom dispatcher path works when untrusted TLS is enabled', async () => {
|
||||
const server = await startStandaloneEsxiSoapServer();
|
||||
try {
|
||||
const connection = {
|
||||
id: 'vc_host_dispatcher',
|
||||
name: 'ESXi Dispatcher',
|
||||
target_type: 'host',
|
||||
base_url: server.baseUrl,
|
||||
username: 'root',
|
||||
password: 'secret',
|
||||
enabled: 1,
|
||||
allow_untrusted_tls: 1
|
||||
};
|
||||
const preview = await previewVCenterHosts({ connection, filter: { field: 'hostname', operator: 'contains', value: 'HTTP-ESXI' } });
|
||||
|
||||
assert.equal(preview.count, 1);
|
||||
assert.equal(preview.candidates[0].name, 'HTTP-ESXI-VM01');
|
||||
assert.ok(server.requests.some((request) => request.url === '/sdk/vimService.wsdl'));
|
||||
assert.ok(server.requests.some((request) => request.cookie.includes('vmware_soap_session')));
|
||||
} finally {
|
||||
await server.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('standalone VMware host candidates build managed host payloads', () => {
|
||||
const candidate = buildStandaloneHostCandidate(
|
||||
{ id: 'vc_host', name: 'ESXi 01' },
|
||||
|
||||
Reference in New Issue
Block a user