@underpostnet/underpost 2.99.1 → 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/LICENSE +1 -1
- package/README.md +30 -30
- package/baremetal/commission-workflows.json +52 -0
- package/bin/deploy.js +101 -47
- package/cli.md +47 -43
- package/examples/static-page/README.md +55 -378
- package/examples/static-page/ssr-components/CustomPage.js +1 -13
- package/jsconfig.json +4 -2
- package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
- package/manifests/deployment/dd-test-development/deployment.yaml +2 -2
- package/manifests/deployment/playwright/deployment.yaml +52 -0
- package/package.json +2 -2
- package/scripts/disk-devices.sh +13 -0
- package/scripts/rocky-pwa.sh +2 -2
- package/scripts/ssl.sh +12 -6
- package/src/api/user/user.model.js +1 -0
- package/src/cli/baremetal.js +576 -176
- package/src/cli/cloud-init.js +97 -79
- package/src/cli/deploy.js +6 -24
- package/src/cli/env.js +4 -1
- package/src/cli/image.js +7 -40
- package/src/cli/index.js +37 -7
- package/src/cli/repository.js +3 -1
- package/src/cli/run.js +109 -92
- package/src/cli/secrets.js +0 -34
- package/src/cli/static.js +0 -26
- package/src/cli/test.js +13 -1
- package/src/client/components/core/Polyhedron.js +896 -7
- package/src/client/components/core/Translate.js +4 -0
- package/src/client/services/default/default.management.js +12 -2
- package/src/index.js +27 -1
- package/src/runtime/express/Express.js +3 -3
- package/src/server/conf.js +6 -4
- package/src/server/logger.js +33 -31
- package/src/server/process.js +27 -2
- package/src/server/proxy.js +4 -6
- package/src/server/tls.js +30 -25
- 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.
|
|
@@ -47,13 +35,13 @@ class UnderpostBaremetal {
|
|
|
47
35
|
* It handles NFS root filesystem building, control server installation/uninstallation,
|
|
48
36
|
* and system-level provisioning tasks like timezone and keyboard configuration.
|
|
49
37
|
* @param {string} [workflowId='rpi4mb'] - Identifier for the specific workflow configuration to use.
|
|
50
|
-
* @param {string} [ipAddress=getLocalIPv4Address()] - The IP address of the control server or the local machine.
|
|
51
|
-
* @param {string} [hostname=workflowId] - The hostname of the target baremetal machine.
|
|
52
|
-
* @param {string} [ipFileServer=getLocalIPv4Address()] - The IP address of the file server (NFS/TFTP).
|
|
53
|
-
* @param {string} [ipConfig=''] - IP configuration string for the baremetal machine.
|
|
54
|
-
* @param {string} [netmask=''] - Netmask of network
|
|
55
|
-
* @param {string} [dnsServer=''] - DNS server IP address.
|
|
56
38
|
* @param {object} [options] - An object containing boolean flags for various operations.
|
|
39
|
+
* @param {string} [options.ipAddress=getLocalIPv4Address()] - The IP address of the control server or the local machine.
|
|
40
|
+
* @param {string} [options.hostname=workflowId] - The hostname of the target baremetal machine.
|
|
41
|
+
* @param {string} [options.ipFileServer=getLocalIPv4Address()] - The IP address of the file server (NFS/TFTP).
|
|
42
|
+
* @param {string} [options.ipConfig=''] - IP configuration string for the baremetal machine.
|
|
43
|
+
* @param {string} [options.netmask=''] - Netmask of network
|
|
44
|
+
* @param {string} [options.dnsServer=''] - DNS server IP address.
|
|
57
45
|
* @param {boolean} [options.dev=false] - Development mode flag.
|
|
58
46
|
* @param {boolean} [options.controlServerInstall=false] - Flag to install the control server (e.g., MAAS).
|
|
59
47
|
* @param {boolean} [options.controlServerUninstall=false] - Flag to uninstall the control server.
|
|
@@ -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).
|
|
@@ -94,13 +83,13 @@ class UnderpostBaremetal {
|
|
|
94
83
|
*/
|
|
95
84
|
async callback(
|
|
96
85
|
workflowId,
|
|
97
|
-
ipAddress,
|
|
98
|
-
hostname,
|
|
99
|
-
ipFileServer,
|
|
100
|
-
ipConfig,
|
|
101
|
-
netmask,
|
|
102
|
-
dnsServer,
|
|
103
86
|
options = {
|
|
87
|
+
ipAddress: undefined,
|
|
88
|
+
hostname: undefined,
|
|
89
|
+
ipFileServer: undefined,
|
|
90
|
+
ipConfig: undefined,
|
|
91
|
+
netmask: undefined,
|
|
92
|
+
dnsServer: undefined,
|
|
104
93
|
dev: false,
|
|
105
94
|
controlServerInstall: false,
|
|
106
95
|
controlServerUninstall: false,
|
|
@@ -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: '',
|
|
@@ -139,6 +129,8 @@ class UnderpostBaremetal {
|
|
|
139
129
|
logs: '',
|
|
140
130
|
},
|
|
141
131
|
) {
|
|
132
|
+
let { ipAddress, hostname, ipFileServer, ipConfig, netmask, dnsServer } = options;
|
|
133
|
+
|
|
142
134
|
// Load environment variables from .env file, overriding existing ones if present.
|
|
143
135
|
dotenv.config({ path: `${getUnderpostRootPath()}/.env`, override: true });
|
|
144
136
|
|
|
@@ -192,6 +184,47 @@ class UnderpostBaremetal {
|
|
|
192
184
|
// Define the TFTP root prefix path based
|
|
193
185
|
const tftpRootPath = `${process.env.TFTP_ROOT}/${tftpPrefix}`;
|
|
194
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
|
+
|
|
195
228
|
// Define the iPXE cache directory to preserve builds across tftproot cleanups
|
|
196
229
|
const ipxeCacheDir = `/tmp/ipxe-cache/${tftpPrefix}`;
|
|
197
230
|
|
|
@@ -216,12 +249,7 @@ class UnderpostBaremetal {
|
|
|
216
249
|
// Create a new machine in MAAS if the option is set.
|
|
217
250
|
let machine;
|
|
218
251
|
if (options.createMachine === true) {
|
|
219
|
-
const [searhMachine] =
|
|
220
|
-
shellExec(`maas maas machines read hostname=${hostname}`, {
|
|
221
|
-
stdout: true,
|
|
222
|
-
silent: true,
|
|
223
|
-
}),
|
|
224
|
-
);
|
|
252
|
+
const [searhMachine] = Underpost.baremetal.maasCliExec(`machines read hostname=${hostname}`);
|
|
225
253
|
|
|
226
254
|
if (searhMachine) {
|
|
227
255
|
// Check if existing machine's MAC matches the specified MAC
|
|
@@ -236,16 +264,14 @@ class UnderpostBaremetal {
|
|
|
236
264
|
logger.info(`Deleting existing machine ${searhMachine.system_id} to recreate with correct MAC...`);
|
|
237
265
|
|
|
238
266
|
// Delete the existing machine
|
|
239
|
-
|
|
240
|
-
silent: true,
|
|
241
|
-
});
|
|
267
|
+
Underpost.baremetal.maasCliExec(`machine delete ${searhMachine.system_id}`);
|
|
242
268
|
|
|
243
269
|
// Create new machine with correct MAC
|
|
244
270
|
machine = Underpost.baremetal.machineFactory({
|
|
245
271
|
hostname,
|
|
246
272
|
ipAddress,
|
|
247
273
|
macAddress,
|
|
248
|
-
|
|
274
|
+
architecture: workflowsConfig[workflowId].architecture,
|
|
249
275
|
}).machine;
|
|
250
276
|
|
|
251
277
|
logger.info(`✓ Machine recreated with MAC ${macAddress}`);
|
|
@@ -264,7 +290,7 @@ class UnderpostBaremetal {
|
|
|
264
290
|
hostname,
|
|
265
291
|
ipAddress,
|
|
266
292
|
macAddress,
|
|
267
|
-
|
|
293
|
+
architecture: workflowsConfig[workflowId].architecture,
|
|
268
294
|
}).machine;
|
|
269
295
|
}
|
|
270
296
|
}
|
|
@@ -349,7 +375,7 @@ class UnderpostBaremetal {
|
|
|
349
375
|
|
|
350
376
|
// Build phase (skip if upload-only mode)
|
|
351
377
|
if (options.packerMaasImageBuild) {
|
|
352
|
-
if (shellExec('packer version'
|
|
378
|
+
if (shellExec('packer version').code !== 0) {
|
|
353
379
|
throw new Error('Packer is not installed. Please install Packer to proceed.');
|
|
354
380
|
}
|
|
355
381
|
|
|
@@ -733,12 +759,7 @@ rm -rf ${artifacts.join(' ')}`);
|
|
|
733
759
|
|
|
734
760
|
// Fetch boot resources and machines if commissioning or listing.
|
|
735
761
|
|
|
736
|
-
let resources =
|
|
737
|
-
shellExec(`maas ${process.env.MAAS_ADMIN_USERNAME} boot-resources read`, {
|
|
738
|
-
silent: true,
|
|
739
|
-
stdout: true,
|
|
740
|
-
}),
|
|
741
|
-
).map((o) => ({
|
|
762
|
+
let resources = Underpost.baremetal.maasCliExec(`boot-resources read`).map((o) => ({
|
|
742
763
|
id: o.id,
|
|
743
764
|
name: o.name,
|
|
744
765
|
architecture: o.architecture,
|
|
@@ -746,12 +767,7 @@ rm -rf ${artifacts.join(' ')}`);
|
|
|
746
767
|
if (options.ls === true) {
|
|
747
768
|
console.table(resources);
|
|
748
769
|
}
|
|
749
|
-
let machines =
|
|
750
|
-
shellExec(`maas ${process.env.MAAS_ADMIN_USERNAME} machines read`, {
|
|
751
|
-
stdout: true,
|
|
752
|
-
silent: true,
|
|
753
|
-
}),
|
|
754
|
-
).map((m) => ({
|
|
770
|
+
let machines = Underpost.baremetal.maasCliExec(`machines read`).map((m) => ({
|
|
755
771
|
system_id: m.interface_set[0].system_id,
|
|
756
772
|
mac_address: m.interface_set[0].mac_address,
|
|
757
773
|
hostname: m.hostname,
|
|
@@ -873,10 +889,27 @@ rm -rf ${artifacts.join(' ')}`);
|
|
|
873
889
|
});
|
|
874
890
|
}
|
|
875
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
|
+
|
|
876
897
|
if (options.cloudInit || options.cloudInitUpdate) {
|
|
877
898
|
const { chronyc, networkInterfaceName } = workflowsConfig[workflowId];
|
|
878
899
|
const { timezone, chronyConfPath } = chronyc;
|
|
879
|
-
|
|
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
|
+
|
|
880
913
|
const { cloudConfigSrc } = Underpost.cloudInit.configFactory(
|
|
881
914
|
{
|
|
882
915
|
controlServerIp: callbackMetaData.runnerHost.ip,
|
|
@@ -889,15 +922,16 @@ rm -rf ${artifacts.join(' ')}`);
|
|
|
889
922
|
networkInterfaceName,
|
|
890
923
|
ubuntuToolsBuild: options.ubuntuToolsBuild,
|
|
891
924
|
bootcmd: options.bootcmd,
|
|
892
|
-
runcmd
|
|
925
|
+
runcmd,
|
|
926
|
+
write_files,
|
|
893
927
|
},
|
|
894
928
|
authCredentials,
|
|
895
929
|
);
|
|
896
|
-
|
|
897
930
|
Underpost.baremetal.httpBootstrapServerStaticFactory({
|
|
898
931
|
bootstrapHttpServerPath,
|
|
899
932
|
hostname,
|
|
900
933
|
cloudConfigSrc,
|
|
934
|
+
isoUrl: workflowsConfig[workflowId].isoUrl,
|
|
901
935
|
});
|
|
902
936
|
}
|
|
903
937
|
|
|
@@ -908,7 +942,7 @@ rm -rf ${artifacts.join(' ')}`);
|
|
|
908
942
|
workflowsConfig[workflowId].type === 'chroot-debootstrap' ||
|
|
909
943
|
workflowsConfig[workflowId].type === 'chroot-container')
|
|
910
944
|
) {
|
|
911
|
-
shellExec(`${underpostRoot}/scripts/nat-iptables.sh
|
|
945
|
+
shellExec(`${underpostRoot}/scripts/nat-iptables.sh`);
|
|
912
946
|
Underpost.baremetal.rebuildNfsServer({
|
|
913
947
|
nfsHostPath,
|
|
914
948
|
});
|
|
@@ -1002,15 +1036,22 @@ rm -rf ${artifacts.join(' ')}`);
|
|
|
1002
1036
|
hostname,
|
|
1003
1037
|
dnsServer,
|
|
1004
1038
|
networkInterfaceName,
|
|
1005
|
-
fileSystemUrl:
|
|
1006
|
-
|
|
1007
|
-
|
|
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
|
+
}),
|
|
1008
1048
|
type,
|
|
1009
1049
|
macAddress,
|
|
1010
1050
|
cloudInit: options.cloudInit,
|
|
1011
1051
|
machine,
|
|
1012
1052
|
dev: options.dev,
|
|
1013
1053
|
osIdLike: workflowsConfig[workflowId].osIdLike || '',
|
|
1054
|
+
authCredentials,
|
|
1014
1055
|
});
|
|
1015
1056
|
|
|
1016
1057
|
// Check if iPXE mode is enabled AND the iPXE EFI binary exists
|
|
@@ -1084,8 +1125,11 @@ rm -rf ${artifacts.join(' ')}`);
|
|
|
1084
1125
|
Underpost.baremetal.httpBootstrapServerRunnerFactory({
|
|
1085
1126
|
hostname,
|
|
1086
1127
|
bootstrapHttpServerPath,
|
|
1087
|
-
bootstrapHttpServerPort:
|
|
1088
|
-
options.bootstrapHttpServerPort
|
|
1128
|
+
bootstrapHttpServerPort: Underpost.baremetal.bootstrapHttpServerPortFactory({
|
|
1129
|
+
port: options.bootstrapHttpServerPort,
|
|
1130
|
+
workflowId,
|
|
1131
|
+
workflowsConfig,
|
|
1132
|
+
}),
|
|
1089
1133
|
});
|
|
1090
1134
|
|
|
1091
1135
|
if (type === 'chroot-debootstrap' || type === 'chroot-container')
|
|
@@ -1100,11 +1144,7 @@ rm -rf ${artifacts.join(' ')}`);
|
|
|
1100
1144
|
macAddress,
|
|
1101
1145
|
ipAddress,
|
|
1102
1146
|
hostname,
|
|
1103
|
-
architecture:
|
|
1104
|
-
workflowsConfig[workflowId].maas?.commissioning?.architecture ||
|
|
1105
|
-
workflowsConfig[workflowId].container?.architecture ||
|
|
1106
|
-
workflowsConfig[workflowId].debootstrap?.image?.architecture ||
|
|
1107
|
-
'arm64/generic',
|
|
1147
|
+
architecture: Underpost.baremetal.fallbackArchitecture(workflowsConfig[workflowId]),
|
|
1108
1148
|
machine,
|
|
1109
1149
|
};
|
|
1110
1150
|
logger.info('Waiting for commissioning...', {
|
|
@@ -1112,7 +1152,42 @@ rm -rf ${artifacts.join(' ')}`);
|
|
|
1112
1152
|
machine: machine ? machine.system_id : null,
|
|
1113
1153
|
});
|
|
1114
1154
|
|
|
1115
|
-
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
|
+
}
|
|
1116
1191
|
|
|
1117
1192
|
if ((type === 'chroot-debootstrap' || type === 'chroot-container') && options.cloudInit === true) {
|
|
1118
1193
|
openTerminal(`node ${underpostRoot}/bin baremetal ${workflowId} ${ipAddress} ${hostname} --logs cloud-init`);
|
|
@@ -1126,6 +1201,164 @@ rm -rf ${artifacts.join(' ')}`);
|
|
|
1126
1201
|
}
|
|
1127
1202
|
},
|
|
1128
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
|
+
|
|
1129
1362
|
/**
|
|
1130
1363
|
* @method macAddressFactory
|
|
1131
1364
|
* @description Generates or returns a MAC address based on options.
|
|
@@ -1209,7 +1442,7 @@ rm -rf ${artifacts.join(' ')}`);
|
|
|
1209
1442
|
shellExec(`mkdir -p ${mountPoint}`);
|
|
1210
1443
|
|
|
1211
1444
|
// Ensure mount point is not already mounted
|
|
1212
|
-
shellExec(`sudo umount ${mountPoint} 2>/dev/null
|
|
1445
|
+
shellExec(`sudo umount ${mountPoint} 2>/dev/null`);
|
|
1213
1446
|
|
|
1214
1447
|
try {
|
|
1215
1448
|
// Mount the ISO
|
|
@@ -1231,10 +1464,10 @@ rm -rf ${artifacts.join(' ')}`);
|
|
|
1231
1464
|
|
|
1232
1465
|
shellExec(`sudo chown -R $(whoami):$(whoami) ${extractDir}`);
|
|
1233
1466
|
// Unmount ISO
|
|
1234
|
-
shellExec(`sudo umount ${mountPoint}
|
|
1467
|
+
shellExec(`sudo umount ${mountPoint}`);
|
|
1235
1468
|
logger.info(`Unmounted ISO`);
|
|
1236
1469
|
// Clean up temporary mount point
|
|
1237
|
-
shellExec(`rmdir ${mountPoint}
|
|
1470
|
+
shellExec(`rmdir ${mountPoint}`);
|
|
1238
1471
|
}
|
|
1239
1472
|
|
|
1240
1473
|
return {
|
|
@@ -1251,8 +1484,8 @@ rm -rf ${artifacts.join(' ')}`);
|
|
|
1251
1484
|
* @param {string} options.macAddress - The MAC address of the machine.
|
|
1252
1485
|
* @param {string} options.hostname - The hostname for the machine.
|
|
1253
1486
|
* @param {string} options.ipAddress - The IP address for the machine.
|
|
1487
|
+
* @param {string} options.architecture - The architecture for the machine (default is 'arm64').
|
|
1254
1488
|
* @param {string} options.powerType - The power type for the machine (default is 'manual').
|
|
1255
|
-
* @param {object} options.maas - Additional MAAS-specific options.
|
|
1256
1489
|
* @returns {object} An object containing the created machine details.
|
|
1257
1490
|
* @memberof UnderpostBaremetal
|
|
1258
1491
|
*/
|
|
@@ -1261,13 +1494,13 @@ rm -rf ${artifacts.join(' ')}`);
|
|
|
1261
1494
|
macAddress: '',
|
|
1262
1495
|
hostname: '',
|
|
1263
1496
|
ipAddress: '',
|
|
1497
|
+
architecture: 'arm64',
|
|
1264
1498
|
powerType: 'manual',
|
|
1265
|
-
architecture: 'arm64/generic',
|
|
1266
1499
|
},
|
|
1267
1500
|
) {
|
|
1268
1501
|
if (!options.powerType) options.powerType = 'manual';
|
|
1269
1502
|
const payload = {
|
|
1270
|
-
architecture:
|
|
1503
|
+
architecture: options.architecture.match('arm') ? 'arm64/generic' : 'amd64/generic',
|
|
1271
1504
|
mac_address: options.macAddress,
|
|
1272
1505
|
mac_addresses: options.macAddress,
|
|
1273
1506
|
hostname: options.hostname,
|
|
@@ -1275,18 +1508,13 @@ rm -rf ${artifacts.join(' ')}`);
|
|
|
1275
1508
|
ip: options.ipAddress,
|
|
1276
1509
|
};
|
|
1277
1510
|
logger.info('Creating MAAS machine', payload);
|
|
1278
|
-
const machine =
|
|
1279
|
-
`
|
|
1511
|
+
const machine = Underpost.baremetal.maasCliExec(
|
|
1512
|
+
`machines create ${Object.keys(payload)
|
|
1280
1513
|
.map((k) => `${k}="${payload[k]}"`)
|
|
1281
1514
|
.join(' ')}`,
|
|
1282
|
-
{
|
|
1283
|
-
silent: true,
|
|
1284
|
-
stdout: true,
|
|
1285
|
-
},
|
|
1286
1515
|
);
|
|
1287
|
-
// console.log(machine);
|
|
1288
1516
|
try {
|
|
1289
|
-
return { machine
|
|
1517
|
+
return { machine };
|
|
1290
1518
|
} catch (error) {
|
|
1291
1519
|
console.log(error);
|
|
1292
1520
|
logger.error(error);
|
|
@@ -1323,13 +1551,7 @@ rm -rf ${artifacts.join(' ')}`);
|
|
|
1323
1551
|
return { kernelFilesPaths, resourcesPath };
|
|
1324
1552
|
}
|
|
1325
1553
|
|
|
1326
|
-
const resourceData =
|
|
1327
|
-
shellExec(`maas ${process.env.MAAS_ADMIN_USERNAME} boot-resource read ${resource.id}`, {
|
|
1328
|
-
stdout: true,
|
|
1329
|
-
silent: true,
|
|
1330
|
-
disableLog: true,
|
|
1331
|
-
}),
|
|
1332
|
-
);
|
|
1554
|
+
const resourceData = Underpost.baremetal.maasCliExec(`boot-resource read ${resource.id}`);
|
|
1333
1555
|
let kernelFilesPaths = {};
|
|
1334
1556
|
const bootFiles = resourceData.sets[Object.keys(resourceData.sets)[0]].files;
|
|
1335
1557
|
const arch = resource.architecture.split('/')[0];
|
|
@@ -1390,7 +1612,7 @@ rm -rf ${artifacts.join(' ')}`);
|
|
|
1390
1612
|
shellExec(`mkdir -p ${tempExtractDir}`);
|
|
1391
1613
|
|
|
1392
1614
|
// List files in archive to find kernel and initrd
|
|
1393
|
-
const tarList = shellExec(`tar -tf ${rootArchivePath}
|
|
1615
|
+
const tarList = shellExec(`tar -tf ${rootArchivePath}`).stdout.split('\n');
|
|
1394
1616
|
|
|
1395
1617
|
// Look for boot/vmlinuz* and boot/initrd* (handling potential leading ./)
|
|
1396
1618
|
// Skip rescue, kdump, and other special images
|
|
@@ -1522,7 +1744,7 @@ rm -rf ${artifacts.join(' ')}`);
|
|
|
1522
1744
|
*/
|
|
1523
1745
|
removeDiscoveredMachines() {
|
|
1524
1746
|
logger.info('Removing all discovered machines from MAAS...');
|
|
1525
|
-
|
|
1747
|
+
Underpost.baremetal.maasCliExec(`discoveries clear all=true`);
|
|
1526
1748
|
},
|
|
1527
1749
|
|
|
1528
1750
|
/**
|
|
@@ -1854,6 +2076,73 @@ shell
|
|
|
1854
2076
|
};
|
|
1855
2077
|
},
|
|
1856
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
|
+
|
|
1857
2146
|
/**
|
|
1858
2147
|
* @method httpBootstrapServerStaticFactory
|
|
1859
2148
|
* @description Creates static files for the bootstrap HTTP server including cloud-init configuration.
|
|
@@ -1863,6 +2152,7 @@ shell
|
|
|
1863
2152
|
* @param {string} params.cloudConfigSrc - The cloud-init configuration YAML source.
|
|
1864
2153
|
* @param {object} [params.metadata] - Optional metadata to include in meta-data file.
|
|
1865
2154
|
* @param {string} [params.vendorData] - Optional vendor-data content (default: empty string).
|
|
2155
|
+
* @param {string} [params.isoUrl] - Optional ISO URL to cache and serve.
|
|
1866
2156
|
* @memberof UnderpostBaremetal
|
|
1867
2157
|
* @returns {void}
|
|
1868
2158
|
*/
|
|
@@ -1872,16 +2162,13 @@ shell
|
|
|
1872
2162
|
cloudConfigSrc,
|
|
1873
2163
|
metadata = {},
|
|
1874
2164
|
vendorData = '',
|
|
2165
|
+
isoUrl = '',
|
|
1875
2166
|
}) {
|
|
1876
2167
|
// Create directory structure
|
|
1877
2168
|
shellExec(`mkdir -p ${bootstrapHttpServerPath}/${hostname}/cloud-init`);
|
|
1878
2169
|
|
|
1879
2170
|
// Write user-data file
|
|
1880
|
-
fs.writeFileSync(
|
|
1881
|
-
`${bootstrapHttpServerPath}/${hostname}/cloud-init/user-data`,
|
|
1882
|
-
`#cloud-config\n${cloudConfigSrc}`,
|
|
1883
|
-
'utf8',
|
|
1884
|
-
);
|
|
2171
|
+
fs.writeFileSync(`${bootstrapHttpServerPath}/${hostname}/cloud-init/user-data`, cloudConfigSrc, 'utf8');
|
|
1885
2172
|
|
|
1886
2173
|
// Write meta-data file
|
|
1887
2174
|
const metaDataContent = `instance-id: ${metadata.instanceId || hostname}\nlocal-hostname: ${metadata.localHostname || hostname}`;
|
|
@@ -1891,6 +2178,22 @@ shell
|
|
|
1891
2178
|
fs.writeFileSync(`${bootstrapHttpServerPath}/${hostname}/cloud-init/vendor-data`, vendorData, 'utf8');
|
|
1892
2179
|
|
|
1893
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
|
+
}
|
|
1894
2197
|
},
|
|
1895
2198
|
|
|
1896
2199
|
/**
|
|
@@ -1911,14 +2214,17 @@ shell
|
|
|
1911
2214
|
const hostname = options.hostname || 'localhost';
|
|
1912
2215
|
|
|
1913
2216
|
shellExec(`mkdir -p ${bootstrapHttpServerPath}/${hostname}/cloud-init`);
|
|
2217
|
+
shellExec(`node bin run kill ${port}`);
|
|
1914
2218
|
|
|
1915
|
-
|
|
1916
|
-
shellExec(`sudo pkill -f 'python3 -m http.server ${port}'`, { silent: true });
|
|
2219
|
+
const app = express();
|
|
1917
2220
|
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
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
|
+
});
|
|
1922
2228
|
|
|
1923
2229
|
// Configure iptables to allow incoming LAN connections
|
|
1924
2230
|
shellExec(
|
|
@@ -1961,7 +2267,7 @@ shell
|
|
|
1961
2267
|
// GRUB on ARM64 often crashes with synchronous exception (0x200) if handling large compressed kernels directly.
|
|
1962
2268
|
if (file === 'vmlinuz-efi') {
|
|
1963
2269
|
const kernelDest = `${tftpRootPath}/pxe/${file}`;
|
|
1964
|
-
const fileType = shellExec(`file ${kernelDest}
|
|
2270
|
+
const fileType = shellExec(`file ${kernelDest}`).stdout;
|
|
1965
2271
|
|
|
1966
2272
|
// Handle gzip compressed kernels
|
|
1967
2273
|
if (fileType.includes('gzip compressed data')) {
|
|
@@ -1999,8 +2305,14 @@ shell
|
|
|
1999
2305
|
* @param {string} options.macAddress - The MAC address of the client.
|
|
2000
2306
|
* @param {boolean} options.cloudInit - Whether to include cloud-init parameters.
|
|
2001
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).
|
|
2002
2309
|
* @param {boolean} [options.dev=false] - Whether to enable dev mode with dracut debugging parameters.
|
|
2003
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.
|
|
2004
2316
|
* @returns {object} An object containing the constructed command line string.
|
|
2005
2317
|
* @memberof UnderpostBaremetal
|
|
2006
2318
|
*/
|
|
@@ -2022,6 +2334,7 @@ shell
|
|
|
2022
2334
|
machine: { system_id: '' },
|
|
2023
2335
|
dev: false,
|
|
2024
2336
|
osIdLike: '',
|
|
2337
|
+
authCredentials: { consumer_key: '', consumer_secret: '', token_key: '', token_secret: '' },
|
|
2025
2338
|
},
|
|
2026
2339
|
) {
|
|
2027
2340
|
// Construct kernel command line arguments for NFS boot.
|
|
@@ -2042,10 +2355,9 @@ shell
|
|
|
2042
2355
|
osIdLike,
|
|
2043
2356
|
} = options;
|
|
2044
2357
|
|
|
2045
|
-
const ipParam =
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
: 'ip=dhcp';
|
|
2358
|
+
const ipParam =
|
|
2359
|
+
`ip=${ipClient}:${ipFileServer}:${ipDhcpServer}:${netmask}:${hostname}` +
|
|
2360
|
+
`:${networkInterfaceName ? networkInterfaceName : 'eth0'}:${ipConfig}:${dnsServer}`;
|
|
2049
2361
|
|
|
2050
2362
|
const nfsOptions = `${
|
|
2051
2363
|
type === 'chroot-debootstrap' || type === 'chroot-container'
|
|
@@ -2092,7 +2404,7 @@ shell
|
|
|
2092
2404
|
// `toram`,
|
|
2093
2405
|
'nomodeset',
|
|
2094
2406
|
`editable_rootfs=tmpfs`,
|
|
2095
|
-
`ramdisk_size=3550000`,
|
|
2407
|
+
// `ramdisk_size=3550000`,
|
|
2096
2408
|
// `root=/dev/sda1`, // rpi4 usb port unit
|
|
2097
2409
|
'apparmor=0', // Disable AppArmor security
|
|
2098
2410
|
...(networkInterfaceName === 'eth0'
|
|
@@ -2136,49 +2448,32 @@ shell
|
|
|
2136
2448
|
if (type === 'iso-ram') {
|
|
2137
2449
|
const netBootParams = [`netboot=url`];
|
|
2138
2450
|
if (fileSystemUrl) netBootParams.push(`url=${fileSystemUrl.replace('https', 'http')}`);
|
|
2139
|
-
cmd = [ipParam, `boot=casper`, ...netBootParams, ...kernelParams];
|
|
2451
|
+
cmd = [ipParam, `boot=casper`, 'toram', ...netBootParams, ...kernelParams, ...performanceParams];
|
|
2140
2452
|
} else if (type === 'chroot-debootstrap' || type === 'chroot-container') {
|
|
2141
2453
|
let qemuNfsRootParams = [`root=/dev/nfs`, `rootfstype=nfs`];
|
|
2142
|
-
|
|
2143
|
-
// Determine OS family from osIdLike configuration
|
|
2144
|
-
const isRhelBased = osIdLike && osIdLike.match(/rhel|centos|fedora|alma|rocky/i);
|
|
2145
|
-
const isDebianBased = osIdLike && osIdLike.match(/debian|ubuntu/i);
|
|
2146
|
-
|
|
2147
|
-
// Add RHEL/Rocky/Fedora based images specific parameters
|
|
2148
|
-
if (isRhelBased) {
|
|
2149
|
-
qemuNfsRootParams = qemuNfsRootParams.concat([`rd.neednet=1`, `rd.timeout=180`, `selinux=0`, `enforcing=0`]);
|
|
2150
|
-
}
|
|
2151
|
-
// Add Debian/Ubuntu based images specific parameters
|
|
2152
|
-
else if (isDebianBased) {
|
|
2153
|
-
qemuNfsRootParams = qemuNfsRootParams.concat([`initrd=initrd.img`, `init=/sbin/init`]);
|
|
2154
|
-
}
|
|
2155
|
-
|
|
2156
|
-
// Add debugging parameters in dev mode for dracut troubleshooting
|
|
2157
|
-
if (options.dev) {
|
|
2158
|
-
// qemuNfsRootParams = qemuNfsRootParams.concat([`rd.shell`, `rd.debug`]);
|
|
2159
|
-
}
|
|
2160
|
-
|
|
2161
2454
|
cmd = [ipParam, ...qemuNfsRootParams, nfsRootParam, ...kernelParams];
|
|
2162
2455
|
} else {
|
|
2163
2456
|
// 'iso-nfs'
|
|
2164
2457
|
cmd = [ipParam, `netboot=nfs`, nfsRootParam, ...kernelParams, ...performanceParams];
|
|
2458
|
+
// cmd.push(`ifname=${networkInterfaceName}:${macAddress}`);
|
|
2459
|
+
}
|
|
2165
2460
|
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
`ds=nocloud-net;s=${cloudInitPreseedUrl}`,
|
|
2175
|
-
`log_host=${ipDhcpServer}`,
|
|
2176
|
-
`log_port=5247`,
|
|
2177
|
-
// `BOOTIF=${macAddress}`,
|
|
2178
|
-
// `cc:{'datasource_list': ['MAAS']}end_cc`,
|
|
2179
|
-
]);
|
|
2180
|
-
}
|
|
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`]);
|
|
2181
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`]);
|
|
2474
|
+
}
|
|
2475
|
+
|
|
2476
|
+
if (cloudInit) cmd = Underpost.cloudInit.kernelParamsFactory(macAddress, cmd, options);
|
|
2182
2477
|
// cmd.push('---');
|
|
2183
2478
|
const cmdStr = cmd.join(' ');
|
|
2184
2479
|
logger.info('Constructed kernel command line');
|
|
@@ -2202,12 +2497,7 @@ shell
|
|
|
2202
2497
|
async commissionMonitor({ macAddress, ipAddress, hostname, architecture, machine }) {
|
|
2203
2498
|
{
|
|
2204
2499
|
// Query observed discoveries from MAAS.
|
|
2205
|
-
const discoveries =
|
|
2206
|
-
shellExec(`maas ${process.env.MAAS_ADMIN_USERNAME} discoveries read`, {
|
|
2207
|
-
silent: true,
|
|
2208
|
-
stdout: true,
|
|
2209
|
-
}),
|
|
2210
|
-
);
|
|
2500
|
+
const discoveries = Underpost.baremetal.maasCliExec(`discoveries read`);
|
|
2211
2501
|
|
|
2212
2502
|
for (const discovery of discoveries) {
|
|
2213
2503
|
const discoverHostname = discovery.hostname
|
|
@@ -2225,6 +2515,20 @@ shell
|
|
|
2225
2515
|
if (discovery.ip === ipAddress) {
|
|
2226
2516
|
logger.info('Machine discovered!', discovery);
|
|
2227
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
|
+
|
|
2228
2532
|
logger.info('Creating new machine with discovered hardware MAC...', {
|
|
2229
2533
|
discoveredMAC: discovery.mac_address,
|
|
2230
2534
|
ipAddress,
|
|
@@ -2236,12 +2540,18 @@ shell
|
|
|
2236
2540
|
hostname,
|
|
2237
2541
|
architecture,
|
|
2238
2542
|
}).machine;
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
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
|
+
}
|
|
2245
2555
|
} else {
|
|
2246
2556
|
const systemId = machine.system_id;
|
|
2247
2557
|
console.log('Using pre-registered machine system_id:', systemId.bgYellow.bold.black);
|
|
@@ -2254,22 +2564,45 @@ shell
|
|
|
2254
2564
|
discoveredMAC: discovery.mac_address,
|
|
2255
2565
|
});
|
|
2256
2566
|
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
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
|
+
}
|
|
2260
2587
|
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
`maas ${process.env.MAAS_ADMIN_USERNAME} interface update ${systemId} ${machine.boot_interface.id}` +
|
|
2264
|
-
` mac_address=${discovery.mac_address}`,
|
|
2265
|
-
{
|
|
2266
|
-
silent: true,
|
|
2267
|
-
},
|
|
2588
|
+
Underpost.baremetal.maasCliExec(
|
|
2589
|
+
`interface update ${systemId} ${machine.boot_interface.id}` + ` mac_address=${discovery.mac_address}`,
|
|
2268
2590
|
);
|
|
2269
2591
|
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
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
|
+
}
|
|
2273
2606
|
|
|
2274
2607
|
logger.info('✓ Machine interface MAC address updated successfully');
|
|
2275
2608
|
|
|
@@ -2298,6 +2631,73 @@ shell
|
|
|
2298
2631
|
}
|
|
2299
2632
|
},
|
|
2300
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
|
+
|
|
2301
2701
|
/**
|
|
2302
2702
|
* @method mountBinfmtMisc
|
|
2303
2703
|
* @description Mounts the binfmt_misc filesystem to enable QEMU user-static binfmt support.
|
|
@@ -2333,7 +2733,7 @@ shell
|
|
|
2333
2733
|
const systemId = typeof machine === 'string' ? machine : machine.system_id;
|
|
2334
2734
|
if (ignore && ignore.find((mId) => mId === systemId)) continue;
|
|
2335
2735
|
logger.info(`Removing machine: ${systemId}`);
|
|
2336
|
-
|
|
2736
|
+
Underpost.baremetal.maasCliExec(`machine delete ${systemId}`);
|
|
2337
2737
|
}
|
|
2338
2738
|
return [];
|
|
2339
2739
|
},
|
|
@@ -2347,9 +2747,9 @@ shell
|
|
|
2347
2747
|
* @returns {void}
|
|
2348
2748
|
*/
|
|
2349
2749
|
clearDiscoveries({ force }) {
|
|
2350
|
-
|
|
2750
|
+
Underpost.baremetal.maasCliExec(`discoveries clear all=true`);
|
|
2351
2751
|
if (force === true) {
|
|
2352
|
-
|
|
2752
|
+
Underpost.baremetal.maasCliExec(`discoveries scan force=true`);
|
|
2353
2753
|
}
|
|
2354
2754
|
},
|
|
2355
2755
|
|
|
@@ -2965,10 +3365,10 @@ udp-port = 32766
|
|
|
2965
3365
|
// Check both /usr/local/bin (compiled) and system paths
|
|
2966
3366
|
let qemuAarch64Path = null;
|
|
2967
3367
|
|
|
2968
|
-
if (shellExec('test -x /usr/local/bin/qemu-system-aarch64'
|
|
3368
|
+
if (shellExec('test -x /usr/local/bin/qemu-system-aarch64').code === 0) {
|
|
2969
3369
|
qemuAarch64Path = '/usr/local/bin/qemu-system-aarch64';
|
|
2970
|
-
} else if (shellExec('which qemu-system-aarch64'
|
|
2971
|
-
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();
|
|
2972
3372
|
}
|
|
2973
3373
|
|
|
2974
3374
|
if (!qemuAarch64Path) {
|
|
@@ -2981,7 +3381,7 @@ udp-port = 32766
|
|
|
2981
3381
|
logger.info(`Found qemu-system-aarch64 at: ${qemuAarch64Path}`);
|
|
2982
3382
|
|
|
2983
3383
|
// Verify that the installed qemu supports the 'virt' machine type (required for arm64)
|
|
2984
|
-
const machineHelp = shellExec(`${qemuAarch64Path} -machine help
|
|
3384
|
+
const machineHelp = shellExec(`${qemuAarch64Path} -machine help`).stdout;
|
|
2985
3385
|
if (!machineHelp.includes('virt')) {
|
|
2986
3386
|
throw new Error(
|
|
2987
3387
|
'The installed qemu-system-aarch64 does not support the "virt" machine type.\n' +
|
|
@@ -2994,10 +3394,10 @@ udp-port = 32766
|
|
|
2994
3394
|
// Check both /usr/local/bin (compiled) and system paths
|
|
2995
3395
|
let qemuX86Path = null;
|
|
2996
3396
|
|
|
2997
|
-
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) {
|
|
2998
3398
|
qemuX86Path = '/usr/local/bin/qemu-system-x86_64';
|
|
2999
|
-
} else if (shellExec('which qemu-system-x86_64'
|
|
3000
|
-
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();
|
|
3001
3401
|
}
|
|
3002
3402
|
|
|
3003
3403
|
if (!qemuX86Path) {
|
|
@@ -3010,7 +3410,7 @@ udp-port = 32766
|
|
|
3010
3410
|
logger.info(`Found qemu-system-x86_64 at: ${qemuX86Path}`);
|
|
3011
3411
|
|
|
3012
3412
|
// Verify that the installed qemu supports the 'pc' or 'q35' machine type (required for x86_64)
|
|
3013
|
-
const machineHelp = shellExec(`${qemuX86Path} -machine help
|
|
3413
|
+
const machineHelp = shellExec(`${qemuX86Path} -machine help`).stdout;
|
|
3014
3414
|
if (!machineHelp.includes('pc') && !machineHelp.includes('q35')) {
|
|
3015
3415
|
throw new Error(
|
|
3016
3416
|
'The installed qemu-system-x86_64 does not support the "pc" or "q35" machine type.\n' +
|