@underpostnet/underpost 2.99.4 → 2.99.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.development +0 -3
- package/.env.production +1 -3
- package/.env.test +0 -3
- package/README.md +2 -2
- package/baremetal/commission-workflows.json +52 -0
- package/bin/deploy.js +53 -45
- package/cli.md +7 -14
- package/examples/static-page/README.md +55 -378
- package/examples/static-page/ssr-components/CustomPage.js +1 -13
- package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
- package/manifests/deployment/dd-test-development/deployment.yaml +2 -2
- package/package.json +2 -2
- package/scripts/disk-devices.sh +13 -0
- package/src/cli/baremetal.js +562 -164
- package/src/cli/cloud-init.js +97 -79
- package/src/cli/env.js +4 -1
- package/src/cli/image.js +4 -37
- package/src/cli/index.js +16 -6
- package/src/cli/repository.js +3 -1
- package/src/cli/run.js +13 -0
- package/src/cli/secrets.js +0 -34
- package/src/cli/static.js +0 -26
- package/src/index.js +19 -5
- package/src/server/logger.js +22 -27
- package/src/server/tls.js +14 -14
- package/examples/static-page/QUICK-REFERENCE.md +0 -481
- package/examples/static-page/STATIC-GENERATOR-GUIDE.md +0 -757
package/src/cli/baremetal.js
CHANGED
|
@@ -8,13 +8,14 @@ import { fileURLToPath } from 'url';
|
|
|
8
8
|
import { getNpmRootPath, getUnderpostRootPath } from '../server/conf.js';
|
|
9
9
|
import { openTerminal, pbcopy, shellExec } from '../server/process.js';
|
|
10
10
|
import dotenv from 'dotenv';
|
|
11
|
-
import { loggerFactory } from '../server/logger.js';
|
|
11
|
+
import { loggerFactory, loggerMiddleware } from '../server/logger.js';
|
|
12
12
|
import fs from 'fs-extra';
|
|
13
13
|
import path from 'path';
|
|
14
14
|
import Downloader from '../server/downloader.js';
|
|
15
15
|
import { newInstance, range, s4, timer } from '../client/components/core/CommonJs.js';
|
|
16
16
|
import { spawnSync } from 'child_process';
|
|
17
17
|
import Underpost from '../index.js';
|
|
18
|
+
import express from 'express';
|
|
18
19
|
|
|
19
20
|
const logger = loggerFactory(import.meta);
|
|
20
21
|
|
|
@@ -27,19 +28,6 @@ const logger = loggerFactory(import.meta);
|
|
|
27
28
|
*/
|
|
28
29
|
class UnderpostBaremetal {
|
|
29
30
|
static API = {
|
|
30
|
-
/**
|
|
31
|
-
* @method installPacker
|
|
32
|
-
* @description Installs Packer CLI.
|
|
33
|
-
* @memberof UnderpostBaremetal
|
|
34
|
-
* @returns {Promise<void>}
|
|
35
|
-
*/
|
|
36
|
-
async installPacker(underpostRoot) {
|
|
37
|
-
const scriptPath = `${underpostRoot}/scripts/packer-setup.sh`;
|
|
38
|
-
logger.info(`Installing Packer using script: ${scriptPath}`);
|
|
39
|
-
shellExec(`sudo chmod +x ${scriptPath}`);
|
|
40
|
-
shellExec(`sudo ${scriptPath}`);
|
|
41
|
-
},
|
|
42
|
-
|
|
43
31
|
/**
|
|
44
32
|
* @method callback
|
|
45
33
|
* @description Initiates a baremetal provisioning workflow based on the provided options.
|
|
@@ -64,6 +52,7 @@ class UnderpostBaremetal {
|
|
|
64
52
|
* @param {string} [options.mac=''] - MAC address of the baremetal machine.
|
|
65
53
|
* @param {boolean} [options.ipxe=false] - Flag to use iPXE for booting.
|
|
66
54
|
* @param {boolean} [options.ipxeRebuild=false] - Flag to rebuild the iPXE binary with embedded script.
|
|
55
|
+
* @param {string} [options.ipxeBuildIso=''] - Builds a standalone iPXE ISO with embedded script for the specified workflow ID.
|
|
67
56
|
* @param {boolean} [options.installPacker=false] - Flag to install Packer CLI.
|
|
68
57
|
* @param {string} [options.packerMaasImageTemplate] - Template path from canonical/packer-maas to extract (requires workflow-id).
|
|
69
58
|
* @param {string} [options.packerWorkflowId] - Workflow ID for Packer MAAS image operations (used with --packer-maas-image-build or --packer-maas-image-upload).
|
|
@@ -111,6 +100,7 @@ class UnderpostBaremetal {
|
|
|
111
100
|
mac: '',
|
|
112
101
|
ipxe: false,
|
|
113
102
|
ipxeRebuild: false,
|
|
103
|
+
ipxeBuildIso: '',
|
|
114
104
|
installPacker: false,
|
|
115
105
|
packerMaasImageTemplate: false,
|
|
116
106
|
packerWorkflowId: '',
|
|
@@ -194,6 +184,47 @@ class UnderpostBaremetal {
|
|
|
194
184
|
// Define the TFTP root prefix path based
|
|
195
185
|
const tftpRootPath = `${process.env.TFTP_ROOT}/${tftpPrefix}`;
|
|
196
186
|
|
|
187
|
+
if (options.ipxeBuildIso) {
|
|
188
|
+
let machine = null;
|
|
189
|
+
|
|
190
|
+
if (options.cloudInit) {
|
|
191
|
+
// Search for an existing machine by hostname to extract system_id for cloud-init
|
|
192
|
+
const [searchMachine] = Underpost.baremetal.maasCliExec(`machines read hostname=${hostname}`);
|
|
193
|
+
|
|
194
|
+
if (searchMachine) {
|
|
195
|
+
logger.info(`Found existing machine ${hostname} with system_id ${searchMachine.system_id}`);
|
|
196
|
+
machine = searchMachine;
|
|
197
|
+
} else {
|
|
198
|
+
// Machine does not exist, create it to obtain a system_id
|
|
199
|
+
logger.info(`Machine ${hostname} not found, creating new machine for cloud-init system_id...`);
|
|
200
|
+
machine = Underpost.baremetal.machineFactory({
|
|
201
|
+
hostname,
|
|
202
|
+
ipAddress,
|
|
203
|
+
macAddress,
|
|
204
|
+
architecture: workflowsConfig[workflowId].architecture,
|
|
205
|
+
}).machine;
|
|
206
|
+
logger.info(`✓ Machine created with system_id ${machine.system_id}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
await Underpost.baremetal.ipxeBuildIso({
|
|
211
|
+
workflowId,
|
|
212
|
+
isoOutputPath: options.ipxeBuildIso,
|
|
213
|
+
tftpPrefix,
|
|
214
|
+
ipFileServer,
|
|
215
|
+
ipAddress,
|
|
216
|
+
ipConfig,
|
|
217
|
+
netmask,
|
|
218
|
+
dnsServer,
|
|
219
|
+
macAddress,
|
|
220
|
+
cloudInit: options.cloudInit,
|
|
221
|
+
machine,
|
|
222
|
+
dev: options.dev,
|
|
223
|
+
bootstrapHttpServerPort: options.bootstrapHttpServerPort,
|
|
224
|
+
});
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
197
228
|
// Define the iPXE cache directory to preserve builds across tftproot cleanups
|
|
198
229
|
const ipxeCacheDir = `/tmp/ipxe-cache/${tftpPrefix}`;
|
|
199
230
|
|
|
@@ -218,12 +249,7 @@ class UnderpostBaremetal {
|
|
|
218
249
|
// Create a new machine in MAAS if the option is set.
|
|
219
250
|
let machine;
|
|
220
251
|
if (options.createMachine === true) {
|
|
221
|
-
const [searhMachine] =
|
|
222
|
-
shellExec(`maas maas machines read hostname=${hostname}`, {
|
|
223
|
-
stdout: true,
|
|
224
|
-
silent: true,
|
|
225
|
-
}),
|
|
226
|
-
);
|
|
252
|
+
const [searhMachine] = Underpost.baremetal.maasCliExec(`machines read hostname=${hostname}`);
|
|
227
253
|
|
|
228
254
|
if (searhMachine) {
|
|
229
255
|
// Check if existing machine's MAC matches the specified MAC
|
|
@@ -238,16 +264,14 @@ class UnderpostBaremetal {
|
|
|
238
264
|
logger.info(`Deleting existing machine ${searhMachine.system_id} to recreate with correct MAC...`);
|
|
239
265
|
|
|
240
266
|
// Delete the existing machine
|
|
241
|
-
|
|
242
|
-
silent: true,
|
|
243
|
-
});
|
|
267
|
+
Underpost.baremetal.maasCliExec(`machine delete ${searhMachine.system_id}`);
|
|
244
268
|
|
|
245
269
|
// Create new machine with correct MAC
|
|
246
270
|
machine = Underpost.baremetal.machineFactory({
|
|
247
271
|
hostname,
|
|
248
272
|
ipAddress,
|
|
249
273
|
macAddress,
|
|
250
|
-
|
|
274
|
+
architecture: workflowsConfig[workflowId].architecture,
|
|
251
275
|
}).machine;
|
|
252
276
|
|
|
253
277
|
logger.info(`✓ Machine recreated with MAC ${macAddress}`);
|
|
@@ -266,7 +290,7 @@ class UnderpostBaremetal {
|
|
|
266
290
|
hostname,
|
|
267
291
|
ipAddress,
|
|
268
292
|
macAddress,
|
|
269
|
-
|
|
293
|
+
architecture: workflowsConfig[workflowId].architecture,
|
|
270
294
|
}).machine;
|
|
271
295
|
}
|
|
272
296
|
}
|
|
@@ -351,7 +375,7 @@ class UnderpostBaremetal {
|
|
|
351
375
|
|
|
352
376
|
// Build phase (skip if upload-only mode)
|
|
353
377
|
if (options.packerMaasImageBuild) {
|
|
354
|
-
if (shellExec('packer version'
|
|
378
|
+
if (shellExec('packer version').code !== 0) {
|
|
355
379
|
throw new Error('Packer is not installed. Please install Packer to proceed.');
|
|
356
380
|
}
|
|
357
381
|
|
|
@@ -735,12 +759,7 @@ rm -rf ${artifacts.join(' ')}`);
|
|
|
735
759
|
|
|
736
760
|
// Fetch boot resources and machines if commissioning or listing.
|
|
737
761
|
|
|
738
|
-
let resources =
|
|
739
|
-
shellExec(`maas ${process.env.MAAS_ADMIN_USERNAME} boot-resources read`, {
|
|
740
|
-
silent: true,
|
|
741
|
-
stdout: true,
|
|
742
|
-
}),
|
|
743
|
-
).map((o) => ({
|
|
762
|
+
let resources = Underpost.baremetal.maasCliExec(`boot-resources read`).map((o) => ({
|
|
744
763
|
id: o.id,
|
|
745
764
|
name: o.name,
|
|
746
765
|
architecture: o.architecture,
|
|
@@ -748,12 +767,7 @@ rm -rf ${artifacts.join(' ')}`);
|
|
|
748
767
|
if (options.ls === true) {
|
|
749
768
|
console.table(resources);
|
|
750
769
|
}
|
|
751
|
-
let machines =
|
|
752
|
-
shellExec(`maas ${process.env.MAAS_ADMIN_USERNAME} machines read`, {
|
|
753
|
-
stdout: true,
|
|
754
|
-
silent: true,
|
|
755
|
-
}),
|
|
756
|
-
).map((m) => ({
|
|
770
|
+
let machines = Underpost.baremetal.maasCliExec(`machines read`).map((m) => ({
|
|
757
771
|
system_id: m.interface_set[0].system_id,
|
|
758
772
|
mac_address: m.interface_set[0].mac_address,
|
|
759
773
|
hostname: m.hostname,
|
|
@@ -875,10 +889,27 @@ rm -rf ${artifacts.join(' ')}`);
|
|
|
875
889
|
});
|
|
876
890
|
}
|
|
877
891
|
|
|
892
|
+
const authCredentials =
|
|
893
|
+
options.commission || options.cloudInit || options.cloudInitUpdate
|
|
894
|
+
? Underpost.baremetal.maasAuthCredentialsFactory()
|
|
895
|
+
: { consumer_key: '', consumer_secret: '', token_key: '', token_secret: '' };
|
|
896
|
+
|
|
878
897
|
if (options.cloudInit || options.cloudInitUpdate) {
|
|
879
898
|
const { chronyc, networkInterfaceName } = workflowsConfig[workflowId];
|
|
880
899
|
const { timezone, chronyConfPath } = chronyc;
|
|
881
|
-
|
|
900
|
+
|
|
901
|
+
let write_files = [];
|
|
902
|
+
let runcmd = options.runcmd;
|
|
903
|
+
|
|
904
|
+
if (machine && options.commission) {
|
|
905
|
+
write_files = Underpost.baremetal.commissioningWriteFilesFactory({
|
|
906
|
+
machine,
|
|
907
|
+
authCredentials,
|
|
908
|
+
runnerHostIp: callbackMetaData.runnerHost.ip,
|
|
909
|
+
});
|
|
910
|
+
runcmd = '/usr/local/bin/underpost-enlist.sh';
|
|
911
|
+
}
|
|
912
|
+
|
|
882
913
|
const { cloudConfigSrc } = Underpost.cloudInit.configFactory(
|
|
883
914
|
{
|
|
884
915
|
controlServerIp: callbackMetaData.runnerHost.ip,
|
|
@@ -891,15 +922,16 @@ rm -rf ${artifacts.join(' ')}`);
|
|
|
891
922
|
networkInterfaceName,
|
|
892
923
|
ubuntuToolsBuild: options.ubuntuToolsBuild,
|
|
893
924
|
bootcmd: options.bootcmd,
|
|
894
|
-
runcmd
|
|
925
|
+
runcmd,
|
|
926
|
+
write_files,
|
|
895
927
|
},
|
|
896
928
|
authCredentials,
|
|
897
929
|
);
|
|
898
|
-
|
|
899
930
|
Underpost.baremetal.httpBootstrapServerStaticFactory({
|
|
900
931
|
bootstrapHttpServerPath,
|
|
901
932
|
hostname,
|
|
902
933
|
cloudConfigSrc,
|
|
934
|
+
isoUrl: workflowsConfig[workflowId].isoUrl,
|
|
903
935
|
});
|
|
904
936
|
}
|
|
905
937
|
|
|
@@ -910,7 +942,7 @@ rm -rf ${artifacts.join(' ')}`);
|
|
|
910
942
|
workflowsConfig[workflowId].type === 'chroot-debootstrap' ||
|
|
911
943
|
workflowsConfig[workflowId].type === 'chroot-container')
|
|
912
944
|
) {
|
|
913
|
-
shellExec(`${underpostRoot}/scripts/nat-iptables.sh
|
|
945
|
+
shellExec(`${underpostRoot}/scripts/nat-iptables.sh`);
|
|
914
946
|
Underpost.baremetal.rebuildNfsServer({
|
|
915
947
|
nfsHostPath,
|
|
916
948
|
});
|
|
@@ -1004,15 +1036,22 @@ rm -rf ${artifacts.join(' ')}`);
|
|
|
1004
1036
|
hostname,
|
|
1005
1037
|
dnsServer,
|
|
1006
1038
|
networkInterfaceName,
|
|
1007
|
-
fileSystemUrl:
|
|
1008
|
-
|
|
1009
|
-
|
|
1039
|
+
fileSystemUrl:
|
|
1040
|
+
type === 'iso-ram'
|
|
1041
|
+
? `http://${callbackMetaData.runnerHost.ip}:${Underpost.baremetal.bootstrapHttpServerPortFactory({ port: options.bootstrapHttpServerPort, workflowId, workflowsConfig })}/${hostname}/${kernelFilesPaths.isoUrl.split('/').pop()}`
|
|
1042
|
+
: kernelFilesPaths.isoUrl,
|
|
1043
|
+
bootstrapHttpServerPort: Underpost.baremetal.bootstrapHttpServerPortFactory({
|
|
1044
|
+
port: options.bootstrapHttpServerPort,
|
|
1045
|
+
workflowId,
|
|
1046
|
+
workflowsConfig,
|
|
1047
|
+
}),
|
|
1010
1048
|
type,
|
|
1011
1049
|
macAddress,
|
|
1012
1050
|
cloudInit: options.cloudInit,
|
|
1013
1051
|
machine,
|
|
1014
1052
|
dev: options.dev,
|
|
1015
1053
|
osIdLike: workflowsConfig[workflowId].osIdLike || '',
|
|
1054
|
+
authCredentials,
|
|
1016
1055
|
});
|
|
1017
1056
|
|
|
1018
1057
|
// Check if iPXE mode is enabled AND the iPXE EFI binary exists
|
|
@@ -1086,8 +1125,11 @@ rm -rf ${artifacts.join(' ')}`);
|
|
|
1086
1125
|
Underpost.baremetal.httpBootstrapServerRunnerFactory({
|
|
1087
1126
|
hostname,
|
|
1088
1127
|
bootstrapHttpServerPath,
|
|
1089
|
-
bootstrapHttpServerPort:
|
|
1090
|
-
options.bootstrapHttpServerPort
|
|
1128
|
+
bootstrapHttpServerPort: Underpost.baremetal.bootstrapHttpServerPortFactory({
|
|
1129
|
+
port: options.bootstrapHttpServerPort,
|
|
1130
|
+
workflowId,
|
|
1131
|
+
workflowsConfig,
|
|
1132
|
+
}),
|
|
1091
1133
|
});
|
|
1092
1134
|
|
|
1093
1135
|
if (type === 'chroot-debootstrap' || type === 'chroot-container')
|
|
@@ -1102,11 +1144,7 @@ rm -rf ${artifacts.join(' ')}`);
|
|
|
1102
1144
|
macAddress,
|
|
1103
1145
|
ipAddress,
|
|
1104
1146
|
hostname,
|
|
1105
|
-
architecture:
|
|
1106
|
-
workflowsConfig[workflowId].maas?.commissioning?.architecture ||
|
|
1107
|
-
workflowsConfig[workflowId].container?.architecture ||
|
|
1108
|
-
workflowsConfig[workflowId].debootstrap?.image?.architecture ||
|
|
1109
|
-
'arm64/generic',
|
|
1147
|
+
architecture: Underpost.baremetal.fallbackArchitecture(workflowsConfig[workflowId]),
|
|
1110
1148
|
machine,
|
|
1111
1149
|
};
|
|
1112
1150
|
logger.info('Waiting for commissioning...', {
|
|
@@ -1114,7 +1152,42 @@ rm -rf ${artifacts.join(' ')}`);
|
|
|
1114
1152
|
machine: machine ? machine.system_id : null,
|
|
1115
1153
|
});
|
|
1116
1154
|
|
|
1117
|
-
const { discovery } =
|
|
1155
|
+
const { discovery, machine: discoveredMachine } =
|
|
1156
|
+
await Underpost.baremetal.commissionMonitor(commissionMonitorPayload);
|
|
1157
|
+
if (discoveredMachine) machine = discoveredMachine;
|
|
1158
|
+
|
|
1159
|
+
if (machine) {
|
|
1160
|
+
const write_files = Underpost.baremetal.commissioningWriteFilesFactory({
|
|
1161
|
+
machine,
|
|
1162
|
+
authCredentials,
|
|
1163
|
+
runnerHostIp: callbackMetaData.runnerHost.ip,
|
|
1164
|
+
});
|
|
1165
|
+
|
|
1166
|
+
const { cloudConfigSrc } = Underpost.cloudInit.configFactory(
|
|
1167
|
+
{
|
|
1168
|
+
controlServerIp: callbackMetaData.runnerHost.ip,
|
|
1169
|
+
hostname,
|
|
1170
|
+
commissioningDeviceIp: ipAddress,
|
|
1171
|
+
gatewayip: callbackMetaData.runnerHost.ip,
|
|
1172
|
+
mac: macAddress,
|
|
1173
|
+
timezone: workflowsConfig[workflowId].chronyc.timezone,
|
|
1174
|
+
chronyConfPath: workflowsConfig[workflowId].chronyc.chronyConfPath,
|
|
1175
|
+
networkInterfaceName: workflowsConfig[workflowId].networkInterfaceName,
|
|
1176
|
+
ubuntuToolsBuild: options.ubuntuToolsBuild,
|
|
1177
|
+
bootcmd: options.bootcmd,
|
|
1178
|
+
runcmd: '/usr/local/bin/underpost-enlist.sh',
|
|
1179
|
+
write_files,
|
|
1180
|
+
},
|
|
1181
|
+
authCredentials,
|
|
1182
|
+
);
|
|
1183
|
+
|
|
1184
|
+
Underpost.baremetal.httpBootstrapServerStaticFactory({
|
|
1185
|
+
bootstrapHttpServerPath,
|
|
1186
|
+
hostname,
|
|
1187
|
+
cloudConfigSrc,
|
|
1188
|
+
isoUrl: workflowsConfig[workflowId].isoUrl,
|
|
1189
|
+
});
|
|
1190
|
+
}
|
|
1118
1191
|
|
|
1119
1192
|
if ((type === 'chroot-debootstrap' || type === 'chroot-container') && options.cloudInit === true) {
|
|
1120
1193
|
openTerminal(`node ${underpostRoot}/bin baremetal ${workflowId} ${ipAddress} ${hostname} --logs cloud-init`);
|
|
@@ -1128,6 +1201,164 @@ rm -rf ${artifacts.join(' ')}`);
|
|
|
1128
1201
|
}
|
|
1129
1202
|
},
|
|
1130
1203
|
|
|
1204
|
+
/**
|
|
1205
|
+
* @method installPacker
|
|
1206
|
+
* @description Installs Packer CLI.
|
|
1207
|
+
* @memberof UnderpostBaremetal
|
|
1208
|
+
* @returns {Promise<void>}
|
|
1209
|
+
*/
|
|
1210
|
+
async installPacker(underpostRoot) {
|
|
1211
|
+
const scriptPath = `${underpostRoot}/scripts/packer-setup.sh`;
|
|
1212
|
+
logger.info(`Installing Packer using script: ${scriptPath}`);
|
|
1213
|
+
shellExec(`sudo chmod +x ${scriptPath}`);
|
|
1214
|
+
shellExec(`sudo ${scriptPath}`);
|
|
1215
|
+
},
|
|
1216
|
+
|
|
1217
|
+
/**
|
|
1218
|
+
* @method ipxeBuildIso
|
|
1219
|
+
* @description Builds a UEFI-bootable iPXE ISO with an embedded bridge script.
|
|
1220
|
+
* @param {object} params
|
|
1221
|
+
* @param {string} params.workflowId - The workflow identifier (e.g., 'hp-envy-iso-ram').
|
|
1222
|
+
* @param {string} params.isoOutputPath - Output path for the generated ISO file.
|
|
1223
|
+
* @param {string} params.tftpPrefix - TFTP prefix directory (e.g., 'envy').
|
|
1224
|
+
* @param {string} params.ipFileServer - IP address of the TFTP/file server to chain to.
|
|
1225
|
+
* @param {string} [params.ipAddress='192.168.1.191'] - The IP address of the client machine.
|
|
1226
|
+
* @param {string} [params.ipConfig='none'] - IP configuration method (e.g., 'dhcp', 'none').
|
|
1227
|
+
* @param {string} [params.netmask='255.255.255.0'] - The network mask.
|
|
1228
|
+
* @param {string} [params.dnsServer='8.8.8.8'] - The DNS server address.
|
|
1229
|
+
* @param {string} [params.macAddress=''] - The MAC address of the client machine.
|
|
1230
|
+
* @param {boolean} [params.cloudInit=false] - Flag to enable cloud-init.
|
|
1231
|
+
* @param {object} [params.machine=null] - The machine object containing system_id for cloud-init.
|
|
1232
|
+
* @param {boolean} [params.dev=false] - Development mode flag to determine paths.
|
|
1233
|
+
* @param {number} [params.bootstrapHttpServerPort=8888] - Port for the bootstrap HTTP server used in ISO RAM workflows.
|
|
1234
|
+
* @memberof UnderpostBaremetal
|
|
1235
|
+
* @returns {Promise<void>}
|
|
1236
|
+
*/
|
|
1237
|
+
async ipxeBuildIso({
|
|
1238
|
+
workflowId,
|
|
1239
|
+
isoOutputPath,
|
|
1240
|
+
tftpPrefix,
|
|
1241
|
+
ipFileServer,
|
|
1242
|
+
ipAddress,
|
|
1243
|
+
ipConfig,
|
|
1244
|
+
netmask,
|
|
1245
|
+
dnsServer,
|
|
1246
|
+
macAddress,
|
|
1247
|
+
cloudInit,
|
|
1248
|
+
machine,
|
|
1249
|
+
dev,
|
|
1250
|
+
bootstrapHttpServerPort,
|
|
1251
|
+
}) {
|
|
1252
|
+
const outputPath = !isoOutputPath || isoOutputPath === '.' ? `./ipxe-${workflowId}.iso` : isoOutputPath;
|
|
1253
|
+
if (fs.existsSync(outputPath)) fs.removeSync(outputPath);
|
|
1254
|
+
shellExec(`mkdir -p $(dirname ${outputPath})`);
|
|
1255
|
+
|
|
1256
|
+
const workflowsConfig = Underpost.baremetal.loadWorkflowsConfig();
|
|
1257
|
+
if (!workflowsConfig[workflowId]) {
|
|
1258
|
+
throw new Error(`Workflow configuration not found for ID: ${workflowId}`);
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
const authCredentials = cloudInit
|
|
1262
|
+
? Underpost.baremetal.maasAuthCredentialsFactory()
|
|
1263
|
+
: { consumer_key: '', consumer_secret: '', token_key: '', token_secret: '' };
|
|
1264
|
+
|
|
1265
|
+
const { cmd } = Underpost.baremetal.kernelCmdBootParamsFactory({
|
|
1266
|
+
ipClient: ipAddress,
|
|
1267
|
+
ipDhcpServer: ipFileServer,
|
|
1268
|
+
ipFileServer,
|
|
1269
|
+
ipConfig,
|
|
1270
|
+
netmask,
|
|
1271
|
+
hostname: workflowId,
|
|
1272
|
+
dnsServer,
|
|
1273
|
+
fileSystemUrl:
|
|
1274
|
+
dev && workflowsConfig[workflowId].type === 'iso-ram'
|
|
1275
|
+
? `http://${ipFileServer}:${Underpost.baremetal.bootstrapHttpServerPortFactory({ port: bootstrapHttpServerPort, workflowId, workflowsConfig })}/${workflowId}/${workflowsConfig[workflowId].isoUrl.split('/').pop()}`
|
|
1276
|
+
: workflowsConfig[workflowId].isoUrl,
|
|
1277
|
+
type: workflowsConfig[workflowId].type,
|
|
1278
|
+
macAddress,
|
|
1279
|
+
cloudInit,
|
|
1280
|
+
machine,
|
|
1281
|
+
osIdLike: workflowsConfig[workflowId].osIdLike,
|
|
1282
|
+
networkInterfaceName: workflowsConfig[workflowId].networkInterfaceName,
|
|
1283
|
+
authCredentials,
|
|
1284
|
+
bootstrapHttpServerPort: Underpost.baremetal.bootstrapHttpServerPortFactory({
|
|
1285
|
+
port: bootstrapHttpServerPort,
|
|
1286
|
+
workflowId,
|
|
1287
|
+
workflowsConfig,
|
|
1288
|
+
}),
|
|
1289
|
+
dev,
|
|
1290
|
+
});
|
|
1291
|
+
|
|
1292
|
+
const ipxeSrcDir = '/home/dd/ipxe/src';
|
|
1293
|
+
const embedScriptName = `embed_${workflowId}.ipxe`;
|
|
1294
|
+
const embedScriptPath = path.join(ipxeSrcDir, embedScriptName);
|
|
1295
|
+
|
|
1296
|
+
const embedScriptContent = `#!ipxe
|
|
1297
|
+
dhcp
|
|
1298
|
+
set server_ip ${ipFileServer}
|
|
1299
|
+
set tftp_prefix ${tftpPrefix}
|
|
1300
|
+
kernel tftp://\${server_ip}/\${tftp_prefix}/pxe/vmlinuz-efi ${cmd}
|
|
1301
|
+
initrd tftp://\${server_ip}/\${tftp_prefix}/pxe/initrd.img
|
|
1302
|
+
boot || shell
|
|
1303
|
+
`;
|
|
1304
|
+
|
|
1305
|
+
fs.writeFileSync(embedScriptPath, embedScriptContent);
|
|
1306
|
+
logger.info(`Created embedded script at ${embedScriptPath}`);
|
|
1307
|
+
|
|
1308
|
+
// Determine target architecture
|
|
1309
|
+
let targetArch = 'x86_64'; // Default to x86_64
|
|
1310
|
+
if (
|
|
1311
|
+
workflowsConfig[workflowId].architecture === 'arm64' ||
|
|
1312
|
+
workflowsConfig[workflowId].architecture === 'aarch64'
|
|
1313
|
+
) {
|
|
1314
|
+
targetArch = 'arm64';
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
// Determine host architecture
|
|
1318
|
+
const hostArch = process.arch === 'arm64' ? 'arm64' : 'x86_64';
|
|
1319
|
+
|
|
1320
|
+
let crossCompile = '';
|
|
1321
|
+
if (hostArch === 'x86_64' && targetArch === 'arm64') {
|
|
1322
|
+
crossCompile = 'CROSS_COMPILE=aarch64-linux-gnu-';
|
|
1323
|
+
} else if (hostArch === 'arm64' && targetArch === 'x86_64') {
|
|
1324
|
+
crossCompile = 'CROSS_COMPILE=x86_64-linux-gnu-';
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
const platformDir = targetArch === 'arm64' ? 'bin-arm64-efi' : 'bin-x86_64-efi';
|
|
1328
|
+
const makeTarget = `${platformDir}/ipxe.iso`;
|
|
1329
|
+
|
|
1330
|
+
logger.info(
|
|
1331
|
+
`Building iPXE ISO for ${targetArch} on ${hostArch}: make ${makeTarget} ${crossCompile} EMBED=${embedScriptName}`,
|
|
1332
|
+
);
|
|
1333
|
+
|
|
1334
|
+
const buildCmd = `cd ${ipxeSrcDir} && make ${makeTarget} ${crossCompile} EMBED=${embedScriptName}`;
|
|
1335
|
+
shellExec(buildCmd);
|
|
1336
|
+
|
|
1337
|
+
const builtIsoPath = path.join(ipxeSrcDir, makeTarget);
|
|
1338
|
+
if (fs.existsSync(builtIsoPath)) {
|
|
1339
|
+
fs.copySync(builtIsoPath, outputPath);
|
|
1340
|
+
logger.info(`ISO successfully built and copied to ${outputPath}`);
|
|
1341
|
+
} else {
|
|
1342
|
+
logger.error(`Failed to build ISO at ${builtIsoPath}`);
|
|
1343
|
+
}
|
|
1344
|
+
},
|
|
1345
|
+
|
|
1346
|
+
/**
|
|
1347
|
+
* @method fallbackArchitecture
|
|
1348
|
+
* @description Determines the architecture to use for boot resources, with a fallback mechanism.
|
|
1349
|
+
* @param {object} workflowsConfig - The configuration object for the current workflow.
|
|
1350
|
+
* @returns {string} The architecture string (e.g., 'arm64', 'amd64') to use for boot resources.
|
|
1351
|
+
* @memberof UnderpostBaremetal
|
|
1352
|
+
*/
|
|
1353
|
+
fallbackArchitecture(workflowsConfig) {
|
|
1354
|
+
return (
|
|
1355
|
+
workflowsConfig.architecture ||
|
|
1356
|
+
workflowsConfig.maas?.commissioning?.architecture ||
|
|
1357
|
+
workflowsConfig.container?.architecture ||
|
|
1358
|
+
workflowsConfig.debootstrap?.image?.architecture
|
|
1359
|
+
);
|
|
1360
|
+
},
|
|
1361
|
+
|
|
1131
1362
|
/**
|
|
1132
1363
|
* @method macAddressFactory
|
|
1133
1364
|
* @description Generates or returns a MAC address based on options.
|
|
@@ -1211,7 +1442,7 @@ rm -rf ${artifacts.join(' ')}`);
|
|
|
1211
1442
|
shellExec(`mkdir -p ${mountPoint}`);
|
|
1212
1443
|
|
|
1213
1444
|
// Ensure mount point is not already mounted
|
|
1214
|
-
shellExec(`sudo umount ${mountPoint} 2>/dev/null
|
|
1445
|
+
shellExec(`sudo umount ${mountPoint} 2>/dev/null`);
|
|
1215
1446
|
|
|
1216
1447
|
try {
|
|
1217
1448
|
// Mount the ISO
|
|
@@ -1233,10 +1464,10 @@ rm -rf ${artifacts.join(' ')}`);
|
|
|
1233
1464
|
|
|
1234
1465
|
shellExec(`sudo chown -R $(whoami):$(whoami) ${extractDir}`);
|
|
1235
1466
|
// Unmount ISO
|
|
1236
|
-
shellExec(`sudo umount ${mountPoint}
|
|
1467
|
+
shellExec(`sudo umount ${mountPoint}`);
|
|
1237
1468
|
logger.info(`Unmounted ISO`);
|
|
1238
1469
|
// Clean up temporary mount point
|
|
1239
|
-
shellExec(`rmdir ${mountPoint}
|
|
1470
|
+
shellExec(`rmdir ${mountPoint}`);
|
|
1240
1471
|
}
|
|
1241
1472
|
|
|
1242
1473
|
return {
|
|
@@ -1253,8 +1484,8 @@ rm -rf ${artifacts.join(' ')}`);
|
|
|
1253
1484
|
* @param {string} options.macAddress - The MAC address of the machine.
|
|
1254
1485
|
* @param {string} options.hostname - The hostname for the machine.
|
|
1255
1486
|
* @param {string} options.ipAddress - The IP address for the machine.
|
|
1487
|
+
* @param {string} options.architecture - The architecture for the machine (default is 'arm64').
|
|
1256
1488
|
* @param {string} options.powerType - The power type for the machine (default is 'manual').
|
|
1257
|
-
* @param {object} options.maas - Additional MAAS-specific options.
|
|
1258
1489
|
* @returns {object} An object containing the created machine details.
|
|
1259
1490
|
* @memberof UnderpostBaremetal
|
|
1260
1491
|
*/
|
|
@@ -1263,13 +1494,13 @@ rm -rf ${artifacts.join(' ')}`);
|
|
|
1263
1494
|
macAddress: '',
|
|
1264
1495
|
hostname: '',
|
|
1265
1496
|
ipAddress: '',
|
|
1497
|
+
architecture: 'arm64',
|
|
1266
1498
|
powerType: 'manual',
|
|
1267
|
-
architecture: 'arm64/generic',
|
|
1268
1499
|
},
|
|
1269
1500
|
) {
|
|
1270
1501
|
if (!options.powerType) options.powerType = 'manual';
|
|
1271
1502
|
const payload = {
|
|
1272
|
-
architecture:
|
|
1503
|
+
architecture: options.architecture.match('arm') ? 'arm64/generic' : 'amd64/generic',
|
|
1273
1504
|
mac_address: options.macAddress,
|
|
1274
1505
|
mac_addresses: options.macAddress,
|
|
1275
1506
|
hostname: options.hostname,
|
|
@@ -1277,18 +1508,13 @@ rm -rf ${artifacts.join(' ')}`);
|
|
|
1277
1508
|
ip: options.ipAddress,
|
|
1278
1509
|
};
|
|
1279
1510
|
logger.info('Creating MAAS machine', payload);
|
|
1280
|
-
const machine =
|
|
1281
|
-
`
|
|
1511
|
+
const machine = Underpost.baremetal.maasCliExec(
|
|
1512
|
+
`machines create ${Object.keys(payload)
|
|
1282
1513
|
.map((k) => `${k}="${payload[k]}"`)
|
|
1283
1514
|
.join(' ')}`,
|
|
1284
|
-
{
|
|
1285
|
-
silent: true,
|
|
1286
|
-
stdout: true,
|
|
1287
|
-
},
|
|
1288
1515
|
);
|
|
1289
|
-
// console.log(machine);
|
|
1290
1516
|
try {
|
|
1291
|
-
return { machine
|
|
1517
|
+
return { machine };
|
|
1292
1518
|
} catch (error) {
|
|
1293
1519
|
console.log(error);
|
|
1294
1520
|
logger.error(error);
|
|
@@ -1325,13 +1551,7 @@ rm -rf ${artifacts.join(' ')}`);
|
|
|
1325
1551
|
return { kernelFilesPaths, resourcesPath };
|
|
1326
1552
|
}
|
|
1327
1553
|
|
|
1328
|
-
const resourceData =
|
|
1329
|
-
shellExec(`maas ${process.env.MAAS_ADMIN_USERNAME} boot-resource read ${resource.id}`, {
|
|
1330
|
-
stdout: true,
|
|
1331
|
-
silent: true,
|
|
1332
|
-
disableLog: true,
|
|
1333
|
-
}),
|
|
1334
|
-
);
|
|
1554
|
+
const resourceData = Underpost.baremetal.maasCliExec(`boot-resource read ${resource.id}`);
|
|
1335
1555
|
let kernelFilesPaths = {};
|
|
1336
1556
|
const bootFiles = resourceData.sets[Object.keys(resourceData.sets)[0]].files;
|
|
1337
1557
|
const arch = resource.architecture.split('/')[0];
|
|
@@ -1392,7 +1612,7 @@ rm -rf ${artifacts.join(' ')}`);
|
|
|
1392
1612
|
shellExec(`mkdir -p ${tempExtractDir}`);
|
|
1393
1613
|
|
|
1394
1614
|
// List files in archive to find kernel and initrd
|
|
1395
|
-
const tarList = shellExec(`tar -tf ${rootArchivePath}
|
|
1615
|
+
const tarList = shellExec(`tar -tf ${rootArchivePath}`).stdout.split('\n');
|
|
1396
1616
|
|
|
1397
1617
|
// Look for boot/vmlinuz* and boot/initrd* (handling potential leading ./)
|
|
1398
1618
|
// Skip rescue, kdump, and other special images
|
|
@@ -1524,7 +1744,7 @@ rm -rf ${artifacts.join(' ')}`);
|
|
|
1524
1744
|
*/
|
|
1525
1745
|
removeDiscoveredMachines() {
|
|
1526
1746
|
logger.info('Removing all discovered machines from MAAS...');
|
|
1527
|
-
|
|
1747
|
+
Underpost.baremetal.maasCliExec(`discoveries clear all=true`);
|
|
1528
1748
|
},
|
|
1529
1749
|
|
|
1530
1750
|
/**
|
|
@@ -1856,6 +2076,73 @@ shell
|
|
|
1856
2076
|
};
|
|
1857
2077
|
},
|
|
1858
2078
|
|
|
2079
|
+
/**
|
|
2080
|
+
* @method bootstrapHttpServerPortFactory
|
|
2081
|
+
* @description Determines the bootstrap HTTP server port.
|
|
2082
|
+
* @param {object} params - Parameters for determining the port.
|
|
2083
|
+
* @param {number} [params.port] - The port passed via options.
|
|
2084
|
+
* @param {string} params.workflowId - The workflow identifier.
|
|
2085
|
+
* @param {object} params.workflowsConfig - The loaded workflows configuration.
|
|
2086
|
+
* @returns {number} The determined port number.
|
|
2087
|
+
* @memberof UnderpostBaremetal
|
|
2088
|
+
*/
|
|
2089
|
+
bootstrapHttpServerPortFactory({ port, workflowId, workflowsConfig }) {
|
|
2090
|
+
return port || workflowsConfig[workflowId]?.bootstrapHttpServerPort || 8888;
|
|
2091
|
+
},
|
|
2092
|
+
|
|
2093
|
+
/**
|
|
2094
|
+
* @method commissioningWriteFilesFactory
|
|
2095
|
+
* @description Generates the write_files configuration for the commissioning script.
|
|
2096
|
+
* @param {object} params
|
|
2097
|
+
* @param {object} params.machine - The machine object.
|
|
2098
|
+
* @param {object} params.authCredentials - MAAS authentication credentials.
|
|
2099
|
+
* @param {string} params.runnerHostIp - The IP address of the runner host.
|
|
2100
|
+
* @memberof UnderpostBaremetal
|
|
2101
|
+
* @returns {Array} The write_files array.
|
|
2102
|
+
*/
|
|
2103
|
+
commissioningWriteFilesFactory({ machine, authCredentials, runnerHostIp }) {
|
|
2104
|
+
const { consumer_key, token_key, token_secret } = authCredentials;
|
|
2105
|
+
return [
|
|
2106
|
+
{
|
|
2107
|
+
path: '/usr/local/bin/underpost-enlist.sh',
|
|
2108
|
+
permissions: '0755',
|
|
2109
|
+
owner: 'root:root',
|
|
2110
|
+
content: `#!/bin/bash
|
|
2111
|
+
# set -euo pipefail
|
|
2112
|
+
CONSUMER_KEY="${consumer_key}"
|
|
2113
|
+
TOKEN_KEY="${token_key}"
|
|
2114
|
+
TOKEN_SECRET="${token_secret}"
|
|
2115
|
+
LOG_FILE="/var/log/underpost-enlistment.log"
|
|
2116
|
+
RESPONSE_FILE="/tmp/maas_response.txt"
|
|
2117
|
+
STATUS_FILE="/tmp/maas_status.txt"
|
|
2118
|
+
|
|
2119
|
+
echo "Starting MAAS Commissioning Request..." | tee -a "$LOG_FILE"
|
|
2120
|
+
|
|
2121
|
+
curl -X POST \\
|
|
2122
|
+
--location --verbose \\
|
|
2123
|
+
--header "Authorization: OAuth oauth_version=\\"1.0\\", oauth_signature_method=\\"PLAINTEXT\\", oauth_consumer_key=\\"$CONSUMER_KEY\\", oauth_token=\\"$TOKEN_KEY\\", oauth_signature=\\"&$TOKEN_SECRET\\", oauth_nonce=\\"$(uuidgen)\\", oauth_timestamp=\\"$(date +%s)\\"" \\
|
|
2124
|
+
-F "enable_ssh=1" \\
|
|
2125
|
+
http://${runnerHostIp}:5240/MAAS/api/2.0/machines/${machine.system_id}/op-commission \\
|
|
2126
|
+
--output "$RESPONSE_FILE" --write-out "%{http_code}" > "$STATUS_FILE" 2>> "$LOG_FILE"
|
|
2127
|
+
|
|
2128
|
+
HTTP_STATUS=$(cat "$STATUS_FILE")
|
|
2129
|
+
|
|
2130
|
+
echo "HTTP Status: $HTTP_STATUS" | tee -a "$LOG_FILE"
|
|
2131
|
+
echo "Response Body:" | tee -a "$LOG_FILE"
|
|
2132
|
+
cat "$RESPONSE_FILE" | tee -a "$LOG_FILE"
|
|
2133
|
+
|
|
2134
|
+
if [ "$HTTP_STATUS" -eq 200 ]; then
|
|
2135
|
+
echo "Commissioning requested successfully. Rebooting to start commissioning..." | tee -a "$LOG_FILE"
|
|
2136
|
+
reboot
|
|
2137
|
+
else
|
|
2138
|
+
echo "ERROR: MAAS commissioning failed with status $HTTP_STATUS" | tee -a "$LOG_FILE"
|
|
2139
|
+
exit 0
|
|
2140
|
+
fi
|
|
2141
|
+
`,
|
|
2142
|
+
},
|
|
2143
|
+
];
|
|
2144
|
+
},
|
|
2145
|
+
|
|
1859
2146
|
/**
|
|
1860
2147
|
* @method httpBootstrapServerStaticFactory
|
|
1861
2148
|
* @description Creates static files for the bootstrap HTTP server including cloud-init configuration.
|
|
@@ -1865,6 +2152,7 @@ shell
|
|
|
1865
2152
|
* @param {string} params.cloudConfigSrc - The cloud-init configuration YAML source.
|
|
1866
2153
|
* @param {object} [params.metadata] - Optional metadata to include in meta-data file.
|
|
1867
2154
|
* @param {string} [params.vendorData] - Optional vendor-data content (default: empty string).
|
|
2155
|
+
* @param {string} [params.isoUrl] - Optional ISO URL to cache and serve.
|
|
1868
2156
|
* @memberof UnderpostBaremetal
|
|
1869
2157
|
* @returns {void}
|
|
1870
2158
|
*/
|
|
@@ -1874,16 +2162,13 @@ shell
|
|
|
1874
2162
|
cloudConfigSrc,
|
|
1875
2163
|
metadata = {},
|
|
1876
2164
|
vendorData = '',
|
|
2165
|
+
isoUrl = '',
|
|
1877
2166
|
}) {
|
|
1878
2167
|
// Create directory structure
|
|
1879
2168
|
shellExec(`mkdir -p ${bootstrapHttpServerPath}/${hostname}/cloud-init`);
|
|
1880
2169
|
|
|
1881
2170
|
// Write user-data file
|
|
1882
|
-
fs.writeFileSync(
|
|
1883
|
-
`${bootstrapHttpServerPath}/${hostname}/cloud-init/user-data`,
|
|
1884
|
-
`#cloud-config\n${cloudConfigSrc}`,
|
|
1885
|
-
'utf8',
|
|
1886
|
-
);
|
|
2171
|
+
fs.writeFileSync(`${bootstrapHttpServerPath}/${hostname}/cloud-init/user-data`, cloudConfigSrc, 'utf8');
|
|
1887
2172
|
|
|
1888
2173
|
// Write meta-data file
|
|
1889
2174
|
const metaDataContent = `instance-id: ${metadata.instanceId || hostname}\nlocal-hostname: ${metadata.localHostname || hostname}`;
|
|
@@ -1893,6 +2178,22 @@ shell
|
|
|
1893
2178
|
fs.writeFileSync(`${bootstrapHttpServerPath}/${hostname}/cloud-init/vendor-data`, vendorData, 'utf8');
|
|
1894
2179
|
|
|
1895
2180
|
logger.info(`Cloud-init files written to ${bootstrapHttpServerPath}/${hostname}/cloud-init`);
|
|
2181
|
+
|
|
2182
|
+
if (isoUrl) {
|
|
2183
|
+
const isoFilename = isoUrl.split('/').pop();
|
|
2184
|
+
const isoCacheDir = `/var/tmp/live-iso`;
|
|
2185
|
+
const isoCachePath = `${isoCacheDir}/${isoFilename}`;
|
|
2186
|
+
const isoDestPath = `${bootstrapHttpServerPath}/${hostname}/${isoFilename}`;
|
|
2187
|
+
|
|
2188
|
+
if (!fs.existsSync(isoCachePath)) {
|
|
2189
|
+
logger.info(`Downloading ISO to cache: ${isoUrl}`);
|
|
2190
|
+
shellExec(`mkdir -p ${isoCacheDir}`);
|
|
2191
|
+
shellExec(`wget --progress=bar:force -O ${isoCachePath} "${isoUrl}"`);
|
|
2192
|
+
}
|
|
2193
|
+
|
|
2194
|
+
logger.info(`Copying ISO to bootstrap server: ${isoDestPath}`);
|
|
2195
|
+
shellExec(`cp ${isoCachePath} ${isoDestPath}`);
|
|
2196
|
+
}
|
|
1896
2197
|
},
|
|
1897
2198
|
|
|
1898
2199
|
/**
|
|
@@ -1913,14 +2214,17 @@ shell
|
|
|
1913
2214
|
const hostname = options.hostname || 'localhost';
|
|
1914
2215
|
|
|
1915
2216
|
shellExec(`mkdir -p ${bootstrapHttpServerPath}/${hostname}/cloud-init`);
|
|
2217
|
+
shellExec(`node bin run kill ${port}`);
|
|
1916
2218
|
|
|
1917
|
-
|
|
1918
|
-
shellExec(`sudo pkill -f 'python3 -m http.server ${port}'`, { silent: true });
|
|
2219
|
+
const app = express();
|
|
1919
2220
|
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
2221
|
+
app.use(loggerMiddleware(import.meta, 'debug', () => false));
|
|
2222
|
+
|
|
2223
|
+
app.use('/', express.static(bootstrapHttpServerPath));
|
|
2224
|
+
|
|
2225
|
+
app.listen(port, () => {
|
|
2226
|
+
logger.info(`Static file server running on port ${port}`);
|
|
2227
|
+
});
|
|
1924
2228
|
|
|
1925
2229
|
// Configure iptables to allow incoming LAN connections
|
|
1926
2230
|
shellExec(
|
|
@@ -1963,7 +2267,7 @@ shell
|
|
|
1963
2267
|
// GRUB on ARM64 often crashes with synchronous exception (0x200) if handling large compressed kernels directly.
|
|
1964
2268
|
if (file === 'vmlinuz-efi') {
|
|
1965
2269
|
const kernelDest = `${tftpRootPath}/pxe/${file}`;
|
|
1966
|
-
const fileType = shellExec(`file ${kernelDest}
|
|
2270
|
+
const fileType = shellExec(`file ${kernelDest}`).stdout;
|
|
1967
2271
|
|
|
1968
2272
|
// Handle gzip compressed kernels
|
|
1969
2273
|
if (fileType.includes('gzip compressed data')) {
|
|
@@ -2001,8 +2305,14 @@ shell
|
|
|
2001
2305
|
* @param {string} options.macAddress - The MAC address of the client.
|
|
2002
2306
|
* @param {boolean} options.cloudInit - Whether to include cloud-init parameters.
|
|
2003
2307
|
* @param {object} options.machine - The machine object containing system_id.
|
|
2308
|
+
* @param {string} options.machine.system_id - The system ID of the machine (for MAAS metadata).
|
|
2004
2309
|
* @param {boolean} [options.dev=false] - Whether to enable dev mode with dracut debugging parameters.
|
|
2005
2310
|
* @param {string} [options.osIdLike=''] - OS family identifier (e.g., 'rhel centos fedora' or 'debian ubuntu').
|
|
2311
|
+
* @param {object} options.authCredentials - Authentication credentials for fetching files (if needed).
|
|
2312
|
+
* @param {string} options.authCredentials.consumer_key - Consumer key for authentication.
|
|
2313
|
+
* @param {string} options.authCredentials.consumer_secret - Consumer secret for authentication.
|
|
2314
|
+
* @param {string} options.authCredentials.token_key - Token key for authentication.
|
|
2315
|
+
* @param {string} options.authCredentials.token_secret - Token secret for authentication.
|
|
2006
2316
|
* @returns {object} An object containing the constructed command line string.
|
|
2007
2317
|
* @memberof UnderpostBaremetal
|
|
2008
2318
|
*/
|
|
@@ -2024,6 +2334,7 @@ shell
|
|
|
2024
2334
|
machine: { system_id: '' },
|
|
2025
2335
|
dev: false,
|
|
2026
2336
|
osIdLike: '',
|
|
2337
|
+
authCredentials: { consumer_key: '', consumer_secret: '', token_key: '', token_secret: '' },
|
|
2027
2338
|
},
|
|
2028
2339
|
) {
|
|
2029
2340
|
// Construct kernel command line arguments for NFS boot.
|
|
@@ -2044,10 +2355,9 @@ shell
|
|
|
2044
2355
|
osIdLike,
|
|
2045
2356
|
} = options;
|
|
2046
2357
|
|
|
2047
|
-
const ipParam =
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
: 'ip=dhcp';
|
|
2358
|
+
const ipParam =
|
|
2359
|
+
`ip=${ipClient}:${ipFileServer}:${ipDhcpServer}:${netmask}:${hostname}` +
|
|
2360
|
+
`:${networkInterfaceName ? networkInterfaceName : 'eth0'}:${ipConfig}:${dnsServer}`;
|
|
2051
2361
|
|
|
2052
2362
|
const nfsOptions = `${
|
|
2053
2363
|
type === 'chroot-debootstrap' || type === 'chroot-container'
|
|
@@ -2094,7 +2404,7 @@ shell
|
|
|
2094
2404
|
// `toram`,
|
|
2095
2405
|
'nomodeset',
|
|
2096
2406
|
`editable_rootfs=tmpfs`,
|
|
2097
|
-
`ramdisk_size=3550000`,
|
|
2407
|
+
// `ramdisk_size=3550000`,
|
|
2098
2408
|
// `root=/dev/sda1`, // rpi4 usb port unit
|
|
2099
2409
|
'apparmor=0', // Disable AppArmor security
|
|
2100
2410
|
...(networkInterfaceName === 'eth0'
|
|
@@ -2138,49 +2448,32 @@ shell
|
|
|
2138
2448
|
if (type === 'iso-ram') {
|
|
2139
2449
|
const netBootParams = [`netboot=url`];
|
|
2140
2450
|
if (fileSystemUrl) netBootParams.push(`url=${fileSystemUrl.replace('https', 'http')}`);
|
|
2141
|
-
cmd = [ipParam, `boot=casper`, ...netBootParams, ...kernelParams];
|
|
2451
|
+
cmd = [ipParam, `boot=casper`, 'toram', ...netBootParams, ...kernelParams, ...performanceParams];
|
|
2142
2452
|
} else if (type === 'chroot-debootstrap' || type === 'chroot-container') {
|
|
2143
2453
|
let qemuNfsRootParams = [`root=/dev/nfs`, `rootfstype=nfs`];
|
|
2144
|
-
|
|
2145
|
-
// Determine OS family from osIdLike configuration
|
|
2146
|
-
const isRhelBased = osIdLike && osIdLike.match(/rhel|centos|fedora|alma|rocky/i);
|
|
2147
|
-
const isDebianBased = osIdLike && osIdLike.match(/debian|ubuntu/i);
|
|
2148
|
-
|
|
2149
|
-
// Add RHEL/Rocky/Fedora based images specific parameters
|
|
2150
|
-
if (isRhelBased) {
|
|
2151
|
-
qemuNfsRootParams = qemuNfsRootParams.concat([`rd.neednet=1`, `rd.timeout=180`, `selinux=0`, `enforcing=0`]);
|
|
2152
|
-
}
|
|
2153
|
-
// Add Debian/Ubuntu based images specific parameters
|
|
2154
|
-
else if (isDebianBased) {
|
|
2155
|
-
qemuNfsRootParams = qemuNfsRootParams.concat([`initrd=initrd.img`, `init=/sbin/init`]);
|
|
2156
|
-
}
|
|
2157
|
-
|
|
2158
|
-
// Add debugging parameters in dev mode for dracut troubleshooting
|
|
2159
|
-
if (options.dev) {
|
|
2160
|
-
// qemuNfsRootParams = qemuNfsRootParams.concat([`rd.shell`, `rd.debug`]);
|
|
2161
|
-
}
|
|
2162
|
-
|
|
2163
2454
|
cmd = [ipParam, ...qemuNfsRootParams, nfsRootParam, ...kernelParams];
|
|
2164
2455
|
} else {
|
|
2165
2456
|
// 'iso-nfs'
|
|
2166
2457
|
cmd = [ipParam, `netboot=nfs`, nfsRootParam, ...kernelParams, ...performanceParams];
|
|
2458
|
+
// cmd.push(`ifname=${networkInterfaceName}:${macAddress}`);
|
|
2459
|
+
}
|
|
2167
2460
|
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
]);
|
|
2182
|
-
}
|
|
2461
|
+
// Determine OS family from osIdLike configuration
|
|
2462
|
+
const isRhelBased = osIdLike && osIdLike.match(/rhel|centos|fedora|alma|rocky/i);
|
|
2463
|
+
const isDebianBased = osIdLike && osIdLike.match(/debian|ubuntu/i);
|
|
2464
|
+
|
|
2465
|
+
// Add RHEL/Rocky/Fedora based images specific parameters
|
|
2466
|
+
if (isRhelBased) {
|
|
2467
|
+
cmd = cmd.concat([`rd.neednet=1`, `rd.timeout=180`, `selinux=0`, `enforcing=0`]);
|
|
2468
|
+
if (options.dev) cmd = cmd.concat([`rd.shell`, `rd.debug`]);
|
|
2469
|
+
}
|
|
2470
|
+
// Add Debian/Ubuntu based images specific parameters
|
|
2471
|
+
else if (isDebianBased) {
|
|
2472
|
+
cmd = cmd.concat([`initrd=initrd.img`, `init=/sbin/init`]);
|
|
2473
|
+
if (options.dev) cmd = cmd.concat([`debug`, `ignore_loglevel`]);
|
|
2183
2474
|
}
|
|
2475
|
+
|
|
2476
|
+
if (cloudInit) cmd = Underpost.cloudInit.kernelParamsFactory(macAddress, cmd, options);
|
|
2184
2477
|
// cmd.push('---');
|
|
2185
2478
|
const cmdStr = cmd.join(' ');
|
|
2186
2479
|
logger.info('Constructed kernel command line');
|
|
@@ -2204,12 +2497,7 @@ shell
|
|
|
2204
2497
|
async commissionMonitor({ macAddress, ipAddress, hostname, architecture, machine }) {
|
|
2205
2498
|
{
|
|
2206
2499
|
// Query observed discoveries from MAAS.
|
|
2207
|
-
const discoveries =
|
|
2208
|
-
shellExec(`maas ${process.env.MAAS_ADMIN_USERNAME} discoveries read`, {
|
|
2209
|
-
silent: true,
|
|
2210
|
-
stdout: true,
|
|
2211
|
-
}),
|
|
2212
|
-
);
|
|
2500
|
+
const discoveries = Underpost.baremetal.maasCliExec(`discoveries read`);
|
|
2213
2501
|
|
|
2214
2502
|
for (const discovery of discoveries) {
|
|
2215
2503
|
const discoverHostname = discovery.hostname
|
|
@@ -2227,6 +2515,20 @@ shell
|
|
|
2227
2515
|
if (discovery.ip === ipAddress) {
|
|
2228
2516
|
logger.info('Machine discovered!', discovery);
|
|
2229
2517
|
if (!machine) {
|
|
2518
|
+
// Check if a machine with the discovered MAC already exists to avoid conflicts
|
|
2519
|
+
const [existingMachine] =
|
|
2520
|
+
Underpost.baremetal.maasCliExec(`machines read mac_address=${discovery.mac_address}`) || [];
|
|
2521
|
+
|
|
2522
|
+
if (existingMachine) {
|
|
2523
|
+
logger.warn(
|
|
2524
|
+
`Machine ${existingMachine.hostname} (${existingMachine.system_id}) already exists with MAC ${discovery.mac_address}`,
|
|
2525
|
+
);
|
|
2526
|
+
logger.info(
|
|
2527
|
+
`Deleting existing machine ${existingMachine.system_id} to create new machine ${hostname}...`,
|
|
2528
|
+
);
|
|
2529
|
+
Underpost.baremetal.maasCliExec(`machine delete ${existingMachine.system_id}`);
|
|
2530
|
+
}
|
|
2531
|
+
|
|
2230
2532
|
logger.info('Creating new machine with discovered hardware MAC...', {
|
|
2231
2533
|
discoveredMAC: discovery.mac_address,
|
|
2232
2534
|
ipAddress,
|
|
@@ -2238,12 +2540,18 @@ shell
|
|
|
2238
2540
|
hostname,
|
|
2239
2541
|
architecture,
|
|
2240
2542
|
}).machine;
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2543
|
+
|
|
2544
|
+
if (machine && machine.system_id) {
|
|
2545
|
+
console.log('New machine system id:', machine.system_id.bgYellow.bold.black);
|
|
2546
|
+
Underpost.baremetal.writeGrubConfigToFile({
|
|
2547
|
+
grubCfgSrc: Underpost.baremetal
|
|
2548
|
+
.getGrubConfigFromFile()
|
|
2549
|
+
.grubCfgSrc.replaceAll('system-id', machine.system_id),
|
|
2550
|
+
});
|
|
2551
|
+
} else {
|
|
2552
|
+
logger.error('Failed to create machine or obtain system_id', machine);
|
|
2553
|
+
throw new Error('Machine creation failed');
|
|
2554
|
+
}
|
|
2247
2555
|
} else {
|
|
2248
2556
|
const systemId = machine.system_id;
|
|
2249
2557
|
console.log('Using pre-registered machine system_id:', systemId.bgYellow.bold.black);
|
|
@@ -2256,22 +2564,45 @@ shell
|
|
|
2256
2564
|
discoveredMAC: discovery.mac_address,
|
|
2257
2565
|
});
|
|
2258
2566
|
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2567
|
+
// Check current machine status before attempting state transitions
|
|
2568
|
+
const currentMachine = Underpost.baremetal.maasCliExec(`machine read ${systemId}`);
|
|
2569
|
+
const currentStatus = currentMachine ? currentMachine.status_name : 'Unknown';
|
|
2570
|
+
logger.info('Current machine status before interface update:', { systemId, status: currentStatus });
|
|
2571
|
+
|
|
2572
|
+
// Only mark-broken if the machine is in a state that supports it (e.g. Ready, New, Allocated)
|
|
2573
|
+
// Machines already in Broken state don't need to be marked broken again
|
|
2574
|
+
if (currentStatus !== 'Broken') {
|
|
2575
|
+
try {
|
|
2576
|
+
Underpost.baremetal.maasCliExec(`machine mark-broken ${systemId}`);
|
|
2577
|
+
logger.info('Machine marked as broken successfully');
|
|
2578
|
+
} catch (markBrokenError) {
|
|
2579
|
+
logger.warn('Failed to mark machine as broken, attempting interface update anyway...', {
|
|
2580
|
+
error: markBrokenError.message,
|
|
2581
|
+
currentStatus,
|
|
2582
|
+
});
|
|
2583
|
+
}
|
|
2584
|
+
} else {
|
|
2585
|
+
logger.info('Machine is already in Broken state, skipping mark-broken');
|
|
2586
|
+
}
|
|
2262
2587
|
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
`maas ${process.env.MAAS_ADMIN_USERNAME} interface update ${systemId} ${machine.boot_interface.id}` +
|
|
2266
|
-
` mac_address=${discovery.mac_address}`,
|
|
2267
|
-
{
|
|
2268
|
-
silent: true,
|
|
2269
|
-
},
|
|
2588
|
+
Underpost.baremetal.maasCliExec(
|
|
2589
|
+
`interface update ${systemId} ${machine.boot_interface.id}` + ` mac_address=${discovery.mac_address}`,
|
|
2270
2590
|
);
|
|
2271
2591
|
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2592
|
+
// Re-check status before mark-fixed — only attempt if actually Broken
|
|
2593
|
+
const updatedMachine = Underpost.baremetal.maasCliExec(`machine read ${systemId}`);
|
|
2594
|
+
const updatedStatus = updatedMachine ? updatedMachine.status_name : 'Unknown';
|
|
2595
|
+
|
|
2596
|
+
if (updatedStatus === 'Broken') {
|
|
2597
|
+
try {
|
|
2598
|
+
Underpost.baremetal.maasCliExec(`machine mark-fixed ${systemId}`);
|
|
2599
|
+
logger.info('Machine marked as fixed successfully');
|
|
2600
|
+
} catch (markFixedError) {
|
|
2601
|
+
logger.warn('Failed to mark machine as fixed:', { error: markFixedError.message });
|
|
2602
|
+
}
|
|
2603
|
+
} else {
|
|
2604
|
+
logger.info('Machine is not in Broken state, skipping mark-fixed', { status: updatedStatus });
|
|
2605
|
+
}
|
|
2275
2606
|
|
|
2276
2607
|
logger.info('✓ Machine interface MAC address updated successfully');
|
|
2277
2608
|
|
|
@@ -2300,6 +2631,73 @@ shell
|
|
|
2300
2631
|
}
|
|
2301
2632
|
},
|
|
2302
2633
|
|
|
2634
|
+
/**
|
|
2635
|
+
* @method maasCliExec
|
|
2636
|
+
* @description Executes a MAAS CLI command and returns the parsed JSON output.
|
|
2637
|
+
* This method abstracts the execution of MAAS CLI commands, ensuring that the output is captured and parsed correctly.
|
|
2638
|
+
* @param {string} cmd - The MAAS CLI command to execute (e.g., 'machines read').
|
|
2639
|
+
* @returns {object|null} The parsed JSON output from the MAAS CLI command, or null if there is no output.
|
|
2640
|
+
* @memberof UnderpostBaremetal
|
|
2641
|
+
*/
|
|
2642
|
+
maasCliExec(cmd) {
|
|
2643
|
+
const output = shellExec(`maas ${process.env.MAAS_ADMIN_USERNAME} ${cmd}`, {
|
|
2644
|
+
stdout: true,
|
|
2645
|
+
silent: true,
|
|
2646
|
+
}).trim();
|
|
2647
|
+
try {
|
|
2648
|
+
return output ? JSON.parse(output) : null;
|
|
2649
|
+
} catch (error) {
|
|
2650
|
+
console.log('output', output);
|
|
2651
|
+
logger.error(error);
|
|
2652
|
+
throw error;
|
|
2653
|
+
}
|
|
2654
|
+
},
|
|
2655
|
+
|
|
2656
|
+
/**
|
|
2657
|
+
* @method maasAuthCredentialsFactory
|
|
2658
|
+
* @description Retrieves MAAS API key credentials from the MAAS CLI.
|
|
2659
|
+
* This method parses the output of `maas apikey` to extract the consumer key,
|
|
2660
|
+
* consumer secret, token key, and token secret.
|
|
2661
|
+
* @returns {object} An object containing the MAAS authentication credentials.
|
|
2662
|
+
* @memberof UnderpostBaremetal
|
|
2663
|
+
* @throws {Error} If the MAAS API key format is invalid.
|
|
2664
|
+
*/
|
|
2665
|
+
maasAuthCredentialsFactory() {
|
|
2666
|
+
// Expected formats:
|
|
2667
|
+
// <consumer_key>:<consumer_token>:<secret> (older format)
|
|
2668
|
+
// <consumer_key>:<consumer_secret>:<token_key>:<token_secret> (newer format)
|
|
2669
|
+
// Commands used to generate API keys:
|
|
2670
|
+
// maas apikey --with-names --username ${process.env.MAAS_ADMIN_USERNAME}
|
|
2671
|
+
// maas ${process.env.MAAS_ADMIN_USERNAME} account create-authorisation-token
|
|
2672
|
+
// maas apikey --generate --username ${process.env.MAAS_ADMIN_USERNAME}
|
|
2673
|
+
// Reference: https://github.com/CanonicalLtd/maas-docs/issues/647
|
|
2674
|
+
|
|
2675
|
+
const parts = shellExec(`maas apikey --with-names --username ${process.env.MAAS_ADMIN_USERNAME}`, {
|
|
2676
|
+
stdout: true,
|
|
2677
|
+
})
|
|
2678
|
+
.trim()
|
|
2679
|
+
.split(`\n`)[0] // Take only the first line of output.
|
|
2680
|
+
.split(':'); // Split by colon to get individual parts.
|
|
2681
|
+
|
|
2682
|
+
let consumer_key, consumer_secret, token_key, token_secret;
|
|
2683
|
+
|
|
2684
|
+
// Determine the format of the API key and assign parts accordingly.
|
|
2685
|
+
if (parts.length === 4) {
|
|
2686
|
+
[consumer_key, consumer_secret, token_key, token_secret] = parts;
|
|
2687
|
+
} else if (parts.length === 3) {
|
|
2688
|
+
// Handle older 3-part format, setting consumer_secret as empty.
|
|
2689
|
+
[consumer_key, token_key, token_secret] = parts;
|
|
2690
|
+
consumer_secret = '';
|
|
2691
|
+
token_secret = token_secret.split(' MAAS consumer')[0].trim(); // Clean up token secret.
|
|
2692
|
+
} else {
|
|
2693
|
+
// Throw an error if the format is not recognized.
|
|
2694
|
+
throw new Error('Invalid token format');
|
|
2695
|
+
}
|
|
2696
|
+
|
|
2697
|
+
logger.info('Maas api token generated', { consumer_key, consumer_secret, token_key, token_secret });
|
|
2698
|
+
return { consumer_key, consumer_secret, token_key, token_secret };
|
|
2699
|
+
},
|
|
2700
|
+
|
|
2303
2701
|
/**
|
|
2304
2702
|
* @method mountBinfmtMisc
|
|
2305
2703
|
* @description Mounts the binfmt_misc filesystem to enable QEMU user-static binfmt support.
|
|
@@ -2335,7 +2733,7 @@ shell
|
|
|
2335
2733
|
const systemId = typeof machine === 'string' ? machine : machine.system_id;
|
|
2336
2734
|
if (ignore && ignore.find((mId) => mId === systemId)) continue;
|
|
2337
2735
|
logger.info(`Removing machine: ${systemId}`);
|
|
2338
|
-
|
|
2736
|
+
Underpost.baremetal.maasCliExec(`machine delete ${systemId}`);
|
|
2339
2737
|
}
|
|
2340
2738
|
return [];
|
|
2341
2739
|
},
|
|
@@ -2349,9 +2747,9 @@ shell
|
|
|
2349
2747
|
* @returns {void}
|
|
2350
2748
|
*/
|
|
2351
2749
|
clearDiscoveries({ force }) {
|
|
2352
|
-
|
|
2750
|
+
Underpost.baremetal.maasCliExec(`discoveries clear all=true`);
|
|
2353
2751
|
if (force === true) {
|
|
2354
|
-
|
|
2752
|
+
Underpost.baremetal.maasCliExec(`discoveries scan force=true`);
|
|
2355
2753
|
}
|
|
2356
2754
|
},
|
|
2357
2755
|
|
|
@@ -2967,10 +3365,10 @@ udp-port = 32766
|
|
|
2967
3365
|
// Check both /usr/local/bin (compiled) and system paths
|
|
2968
3366
|
let qemuAarch64Path = null;
|
|
2969
3367
|
|
|
2970
|
-
if (shellExec('test -x /usr/local/bin/qemu-system-aarch64'
|
|
3368
|
+
if (shellExec('test -x /usr/local/bin/qemu-system-aarch64').code === 0) {
|
|
2971
3369
|
qemuAarch64Path = '/usr/local/bin/qemu-system-aarch64';
|
|
2972
|
-
} else if (shellExec('which qemu-system-aarch64'
|
|
2973
|
-
qemuAarch64Path = shellExec('which qemu-system-aarch64'
|
|
3370
|
+
} else if (shellExec('which qemu-system-aarch64').code === 0) {
|
|
3371
|
+
qemuAarch64Path = shellExec('which qemu-system-aarch64').stdout.trim();
|
|
2974
3372
|
}
|
|
2975
3373
|
|
|
2976
3374
|
if (!qemuAarch64Path) {
|
|
@@ -2983,7 +3381,7 @@ udp-port = 32766
|
|
|
2983
3381
|
logger.info(`Found qemu-system-aarch64 at: ${qemuAarch64Path}`);
|
|
2984
3382
|
|
|
2985
3383
|
// Verify that the installed qemu supports the 'virt' machine type (required for arm64)
|
|
2986
|
-
const machineHelp = shellExec(`${qemuAarch64Path} -machine help
|
|
3384
|
+
const machineHelp = shellExec(`${qemuAarch64Path} -machine help`).stdout;
|
|
2987
3385
|
if (!machineHelp.includes('virt')) {
|
|
2988
3386
|
throw new Error(
|
|
2989
3387
|
'The installed qemu-system-aarch64 does not support the "virt" machine type.\n' +
|
|
@@ -2996,10 +3394,10 @@ udp-port = 32766
|
|
|
2996
3394
|
// Check both /usr/local/bin (compiled) and system paths
|
|
2997
3395
|
let qemuX86Path = null;
|
|
2998
3396
|
|
|
2999
|
-
if (shellExec('test -x /usr/local/bin/qemu-system-x86_64'
|
|
3397
|
+
if (shellExec('test -x /usr/local/bin/qemu-system-x86_64').code === 0) {
|
|
3000
3398
|
qemuX86Path = '/usr/local/bin/qemu-system-x86_64';
|
|
3001
|
-
} else if (shellExec('which qemu-system-x86_64'
|
|
3002
|
-
qemuX86Path = shellExec('which qemu-system-x86_64'
|
|
3399
|
+
} else if (shellExec('which qemu-system-x86_64').code === 0) {
|
|
3400
|
+
qemuX86Path = shellExec('which qemu-system-x86_64').stdout.trim();
|
|
3003
3401
|
}
|
|
3004
3402
|
|
|
3005
3403
|
if (!qemuX86Path) {
|
|
@@ -3012,7 +3410,7 @@ udp-port = 32766
|
|
|
3012
3410
|
logger.info(`Found qemu-system-x86_64 at: ${qemuX86Path}`);
|
|
3013
3411
|
|
|
3014
3412
|
// Verify that the installed qemu supports the 'pc' or 'q35' machine type (required for x86_64)
|
|
3015
|
-
const machineHelp = shellExec(`${qemuX86Path} -machine help
|
|
3413
|
+
const machineHelp = shellExec(`${qemuX86Path} -machine help`).stdout;
|
|
3016
3414
|
if (!machineHelp.includes('pc') && !machineHelp.includes('q35')) {
|
|
3017
3415
|
throw new Error(
|
|
3018
3416
|
'The installed qemu-system-x86_64 does not support the "pc" or "q35" machine type.\n' +
|