underpost 2.99.4 → 2.99.6
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 +3 -3
- package/baremetal/commission-workflows.json +93 -4
- package/bin/deploy.js +56 -45
- package/cli.md +45 -28
- package/examples/static-page/README.md +101 -357
- package/examples/static-page/ssr-components/CustomPage.js +1 -13
- package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +40 -0
- package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +40 -0
- package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
- package/manifests/deployment/dd-test-development/deployment.yaml +2 -2
- package/package.json +3 -4
- package/scripts/disk-devices.sh +13 -0
- package/scripts/maas-setup.sh +13 -9
- package/scripts/rocky-kickstart.sh +294 -0
- package/src/cli/baremetal.js +657 -263
- package/src/cli/cloud-init.js +120 -120
- package/src/cli/env.js +4 -1
- package/src/cli/image.js +4 -37
- package/src/cli/index.js +56 -11
- package/src/cli/kickstart.js +149 -0
- package/src/cli/repository.js +3 -1
- package/src/cli/run.js +56 -10
- package/src/cli/secrets.js +0 -34
- package/src/cli/static.js +23 -23
- package/src/client/components/core/Docs.js +22 -3
- package/src/index.js +30 -5
- package/src/server/backup.js +11 -4
- package/src/server/client-build-docs.js +1 -1
- package/src/server/conf.js +0 -22
- package/src/server/cron.js +339 -130
- package/src/server/dns.js +10 -0
- 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
|
@@ -6,15 +6,16 @@
|
|
|
6
6
|
|
|
7
7
|
import { fileURLToPath } from 'url';
|
|
8
8
|
import { getNpmRootPath, getUnderpostRootPath } from '../server/conf.js';
|
|
9
|
-
import {
|
|
9
|
+
import { 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).
|
|
@@ -76,6 +65,7 @@ class UnderpostBaremetal {
|
|
|
76
65
|
* @param {boolean} [options.commission=false] - Flag to commission the baremetal machine.
|
|
77
66
|
* @param {number} [options.bootstrapHttpServerPort=8888] - Port for the bootstrap HTTP server.
|
|
78
67
|
* @param {string} [options.bootstrapHttpServerPath='./public/localhost'] - Path for the bootstrap HTTP server files.
|
|
68
|
+
* @param {boolean} [options.bootstrapHttpServerRun=false] - Flag to start the bootstrap HTTP server.
|
|
79
69
|
* @param {string} [options.isoUrl=''] - Uses a custom ISO URL for baremetal machine commissioning.
|
|
80
70
|
* @param {boolean} [options.ubuntuToolsBuild=false] - Builds ubuntu tools for chroot environment.
|
|
81
71
|
* @param {boolean} [options.ubuntuToolsTest=false] - Tests ubuntu tools in chroot environment.
|
|
@@ -86,6 +76,7 @@ class UnderpostBaremetal {
|
|
|
86
76
|
* @param {boolean} [options.nfsBuild=false] - Flag to build the NFS root filesystem.
|
|
87
77
|
* @param {boolean} [options.nfsBuildServer=false] - Flag to build the NFS server components.
|
|
88
78
|
* @param {boolean} [options.nfsMount=false] - Flag to mount the NFS root filesystem.
|
|
79
|
+
* @param {boolean} [options.nfsReset=false] - Flag to reset the NFS environment by unmounting and cleaning the host path.
|
|
89
80
|
* @param {boolean} [options.nfsUnmount=false] - Flag to unmount the NFS root filesystem.
|
|
90
81
|
* @param {boolean} [options.nfsSh=false] - Flag to chroot into the NFS environment for shell access.
|
|
91
82
|
* @param {string} [options.logs=''] - Specifies which logs to display ('dhcp', 'cloud', 'machine', 'cloud-config').
|
|
@@ -111,6 +102,7 @@ class UnderpostBaremetal {
|
|
|
111
102
|
mac: '',
|
|
112
103
|
ipxe: false,
|
|
113
104
|
ipxeRebuild: false,
|
|
105
|
+
ipxeBuildIso: '',
|
|
114
106
|
installPacker: false,
|
|
115
107
|
packerMaasImageTemplate: false,
|
|
116
108
|
packerWorkflowId: '',
|
|
@@ -124,6 +116,7 @@ class UnderpostBaremetal {
|
|
|
124
116
|
commission: false,
|
|
125
117
|
bootstrapHttpServerPort: 8888,
|
|
126
118
|
bootstrapHttpServerPath: './public/localhost',
|
|
119
|
+
bootstrapHttpServerRun: false,
|
|
127
120
|
isoUrl: '',
|
|
128
121
|
ubuntuToolsBuild: false,
|
|
129
122
|
ubuntuToolsTest: false,
|
|
@@ -134,6 +127,7 @@ class UnderpostBaremetal {
|
|
|
134
127
|
nfsBuild: false,
|
|
135
128
|
nfsBuildServer: false,
|
|
136
129
|
nfsMount: false,
|
|
130
|
+
nfsReset: false,
|
|
137
131
|
nfsUnmount: false,
|
|
138
132
|
nfsSh: false,
|
|
139
133
|
logs: '',
|
|
@@ -218,12 +212,7 @@ class UnderpostBaremetal {
|
|
|
218
212
|
// Create a new machine in MAAS if the option is set.
|
|
219
213
|
let machine;
|
|
220
214
|
if (options.createMachine === true) {
|
|
221
|
-
const [searhMachine] =
|
|
222
|
-
shellExec(`maas maas machines read hostname=${hostname}`, {
|
|
223
|
-
stdout: true,
|
|
224
|
-
silent: true,
|
|
225
|
-
}),
|
|
226
|
-
);
|
|
215
|
+
const [searhMachine] = Underpost.baremetal.maasCliExec(`machines read hostname=${hostname}`);
|
|
227
216
|
|
|
228
217
|
if (searhMachine) {
|
|
229
218
|
// Check if existing machine's MAC matches the specified MAC
|
|
@@ -238,16 +227,14 @@ class UnderpostBaremetal {
|
|
|
238
227
|
logger.info(`Deleting existing machine ${searhMachine.system_id} to recreate with correct MAC...`);
|
|
239
228
|
|
|
240
229
|
// Delete the existing machine
|
|
241
|
-
|
|
242
|
-
silent: true,
|
|
243
|
-
});
|
|
230
|
+
Underpost.baremetal.maasCliExec(`machine delete ${searhMachine.system_id}`);
|
|
244
231
|
|
|
245
232
|
// Create new machine with correct MAC
|
|
246
233
|
machine = Underpost.baremetal.machineFactory({
|
|
247
234
|
hostname,
|
|
248
235
|
ipAddress,
|
|
249
236
|
macAddress,
|
|
250
|
-
|
|
237
|
+
architecture: workflowsConfig[workflowId].architecture,
|
|
251
238
|
}).machine;
|
|
252
239
|
|
|
253
240
|
logger.info(`✓ Machine recreated with MAC ${macAddress}`);
|
|
@@ -266,12 +253,33 @@ class UnderpostBaremetal {
|
|
|
266
253
|
hostname,
|
|
267
254
|
ipAddress,
|
|
268
255
|
macAddress,
|
|
269
|
-
|
|
256
|
+
architecture: workflowsConfig[workflowId].architecture,
|
|
270
257
|
}).machine;
|
|
271
258
|
}
|
|
272
259
|
}
|
|
273
260
|
}
|
|
274
261
|
|
|
262
|
+
if (options.ipxeBuildIso)
|
|
263
|
+
return await Underpost.baremetal.ipxeBuildIso({
|
|
264
|
+
workflowId,
|
|
265
|
+
isoOutputPath: options.ipxeBuildIso,
|
|
266
|
+
tftpPrefix,
|
|
267
|
+
ipFileServer,
|
|
268
|
+
ipAddress,
|
|
269
|
+
ipConfig,
|
|
270
|
+
netmask,
|
|
271
|
+
dnsServer,
|
|
272
|
+
macAddress,
|
|
273
|
+
cloudInit: options.cloudInit,
|
|
274
|
+
dev: options.dev,
|
|
275
|
+
forceRebuild: options.ipxeRebuild,
|
|
276
|
+
bootstrapHttpServerPort: Underpost.baremetal.bootstrapHttpServerPortFactory({
|
|
277
|
+
port: options.bootstrapHttpServerPort,
|
|
278
|
+
workflowId,
|
|
279
|
+
workflowsConfig,
|
|
280
|
+
}),
|
|
281
|
+
});
|
|
282
|
+
|
|
275
283
|
if (options.installPacker) {
|
|
276
284
|
await Underpost.baremetal.installPacker(underpostRoot);
|
|
277
285
|
return;
|
|
@@ -351,7 +359,7 @@ class UnderpostBaremetal {
|
|
|
351
359
|
|
|
352
360
|
// Build phase (skip if upload-only mode)
|
|
353
361
|
if (options.packerMaasImageBuild) {
|
|
354
|
-
if (shellExec('packer version'
|
|
362
|
+
if (shellExec('packer version').code !== 0) {
|
|
355
363
|
throw new Error('Packer is not installed. Please install Packer to proceed.');
|
|
356
364
|
}
|
|
357
365
|
|
|
@@ -414,29 +422,9 @@ rm -rf ${artifacts.join(' ')}`);
|
|
|
414
422
|
|
|
415
423
|
logger.info(`Uploading image to MAAS...`);
|
|
416
424
|
|
|
417
|
-
// Detect MAAS profile from 'maas list' output
|
|
418
|
-
let maasProfile = process.env.MAAS_ADMIN_USERNAME;
|
|
419
|
-
if (!maasProfile) {
|
|
420
|
-
const profileList = shellExec('maas list', { silent: true, stdout: true });
|
|
421
|
-
if (profileList) {
|
|
422
|
-
const firstLine = profileList.trim().split('\n')[0];
|
|
423
|
-
const match = firstLine.match(/^(\S+)\s+http/);
|
|
424
|
-
if (match) {
|
|
425
|
-
maasProfile = match[1];
|
|
426
|
-
logger.info(`Detected MAAS profile: ${maasProfile}`);
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
if (!maasProfile) {
|
|
432
|
-
throw new Error(
|
|
433
|
-
'MAAS profile not found. Please run "maas login" first or set MAAS_ADMIN_USERNAME environment variable.',
|
|
434
|
-
);
|
|
435
|
-
}
|
|
436
|
-
|
|
437
425
|
// Use the upload script to avoid MAAS CLI bugs
|
|
438
426
|
const uploadScript = `${underpostRoot}/scripts/maas-upload-boot-resource.sh`;
|
|
439
|
-
const uploadCmd = `${uploadScript} ${
|
|
427
|
+
const uploadCmd = `${uploadScript} ${process.env.MAAS_ADMIN_USERNAME} "${workflow.maas.name}" "${workflow.maas.title}" "${workflow.maas.architecture}" "${workflow.maas.base_image}" "${workflow.maas.filetype}" "${tarballPath}"`;
|
|
440
428
|
|
|
441
429
|
logger.info(`Uploading to MAAS using: ${uploadScript}`);
|
|
442
430
|
const uploadResult = shellExec(uploadCmd);
|
|
@@ -679,7 +667,11 @@ rm -rf ${artifacts.join(' ')}`);
|
|
|
679
667
|
bootstrapArch,
|
|
680
668
|
callbackMetaData,
|
|
681
669
|
steps: [
|
|
682
|
-
`dnf install -y --allowerasing ${allPackages.join(
|
|
670
|
+
`dnf install -y --allowerasing ${allPackages.join(
|
|
671
|
+
' ',
|
|
672
|
+
)} 2>/dev/null || yum install -y --allowerasing ${allPackages.join(
|
|
673
|
+
' ',
|
|
674
|
+
)} 2>/dev/null || echo "Package install completed"`,
|
|
683
675
|
`dnf clean all`,
|
|
684
676
|
`echo "=== Installed packages verification ==="`,
|
|
685
677
|
`rpm -qa | grep -E "dracut|kernel|nfs" | sort`,
|
|
@@ -735,12 +727,7 @@ rm -rf ${artifacts.join(' ')}`);
|
|
|
735
727
|
|
|
736
728
|
// Fetch boot resources and machines if commissioning or listing.
|
|
737
729
|
|
|
738
|
-
let resources =
|
|
739
|
-
shellExec(`maas ${process.env.MAAS_ADMIN_USERNAME} boot-resources read`, {
|
|
740
|
-
silent: true,
|
|
741
|
-
stdout: true,
|
|
742
|
-
}),
|
|
743
|
-
).map((o) => ({
|
|
730
|
+
let resources = Underpost.baremetal.maasCliExec(`boot-resources read`).map((o) => ({
|
|
744
731
|
id: o.id,
|
|
745
732
|
name: o.name,
|
|
746
733
|
architecture: o.architecture,
|
|
@@ -748,12 +735,7 @@ rm -rf ${artifacts.join(' ')}`);
|
|
|
748
735
|
if (options.ls === true) {
|
|
749
736
|
console.table(resources);
|
|
750
737
|
}
|
|
751
|
-
let machines =
|
|
752
|
-
shellExec(`maas ${process.env.MAAS_ADMIN_USERNAME} machines read`, {
|
|
753
|
-
stdout: true,
|
|
754
|
-
silent: true,
|
|
755
|
-
}),
|
|
756
|
-
).map((m) => ({
|
|
738
|
+
let machines = Underpost.baremetal.maasCliExec(`machines read`).map((m) => ({
|
|
757
739
|
system_id: m.interface_set[0].system_id,
|
|
758
740
|
mac_address: m.interface_set[0].mac_address,
|
|
759
741
|
hostname: m.hostname,
|
|
@@ -819,7 +801,6 @@ rm -rf ${artifacts.join(' ')}`);
|
|
|
819
801
|
`chmod +x /underpost/shutdown.sh`,
|
|
820
802
|
`chmod +x /underpost/device_scan.sh`,
|
|
821
803
|
`chmod +x /underpost/mac.sh`,
|
|
822
|
-
`chmod +x /underpost/enlistment.sh`,
|
|
823
804
|
`sudo chmod 700 ~/.ssh/`, // Set secure permissions for .ssh directory.
|
|
824
805
|
`sudo chmod 600 ~/.ssh/authorized_keys`, // Set secure permissions for authorized_keys.
|
|
825
806
|
`sudo chmod 644 ~/.ssh/known_hosts`, // Set permissions for known_hosts.
|
|
@@ -875,11 +856,31 @@ rm -rf ${artifacts.join(' ')}`);
|
|
|
875
856
|
});
|
|
876
857
|
}
|
|
877
858
|
|
|
859
|
+
// Generate MAAS authentication credentials
|
|
860
|
+
const authCredentials =
|
|
861
|
+
options.commission || options.cloudInit || options.cloudInitUpdate
|
|
862
|
+
? Underpost.baremetal.maasAuthCredentialsFactory()
|
|
863
|
+
: { consumer_key: '', consumer_secret: '', token_key: '', token_secret: '' };
|
|
864
|
+
|
|
865
|
+
// Generate cloud-init configuration if needed for commissioning or cloud-init update workflows.
|
|
866
|
+
let cloudConfigSrc = '';
|
|
878
867
|
if (options.cloudInit || options.cloudInitUpdate) {
|
|
879
868
|
const { chronyc, networkInterfaceName } = workflowsConfig[workflowId];
|
|
880
869
|
const { timezone, chronyConfPath } = chronyc;
|
|
881
|
-
|
|
882
|
-
|
|
870
|
+
|
|
871
|
+
let write_files = [];
|
|
872
|
+
let runcmd = options.runcmd;
|
|
873
|
+
|
|
874
|
+
if (machine && options.commission) {
|
|
875
|
+
write_files = Underpost.baremetal.commissioningWriteFilesFactory({
|
|
876
|
+
machine,
|
|
877
|
+
authCredentials,
|
|
878
|
+
runnerHostIp: callbackMetaData.runnerHost.ip,
|
|
879
|
+
});
|
|
880
|
+
runcmd = '/usr/local/bin/underpost-enlist.sh';
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
cloudConfigSrc = Underpost.cloudInit.configFactory(
|
|
883
884
|
{
|
|
884
885
|
controlServerIp: callbackMetaData.runnerHost.ip,
|
|
885
886
|
hostname,
|
|
@@ -891,15 +892,48 @@ rm -rf ${artifacts.join(' ')}`);
|
|
|
891
892
|
networkInterfaceName,
|
|
892
893
|
ubuntuToolsBuild: options.ubuntuToolsBuild,
|
|
893
894
|
bootcmd: options.bootcmd,
|
|
894
|
-
runcmd
|
|
895
|
+
runcmd,
|
|
896
|
+
write_files,
|
|
895
897
|
},
|
|
896
898
|
authCredentials,
|
|
897
|
-
);
|
|
899
|
+
).cloudConfigSrc;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
// Rocky/RHEL Kickstart generation
|
|
903
|
+
let kickstartSrc = '';
|
|
904
|
+
if (Underpost.baremetal.getFamilyBaseOs(workflowsConfig[workflowId].osIdLike).isRhelBased) {
|
|
905
|
+
kickstartSrc = Underpost.kickstart.kickstartFactory({
|
|
906
|
+
lang: 'en_US.UTF-8',
|
|
907
|
+
keyboard: workflowsConfig[workflowId].keyboard?.layout,
|
|
908
|
+
timezone: workflowsConfig[workflowId].chronyc?.timezone,
|
|
909
|
+
rootPassword: process.env.MAAS_ADMIN_PASS,
|
|
910
|
+
authorizedKeys: fs.readFileSync('/home/dd/engine/engine-private/deploy/id_rsa.pub', 'utf8').trim(),
|
|
911
|
+
});
|
|
912
|
+
}
|
|
898
913
|
|
|
914
|
+
// Build and optionally run the HTTP bootstrap server to serve cloud-init, kickstart, and ISO resources for commissioning and provisioning.
|
|
915
|
+
if (cloudConfigSrc || kickstartSrc || workflowsConfig[workflowId].isoUrl)
|
|
899
916
|
Underpost.baremetal.httpBootstrapServerStaticFactory({
|
|
900
917
|
bootstrapHttpServerPath,
|
|
901
918
|
hostname,
|
|
902
919
|
cloudConfigSrc,
|
|
920
|
+
kickstartSrc,
|
|
921
|
+
isoUrl: workflowsConfig[workflowId].isoUrl,
|
|
922
|
+
});
|
|
923
|
+
|
|
924
|
+
// Set up iptables rules for NAT and port forwarding to enable network connectivity for the baremetal machines.
|
|
925
|
+
shellExec(`${underpostRoot}/scripts/nat-iptables.sh`, { silent: true });
|
|
926
|
+
|
|
927
|
+
// Start HTTP bootstrap server if commissioning or if ISO URL is used (for ISO-based workflows).
|
|
928
|
+
if (options.bootstrapHttpServerRun || options.commission) {
|
|
929
|
+
Underpost.baremetal.httpBootstrapServerRunnerFactory({
|
|
930
|
+
hostname,
|
|
931
|
+
bootstrapHttpServerPath,
|
|
932
|
+
bootstrapHttpServerPort: Underpost.baremetal.bootstrapHttpServerPortFactory({
|
|
933
|
+
port: options.bootstrapHttpServerPort,
|
|
934
|
+
workflowId,
|
|
935
|
+
workflowsConfig,
|
|
936
|
+
}),
|
|
903
937
|
});
|
|
904
938
|
}
|
|
905
939
|
|
|
@@ -909,12 +943,12 @@ rm -rf ${artifacts.join(' ')}`);
|
|
|
909
943
|
(workflowsConfig[workflowId].type === 'iso-nfs' ||
|
|
910
944
|
workflowsConfig[workflowId].type === 'chroot-debootstrap' ||
|
|
911
945
|
workflowsConfig[workflowId].type === 'chroot-container')
|
|
912
|
-
)
|
|
913
|
-
shellExec(`${underpostRoot}/scripts/nat-iptables.sh`, { silent: true });
|
|
946
|
+
)
|
|
914
947
|
Underpost.baremetal.rebuildNfsServer({
|
|
915
948
|
nfsHostPath,
|
|
949
|
+
nfsReset: options.nfsReset,
|
|
916
950
|
});
|
|
917
|
-
|
|
951
|
+
|
|
918
952
|
// Handle commissioning tasks
|
|
919
953
|
if (options.commission === true) {
|
|
920
954
|
let { firmwares, networkInterfaceName, maas, menuentryStr, type } = workflowsConfig[workflowId];
|
|
@@ -929,9 +963,11 @@ rm -rf ${artifacts.join(' ')}`);
|
|
|
929
963
|
);
|
|
930
964
|
logger.info('Commissioning resource', resource);
|
|
931
965
|
|
|
932
|
-
if (
|
|
966
|
+
if (
|
|
967
|
+
Underpost.baremetal.getFamilyBaseOs(workflowsConfig[workflowId].osIdLike).isDebianBased &&
|
|
968
|
+
(type === 'iso-nfs' || type === 'chroot-debootstrap' || type === 'chroot-container')
|
|
969
|
+
) {
|
|
933
970
|
// Prepare NFS casper path if using NFS boot.
|
|
934
|
-
shellExec(`sudo rm -rf ${nfsHostPath}`);
|
|
935
971
|
shellExec(`mkdir -p ${nfsHostPath}/casper`);
|
|
936
972
|
}
|
|
937
973
|
|
|
@@ -1004,15 +1040,26 @@ rm -rf ${artifacts.join(' ')}`);
|
|
|
1004
1040
|
hostname,
|
|
1005
1041
|
dnsServer,
|
|
1006
1042
|
networkInterfaceName,
|
|
1007
|
-
fileSystemUrl:
|
|
1008
|
-
|
|
1009
|
-
|
|
1043
|
+
fileSystemUrl:
|
|
1044
|
+
type === 'iso-ram'
|
|
1045
|
+
? `http://${callbackMetaData.runnerHost.ip}:${Underpost.baremetal.bootstrapHttpServerPortFactory({
|
|
1046
|
+
port: options.bootstrapHttpServerPort,
|
|
1047
|
+
workflowId,
|
|
1048
|
+
workflowsConfig,
|
|
1049
|
+
})}/${hostname}/${kernelFilesPaths.isoUrl.split('/').pop()}`
|
|
1050
|
+
: kernelFilesPaths.isoUrl,
|
|
1051
|
+
bootstrapHttpServerPort: Underpost.baremetal.bootstrapHttpServerPortFactory({
|
|
1052
|
+
port: options.bootstrapHttpServerPort,
|
|
1053
|
+
workflowId,
|
|
1054
|
+
workflowsConfig,
|
|
1055
|
+
}),
|
|
1010
1056
|
type,
|
|
1011
1057
|
macAddress,
|
|
1012
1058
|
cloudInit: options.cloudInit,
|
|
1013
|
-
machine,
|
|
1014
1059
|
dev: options.dev,
|
|
1015
1060
|
osIdLike: workflowsConfig[workflowId].osIdLike || '',
|
|
1061
|
+
authCredentials,
|
|
1062
|
+
architecture: workflowsConfig[workflowId].architecture,
|
|
1016
1063
|
});
|
|
1017
1064
|
|
|
1018
1065
|
// Check if iPXE mode is enabled AND the iPXE EFI binary exists
|
|
@@ -1083,13 +1130,6 @@ rm -rf ${artifacts.join(' ')}`);
|
|
|
1083
1130
|
shellExec(`sudo chown -R $(whoami):$(whoami) ${process.env.TFTP_ROOT}`);
|
|
1084
1131
|
shellExec(`sudo sudo chmod 755 ${process.env.TFTP_ROOT}`);
|
|
1085
1132
|
|
|
1086
|
-
Underpost.baremetal.httpBootstrapServerRunnerFactory({
|
|
1087
|
-
hostname,
|
|
1088
|
-
bootstrapHttpServerPath,
|
|
1089
|
-
bootstrapHttpServerPort:
|
|
1090
|
-
options.bootstrapHttpServerPort || workflowsConfig[workflowId].bootstrapHttpServerPort,
|
|
1091
|
-
});
|
|
1092
|
-
|
|
1093
1133
|
if (type === 'chroot-debootstrap' || type === 'chroot-container')
|
|
1094
1134
|
await Underpost.baremetal.nfsMountCallback({
|
|
1095
1135
|
hostname,
|
|
@@ -1102,11 +1142,7 @@ rm -rf ${artifacts.join(' ')}`);
|
|
|
1102
1142
|
macAddress,
|
|
1103
1143
|
ipAddress,
|
|
1104
1144
|
hostname,
|
|
1105
|
-
architecture:
|
|
1106
|
-
workflowsConfig[workflowId].maas?.commissioning?.architecture ||
|
|
1107
|
-
workflowsConfig[workflowId].container?.architecture ||
|
|
1108
|
-
workflowsConfig[workflowId].debootstrap?.image?.architecture ||
|
|
1109
|
-
'arm64/generic',
|
|
1145
|
+
architecture: Underpost.baremetal.fallbackArchitecture(workflowsConfig[workflowId]),
|
|
1110
1146
|
machine,
|
|
1111
1147
|
};
|
|
1112
1148
|
logger.info('Waiting for commissioning...', {
|
|
@@ -1114,20 +1150,177 @@ rm -rf ${artifacts.join(' ')}`);
|
|
|
1114
1150
|
machine: machine ? machine.system_id : null,
|
|
1115
1151
|
});
|
|
1116
1152
|
|
|
1117
|
-
const { discovery } =
|
|
1153
|
+
const { discovery, machine: discoveredMachine } =
|
|
1154
|
+
await Underpost.baremetal.commissionMonitor(commissionMonitorPayload);
|
|
1155
|
+
if (discoveredMachine) machine = discoveredMachine;
|
|
1156
|
+
}
|
|
1157
|
+
},
|
|
1118
1158
|
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1159
|
+
/**
|
|
1160
|
+
* @method installPacker
|
|
1161
|
+
* @description Installs Packer CLI.
|
|
1162
|
+
* @memberof UnderpostBaremetal
|
|
1163
|
+
* @returns {Promise<void>}
|
|
1164
|
+
*/
|
|
1165
|
+
async installPacker(underpostRoot) {
|
|
1166
|
+
const scriptPath = `${underpostRoot}/scripts/packer-setup.sh`;
|
|
1167
|
+
logger.info(`Installing Packer using script: ${scriptPath}`);
|
|
1168
|
+
shellExec(`sudo chmod +x ${scriptPath}`);
|
|
1169
|
+
shellExec(`sudo ${scriptPath}`);
|
|
1170
|
+
},
|
|
1171
|
+
|
|
1172
|
+
/**
|
|
1173
|
+
* @method ipxeBuildIso
|
|
1174
|
+
* @description Builds a UEFI-bootable iPXE ISO with an embedded bridge script.
|
|
1175
|
+
* @param {object} params
|
|
1176
|
+
* @param {string} params.workflowId - The workflow identifier (e.g., 'hp-envy-iso-ram').
|
|
1177
|
+
* @param {string} params.isoOutputPath - Output path for the generated ISO file.
|
|
1178
|
+
* @param {string} params.tftpPrefix - TFTP prefix directory (e.g., 'envy').
|
|
1179
|
+
* @param {string} params.ipFileServer - IP address of the TFTP/file server to chain to.
|
|
1180
|
+
* @param {string} [params.ipAddress='192.168.1.191'] - The IP address of the client machine.
|
|
1181
|
+
* @param {string} [params.ipConfig='none'] - IP configuration method (e.g., 'dhcp', 'none').
|
|
1182
|
+
* @param {string} [params.netmask='255.255.255.0'] - The network mask.
|
|
1183
|
+
* @param {string} [params.dnsServer='8.8.8.8'] - The DNS server address.
|
|
1184
|
+
* @param {string} [params.macAddress=''] - The MAC address of the client machine.
|
|
1185
|
+
* @param {boolean} [params.cloudInit=false] - Flag to enable cloud-init.
|
|
1186
|
+
* @param {boolean} [params.dev=false] - Development mode flag to determine paths.
|
|
1187
|
+
* @param {boolean} [params.forceRebuild=false] - Force a complete iPXE rebuild. Without this, reuses existing ISO.
|
|
1188
|
+
* @param {number} [params.bootstrapHttpServerPort=8888] - Port for the bootstrap HTTP server used in ISO RAM workflows.
|
|
1189
|
+
* @memberof UnderpostBaremetal
|
|
1190
|
+
* @returns {Promise<void>}
|
|
1191
|
+
*/
|
|
1192
|
+
async ipxeBuildIso({
|
|
1193
|
+
workflowId,
|
|
1194
|
+
isoOutputPath,
|
|
1195
|
+
tftpPrefix,
|
|
1196
|
+
ipFileServer,
|
|
1197
|
+
ipAddress,
|
|
1198
|
+
ipConfig,
|
|
1199
|
+
netmask,
|
|
1200
|
+
dnsServer,
|
|
1201
|
+
macAddress,
|
|
1202
|
+
cloudInit,
|
|
1203
|
+
dev,
|
|
1204
|
+
forceRebuild = false,
|
|
1205
|
+
bootstrapHttpServerPort,
|
|
1206
|
+
}) {
|
|
1207
|
+
const outputPath = !isoOutputPath || isoOutputPath === '.' ? `./ipxe-${workflowId}.iso` : isoOutputPath;
|
|
1208
|
+
shellExec(`mkdir -p $(dirname ${outputPath})`);
|
|
1209
|
+
|
|
1210
|
+
const workflowsConfig = Underpost.baremetal.loadWorkflowsConfig();
|
|
1211
|
+
if (!workflowsConfig[workflowId]) {
|
|
1212
|
+
throw new Error(`Workflow configuration not found for ID: ${workflowId}`);
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
const authCredentials = cloudInit
|
|
1216
|
+
? Underpost.baremetal.maasAuthCredentialsFactory()
|
|
1217
|
+
: { consumer_key: '', consumer_secret: '', token_key: '', token_secret: '' };
|
|
1218
|
+
|
|
1219
|
+
const { cmd } = Underpost.baremetal.kernelCmdBootParamsFactory({
|
|
1220
|
+
ipClient: ipAddress,
|
|
1221
|
+
ipDhcpServer: ipFileServer,
|
|
1222
|
+
ipFileServer,
|
|
1223
|
+
ipConfig,
|
|
1224
|
+
netmask,
|
|
1225
|
+
hostname: workflowId,
|
|
1226
|
+
dnsServer,
|
|
1227
|
+
fileSystemUrl:
|
|
1228
|
+
dev && workflowsConfig[workflowId].type === 'iso-ram'
|
|
1229
|
+
? `http://${ipFileServer}:${Underpost.baremetal.bootstrapHttpServerPortFactory({
|
|
1230
|
+
port: bootstrapHttpServerPort,
|
|
1231
|
+
workflowId,
|
|
1232
|
+
workflowsConfig,
|
|
1233
|
+
})}/${workflowId}/${workflowsConfig[workflowId].isoUrl.split('/').pop()}`
|
|
1234
|
+
: workflowsConfig[workflowId].isoUrl,
|
|
1235
|
+
type: workflowsConfig[workflowId].type,
|
|
1236
|
+
architecture: workflowsConfig[workflowId].architecture,
|
|
1237
|
+
macAddress,
|
|
1238
|
+
cloudInit,
|
|
1239
|
+
osIdLike: workflowsConfig[workflowId].osIdLike,
|
|
1240
|
+
networkInterfaceName: workflowsConfig[workflowId].networkInterfaceName,
|
|
1241
|
+
authCredentials,
|
|
1242
|
+
bootstrapHttpServerPort: Underpost.baremetal.bootstrapHttpServerPortFactory({
|
|
1243
|
+
port: bootstrapHttpServerPort,
|
|
1244
|
+
workflowId,
|
|
1245
|
+
workflowsConfig,
|
|
1246
|
+
}),
|
|
1247
|
+
dev,
|
|
1248
|
+
});
|
|
1249
|
+
|
|
1250
|
+
const ipxeSrcDir = '/home/dd/ipxe/src';
|
|
1251
|
+
const embedScriptName = `embed_${workflowId}.ipxe`;
|
|
1252
|
+
const embedScriptPath = path.join(ipxeSrcDir, embedScriptName);
|
|
1253
|
+
|
|
1254
|
+
const embedScriptContent = Underpost.baremetal.ipxeScriptFactory({
|
|
1255
|
+
maasIp: ipFileServer,
|
|
1256
|
+
tftpPrefix,
|
|
1257
|
+
kernelCmd: cmd,
|
|
1258
|
+
minimal: true,
|
|
1259
|
+
});
|
|
1260
|
+
|
|
1261
|
+
fs.writeFileSync(embedScriptPath, embedScriptContent);
|
|
1262
|
+
logger.info(`Created embedded script at ${embedScriptPath}`);
|
|
1263
|
+
|
|
1264
|
+
// Determine target architecture
|
|
1265
|
+
let targetArch = 'x86_64';
|
|
1266
|
+
if (
|
|
1267
|
+
workflowsConfig[workflowId].architecture === 'arm64' ||
|
|
1268
|
+
workflowsConfig[workflowId].architecture === 'aarch64'
|
|
1269
|
+
) {
|
|
1270
|
+
targetArch = 'arm64';
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
const platformDir = targetArch === 'arm64' ? 'bin-arm64-efi' : 'bin-x86_64-efi';
|
|
1274
|
+
const makeTarget = `${platformDir}/ipxe.iso`;
|
|
1275
|
+
const builtIsoPath = path.join(ipxeSrcDir, makeTarget);
|
|
1276
|
+
|
|
1277
|
+
if (!forceRebuild && fs.existsSync(builtIsoPath)) {
|
|
1278
|
+
fs.copySync(builtIsoPath, outputPath);
|
|
1279
|
+
logger.info(`Reusing existing iPXE ISO: ${builtIsoPath} -> ${outputPath}`);
|
|
1280
|
+
} else {
|
|
1281
|
+
logger.info(`Rebuild: cleaning iPXE build artifacts...`);
|
|
1282
|
+
shellExec(`cd ${ipxeSrcDir} && make clean`);
|
|
1283
|
+
|
|
1284
|
+
const hostArch = process.arch === 'arm64' ? 'arm64' : 'x86_64';
|
|
1285
|
+
let crossCompile = '';
|
|
1286
|
+
if (hostArch === 'x86_64' && targetArch === 'arm64') {
|
|
1287
|
+
crossCompile = 'CROSS_COMPILE=aarch64-linux-gnu-';
|
|
1288
|
+
} else if (hostArch === 'arm64' && targetArch === 'x86_64') {
|
|
1289
|
+
crossCompile = 'CROSS_COMPILE=x86_64-linux-gnu-';
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
logger.info(
|
|
1293
|
+
`Building iPXE ISO for ${targetArch} on ${hostArch}: make ${makeTarget} ${crossCompile} EMBED=${embedScriptName}`,
|
|
1294
|
+
);
|
|
1295
|
+
|
|
1296
|
+
const buildCmd = `cd ${ipxeSrcDir} && make ${makeTarget} ${crossCompile} EMBED=${embedScriptName}`;
|
|
1297
|
+
shellExec(buildCmd);
|
|
1298
|
+
|
|
1299
|
+
if (fs.existsSync(builtIsoPath)) {
|
|
1300
|
+
fs.copySync(builtIsoPath, outputPath);
|
|
1301
|
+
logger.info(`ISO successfully built and copied to ${outputPath}`);
|
|
1302
|
+
} else {
|
|
1303
|
+
logger.error(`Failed to build ISO at ${builtIsoPath}`);
|
|
1127
1304
|
}
|
|
1128
1305
|
}
|
|
1129
1306
|
},
|
|
1130
1307
|
|
|
1308
|
+
/**
|
|
1309
|
+
* @method fallbackArchitecture
|
|
1310
|
+
* @description Determines the architecture to use for boot resources, with a fallback mechanism.
|
|
1311
|
+
* @param {object} workflowsConfig - The configuration object for the current workflow.
|
|
1312
|
+
* @returns {string} The architecture string (e.g., 'arm64', 'amd64') to use for boot resources.
|
|
1313
|
+
* @memberof UnderpostBaremetal
|
|
1314
|
+
*/
|
|
1315
|
+
fallbackArchitecture(workflowsConfig) {
|
|
1316
|
+
return (
|
|
1317
|
+
workflowsConfig.architecture ||
|
|
1318
|
+
workflowsConfig.maas?.commissioning?.architecture ||
|
|
1319
|
+
workflowsConfig.container?.architecture ||
|
|
1320
|
+
workflowsConfig.debootstrap?.image?.architecture
|
|
1321
|
+
);
|
|
1322
|
+
},
|
|
1323
|
+
|
|
1131
1324
|
/**
|
|
1132
1325
|
* @method macAddressFactory
|
|
1133
1326
|
* @description Generates or returns a MAC address based on options.
|
|
@@ -1179,8 +1372,7 @@ rm -rf ${artifacts.join(' ')}`);
|
|
|
1179
1372
|
const isoFilename = isoUrl.split('/').pop();
|
|
1180
1373
|
|
|
1181
1374
|
// Determine OS family from osIdLike
|
|
1182
|
-
const isDebianBased
|
|
1183
|
-
const isRhelBased = osIdLike && osIdLike.match(/rhel|centos|fedora|alma|rocky/i);
|
|
1375
|
+
const { isDebianBased, isRhelBased } = Underpost.baremetal.getFamilyBaseOs(osIdLike);
|
|
1184
1376
|
|
|
1185
1377
|
// Set extraction directory based on OS family
|
|
1186
1378
|
const extractDirName = isDebianBased ? 'casper' : 'iso-extract';
|
|
@@ -1206,16 +1398,16 @@ rm -rf ${artifacts.join(' ')}`);
|
|
|
1206
1398
|
logger.info(`Downloaded ISO to ${isoPath} (${(stats.size / 1024 / 1024 / 1024).toFixed(2)} GB)`);
|
|
1207
1399
|
}
|
|
1208
1400
|
|
|
1209
|
-
//
|
|
1401
|
+
// ISO Logic
|
|
1210
1402
|
const mountPoint = `${nfsHostPath}/mnt-iso-${arch}`;
|
|
1211
1403
|
shellExec(`mkdir -p ${mountPoint}`);
|
|
1212
1404
|
|
|
1213
1405
|
// Ensure mount point is not already mounted
|
|
1214
|
-
shellExec(`sudo umount ${mountPoint} 2>/dev/null
|
|
1406
|
+
shellExec(`sudo umount ${mountPoint} 2>/dev/null`);
|
|
1215
1407
|
|
|
1216
1408
|
try {
|
|
1217
1409
|
// Mount the ISO
|
|
1218
|
-
shellExec(`sudo mount -o loop,ro ${isoPath} ${mountPoint}
|
|
1410
|
+
shellExec(`sudo mount -o loop,ro ${isoPath} ${mountPoint}`);
|
|
1219
1411
|
logger.info(`Mounted ISO at ${mountPoint}`);
|
|
1220
1412
|
|
|
1221
1413
|
// Distribution-specific extraction logic
|
|
@@ -1227,16 +1419,25 @@ rm -rf ${artifacts.join(' ')}`);
|
|
|
1227
1419
|
logger.info(`Checking casper directory contents...`);
|
|
1228
1420
|
shellExec(`ls -la ${mountPoint}/casper/ 2>/dev/null || echo "casper directory not found"`);
|
|
1229
1421
|
shellExec(`sudo cp -a ${mountPoint}/casper/* ${extractDir}/`);
|
|
1422
|
+
} else if (isRhelBased) {
|
|
1423
|
+
// RHEL/Rocky: Extract from images/pxeboot directory
|
|
1424
|
+
const pxebootDir = `${mountPoint}/images/pxeboot`;
|
|
1425
|
+
if (!fs.existsSync(pxebootDir)) {
|
|
1426
|
+
throw new Error(`Failed to mount ISO or images/pxeboot directory not found: ${isoPath}`);
|
|
1427
|
+
}
|
|
1428
|
+
logger.info(`Extracting kernel and initrd from ${pxebootDir}...`);
|
|
1429
|
+
shellExec(`sudo cp -a ${pxebootDir}/vmlinuz ${extractDir}/vmlinuz`);
|
|
1430
|
+
shellExec(`sudo cp -a ${pxebootDir}/initrd.img ${extractDir}/initrd`);
|
|
1230
1431
|
}
|
|
1231
1432
|
} finally {
|
|
1232
1433
|
shellExec(`ls -la ${mountPoint}/`);
|
|
1233
1434
|
|
|
1234
1435
|
shellExec(`sudo chown -R $(whoami):$(whoami) ${extractDir}`);
|
|
1235
1436
|
// Unmount ISO
|
|
1236
|
-
shellExec(`sudo umount ${mountPoint}
|
|
1437
|
+
shellExec(`sudo umount ${mountPoint}`);
|
|
1237
1438
|
logger.info(`Unmounted ISO`);
|
|
1238
1439
|
// Clean up temporary mount point
|
|
1239
|
-
shellExec(`rmdir ${mountPoint}
|
|
1440
|
+
shellExec(`rmdir ${mountPoint}`);
|
|
1240
1441
|
}
|
|
1241
1442
|
|
|
1242
1443
|
return {
|
|
@@ -1253,8 +1454,8 @@ rm -rf ${artifacts.join(' ')}`);
|
|
|
1253
1454
|
* @param {string} options.macAddress - The MAC address of the machine.
|
|
1254
1455
|
* @param {string} options.hostname - The hostname for the machine.
|
|
1255
1456
|
* @param {string} options.ipAddress - The IP address for the machine.
|
|
1457
|
+
* @param {string} options.architecture - The architecture for the machine (default is 'arm64').
|
|
1256
1458
|
* @param {string} options.powerType - The power type for the machine (default is 'manual').
|
|
1257
|
-
* @param {object} options.maas - Additional MAAS-specific options.
|
|
1258
1459
|
* @returns {object} An object containing the created machine details.
|
|
1259
1460
|
* @memberof UnderpostBaremetal
|
|
1260
1461
|
*/
|
|
@@ -1263,13 +1464,13 @@ rm -rf ${artifacts.join(' ')}`);
|
|
|
1263
1464
|
macAddress: '',
|
|
1264
1465
|
hostname: '',
|
|
1265
1466
|
ipAddress: '',
|
|
1467
|
+
architecture: 'arm64',
|
|
1266
1468
|
powerType: 'manual',
|
|
1267
|
-
architecture: 'arm64/generic',
|
|
1268
1469
|
},
|
|
1269
1470
|
) {
|
|
1270
1471
|
if (!options.powerType) options.powerType = 'manual';
|
|
1271
1472
|
const payload = {
|
|
1272
|
-
architecture:
|
|
1473
|
+
architecture: options.architecture.match('arm') ? 'arm64/generic' : 'amd64/generic',
|
|
1273
1474
|
mac_address: options.macAddress,
|
|
1274
1475
|
mac_addresses: options.macAddress,
|
|
1275
1476
|
hostname: options.hostname,
|
|
@@ -1277,18 +1478,13 @@ rm -rf ${artifacts.join(' ')}`);
|
|
|
1277
1478
|
ip: options.ipAddress,
|
|
1278
1479
|
};
|
|
1279
1480
|
logger.info('Creating MAAS machine', payload);
|
|
1280
|
-
const machine =
|
|
1281
|
-
`
|
|
1481
|
+
const machine = Underpost.baremetal.maasCliExec(
|
|
1482
|
+
`machines create ${Object.keys(payload)
|
|
1282
1483
|
.map((k) => `${k}="${payload[k]}"`)
|
|
1283
1484
|
.join(' ')}`,
|
|
1284
|
-
{
|
|
1285
|
-
silent: true,
|
|
1286
|
-
stdout: true,
|
|
1287
|
-
},
|
|
1288
1485
|
);
|
|
1289
|
-
// console.log(machine);
|
|
1290
1486
|
try {
|
|
1291
|
-
return { machine
|
|
1487
|
+
return { machine };
|
|
1292
1488
|
} catch (error) {
|
|
1293
1489
|
console.log(error);
|
|
1294
1490
|
logger.error(error);
|
|
@@ -1296,6 +1492,20 @@ rm -rf ${artifacts.join(' ')}`);
|
|
|
1296
1492
|
}
|
|
1297
1493
|
},
|
|
1298
1494
|
|
|
1495
|
+
/**
|
|
1496
|
+
* @method getFamilyBaseOs
|
|
1497
|
+
* @description Determines if the OS belongs to Debian-based or RHEL-based family based on osIdLike string.
|
|
1498
|
+
* @param {string} osIdLike - The os_id_like string from MAAS boot resource or workflow configuration.
|
|
1499
|
+
* @returns {object} An object with boolean properties isDebianBased and isRhelBased indicating the OS family.
|
|
1500
|
+
* @memberof UnderpostBaremetal
|
|
1501
|
+
*/
|
|
1502
|
+
getFamilyBaseOs(osIdLike = '') {
|
|
1503
|
+
return {
|
|
1504
|
+
isDebianBased: osIdLike.match(/debian|ubuntu/i),
|
|
1505
|
+
isRhelBased: osIdLike.match(/rhel|centos|fedora|alma|rocky/i),
|
|
1506
|
+
};
|
|
1507
|
+
},
|
|
1508
|
+
|
|
1299
1509
|
/**
|
|
1300
1510
|
* @method kernelFactory
|
|
1301
1511
|
* @description Retrieves kernel, initrd, and root filesystem paths from a MAAS boot resource.
|
|
@@ -1312,8 +1522,10 @@ rm -rf ${artifacts.join(' ')}`);
|
|
|
1312
1522
|
// For disk-based commissioning (casper/iso), use live ISO files
|
|
1313
1523
|
if (type === 'iso-ram' || type === 'iso-nfs') {
|
|
1314
1524
|
logger.info('Using live ISO for boot (disk-based commissioning)');
|
|
1315
|
-
const arch = resource.architecture.split('/')[0];
|
|
1316
1525
|
const workflowsConfig = Underpost.baremetal.loadWorkflowsConfig();
|
|
1526
|
+
const arch = resource?.architecture
|
|
1527
|
+
? resource.architecture.split('/')[0]
|
|
1528
|
+
: workflowsConfig[workflowId].architecture;
|
|
1317
1529
|
const kernelFilesPaths = Underpost.baremetal.downloadISO({
|
|
1318
1530
|
resource,
|
|
1319
1531
|
architecture: arch,
|
|
@@ -1325,13 +1537,7 @@ rm -rf ${artifacts.join(' ')}`);
|
|
|
1325
1537
|
return { kernelFilesPaths, resourcesPath };
|
|
1326
1538
|
}
|
|
1327
1539
|
|
|
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
|
-
);
|
|
1540
|
+
const resourceData = Underpost.baremetal.maasCliExec(`boot-resource read ${resource.id}`);
|
|
1335
1541
|
let kernelFilesPaths = {};
|
|
1336
1542
|
const bootFiles = resourceData.sets[Object.keys(resourceData.sets)[0]].files;
|
|
1337
1543
|
const arch = resource.architecture.split('/')[0];
|
|
@@ -1392,7 +1598,7 @@ rm -rf ${artifacts.join(' ')}`);
|
|
|
1392
1598
|
shellExec(`mkdir -p ${tempExtractDir}`);
|
|
1393
1599
|
|
|
1394
1600
|
// List files in archive to find kernel and initrd
|
|
1395
|
-
const tarList = shellExec(`tar -tf ${rootArchivePath}
|
|
1601
|
+
const tarList = shellExec(`tar -tf ${rootArchivePath}`).stdout.split('\n');
|
|
1396
1602
|
|
|
1397
1603
|
// Look for boot/vmlinuz* and boot/initrd* (handling potential leading ./)
|
|
1398
1604
|
// Skip rescue, kdump, and other special images
|
|
@@ -1524,7 +1730,7 @@ rm -rf ${artifacts.join(' ')}`);
|
|
|
1524
1730
|
*/
|
|
1525
1731
|
removeDiscoveredMachines() {
|
|
1526
1732
|
logger.info('Removing all discovered machines from MAAS...');
|
|
1527
|
-
|
|
1733
|
+
Underpost.baremetal.maasCliExec(`discoveries clear all=true`);
|
|
1528
1734
|
},
|
|
1529
1735
|
|
|
1530
1736
|
/**
|
|
@@ -1657,16 +1863,28 @@ shell
|
|
|
1657
1863
|
* @method ipxeScriptFactory
|
|
1658
1864
|
* @description Generates the iPXE script content for stable identity.
|
|
1659
1865
|
* This iPXE script uses directly boots kernel/initrd via TFTP.
|
|
1866
|
+
* When minimal mode is enabled, generates a simple dhcp+kernel+initrd+boot script.
|
|
1660
1867
|
* @param {object} params - The parameters for generating the script.
|
|
1661
|
-
* @param {string} params.maasIp - The IP address of the MAAS server.
|
|
1868
|
+
* @param {string} params.maasIp - The IP address of the MAAS/file server.
|
|
1662
1869
|
* @param {string} [params.macAddress] - The MAC address registered in MAAS (for display only).
|
|
1663
|
-
* @param {string} params.architecture - The architecture (arm64/amd64).
|
|
1870
|
+
* @param {string} [params.architecture] - The architecture (arm64/amd64).
|
|
1664
1871
|
* @param {string} params.tftpPrefix - The TFTP prefix path (e.g., 'rpi4mb').
|
|
1665
1872
|
* @param {string} params.kernelCmd - The kernel command line parameters.
|
|
1873
|
+
* @param {boolean} [params.minimal=false] - Generate a minimal embedded script for ISO builds.
|
|
1666
1874
|
* @returns {string} The iPXE script content.
|
|
1667
1875
|
* @memberof UnderpostBaremetal
|
|
1668
1876
|
*/
|
|
1669
|
-
ipxeScriptFactory({ maasIp, macAddress, architecture, tftpPrefix, kernelCmd }) {
|
|
1877
|
+
ipxeScriptFactory({ maasIp, macAddress, architecture, tftpPrefix, kernelCmd, minimal = false }) {
|
|
1878
|
+
if (minimal) {
|
|
1879
|
+
return `#!ipxe
|
|
1880
|
+
dhcp
|
|
1881
|
+
set server_ip ${maasIp}
|
|
1882
|
+
set tftp_prefix ${tftpPrefix}
|
|
1883
|
+
kernel tftp://\${server_ip}/\${tftp_prefix}/pxe/vmlinuz-efi ${kernelCmd}
|
|
1884
|
+
initrd tftp://\${server_ip}/\${tftp_prefix}/pxe/initrd.img
|
|
1885
|
+
boot || shell
|
|
1886
|
+
`;
|
|
1887
|
+
}
|
|
1670
1888
|
const macInfo =
|
|
1671
1889
|
macAddress && macAddress !== '00:00:00:00:00:00'
|
|
1672
1890
|
? `echo Registered MAC: ${macAddress}`
|
|
@@ -1704,7 +1922,11 @@ echo ========================================
|
|
|
1704
1922
|
echo Loading kernel and initrd via TFTP...
|
|
1705
1923
|
echo Kernel: tftp://${maasIp}/${kernelPath}
|
|
1706
1924
|
echo Initrd: tftp://${maasIp}/${initrdPath}
|
|
1707
|
-
${
|
|
1925
|
+
${
|
|
1926
|
+
macAddress && macAddress !== '00:00:00:00:00:00'
|
|
1927
|
+
? `echo Kernel will use MAC: ${macAddress} (via ifname parameter)`
|
|
1928
|
+
: 'echo Kernel will use hardware MAC'
|
|
1929
|
+
}
|
|
1708
1930
|
echo ========================================
|
|
1709
1931
|
|
|
1710
1932
|
# Load kernel via TFTP
|
|
@@ -1735,7 +1957,9 @@ echo ========================================
|
|
|
1735
1957
|
# Fallback: Try MAAS HTTP bootloader (may have certificate issues)
|
|
1736
1958
|
set boot-url http://${maasIp}:5248/images/bootloader
|
|
1737
1959
|
echo Boot URL: \${boot-url}
|
|
1738
|
-
chain \${boot-url}/uefi/${architecture}/${
|
|
1960
|
+
chain \${boot-url}/uefi/${architecture}/${
|
|
1961
|
+
grubBootloader === 'grubaa64.efi' ? 'bootaa64.efi' : 'bootx64.efi'
|
|
1962
|
+
} || goto error
|
|
1739
1963
|
|
|
1740
1964
|
:error
|
|
1741
1965
|
echo ========================================
|
|
@@ -1771,22 +1995,12 @@ shell
|
|
|
1771
1995
|
* @memberof UnderpostBaremetal
|
|
1772
1996
|
*/
|
|
1773
1997
|
ipxeEfiFactory({ tftpRootPath, ipxeCacheDir, arch, underpostRoot, embeddedScriptPath, forceRebuild = false }) {
|
|
1774
|
-
const
|
|
1775
|
-
|
|
1998
|
+
const embedArg =
|
|
1999
|
+
embeddedScriptPath && fs.existsSync(embeddedScriptPath) ? ` --embed-script ${embeddedScriptPath}` : '';
|
|
2000
|
+
const rebuildArg = forceRebuild ? ' --rebuild' : '';
|
|
1776
2001
|
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
if (embeddedScriptPath && fs.existsSync(embeddedScriptPath)) {
|
|
1780
|
-
logger.info('Rebuilding iPXE with embedded boot script...', {
|
|
1781
|
-
embeddedScriptPath,
|
|
1782
|
-
forced: forceRebuild,
|
|
1783
|
-
});
|
|
1784
|
-
shellExec(
|
|
1785
|
-
`${underpostRoot}/scripts/ipxe-setup.sh ${tftpRootPath} --target-arch ${arch} --embed-script ${embeddedScriptPath} --rebuild`,
|
|
1786
|
-
);
|
|
1787
|
-
} else if (shouldRebuild) {
|
|
1788
|
-
shellExec(`${underpostRoot}/scripts/ipxe-setup.sh ${tftpRootPath} --target-arch ${arch}`);
|
|
1789
|
-
}
|
|
2002
|
+
logger.info('Building iPXE EFI binary...', { embeddedScriptPath, forced: forceRebuild });
|
|
2003
|
+
shellExec(`${underpostRoot}/scripts/ipxe-setup.sh ${tftpRootPath} --target-arch ${arch}${embedArg}${rebuildArg}`);
|
|
1790
2004
|
|
|
1791
2005
|
shellExec(`mkdir -p ${ipxeCacheDir}`);
|
|
1792
2006
|
shellExec(`cp ${tftpRootPath}/ipxe.efi ${ipxeCacheDir}/ipxe.efi`);
|
|
@@ -1840,7 +2054,11 @@ shell
|
|
|
1840
2054
|
echo "${menuentryStr}"
|
|
1841
2055
|
echo " ${Underpost.version}"
|
|
1842
2056
|
echo "Date: ${new Date().toISOString()}"
|
|
1843
|
-
${
|
|
2057
|
+
${
|
|
2058
|
+
cmd.match('/MAAS/metadata/by-id/')
|
|
2059
|
+
? `echo "System ID: ${cmd.split('/MAAS/metadata/by-id/')[1].split('/')[0]}"`
|
|
2060
|
+
: ''
|
|
2061
|
+
}
|
|
1844
2062
|
echo "TFTP server: ${tftpIp}"
|
|
1845
2063
|
echo "Kernel path: ${kernelPath}"
|
|
1846
2064
|
echo "Initrd path: ${initrdPath}"
|
|
@@ -1856,6 +2074,72 @@ shell
|
|
|
1856
2074
|
};
|
|
1857
2075
|
},
|
|
1858
2076
|
|
|
2077
|
+
/**
|
|
2078
|
+
* @method bootstrapHttpServerPortFactory
|
|
2079
|
+
* @description Determines the bootstrap HTTP server port.
|
|
2080
|
+
* @param {object} params - Parameters for determining the port.
|
|
2081
|
+
* @param {number} [params.port] - The port passed via options.
|
|
2082
|
+
* @param {string} params.workflowId - The workflow identifier.
|
|
2083
|
+
* @param {object} params.workflowsConfig - The loaded workflows configuration.
|
|
2084
|
+
* @returns {number} The determined port number.
|
|
2085
|
+
* @memberof UnderpostBaremetal
|
|
2086
|
+
*/
|
|
2087
|
+
bootstrapHttpServerPortFactory({ port, workflowId, workflowsConfig }) {
|
|
2088
|
+
return port || workflowsConfig[workflowId]?.bootstrapHttpServerPort || 8888;
|
|
2089
|
+
},
|
|
2090
|
+
|
|
2091
|
+
/**
|
|
2092
|
+
* @method commissioningWriteFilesFactory
|
|
2093
|
+
* @description Generates the write_files configuration for the commissioning script.
|
|
2094
|
+
* @param {object} params
|
|
2095
|
+
* @param {object} params.machine - The machine object.
|
|
2096
|
+
* @param {object} params.authCredentials - MAAS authentication credentials.
|
|
2097
|
+
* @param {string} params.runnerHostIp - The IP address of the runner host.
|
|
2098
|
+
* @memberof UnderpostBaremetal
|
|
2099
|
+
* @returns {Array} The write_files array.
|
|
2100
|
+
*/
|
|
2101
|
+
commissioningWriteFilesFactory({ machine, authCredentials, runnerHostIp }) {
|
|
2102
|
+
const { consumer_key, token_key, token_secret } = authCredentials;
|
|
2103
|
+
return [
|
|
2104
|
+
{
|
|
2105
|
+
path: '/usr/local/bin/underpost-enlist.sh',
|
|
2106
|
+
permissions: '0755',
|
|
2107
|
+
owner: 'root:root',
|
|
2108
|
+
content: `#!/bin/bash
|
|
2109
|
+
# set -euo pipefail
|
|
2110
|
+
CONSUMER_KEY="${consumer_key}"
|
|
2111
|
+
TOKEN_KEY="${token_key}"
|
|
2112
|
+
TOKEN_SECRET="${token_secret}"
|
|
2113
|
+
LOG_FILE="/var/log/underpost-enlistment.log"
|
|
2114
|
+
RESPONSE_FILE="/tmp/maas_response.txt"
|
|
2115
|
+
STATUS_FILE="/tmp/maas_status.txt"
|
|
2116
|
+
|
|
2117
|
+
echo "Starting MAAS Commissioning Request..." | tee -a "$LOG_FILE"
|
|
2118
|
+
|
|
2119
|
+
curl -X POST \\
|
|
2120
|
+
--location --verbose \\
|
|
2121
|
+
--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)\\"" \\
|
|
2122
|
+
-F "enable_ssh=1" \\
|
|
2123
|
+
http://${runnerHostIp}:5240/MAAS/api/2.0/machines/${machine.system_id}/op-commission \\
|
|
2124
|
+
--output "$RESPONSE_FILE" --write-out "%{http_code}" > "$STATUS_FILE" 2>> "$LOG_FILE"
|
|
2125
|
+
|
|
2126
|
+
HTTP_STATUS=$(cat "$STATUS_FILE")
|
|
2127
|
+
|
|
2128
|
+
echo "HTTP Status: $HTTP_STATUS" | tee -a "$LOG_FILE"
|
|
2129
|
+
echo "Response Body:" | tee -a "$LOG_FILE"
|
|
2130
|
+
cat "$RESPONSE_FILE" | tee -a "$LOG_FILE"
|
|
2131
|
+
|
|
2132
|
+
if [ "$HTTP_STATUS" -eq 200 ]; then
|
|
2133
|
+
echo "Commissioning requested successfully." | tee -a "$LOG_FILE"
|
|
2134
|
+
else
|
|
2135
|
+
echo "ERROR: MAAS commissioning failed with status $HTTP_STATUS" | tee -a "$LOG_FILE"
|
|
2136
|
+
exit 0
|
|
2137
|
+
fi
|
|
2138
|
+
`,
|
|
2139
|
+
},
|
|
2140
|
+
];
|
|
2141
|
+
},
|
|
2142
|
+
|
|
1859
2143
|
/**
|
|
1860
2144
|
* @method httpBootstrapServerStaticFactory
|
|
1861
2145
|
* @description Creates static files for the bootstrap HTTP server including cloud-init configuration.
|
|
@@ -1863,36 +2147,40 @@ shell
|
|
|
1863
2147
|
* @param {string} params.bootstrapHttpServerPath - The path where static files will be created.
|
|
1864
2148
|
* @param {string} params.hostname - The hostname of the client machine.
|
|
1865
2149
|
* @param {string} params.cloudConfigSrc - The cloud-init configuration YAML source.
|
|
1866
|
-
* @param {
|
|
1867
|
-
* @param {string}
|
|
2150
|
+
* @param {string} params.kickstartSrc - The kickstart configuration content.
|
|
2151
|
+
* @param {string} params.vendorData - The cloud-init vendor-data content.
|
|
2152
|
+
* @param {string} params.isoUrl - Optional ISO URL to cache and serve.
|
|
1868
2153
|
* @memberof UnderpostBaremetal
|
|
1869
2154
|
* @returns {void}
|
|
1870
2155
|
*/
|
|
1871
2156
|
httpBootstrapServerStaticFactory({
|
|
1872
|
-
bootstrapHttpServerPath,
|
|
1873
|
-
hostname,
|
|
1874
|
-
cloudConfigSrc,
|
|
1875
|
-
|
|
2157
|
+
bootstrapHttpServerPath = '',
|
|
2158
|
+
hostname = '',
|
|
2159
|
+
cloudConfigSrc = '',
|
|
2160
|
+
kickstartSrc = '',
|
|
1876
2161
|
vendorData = '',
|
|
2162
|
+
isoUrl = '',
|
|
1877
2163
|
}) {
|
|
1878
|
-
|
|
1879
|
-
shellExec(`mkdir -p ${bootstrapHttpServerPath}/${hostname}/cloud-init`);
|
|
2164
|
+
shellExec(`mkdir -p ${bootstrapHttpServerPath}/${hostname}`);
|
|
1880
2165
|
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
`${bootstrapHttpServerPath}/${hostname}/cloud-init/user-data`,
|
|
1884
|
-
`#cloud-config\n${cloudConfigSrc}`,
|
|
1885
|
-
'utf8',
|
|
1886
|
-
);
|
|
2166
|
+
Underpost.cloudInit.httpServerStaticFactory({ bootstrapHttpServerPath, hostname, cloudConfigSrc, vendorData });
|
|
2167
|
+
Underpost.kickstart.httpServerStaticFactory({ bootstrapHttpServerPath, hostname, kickstartSrc });
|
|
1887
2168
|
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
2169
|
+
if (isoUrl) {
|
|
2170
|
+
const isoFilename = isoUrl.split('/').pop();
|
|
2171
|
+
const isoCacheDir = `/var/tmp/live-iso`;
|
|
2172
|
+
const isoCachePath = `${isoCacheDir}/${isoFilename}`;
|
|
2173
|
+
const isoDestPath = `${bootstrapHttpServerPath}/${hostname}/${isoFilename}`;
|
|
1891
2174
|
|
|
1892
|
-
|
|
1893
|
-
|
|
2175
|
+
if (!fs.existsSync(isoCachePath)) {
|
|
2176
|
+
logger.info(`Downloading ISO to cache: ${isoUrl}`);
|
|
2177
|
+
shellExec(`mkdir -p ${isoCacheDir}`);
|
|
2178
|
+
shellExec(`wget --progress=bar:force -O ${isoCachePath} "${isoUrl}"`);
|
|
2179
|
+
}
|
|
1894
2180
|
|
|
1895
|
-
|
|
2181
|
+
logger.info(`Copying ISO to bootstrap server: ${isoDestPath}`);
|
|
2182
|
+
shellExec(`cp ${isoCachePath} ${isoDestPath}`);
|
|
2183
|
+
}
|
|
1896
2184
|
},
|
|
1897
2185
|
|
|
1898
2186
|
/**
|
|
@@ -1912,15 +2200,17 @@ shell
|
|
|
1912
2200
|
const bootstrapHttpServerPath = options.bootstrapHttpServerPath || './public/localhost';
|
|
1913
2201
|
const hostname = options.hostname || 'localhost';
|
|
1914
2202
|
|
|
1915
|
-
shellExec(`
|
|
2203
|
+
shellExec(`node bin run kill ${port}`, { silent: true });
|
|
1916
2204
|
|
|
1917
|
-
|
|
1918
|
-
shellExec(`sudo pkill -f 'python3 -m http.server ${port}'`, { silent: true });
|
|
2205
|
+
const app = express();
|
|
1919
2206
|
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
2207
|
+
app.use(loggerMiddleware(import.meta, 'debug', () => false));
|
|
2208
|
+
|
|
2209
|
+
app.use('/', express.static(bootstrapHttpServerPath));
|
|
2210
|
+
|
|
2211
|
+
app.listen(port, () => {
|
|
2212
|
+
logger.info(`Static file server running on port ${port}`);
|
|
2213
|
+
});
|
|
1924
2214
|
|
|
1925
2215
|
// Configure iptables to allow incoming LAN connections
|
|
1926
2216
|
shellExec(
|
|
@@ -1949,9 +2239,10 @@ shell
|
|
|
1949
2239
|
*/
|
|
1950
2240
|
updateKernelFiles({ commissioningImage, resourcesPath, tftpRootPath, kernelFilesPaths }) {
|
|
1951
2241
|
// Copy EFI bootloaders to TFTP path.
|
|
1952
|
-
const
|
|
1953
|
-
|
|
1954
|
-
: ['bootx64.efi', 'grubx64.efi'];
|
|
2242
|
+
const arch = resourcesPath.split('/').pop();
|
|
2243
|
+
const efiFiles =
|
|
2244
|
+
arch === 'arm64' || arch === 'aarch64' ? ['bootaa64.efi', 'grubaa64.efi'] : ['bootx64.efi', 'grubx64.efi'];
|
|
2245
|
+
|
|
1955
2246
|
for (const file of efiFiles) {
|
|
1956
2247
|
shellExec(`sudo cp -a ${resourcesPath}/${file} ${tftpRootPath}/pxe/${file}`);
|
|
1957
2248
|
}
|
|
@@ -1963,7 +2254,7 @@ shell
|
|
|
1963
2254
|
// GRUB on ARM64 often crashes with synchronous exception (0x200) if handling large compressed kernels directly.
|
|
1964
2255
|
if (file === 'vmlinuz-efi') {
|
|
1965
2256
|
const kernelDest = `${tftpRootPath}/pxe/${file}`;
|
|
1966
|
-
const fileType = shellExec(`file ${kernelDest}
|
|
2257
|
+
const fileType = shellExec(`file ${kernelDest}`).stdout;
|
|
1967
2258
|
|
|
1968
2259
|
// Handle gzip compressed kernels
|
|
1969
2260
|
if (fileType.includes('gzip compressed data')) {
|
|
@@ -2003,6 +2294,12 @@ shell
|
|
|
2003
2294
|
* @param {object} options.machine - The machine object containing system_id.
|
|
2004
2295
|
* @param {boolean} [options.dev=false] - Whether to enable dev mode with dracut debugging parameters.
|
|
2005
2296
|
* @param {string} [options.osIdLike=''] - OS family identifier (e.g., 'rhel centos fedora' or 'debian ubuntu').
|
|
2297
|
+
* @param {object} options.authCredentials - Authentication credentials for fetching files (if needed).
|
|
2298
|
+
* @param {string} options.authCredentials.consumer_key - Consumer key for authentication.
|
|
2299
|
+
* @param {string} options.authCredentials.consumer_secret - Consumer secret for authentication.
|
|
2300
|
+
* @param {string} options.authCredentials.token_key - Token key for authentication.
|
|
2301
|
+
* @param {string} options.authCredentials.token_secret - Token secret for authentication.
|
|
2302
|
+
* @param {string} options.architecture - The architecture of the machine (e.g., 'amd64', 'arm64').
|
|
2006
2303
|
* @returns {object} An object containing the constructed command line string.
|
|
2007
2304
|
* @memberof UnderpostBaremetal
|
|
2008
2305
|
*/
|
|
@@ -2021,9 +2318,10 @@ shell
|
|
|
2021
2318
|
type: '',
|
|
2022
2319
|
macAddress: '',
|
|
2023
2320
|
cloudInit: false,
|
|
2024
|
-
machine: { system_id: '' },
|
|
2025
2321
|
dev: false,
|
|
2026
2322
|
osIdLike: '',
|
|
2323
|
+
authCredentials: { consumer_key: '', consumer_secret: '', token_key: '', token_secret: '' },
|
|
2324
|
+
architecture,
|
|
2027
2325
|
},
|
|
2028
2326
|
) {
|
|
2029
2327
|
// Construct kernel command line arguments for NFS boot.
|
|
@@ -2042,12 +2340,15 @@ shell
|
|
|
2042
2340
|
macAddress,
|
|
2043
2341
|
cloudInit,
|
|
2044
2342
|
osIdLike,
|
|
2343
|
+
architecture,
|
|
2045
2344
|
} = options;
|
|
2046
2345
|
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2346
|
+
// Determine OS family from osIdLike
|
|
2347
|
+
const { isDebianBased, isRhelBased } = Underpost.baremetal.getFamilyBaseOs(options.osIdLike);
|
|
2348
|
+
|
|
2349
|
+
const ipParam =
|
|
2350
|
+
`ip=${ipClient}:${ipFileServer}:${ipDhcpServer}:${netmask}:${hostname}` +
|
|
2351
|
+
`:${networkInterfaceName ? networkInterfaceName : 'eth0'}:${ipConfig}:${dnsServer}`;
|
|
2051
2352
|
|
|
2052
2353
|
const nfsOptions = `${
|
|
2053
2354
|
type === 'chroot-debootstrap' || type === 'chroot-container'
|
|
@@ -2074,7 +2375,9 @@ shell
|
|
|
2074
2375
|
: []
|
|
2075
2376
|
}`;
|
|
2076
2377
|
|
|
2077
|
-
const nfsRootParam = `nfsroot=${ipFileServer}:${process.env.NFS_EXPORT_PATH}/${hostname}${
|
|
2378
|
+
const nfsRootParam = `nfsroot=${ipFileServer}:${process.env.NFS_EXPORT_PATH}/${hostname}${
|
|
2379
|
+
nfsOptions ? `,${nfsOptions}` : ''
|
|
2380
|
+
}`;
|
|
2078
2381
|
|
|
2079
2382
|
const permissionsParams = [
|
|
2080
2383
|
`rw`,
|
|
@@ -2092,9 +2395,9 @@ shell
|
|
|
2092
2395
|
// `layerfs-path=filesystem.squashfs`,
|
|
2093
2396
|
// `root=/dev/ram0`,
|
|
2094
2397
|
// `toram`,
|
|
2095
|
-
'nomodeset',
|
|
2096
|
-
`editable_rootfs=tmpfs`,
|
|
2097
|
-
`ramdisk_size=3550000`,
|
|
2398
|
+
// 'nomodeset',
|
|
2399
|
+
// `editable_rootfs=tmpfs`, // all writes to rootfs go to RAM, keeping underlying storage pristine
|
|
2400
|
+
// `ramdisk_size=3550000`,
|
|
2098
2401
|
// `root=/dev/sda1`, // rpi4 usb port unit
|
|
2099
2402
|
'apparmor=0', // Disable AppArmor security
|
|
2100
2403
|
...(networkInterfaceName === 'eth0'
|
|
@@ -2136,51 +2439,34 @@ shell
|
|
|
2136
2439
|
|
|
2137
2440
|
let cmd = [];
|
|
2138
2441
|
if (type === 'iso-ram') {
|
|
2139
|
-
const netBootParams = [`netboot=url`];
|
|
2140
|
-
if (fileSystemUrl) netBootParams.push(`url=${fileSystemUrl.replace('https', 'http')}`);
|
|
2141
|
-
cmd = [ipParam, `boot=casper`, ...netBootParams, ...kernelParams];
|
|
2142
|
-
} else if (type === 'chroot-debootstrap' || type === 'chroot-container') {
|
|
2143
|
-
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
2442
|
if (isRhelBased) {
|
|
2151
|
-
|
|
2152
|
-
}
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
// Add debugging parameters in dev mode for dracut troubleshooting
|
|
2159
|
-
if (options.dev) {
|
|
2160
|
-
// qemuNfsRootParams = qemuNfsRootParams.concat([`rd.shell`, `rd.debug`]);
|
|
2443
|
+
cmd = Underpost.kickstart.kernelParamsFactory(macAddress, [ipParam, ...kernelParams], options);
|
|
2444
|
+
} else {
|
|
2445
|
+
// ISO-RAM (Debian/Ubuntu): full live ISO downloaded into RAM via casper toram.
|
|
2446
|
+
const netBootParams = [`netboot=url`];
|
|
2447
|
+
if (fileSystemUrl) netBootParams.push(`url=${fileSystemUrl.replace('https', 'http')}`);
|
|
2448
|
+
cmd = [ipParam, `boot=casper`, 'toram', ...netBootParams, ...kernelParams, ...performanceParams];
|
|
2161
2449
|
}
|
|
2162
|
-
|
|
2450
|
+
} else if (type === 'chroot-debootstrap' || type === 'chroot-container') {
|
|
2451
|
+
let qemuNfsRootParams = [`root=/dev/nfs`, `rootfstype=nfs`];
|
|
2163
2452
|
cmd = [ipParam, ...qemuNfsRootParams, nfsRootParam, ...kernelParams];
|
|
2164
2453
|
} else {
|
|
2165
|
-
// 'iso-nfs'
|
|
2454
|
+
// 'iso-nfs' — Debian/Ubuntu NFS root boot: kernel/initrd from ISO, root filesystem served via NFS.
|
|
2166
2455
|
cmd = [ipParam, `netboot=nfs`, nfsRootParam, ...kernelParams, ...performanceParams];
|
|
2456
|
+
}
|
|
2167
2457
|
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
`log_host=${ipDhcpServer}`,
|
|
2178
|
-
`log_port=5247`,
|
|
2179
|
-
// `BOOTIF=${macAddress}`,
|
|
2180
|
-
// `cc:{'datasource_list': ['MAAS']}end_cc`,
|
|
2181
|
-
]);
|
|
2182
|
-
}
|
|
2458
|
+
// Add RHEL/Rocky/Fedora based images specific parameters
|
|
2459
|
+
if (isRhelBased) {
|
|
2460
|
+
cmd = cmd.concat([`rd.neednet=1`, `rd.timeout=180`, `selinux=0`, `enforcing=0`]);
|
|
2461
|
+
if (options.dev) cmd = cmd.concat([`rd.shell`, `rd.debug`]);
|
|
2462
|
+
}
|
|
2463
|
+
// Add Debian/Ubuntu based images specific parameters
|
|
2464
|
+
else if (isDebianBased) {
|
|
2465
|
+
cmd = cmd.concat([`initrd=initrd.img`, `init=/sbin/init`]);
|
|
2466
|
+
if (options.dev) cmd = cmd.concat([`debug`, `ignore_loglevel`]);
|
|
2183
2467
|
}
|
|
2468
|
+
|
|
2469
|
+
if (cloudInit) cmd = Underpost.cloudInit.kernelParamsFactory(macAddress, cmd, options);
|
|
2184
2470
|
// cmd.push('---');
|
|
2185
2471
|
const cmdStr = cmd.join(' ');
|
|
2186
2472
|
logger.info('Constructed kernel command line');
|
|
@@ -2204,12 +2490,7 @@ shell
|
|
|
2204
2490
|
async commissionMonitor({ macAddress, ipAddress, hostname, architecture, machine }) {
|
|
2205
2491
|
{
|
|
2206
2492
|
// 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
|
-
);
|
|
2493
|
+
const discoveries = Underpost.baremetal.maasCliExec(`discoveries read`);
|
|
2213
2494
|
|
|
2214
2495
|
for (const discovery of discoveries) {
|
|
2215
2496
|
const discoverHostname = discovery.hostname
|
|
@@ -2227,6 +2508,20 @@ shell
|
|
|
2227
2508
|
if (discovery.ip === ipAddress) {
|
|
2228
2509
|
logger.info('Machine discovered!', discovery);
|
|
2229
2510
|
if (!machine) {
|
|
2511
|
+
// Check if a machine with the discovered MAC already exists to avoid conflicts
|
|
2512
|
+
const [existingMachine] =
|
|
2513
|
+
Underpost.baremetal.maasCliExec(`machines read mac_address=${discovery.mac_address}`) || [];
|
|
2514
|
+
|
|
2515
|
+
if (existingMachine) {
|
|
2516
|
+
logger.warn(
|
|
2517
|
+
`Machine ${existingMachine.hostname} (${existingMachine.system_id}) already exists with MAC ${discovery.mac_address}`,
|
|
2518
|
+
);
|
|
2519
|
+
logger.info(
|
|
2520
|
+
`Deleting existing machine ${existingMachine.system_id} to create new machine ${hostname}...`,
|
|
2521
|
+
);
|
|
2522
|
+
Underpost.baremetal.maasCliExec(`machine delete ${existingMachine.system_id}`);
|
|
2523
|
+
}
|
|
2524
|
+
|
|
2230
2525
|
logger.info('Creating new machine with discovered hardware MAC...', {
|
|
2231
2526
|
discoveredMAC: discovery.mac_address,
|
|
2232
2527
|
ipAddress,
|
|
@@ -2238,12 +2533,18 @@ shell
|
|
|
2238
2533
|
hostname,
|
|
2239
2534
|
architecture,
|
|
2240
2535
|
}).machine;
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2536
|
+
|
|
2537
|
+
if (machine && machine.system_id) {
|
|
2538
|
+
console.log('New machine system id:', machine.system_id.bgYellow.bold.black);
|
|
2539
|
+
Underpost.baremetal.writeGrubConfigToFile({
|
|
2540
|
+
grubCfgSrc: Underpost.baremetal
|
|
2541
|
+
.getGrubConfigFromFile()
|
|
2542
|
+
.grubCfgSrc.replaceAll('system-id', machine.system_id),
|
|
2543
|
+
});
|
|
2544
|
+
} else {
|
|
2545
|
+
logger.error('Failed to create machine or obtain system_id', machine);
|
|
2546
|
+
throw new Error('Machine creation failed');
|
|
2547
|
+
}
|
|
2247
2548
|
} else {
|
|
2248
2549
|
const systemId = machine.system_id;
|
|
2249
2550
|
console.log('Using pre-registered machine system_id:', systemId.bgYellow.bold.black);
|
|
@@ -2256,22 +2557,45 @@ shell
|
|
|
2256
2557
|
discoveredMAC: discovery.mac_address,
|
|
2257
2558
|
});
|
|
2258
2559
|
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2560
|
+
// Check current machine status before attempting state transitions
|
|
2561
|
+
const currentMachine = Underpost.baremetal.maasCliExec(`machine read ${systemId}`);
|
|
2562
|
+
const currentStatus = currentMachine ? currentMachine.status_name : 'Unknown';
|
|
2563
|
+
logger.info('Current machine status before interface update:', { systemId, status: currentStatus });
|
|
2564
|
+
|
|
2565
|
+
// Only mark-broken if the machine is in a state that supports it (e.g. Ready, New, Allocated)
|
|
2566
|
+
// Machines already in Broken state don't need to be marked broken again
|
|
2567
|
+
if (currentStatus !== 'Broken') {
|
|
2568
|
+
try {
|
|
2569
|
+
Underpost.baremetal.maasCliExec(`machine mark-broken ${systemId}`);
|
|
2570
|
+
logger.info('Machine marked as broken successfully');
|
|
2571
|
+
} catch (markBrokenError) {
|
|
2572
|
+
logger.warn('Failed to mark machine as broken, attempting interface update anyway...', {
|
|
2573
|
+
error: markBrokenError.message,
|
|
2574
|
+
currentStatus,
|
|
2575
|
+
});
|
|
2576
|
+
}
|
|
2577
|
+
} else {
|
|
2578
|
+
logger.info('Machine is already in Broken state, skipping mark-broken');
|
|
2579
|
+
}
|
|
2262
2580
|
|
|
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
|
-
},
|
|
2581
|
+
Underpost.baremetal.maasCliExec(
|
|
2582
|
+
`interface update ${systemId} ${machine.boot_interface.id}` + ` mac_address=${discovery.mac_address}`,
|
|
2270
2583
|
);
|
|
2271
2584
|
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2585
|
+
// Re-check status before mark-fixed — only attempt if actually Broken
|
|
2586
|
+
const updatedMachine = Underpost.baremetal.maasCliExec(`machine read ${systemId}`);
|
|
2587
|
+
const updatedStatus = updatedMachine ? updatedMachine.status_name : 'Unknown';
|
|
2588
|
+
|
|
2589
|
+
if (updatedStatus === 'Broken') {
|
|
2590
|
+
try {
|
|
2591
|
+
Underpost.baremetal.maasCliExec(`machine mark-fixed ${systemId}`);
|
|
2592
|
+
logger.info('Machine marked as fixed successfully');
|
|
2593
|
+
} catch (markFixedError) {
|
|
2594
|
+
logger.warn('Failed to mark machine as fixed:', { error: markFixedError.message });
|
|
2595
|
+
}
|
|
2596
|
+
} else {
|
|
2597
|
+
logger.info('Machine is not in Broken state, skipping mark-fixed', { status: updatedStatus });
|
|
2598
|
+
}
|
|
2275
2599
|
|
|
2276
2600
|
logger.info('✓ Machine interface MAC address updated successfully');
|
|
2277
2601
|
|
|
@@ -2300,6 +2624,73 @@ shell
|
|
|
2300
2624
|
}
|
|
2301
2625
|
},
|
|
2302
2626
|
|
|
2627
|
+
/**
|
|
2628
|
+
* @method maasCliExec
|
|
2629
|
+
* @description Executes a MAAS CLI command and returns the parsed JSON output.
|
|
2630
|
+
* This method abstracts the execution of MAAS CLI commands, ensuring that the output is captured and parsed correctly.
|
|
2631
|
+
* @param {string} cmd - The MAAS CLI command to execute (e.g., 'machines read').
|
|
2632
|
+
* @returns {object|null} The parsed JSON output from the MAAS CLI command, or null if there is no output.
|
|
2633
|
+
* @memberof UnderpostBaremetal
|
|
2634
|
+
*/
|
|
2635
|
+
maasCliExec(cmd) {
|
|
2636
|
+
const output = shellExec(`maas ${process.env.MAAS_ADMIN_USERNAME} ${cmd}`, {
|
|
2637
|
+
stdout: true,
|
|
2638
|
+
silent: true,
|
|
2639
|
+
}).trim();
|
|
2640
|
+
try {
|
|
2641
|
+
return output ? JSON.parse(output) : null;
|
|
2642
|
+
} catch (error) {
|
|
2643
|
+
console.log('output', output);
|
|
2644
|
+
logger.error(error);
|
|
2645
|
+
throw error;
|
|
2646
|
+
}
|
|
2647
|
+
},
|
|
2648
|
+
|
|
2649
|
+
/**
|
|
2650
|
+
* @method maasAuthCredentialsFactory
|
|
2651
|
+
* @description Retrieves MAAS API key credentials from the MAAS CLI.
|
|
2652
|
+
* This method parses the output of `maas apikey` to extract the consumer key,
|
|
2653
|
+
* consumer secret, token key, and token secret.
|
|
2654
|
+
* @returns {object} An object containing the MAAS authentication credentials.
|
|
2655
|
+
* @memberof UnderpostBaremetal
|
|
2656
|
+
* @throws {Error} If the MAAS API key format is invalid.
|
|
2657
|
+
*/
|
|
2658
|
+
maasAuthCredentialsFactory() {
|
|
2659
|
+
// Expected formats:
|
|
2660
|
+
// <consumer_key>:<consumer_token>:<secret> (older format)
|
|
2661
|
+
// <consumer_key>:<consumer_secret>:<token_key>:<token_secret> (newer format)
|
|
2662
|
+
// Commands used to generate API keys:
|
|
2663
|
+
// maas apikey --with-names --username ${process.env.MAAS_ADMIN_USERNAME}
|
|
2664
|
+
// maas ${process.env.MAAS_ADMIN_USERNAME} account create-authorisation-token
|
|
2665
|
+
// maas apikey --generate --username ${process.env.MAAS_ADMIN_USERNAME}
|
|
2666
|
+
// Reference: https://github.com/CanonicalLtd/maas-docs/issues/647
|
|
2667
|
+
|
|
2668
|
+
const parts = shellExec(`maas apikey --with-names --username ${process.env.MAAS_ADMIN_USERNAME}`, {
|
|
2669
|
+
stdout: true,
|
|
2670
|
+
})
|
|
2671
|
+
.trim()
|
|
2672
|
+
.split(`\n`)[0] // Take only the first line of output.
|
|
2673
|
+
.split(':'); // Split by colon to get individual parts.
|
|
2674
|
+
|
|
2675
|
+
let consumer_key, consumer_secret, token_key, token_secret;
|
|
2676
|
+
|
|
2677
|
+
// Determine the format of the API key and assign parts accordingly.
|
|
2678
|
+
if (parts.length === 4) {
|
|
2679
|
+
[consumer_key, consumer_secret, token_key, token_secret] = parts;
|
|
2680
|
+
} else if (parts.length === 3) {
|
|
2681
|
+
// Handle older 3-part format, setting consumer_secret as empty.
|
|
2682
|
+
[consumer_key, token_key, token_secret] = parts;
|
|
2683
|
+
consumer_secret = '';
|
|
2684
|
+
token_secret = token_secret.split(' MAAS consumer')[0].trim(); // Clean up token secret.
|
|
2685
|
+
} else {
|
|
2686
|
+
// Throw an error if the format is not recognized.
|
|
2687
|
+
throw new Error('Invalid token format');
|
|
2688
|
+
}
|
|
2689
|
+
|
|
2690
|
+
logger.info('Maas api token generated', { consumer_key, consumer_secret, token_key, token_secret });
|
|
2691
|
+
return { consumer_key, consumer_secret, token_key, token_secret };
|
|
2692
|
+
},
|
|
2693
|
+
|
|
2303
2694
|
/**
|
|
2304
2695
|
* @method mountBinfmtMisc
|
|
2305
2696
|
* @description Mounts the binfmt_misc filesystem to enable QEMU user-static binfmt support.
|
|
@@ -2335,7 +2726,7 @@ shell
|
|
|
2335
2726
|
const systemId = typeof machine === 'string' ? machine : machine.system_id;
|
|
2336
2727
|
if (ignore && ignore.find((mId) => mId === systemId)) continue;
|
|
2337
2728
|
logger.info(`Removing machine: ${systemId}`);
|
|
2338
|
-
|
|
2729
|
+
Underpost.baremetal.maasCliExec(`machine delete ${systemId}`);
|
|
2339
2730
|
}
|
|
2340
2731
|
return [];
|
|
2341
2732
|
},
|
|
@@ -2349,9 +2740,9 @@ shell
|
|
|
2349
2740
|
* @returns {void}
|
|
2350
2741
|
*/
|
|
2351
2742
|
clearDiscoveries({ force }) {
|
|
2352
|
-
|
|
2743
|
+
Underpost.baremetal.maasCliExec(`discoveries clear all=true`);
|
|
2353
2744
|
if (force === true) {
|
|
2354
|
-
|
|
2745
|
+
Underpost.baremetal.maasCliExec(`discoveries scan force=true`);
|
|
2355
2746
|
}
|
|
2356
2747
|
},
|
|
2357
2748
|
|
|
@@ -2892,9 +3283,10 @@ logdir /var/log/chrony
|
|
|
2892
3283
|
* @param {string} params.nfsHostPath - The path to the NFS server export.
|
|
2893
3284
|
* @memberof UnderpostBaremetal
|
|
2894
3285
|
* @param {string} [params.subnet='192.168.1.0/24'] - The subnet allowed to access the NFS export.
|
|
3286
|
+
* @param {boolean} [params.nfsReset=false] - Flag to completely reset the NFS server (restart service).
|
|
2895
3287
|
* @returns {void}
|
|
2896
3288
|
*/
|
|
2897
|
-
rebuildNfsServer({ nfsHostPath, subnet }) {
|
|
3289
|
+
rebuildNfsServer({ nfsHostPath, subnet, nfsReset }) {
|
|
2898
3290
|
if (!subnet) subnet = '192.168.1.0/24'; // Default subnet if not provided.
|
|
2899
3291
|
// Write the NFS exports configuration to /etc/exports.
|
|
2900
3292
|
fs.writeFileSync(
|
|
@@ -2943,9 +3335,11 @@ udp-port = 32766
|
|
|
2943
3335
|
|
|
2944
3336
|
// Restart the nfs-server service to apply all configuration changes,
|
|
2945
3337
|
// including port settings from /etc/nfs.conf and export changes.
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
3338
|
+
if (nfsReset) {
|
|
3339
|
+
logger.info('Restarting nfs-server service...');
|
|
3340
|
+
shellExec(`sudo systemctl restart nfs-server`);
|
|
3341
|
+
logger.info('NFS server restarted.');
|
|
3342
|
+
}
|
|
2949
3343
|
},
|
|
2950
3344
|
|
|
2951
3345
|
/**
|
|
@@ -2967,10 +3361,10 @@ udp-port = 32766
|
|
|
2967
3361
|
// Check both /usr/local/bin (compiled) and system paths
|
|
2968
3362
|
let qemuAarch64Path = null;
|
|
2969
3363
|
|
|
2970
|
-
if (shellExec('test -x /usr/local/bin/qemu-system-aarch64'
|
|
3364
|
+
if (shellExec('test -x /usr/local/bin/qemu-system-aarch64').code === 0) {
|
|
2971
3365
|
qemuAarch64Path = '/usr/local/bin/qemu-system-aarch64';
|
|
2972
|
-
} else if (shellExec('which qemu-system-aarch64'
|
|
2973
|
-
qemuAarch64Path = shellExec('which qemu-system-aarch64'
|
|
3366
|
+
} else if (shellExec('which qemu-system-aarch64').code === 0) {
|
|
3367
|
+
qemuAarch64Path = shellExec('which qemu-system-aarch64').stdout.trim();
|
|
2974
3368
|
}
|
|
2975
3369
|
|
|
2976
3370
|
if (!qemuAarch64Path) {
|
|
@@ -2983,7 +3377,7 @@ udp-port = 32766
|
|
|
2983
3377
|
logger.info(`Found qemu-system-aarch64 at: ${qemuAarch64Path}`);
|
|
2984
3378
|
|
|
2985
3379
|
// Verify that the installed qemu supports the 'virt' machine type (required for arm64)
|
|
2986
|
-
const machineHelp = shellExec(`${qemuAarch64Path} -machine help
|
|
3380
|
+
const machineHelp = shellExec(`${qemuAarch64Path} -machine help`).stdout;
|
|
2987
3381
|
if (!machineHelp.includes('virt')) {
|
|
2988
3382
|
throw new Error(
|
|
2989
3383
|
'The installed qemu-system-aarch64 does not support the "virt" machine type.\n' +
|
|
@@ -2996,10 +3390,10 @@ udp-port = 32766
|
|
|
2996
3390
|
// Check both /usr/local/bin (compiled) and system paths
|
|
2997
3391
|
let qemuX86Path = null;
|
|
2998
3392
|
|
|
2999
|
-
if (shellExec('test -x /usr/local/bin/qemu-system-x86_64'
|
|
3393
|
+
if (shellExec('test -x /usr/local/bin/qemu-system-x86_64').code === 0) {
|
|
3000
3394
|
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'
|
|
3395
|
+
} else if (shellExec('which qemu-system-x86_64').code === 0) {
|
|
3396
|
+
qemuX86Path = shellExec('which qemu-system-x86_64').stdout.trim();
|
|
3003
3397
|
}
|
|
3004
3398
|
|
|
3005
3399
|
if (!qemuX86Path) {
|
|
@@ -3012,7 +3406,7 @@ udp-port = 32766
|
|
|
3012
3406
|
logger.info(`Found qemu-system-x86_64 at: ${qemuX86Path}`);
|
|
3013
3407
|
|
|
3014
3408
|
// Verify that the installed qemu supports the 'pc' or 'q35' machine type (required for x86_64)
|
|
3015
|
-
const machineHelp = shellExec(`${qemuX86Path} -machine help
|
|
3409
|
+
const machineHelp = shellExec(`${qemuX86Path} -machine help`).stdout;
|
|
3016
3410
|
if (!machineHelp.includes('pc') && !machineHelp.includes('q35')) {
|
|
3017
3411
|
throw new Error(
|
|
3018
3412
|
'The installed qemu-system-x86_64 does not support the "pc" or "q35" machine type.\n' +
|