@underpostnet/underpost 2.96.1 → 2.97.1
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/.dockerignore +1 -2
- package/.env.development +0 -3
- package/.env.production +0 -3
- package/.env.test +0 -3
- package/.prettierignore +1 -2
- package/README.md +31 -31
- package/baremetal/commission-workflows.json +94 -17
- package/bin/deploy.js +1 -1
- package/cli.md +75 -41
- package/conf.js +1 -0
- package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
- package/manifests/deployment/dd-test-development/deployment.yaml +4 -4
- package/package.json +3 -2
- package/packer/scripts/fuse-tar-root +3 -3
- package/scripts/disk-clean.sh +128 -187
- package/scripts/gpu-diag.sh +2 -2
- package/scripts/ip-info.sh +11 -11
- package/scripts/ipxe-setup.sh +197 -0
- package/scripts/maas-upload-boot-resource.sh +1 -1
- package/scripts/nvim.sh +1 -1
- package/scripts/packer-setup.sh +13 -13
- package/scripts/ports-ls.sh +31 -0
- package/scripts/quick-tftp.sh +19 -0
- package/scripts/rocky-setup.sh +2 -2
- package/scripts/rpmfusion-ffmpeg-setup.sh +4 -4
- package/scripts/ssl.sh +7 -7
- package/src/api/document/document.controller.js +15 -0
- package/src/api/document/document.model.js +44 -1
- package/src/api/document/document.router.js +2 -0
- package/src/api/document/document.service.js +398 -26
- package/src/cli/baremetal.js +2001 -463
- package/src/cli/cloud-init.js +354 -231
- package/src/cli/cluster.js +51 -53
- package/src/cli/db.js +22 -0
- package/src/cli/deploy.js +7 -3
- package/src/cli/image.js +1 -0
- package/src/cli/index.js +40 -37
- package/src/cli/lxd.js +3 -3
- package/src/cli/run.js +78 -12
- package/src/cli/ssh.js +1 -1
- package/src/client/components/core/Css.js +16 -2
- package/src/client/components/core/Input.js +3 -1
- package/src/client/components/core/Modal.js +125 -159
- package/src/client/components/core/Panel.js +436 -31
- package/src/client/components/core/PanelForm.js +222 -37
- package/src/client/components/core/SearchBox.js +801 -0
- package/src/client/components/core/Translate.js +11 -0
- package/src/client/services/document/document.service.js +42 -0
- package/src/index.js +1 -1
- package/src/server/dns.js +12 -6
- package/src/server/start.js +14 -6
package/src/cli/baremetal.js
CHANGED
|
@@ -15,8 +15,9 @@ import path from 'path';
|
|
|
15
15
|
import Downloader from '../server/downloader.js';
|
|
16
16
|
import UnderpostCloudInit from './cloud-init.js';
|
|
17
17
|
import UnderpostRepository from './repository.js';
|
|
18
|
-
import { s4, timer } from '../client/components/core/CommonJs.js';
|
|
18
|
+
import { newInstance, range, s4, timer } from '../client/components/core/CommonJs.js';
|
|
19
19
|
import { spawnSync } from 'child_process';
|
|
20
|
+
import Underpost from '../index.js';
|
|
20
21
|
|
|
21
22
|
const logger = loggerFactory(import.meta);
|
|
22
23
|
|
|
@@ -49,22 +50,44 @@ class UnderpostBaremetal {
|
|
|
49
50
|
* It handles NFS root filesystem building, control server installation/uninstallation,
|
|
50
51
|
* and system-level provisioning tasks like timezone and keyboard configuration.
|
|
51
52
|
* @param {string} [workflowId='rpi4mb'] - Identifier for the specific workflow configuration to use.
|
|
52
|
-
* @param {string} [hostname=workflowId] - The hostname of the target baremetal machine.
|
|
53
53
|
* @param {string} [ipAddress=getLocalIPv4Address()] - The IP address of the control server or the local machine.
|
|
54
|
+
* @param {string} [hostname=workflowId] - The hostname of the target baremetal machine.
|
|
55
|
+
* @param {string} [ipFileServer=getLocalIPv4Address()] - The IP address of the file server (NFS/TFTP).
|
|
56
|
+
* @param {string} [ipConfig=''] - IP configuration string for the baremetal machine.
|
|
57
|
+
* @param {string} [netmask=''] - Netmask of network
|
|
58
|
+
* @param {string} [dnsServer=''] - DNS server IP address.
|
|
54
59
|
* @param {object} [options] - An object containing boolean flags for various operations.
|
|
55
60
|
* @param {boolean} [options.dev=false] - Development mode flag.
|
|
56
61
|
* @param {boolean} [options.controlServerInstall=false] - Flag to install the control server (e.g., MAAS).
|
|
57
62
|
* @param {boolean} [options.controlServerUninstall=false] - Flag to uninstall the control server.
|
|
63
|
+
* @param {boolean} [options.controlServerRestart=false] - Flag to restart the control server.
|
|
58
64
|
* @param {boolean} [options.controlServerDbInstall=false] - Flag to install the control server's database.
|
|
65
|
+
* @param {boolean} [options.createMachine=false] - Flag to create a machine in MAAS.
|
|
59
66
|
* @param {boolean} [options.controlServerDbUninstall=false] - Flag to uninstall the control server's database.
|
|
67
|
+
* @param {string} [options.mac=''] - MAC address of the baremetal machine.
|
|
68
|
+
* @param {boolean} [options.ipxe=false] - Flag to use iPXE for booting.
|
|
69
|
+
* @param {boolean} [options.ipxeRebuild=false] - Flag to rebuild the iPXE binary with embedded script.
|
|
60
70
|
* @param {boolean} [options.installPacker=false] - Flag to install Packer CLI.
|
|
61
71
|
* @param {string} [options.packerMaasImageTemplate] - Template path from canonical/packer-maas to extract (requires workflow-id).
|
|
62
72
|
* @param {string} [options.packerWorkflowId] - Workflow ID for Packer MAAS image operations (used with --packer-maas-image-build or --packer-maas-image-upload).
|
|
63
73
|
* @param {boolean} [options.packerMaasImageBuild=false] - Flag to build a Packer MAAS image for the workflow specified by packerWorkflowId.
|
|
64
74
|
* @param {boolean} [options.packerMaasImageUpload=false] - Flag to upload a Packer MAAS image artifact without rebuilding for the workflow specified by packerWorkflowId.
|
|
75
|
+
* @param {boolean} [options.packerMaasImageCached=false] - Flag to use cached artifacts when building the Packer MAAS image.
|
|
76
|
+
* @param {string} [options.removeMachines=''] - Comma-separated list of machine system IDs or '*' to remove existing machines from MAAS before commissioning.
|
|
77
|
+
* @param {boolean} [options.clearDiscovered=false] - Flag to clear discovered machines from MAAS before commissioning.
|
|
65
78
|
* @param {boolean} [options.cloudInitUpdate=false] - Flag to update cloud-init configuration on the baremetal machine.
|
|
66
79
|
* @param {boolean} [options.commission=false] - Flag to commission the baremetal machine.
|
|
80
|
+
* @param {number} [options.bootstrapHttpServerPort=8888] - Port for the bootstrap HTTP server.
|
|
81
|
+
* @param {string} [options.bootstrapHttpServerPath='./public/localhost'] - Path for the bootstrap HTTP server files.
|
|
82
|
+
* @param {string} [options.isoUrl=''] - Uses a custom ISO URL for baremetal machine commissioning.
|
|
83
|
+
* @param {boolean} [options.ubuntuToolsBuild=false] - Builds ubuntu tools for chroot environment.
|
|
84
|
+
* @param {boolean} [options.ubuntuToolsTest=false] - Tests ubuntu tools in chroot environment.
|
|
85
|
+
* @param {boolean} [options.rockyToolsBuild=false] - Builds rocky linux tools for chroot environment.
|
|
86
|
+
* @param {boolean} [options.rockyToolsTest=false] - Tests rocky linux tools in chroot environment.
|
|
87
|
+
* @param {string} [options.bootcmd=''] - Comma-separated list of boot commands to execute.
|
|
88
|
+
* @param {string} [options.runcmd=''] - Comma-separated list of run commands to execute.
|
|
67
89
|
* @param {boolean} [options.nfsBuild=false] - Flag to build the NFS root filesystem.
|
|
90
|
+
* @param {boolean} [options.nfsBuildServer=false] - Flag to build the NFS server components.
|
|
68
91
|
* @param {boolean} [options.nfsMount=false] - Flag to mount the NFS root filesystem.
|
|
69
92
|
* @param {boolean} [options.nfsUnmount=false] - Flag to unmount the NFS root filesystem.
|
|
70
93
|
* @param {boolean} [options.nfsSh=false] - Flag to chroot into the NFS environment for shell access.
|
|
@@ -74,23 +97,45 @@ class UnderpostBaremetal {
|
|
|
74
97
|
*/
|
|
75
98
|
async callback(
|
|
76
99
|
workflowId,
|
|
77
|
-
hostname,
|
|
78
100
|
ipAddress,
|
|
101
|
+
hostname,
|
|
102
|
+
ipFileServer,
|
|
103
|
+
ipConfig,
|
|
104
|
+
netmask,
|
|
105
|
+
dnsServer,
|
|
79
106
|
options = {
|
|
80
107
|
dev: false,
|
|
81
108
|
controlServerInstall: false,
|
|
82
109
|
controlServerUninstall: false,
|
|
110
|
+
controlServerRestart: false,
|
|
83
111
|
controlServerDbInstall: false,
|
|
84
112
|
controlServerDbUninstall: false,
|
|
113
|
+
createMachine: false,
|
|
114
|
+
mac: '',
|
|
115
|
+
ipxe: false,
|
|
116
|
+
ipxeRebuild: false,
|
|
85
117
|
installPacker: false,
|
|
86
118
|
packerMaasImageTemplate: false,
|
|
87
119
|
packerWorkflowId: '',
|
|
88
120
|
packerMaasImageBuild: false,
|
|
89
121
|
packerMaasImageUpload: false,
|
|
90
122
|
packerMaasImageCached: false,
|
|
123
|
+
removeMachines: '',
|
|
124
|
+
clearDiscovered: false,
|
|
91
125
|
cloudInitUpdate: false,
|
|
126
|
+
cloudInit: false,
|
|
92
127
|
commission: false,
|
|
128
|
+
bootstrapHttpServerPort: 8888,
|
|
129
|
+
bootstrapHttpServerPath: './public/localhost',
|
|
130
|
+
isoUrl: '',
|
|
131
|
+
ubuntuToolsBuild: false,
|
|
132
|
+
ubuntuToolsTest: false,
|
|
133
|
+
rockyToolsBuild: false,
|
|
134
|
+
rockyToolsTest: false,
|
|
135
|
+
bootcmd: '',
|
|
136
|
+
runcmd: '',
|
|
93
137
|
nfsBuild: false,
|
|
138
|
+
nfsBuildServer: false,
|
|
94
139
|
nfsMount: false,
|
|
95
140
|
nfsUnmount: false,
|
|
96
141
|
nfsSh: false,
|
|
@@ -105,15 +150,41 @@ class UnderpostBaremetal {
|
|
|
105
150
|
const underpostRoot = options?.dev === true ? '.' : `${npmRoot}/underpost`;
|
|
106
151
|
|
|
107
152
|
// Set default values if not provided.
|
|
108
|
-
workflowId = workflowId ? workflowId : '
|
|
153
|
+
workflowId = workflowId ? workflowId : 'rpi4mbarm64-iso-ram';
|
|
109
154
|
hostname = hostname ? hostname : workflowId;
|
|
110
|
-
ipAddress = ipAddress ? ipAddress : '192.168.1.
|
|
155
|
+
ipAddress = ipAddress ? ipAddress : '192.168.1.191';
|
|
156
|
+
ipFileServer = ipFileServer ? ipFileServer : getLocalIPv4Address();
|
|
157
|
+
netmask = netmask ? netmask : '255.255.255.0';
|
|
158
|
+
dnsServer = dnsServer ? dnsServer : '8.8.8.8';
|
|
159
|
+
|
|
160
|
+
// IpConfig options:
|
|
161
|
+
// dhcp - DHCP configuration
|
|
162
|
+
// dhpc6 - DHCP IPv6 configuration
|
|
163
|
+
// auto6 - automatic IPv6 configuration
|
|
164
|
+
// on, any - any protocol available in the kernel (default)
|
|
165
|
+
// none, off - no autoconfiguration, static network configuration
|
|
166
|
+
ipConfig = ipConfig ? ipConfig : 'none';
|
|
111
167
|
|
|
112
168
|
// Set default MAC address
|
|
113
|
-
let macAddress =
|
|
169
|
+
let macAddress = UnderpostBaremetal.API.macAddressFactory(options).mac;
|
|
170
|
+
const workflowsConfig = UnderpostBaremetal.API.loadWorkflowsConfig();
|
|
171
|
+
|
|
172
|
+
if (!workflowsConfig[workflowId]) {
|
|
173
|
+
throw new Error(`Workflow configuration not found for ID: ${workflowId}`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const tftpPrefix = workflowsConfig[workflowId].tftpPrefix || 'rpi4mb';
|
|
177
|
+
// Define the bootstrap architecture.
|
|
178
|
+
let bootstrapArch;
|
|
114
179
|
|
|
115
|
-
//
|
|
116
|
-
|
|
180
|
+
// Set bootstrap architecture.
|
|
181
|
+
if (workflowsConfig[workflowId].type === 'chroot-debootstrap') {
|
|
182
|
+
const { architecture } = workflowsConfig[workflowId].debootstrap.image;
|
|
183
|
+
bootstrapArch = architecture;
|
|
184
|
+
} else if (workflowsConfig[workflowId].type === 'chroot-container') {
|
|
185
|
+
const { architecture } = workflowsConfig[workflowId].container;
|
|
186
|
+
bootstrapArch = architecture;
|
|
187
|
+
}
|
|
117
188
|
|
|
118
189
|
// Define the database provider ID.
|
|
119
190
|
const dbProviderId = 'postgresql-17';
|
|
@@ -121,21 +192,87 @@ class UnderpostBaremetal {
|
|
|
121
192
|
// Define the NFS host path based on the environment variable and hostname.
|
|
122
193
|
const nfsHostPath = `${process.env.NFS_EXPORT_PATH}/${hostname}`;
|
|
123
194
|
|
|
124
|
-
// Define the TFTP root path based
|
|
125
|
-
const tftpRootPath = `${process.env.TFTP_ROOT}/${
|
|
195
|
+
// Define the TFTP root prefix path based
|
|
196
|
+
const tftpRootPath = `${process.env.TFTP_ROOT}/${tftpPrefix}`;
|
|
197
|
+
|
|
198
|
+
// Define the iPXE cache directory to preserve builds across tftproot cleanups
|
|
199
|
+
const ipxeCacheDir = `/tmp/ipxe-cache/${tftpPrefix}`;
|
|
200
|
+
|
|
201
|
+
// Define the bootstrap HTTP server path.
|
|
202
|
+
const bootstrapHttpServerPath = options.bootstrapHttpServerPath
|
|
203
|
+
? options.bootstrapHttpServerPath
|
|
204
|
+
: `/tmp/bootstrap-http-server/${workflowId}`;
|
|
126
205
|
|
|
127
206
|
// Capture metadata for the callback execution, useful for logging and auditing.
|
|
128
207
|
const callbackMetaData = {
|
|
129
|
-
args: {
|
|
208
|
+
args: { workflowId, ipAddress, hostname, ipFileServer, ipConfig, netmask, dnsServer },
|
|
130
209
|
options,
|
|
131
210
|
runnerHost: { architecture: UnderpostBaremetal.API.getHostArch().alias, ip: getLocalIPv4Address() },
|
|
132
211
|
nfsHostPath,
|
|
133
212
|
tftpRootPath,
|
|
213
|
+
bootstrapHttpServerPath,
|
|
134
214
|
};
|
|
135
215
|
|
|
136
216
|
// Log the initiation of the baremetal callback with relevant metadata.
|
|
137
217
|
logger.info('Baremetal callback', callbackMetaData);
|
|
138
218
|
|
|
219
|
+
// Create a new machine in MAAS if the option is set.
|
|
220
|
+
let machine;
|
|
221
|
+
if (options.createMachine === true) {
|
|
222
|
+
const [searhMachine] = JSON.parse(
|
|
223
|
+
shellExec(`maas maas machines read hostname=${hostname}`, {
|
|
224
|
+
stdout: true,
|
|
225
|
+
silent: true,
|
|
226
|
+
}),
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
if (searhMachine) {
|
|
230
|
+
// Check if existing machine's MAC matches the specified MAC
|
|
231
|
+
const existingMac = searhMachine.boot_interface?.mac_address || searhMachine.mac_address;
|
|
232
|
+
|
|
233
|
+
// If using hardware MAC (macAddress is null), skip MAC validation and use existing machine
|
|
234
|
+
if (macAddress === null) {
|
|
235
|
+
logger.info(`Using hardware MAC mode - keeping existing machine ${hostname} with MAC ${existingMac}`);
|
|
236
|
+
machine = searhMachine;
|
|
237
|
+
} else if (existingMac && existingMac !== macAddress) {
|
|
238
|
+
logger.warn(`⚠ Machine ${hostname} exists with MAC ${existingMac}, but --mac specified ${macAddress}`);
|
|
239
|
+
logger.info(`Deleting existing machine ${searhMachine.system_id} to recreate with correct MAC...`);
|
|
240
|
+
|
|
241
|
+
// Delete the existing machine
|
|
242
|
+
shellExec(`maas maas machine delete ${searhMachine.system_id}`, {
|
|
243
|
+
silent: true,
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// Create new machine with correct MAC
|
|
247
|
+
machine = UnderpostBaremetal.API.machineFactory({
|
|
248
|
+
hostname,
|
|
249
|
+
ipAddress,
|
|
250
|
+
macAddress,
|
|
251
|
+
maas: workflowsConfig[workflowId].maas,
|
|
252
|
+
}).machine;
|
|
253
|
+
|
|
254
|
+
logger.info(`✓ Machine recreated with MAC ${macAddress}`);
|
|
255
|
+
} else {
|
|
256
|
+
logger.info(`Using existing machine ${hostname} with MAC ${existingMac}`);
|
|
257
|
+
machine = searhMachine;
|
|
258
|
+
}
|
|
259
|
+
} else {
|
|
260
|
+
// No existing machine found, create new one
|
|
261
|
+
// For hardware MAC mode (macAddress is null), we'll create machine after discovery
|
|
262
|
+
if (macAddress === null) {
|
|
263
|
+
logger.info(`Hardware MAC mode - machine will be created after discovery`);
|
|
264
|
+
machine = null;
|
|
265
|
+
} else {
|
|
266
|
+
machine = UnderpostBaremetal.API.machineFactory({
|
|
267
|
+
hostname,
|
|
268
|
+
ipAddress,
|
|
269
|
+
macAddress,
|
|
270
|
+
maas: workflowsConfig[workflowId].maas,
|
|
271
|
+
}).machine;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
139
276
|
if (options.installPacker) {
|
|
140
277
|
await UnderpostBaremetal.API.installPacker(underpostRoot);
|
|
141
278
|
return;
|
|
@@ -183,9 +320,9 @@ class UnderpostBaremetal {
|
|
|
183
320
|
workflows[workflowId] = workflowConfig;
|
|
184
321
|
UnderpostBaremetal.API.writePackerMaasImageBuildWorkflows(workflows);
|
|
185
322
|
|
|
186
|
-
logger.info('
|
|
187
|
-
logger.info(
|
|
188
|
-
logger.info('
|
|
323
|
+
logger.info('Template extracted successfully!');
|
|
324
|
+
logger.info(`Added configuration for ${workflowId} to engine/baremetal/packer-workflows.json`);
|
|
325
|
+
logger.info('Next steps');
|
|
189
326
|
logger.info(`1. Review and customize the Packer template files in: ${targetDir}`);
|
|
190
327
|
logger.info(`2. Review the workflow configuration in engine/baremetal/packer-workflows.json`);
|
|
191
328
|
logger.info(
|
|
@@ -324,31 +461,39 @@ rm -rf ${artifacts.join(' ')}`);
|
|
|
324
461
|
return;
|
|
325
462
|
}
|
|
326
463
|
|
|
327
|
-
if (options.logs === '
|
|
464
|
+
if (options.logs === 'dhcp-lease') {
|
|
465
|
+
shellExec(`cat /var/snap/maas/common/maas/dhcp/dhcpd.leases`);
|
|
466
|
+
shellExec(`cat /var/snap/maas/common/maas/dhcp/dhcpd.pid`);
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
if (options.logs === 'dhcp-lan') {
|
|
471
|
+
shellExec(`sudo tcpdump -l -n -i any -s0 -vv 'udp and (port 67 or 68)'`);
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
if (options.logs === 'cloud-init') {
|
|
328
476
|
shellExec(`tail -f -n 900 ${nfsHostPath}/var/log/cloud-init.log`);
|
|
329
477
|
return;
|
|
330
478
|
}
|
|
331
479
|
|
|
332
|
-
if (options.logs === 'machine') {
|
|
480
|
+
if (options.logs === 'cloud-init-machine') {
|
|
333
481
|
shellExec(`tail -f -n 900 ${nfsHostPath}/var/log/cloud-init-output.log`);
|
|
334
482
|
return;
|
|
335
483
|
}
|
|
336
484
|
|
|
337
|
-
if (options.logs === 'cloud-config') {
|
|
338
|
-
shellExec(`cat ${
|
|
485
|
+
if (options.logs === 'cloud-init-config') {
|
|
486
|
+
shellExec(`cat ${bootstrapHttpServerPath}/${hostname}/cloud-init/user-data`);
|
|
487
|
+
shellExec(`cat ${bootstrapHttpServerPath}/${hostname}/cloud-init/meta-data`);
|
|
488
|
+
shellExec(`cat ${bootstrapHttpServerPath}/${hostname}/cloud-init/vendor-data`);
|
|
339
489
|
return;
|
|
340
490
|
}
|
|
341
491
|
|
|
342
492
|
// Handle NFS shell access option.
|
|
343
493
|
if (options.nfsSh === true) {
|
|
344
|
-
const workflowsConfig = UnderpostBaremetal.API.loadWorkflowsConfig();
|
|
345
|
-
if (!workflowsConfig[workflowId]) {
|
|
346
|
-
throw new Error(`Workflow configuration not found for ID: ${workflowId}`);
|
|
347
|
-
}
|
|
348
|
-
const { debootstrap } = workflowsConfig[workflowId];
|
|
349
494
|
// Copy the chroot command to the clipboard for easy execution.
|
|
350
|
-
if (
|
|
351
|
-
switch (
|
|
495
|
+
if (bootstrapArch && bootstrapArch !== callbackMetaData.runnerHost.architecture)
|
|
496
|
+
switch (bootstrapArch) {
|
|
352
497
|
case 'arm64':
|
|
353
498
|
pbcopy(`sudo chroot ${nfsHostPath} /usr/bin/qemu-aarch64-static /bin/bash`);
|
|
354
499
|
break;
|
|
@@ -371,16 +516,15 @@ rm -rf ${artifacts.join(' ')}`);
|
|
|
371
516
|
shellExec(`chmod +x ${underpostRoot}/scripts/maas-setup.sh`);
|
|
372
517
|
shellExec(`chmod +x ${underpostRoot}/scripts/nat-iptables.sh`);
|
|
373
518
|
shellExec(`${underpostRoot}/scripts/maas-setup.sh`);
|
|
374
|
-
shellExec(`${underpostRoot}/scripts/nat-iptables.sh`);
|
|
375
519
|
return;
|
|
376
520
|
}
|
|
377
521
|
|
|
378
522
|
// Handle control server uninstallation.
|
|
379
523
|
if (options.controlServerUninstall === true) {
|
|
380
524
|
// Stop and remove MAAS services, handling potential errors gracefully.
|
|
381
|
-
shellExec(`sudo snap stop maas.pebble
|
|
525
|
+
shellExec(`sudo snap stop maas.pebble`);
|
|
382
526
|
shellExec(`sudo snap stop maas`);
|
|
383
|
-
shellExec(`sudo snap remove maas --purge
|
|
527
|
+
shellExec(`sudo snap remove maas --purge`);
|
|
384
528
|
|
|
385
529
|
// Remove residual snap data to ensure a clean uninstall.
|
|
386
530
|
shellExec(`sudo rm -rf /var/snap/maas`);
|
|
@@ -393,6 +537,12 @@ rm -rf ${artifacts.join(' ')}`);
|
|
|
393
537
|
return;
|
|
394
538
|
}
|
|
395
539
|
|
|
540
|
+
// Handle control server restart.
|
|
541
|
+
if (options.controlServerRestart === true) {
|
|
542
|
+
shellExec(`sudo snap restart maas`);
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
|
|
396
546
|
// Handle control server database installation.
|
|
397
547
|
if (options.controlServerDbInstall === true) {
|
|
398
548
|
// Deploy the database provider and manage MAAS database.
|
|
@@ -410,43 +560,43 @@ rm -rf ${artifacts.join(' ')}`);
|
|
|
410
560
|
return;
|
|
411
561
|
}
|
|
412
562
|
|
|
413
|
-
const workflowsConfig = UnderpostBaremetal.API.loadWorkflowsConfig();
|
|
414
|
-
if (!workflowsConfig[workflowId]) {
|
|
415
|
-
throw new Error(`Workflow configuration not found for ID: ${workflowId}`);
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
// Set debootstrap architecture.
|
|
419
|
-
{
|
|
420
|
-
const { architecture } = workflowsConfig[workflowId].debootstrap.image;
|
|
421
|
-
debootstrapArch = architecture;
|
|
422
|
-
}
|
|
423
|
-
|
|
424
563
|
// Handle NFS mount operation.
|
|
425
564
|
if (options.nfsMount === true) {
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
565
|
+
await UnderpostBaremetal.API.nfsMountCallback({
|
|
566
|
+
hostname,
|
|
567
|
+
nfsHostPath,
|
|
568
|
+
workflowId,
|
|
569
|
+
mount: true,
|
|
570
|
+
});
|
|
571
|
+
return;
|
|
429
572
|
}
|
|
430
573
|
|
|
431
574
|
// Handle NFS unmount operation.
|
|
432
575
|
if (options.nfsUnmount === true) {
|
|
433
|
-
UnderpostBaremetal.API.nfsMountCallback({
|
|
576
|
+
await UnderpostBaremetal.API.nfsMountCallback({
|
|
577
|
+
hostname,
|
|
578
|
+
nfsHostPath,
|
|
579
|
+
workflowId,
|
|
580
|
+
unmount: true,
|
|
581
|
+
});
|
|
582
|
+
return;
|
|
434
583
|
}
|
|
435
584
|
|
|
436
585
|
// Handle NFS root filesystem build operation.
|
|
437
586
|
if (options.nfsBuild === true) {
|
|
438
|
-
|
|
439
|
-
|
|
587
|
+
await UnderpostBaremetal.API.nfsMountCallback({
|
|
588
|
+
hostname,
|
|
589
|
+
nfsHostPath,
|
|
590
|
+
workflowId,
|
|
591
|
+
unmount: true,
|
|
592
|
+
});
|
|
440
593
|
|
|
441
594
|
// Clean and create the NFS host path.
|
|
442
595
|
shellExec(`sudo rm -rf ${nfsHostPath}/*`);
|
|
443
596
|
shellExec(`mkdir -p ${nfsHostPath}`);
|
|
444
597
|
|
|
445
|
-
// Mount binfmt_misc filesystem.
|
|
446
|
-
UnderpostBaremetal.API.mountBinfmtMisc({ nfsHostPath });
|
|
447
|
-
|
|
448
598
|
// Perform the first stage of debootstrap.
|
|
449
|
-
{
|
|
599
|
+
if (workflowsConfig[workflowId].type === 'chroot-debootstrap') {
|
|
450
600
|
const { architecture, name } = workflowsConfig[workflowId].debootstrap.image;
|
|
451
601
|
shellExec(
|
|
452
602
|
[
|
|
@@ -459,6 +609,12 @@ rm -rf ${artifacts.join(' ')}`);
|
|
|
459
609
|
`http://ports.ubuntu.com/ubuntu-ports/`,
|
|
460
610
|
].join(' '),
|
|
461
611
|
);
|
|
612
|
+
} else if (workflowsConfig[workflowId].type === 'chroot-container') {
|
|
613
|
+
const { image } = workflowsConfig[workflowId].container;
|
|
614
|
+
shellExec(`sudo podman pull --arch=${bootstrapArch} ${image}`);
|
|
615
|
+
shellExec(`sudo podman create --arch=${bootstrapArch} --name chroot-source ${image}`);
|
|
616
|
+
shellExec(`sudo podman export chroot-source | sudo tar -x -C ${nfsHostPath}`);
|
|
617
|
+
shellExec(`sudo podman rm chroot-source`);
|
|
462
618
|
}
|
|
463
619
|
|
|
464
620
|
// Create a podman container to extract QEMU static binaries.
|
|
@@ -466,10 +622,10 @@ rm -rf ${artifacts.join(' ')}`);
|
|
|
466
622
|
shellExec(`podman ps -a`); // List all podman containers for verification.
|
|
467
623
|
|
|
468
624
|
// If cross-architecture, copy the QEMU static binary into the chroot.
|
|
469
|
-
if (
|
|
625
|
+
if (bootstrapArch !== callbackMetaData.runnerHost.architecture)
|
|
470
626
|
UnderpostBaremetal.API.crossArchBinFactory({
|
|
471
627
|
nfsHostPath,
|
|
472
|
-
|
|
628
|
+
bootstrapArch,
|
|
473
629
|
});
|
|
474
630
|
|
|
475
631
|
// Clean up the temporary podman container.
|
|
@@ -477,40 +633,104 @@ rm -rf ${artifacts.join(' ')}`);
|
|
|
477
633
|
shellExec(`podman ps -a`);
|
|
478
634
|
shellExec(`file ${nfsHostPath}/bin/bash`); // Verify the bash executable in the chroot.
|
|
479
635
|
|
|
480
|
-
//
|
|
481
|
-
UnderpostBaremetal.API.
|
|
636
|
+
// Mount necessary filesystems and register binfmt for the second stage.
|
|
637
|
+
await UnderpostBaremetal.API.nfsMountCallback({
|
|
638
|
+
hostname,
|
|
482
639
|
nfsHostPath,
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
steps: [`/debootstrap/debootstrap --second-stage`],
|
|
640
|
+
workflowId,
|
|
641
|
+
mount: true,
|
|
486
642
|
});
|
|
487
643
|
|
|
488
|
-
//
|
|
489
|
-
if (
|
|
490
|
-
UnderpostBaremetal.API.
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
644
|
+
// Perform the second stage of debootstrap within the chroot environment.
|
|
645
|
+
if (workflowsConfig[workflowId].type === 'chroot-debootstrap') {
|
|
646
|
+
UnderpostBaremetal.API.crossArchRunner({
|
|
647
|
+
nfsHostPath,
|
|
648
|
+
bootstrapArch,
|
|
649
|
+
callbackMetaData,
|
|
650
|
+
steps: [`/debootstrap/debootstrap --second-stage`],
|
|
651
|
+
});
|
|
652
|
+
} else if (
|
|
653
|
+
workflowsConfig[workflowId].type === 'chroot-container' &&
|
|
654
|
+
workflowsConfig[workflowId].osIdLike.match('rhel')
|
|
655
|
+
) {
|
|
656
|
+
// Copy resolv.conf to allow network access inside chroot
|
|
657
|
+
shellExec(`sudo cp /etc/resolv.conf ${nfsHostPath}/etc/resolv.conf`);
|
|
658
|
+
|
|
659
|
+
// Consolidate all package installations into one step to avoid redundancy
|
|
660
|
+
const { packages } = workflowsConfig[workflowId].container;
|
|
661
|
+
const basePackages = [
|
|
662
|
+
'findutils',
|
|
663
|
+
'systemd',
|
|
664
|
+
'sudo',
|
|
665
|
+
'dracut',
|
|
666
|
+
'dracut-network',
|
|
667
|
+
'dracut-config-generic',
|
|
668
|
+
'nfs-utils',
|
|
669
|
+
'file',
|
|
670
|
+
'binutils',
|
|
671
|
+
'kernel-modules-core',
|
|
672
|
+
'NetworkManager',
|
|
673
|
+
'dhclient',
|
|
674
|
+
'iputils',
|
|
675
|
+
];
|
|
676
|
+
const allPackages = packages && packages.length > 0 ? [...basePackages, ...packages] : basePackages;
|
|
497
677
|
|
|
498
678
|
UnderpostBaremetal.API.crossArchRunner({
|
|
499
679
|
nfsHostPath,
|
|
500
|
-
|
|
680
|
+
bootstrapArch,
|
|
501
681
|
callbackMetaData,
|
|
502
682
|
steps: [
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
683
|
+
`dnf install -y --allowerasing ${allPackages.join(' ')} 2>/dev/null || yum install -y --allowerasing ${allPackages.join(' ')} 2>/dev/null || echo "Package install completed"`,
|
|
684
|
+
`dnf clean all`,
|
|
685
|
+
`echo "=== Installed packages verification ==="`,
|
|
686
|
+
`rpm -qa | grep -E "dracut|kernel|nfs" | sort`,
|
|
687
|
+
`echo "=== Boot directory contents ==="`,
|
|
688
|
+
`ls -la /boot /lib/modules/*/`,
|
|
689
|
+
// Search for bootable kernel in order of preference:
|
|
690
|
+
// 1. Raw ARM64 Image file (preferred for GRUB)
|
|
691
|
+
// 2. vmlinuz or vmlinux (may be PE32+ on Rocky Linux)
|
|
692
|
+
`echo "Searching for bootable kernel..."`,
|
|
693
|
+
`KERNEL_FILE=""`,
|
|
694
|
+
// First try to find raw Image file
|
|
695
|
+
`if [ -f /boot/Image ]; then KERNEL_FILE=/boot/Image; echo "Found raw ARM64 Image: $KERNEL_FILE"; fi`,
|
|
696
|
+
`if [ -z "$KERNEL_FILE" ]; then KERNEL_FILE=$(find /lib/modules -name "Image" -o -name "Image.gz" 2>/dev/null | head -n 1); test -n "$KERNEL_FILE" && echo "Found kernel Image in modules: $KERNEL_FILE"; fi`,
|
|
697
|
+
// Fallback to vmlinuz
|
|
698
|
+
`if [ -z "$KERNEL_FILE" ]; then KERNEL_FILE=$(find /boot -name "vmlinuz-*" 2>/dev/null | head -n 1); test -n "$KERNEL_FILE" && echo "Found vmlinuz: $KERNEL_FILE"; fi`,
|
|
699
|
+
`if [ -z "$KERNEL_FILE" ]; then KERNEL_FILE=$(find /lib/modules -name "vmlinuz" 2>/dev/null | head -n 1); test -n "$KERNEL_FILE" && echo "Found vmlinuz in modules: $KERNEL_FILE"; fi`,
|
|
700
|
+
// Last resort: any vmlinux
|
|
701
|
+
`if [ -z "$KERNEL_FILE" ]; then KERNEL_FILE=$(find /lib/modules -name "vmlinux" 2>/dev/null | head -n 1); test -n "$KERNEL_FILE" && echo "Found vmlinux: $KERNEL_FILE"; fi`,
|
|
702
|
+
`if [ -z "$KERNEL_FILE" ]; then echo "ERROR: No kernel found!"; exit 1; fi`,
|
|
703
|
+
// Copy and check kernel type
|
|
704
|
+
`cp "$KERNEL_FILE" /boot/vmlinuz-efi.tmp`,
|
|
705
|
+
// Decompress if gzipped
|
|
706
|
+
`if file /boot/vmlinuz-efi.tmp | grep -q gzip; then echo "Decompressing gzipped kernel..."; gunzip -c /boot/vmlinuz-efi.tmp > /boot/vmlinuz-efi && rm /boot/vmlinuz-efi.tmp; else mv /boot/vmlinuz-efi.tmp /boot/vmlinuz-efi; fi`,
|
|
707
|
+
`KERNEL_TYPE=$(file /boot/vmlinuz-efi 2>/dev/null)`,
|
|
708
|
+
`echo "Final kernel file type: $KERNEL_TYPE"`,
|
|
709
|
+
// Handle PE32+ if still present - use kernel directly without extraction since iPXE can boot it
|
|
710
|
+
`case "$KERNEL_TYPE" in *PE32+*|*EFI*application*) echo "WARNING: Kernel is PE32+ EFI executable"; echo "GRUB may fail to boot this - recommend using iPXE chainload or installing kernel-core package"; echo "Keeping PE32+ kernel as-is for now..."; ;; *ARM64*|*aarch64*|*Image*|*data*) echo "Kernel appears to be raw ARM64 format - suitable for GRUB"; ;; *) echo "Unknown kernel format - attempting to use anyway"; ;; esac`,
|
|
711
|
+
// Get kernel version for initramfs rebuild
|
|
712
|
+
`KVER=$(basename $(dirname "$KERNEL_FILE"))`,
|
|
713
|
+
`echo "Kernel version: $KVER"`,
|
|
714
|
+
// Rebuild initramfs with NFS and network support
|
|
715
|
+
`echo "Rebuilding initramfs with NFS and network support..."`,
|
|
716
|
+
`echo "Available dracut modules:"`,
|
|
717
|
+
`dracut --list-modules 2>/dev/null | grep -E "network|nfs" || echo "No network modules listed"`,
|
|
718
|
+
// Use network-manager module (it's available in Rocky 9) for better compatibility
|
|
719
|
+
`dracut --force --add "nfs network base" --add-drivers "nfs sunrpc" --kver "$KVER" /boot/initrd.img "$KVER" 2>&1 || echo "Initramfs rebuild failed"`,
|
|
720
|
+
// Fallback: if rebuild fails, use existing initramfs
|
|
721
|
+
`if [ ! -f /boot/initrd.img ]; then echo "Initramfs rebuild failed, using existing..."; INITRD=$(find /boot -name "initramfs-$KVER.img" 2>/dev/null | head -n 1); if [ -z "$INITRD" ]; then INITRD=$(find /boot -name "initramfs*.img" 2>/dev/null | grep -v kdump | head -n 1); fi; if [ -n "$INITRD" ]; then cp "$INITRD" /boot/initrd.img; echo "Copied existing initramfs: $INITRD"; else echo "ERROR: No initramfs found!"; fi; fi`,
|
|
722
|
+
`echo "=== Final boot files ==="`,
|
|
723
|
+
`ls -lh /boot/vmlinuz-efi /boot/initrd.img`,
|
|
724
|
+
`file /boot/vmlinuz-efi`,
|
|
725
|
+
`file /boot/initrd.img`,
|
|
726
|
+
`echo "=== Setting root password ==="`,
|
|
727
|
+
`echo "root:root" | chpasswd`,
|
|
512
728
|
],
|
|
513
729
|
});
|
|
730
|
+
} else {
|
|
731
|
+
throw new Error(
|
|
732
|
+
`Unsupported workflow type for NFS build: ${workflowsConfig[workflowId].type} and like os ID ${workflowsConfig[workflowId].osIdLike}`,
|
|
733
|
+
);
|
|
514
734
|
}
|
|
515
735
|
}
|
|
516
736
|
|
|
@@ -544,18 +764,187 @@ rm -rf ${artifacts.join(' ')}`);
|
|
|
544
764
|
console.table(machines);
|
|
545
765
|
}
|
|
546
766
|
|
|
547
|
-
|
|
767
|
+
if (options.clearDiscovered) UnderpostBaremetal.API.removeDiscoveredMachines();
|
|
768
|
+
|
|
769
|
+
// Handle remove existing machines from MAAS.
|
|
770
|
+
if (options.removeMachines)
|
|
771
|
+
machines = UnderpostBaremetal.API.removeMachines({
|
|
772
|
+
machines: options.removeMachines === 'all' ? machines : options.removeMachines.split(','),
|
|
773
|
+
ignore: machine ? [machine.system_id] : [],
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
if (workflowsConfig[workflowId].type === 'chroot-debootstrap') {
|
|
777
|
+
if (options.ubuntuToolsBuild) {
|
|
778
|
+
UnderpostCloudInit.API.buildTools({
|
|
779
|
+
workflowId,
|
|
780
|
+
nfsHostPath,
|
|
781
|
+
hostname,
|
|
782
|
+
callbackMetaData,
|
|
783
|
+
dev: options.dev,
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
const { chronyc, keyboard } = workflowsConfig[workflowId];
|
|
787
|
+
const { timezone, chronyConfPath } = chronyc;
|
|
788
|
+
const systemProvisioning = 'ubuntu';
|
|
789
|
+
|
|
790
|
+
UnderpostBaremetal.API.crossArchRunner({
|
|
791
|
+
nfsHostPath,
|
|
792
|
+
bootstrapArch,
|
|
793
|
+
callbackMetaData,
|
|
794
|
+
steps: [
|
|
795
|
+
...UnderpostBaremetal.API.systemProvisioningFactory[systemProvisioning].base(),
|
|
796
|
+
...UnderpostBaremetal.API.systemProvisioningFactory[systemProvisioning].user(),
|
|
797
|
+
...UnderpostBaremetal.API.systemProvisioningFactory[systemProvisioning].timezone({
|
|
798
|
+
timezone,
|
|
799
|
+
chronyConfPath,
|
|
800
|
+
}),
|
|
801
|
+
...UnderpostBaremetal.API.systemProvisioningFactory[systemProvisioning].keyboard(keyboard.layout),
|
|
802
|
+
],
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
if (options.ubuntuToolsTest)
|
|
807
|
+
UnderpostBaremetal.API.crossArchRunner({
|
|
808
|
+
nfsHostPath,
|
|
809
|
+
bootstrapArch,
|
|
810
|
+
callbackMetaData,
|
|
811
|
+
steps: [
|
|
812
|
+
`chmod +x /underpost/date.sh`,
|
|
813
|
+
`chmod +x /underpost/keyboard.sh`,
|
|
814
|
+
`chmod +x /underpost/dns.sh`,
|
|
815
|
+
`chmod +x /underpost/help.sh`,
|
|
816
|
+
`chmod +x /underpost/host.sh`,
|
|
817
|
+
`chmod +x /underpost/test.sh`,
|
|
818
|
+
`chmod +x /underpost/start.sh`,
|
|
819
|
+
`chmod +x /underpost/reset.sh`,
|
|
820
|
+
`chmod +x /underpost/shutdown.sh`,
|
|
821
|
+
`chmod +x /underpost/device_scan.sh`,
|
|
822
|
+
`chmod +x /underpost/mac.sh`,
|
|
823
|
+
`chmod +x /underpost/enlistment.sh`,
|
|
824
|
+
`sudo chmod 700 ~/.ssh/`, // Set secure permissions for .ssh directory.
|
|
825
|
+
`sudo chmod 600 ~/.ssh/authorized_keys`, // Set secure permissions for authorized_keys.
|
|
826
|
+
`sudo chmod 644 ~/.ssh/known_hosts`, // Set permissions for known_hosts.
|
|
827
|
+
`sudo chmod 600 ~/.ssh/id_rsa`, // Set secure permissions for private key.
|
|
828
|
+
`sudo chmod 600 /etc/ssh/ssh_host_ed25519_key`, // Set secure permissions for host key.
|
|
829
|
+
`chown -R root:root ~/.ssh`, // Ensure root owns the .ssh directory.
|
|
830
|
+
`/underpost/test.sh`,
|
|
831
|
+
],
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
if (
|
|
836
|
+
workflowsConfig[workflowId].type === 'chroot-container' &&
|
|
837
|
+
workflowsConfig[workflowId].osIdLike.match('rhel')
|
|
838
|
+
) {
|
|
839
|
+
if (options.rockyToolsBuild) {
|
|
840
|
+
const { chronyc, keyboard } = workflowsConfig[workflowId];
|
|
841
|
+
const { timezone } = chronyc;
|
|
842
|
+
const systemProvisioning = 'rocky';
|
|
843
|
+
|
|
844
|
+
UnderpostBaremetal.API.crossArchRunner({
|
|
845
|
+
nfsHostPath,
|
|
846
|
+
bootstrapArch,
|
|
847
|
+
callbackMetaData,
|
|
848
|
+
steps: [
|
|
849
|
+
...UnderpostBaremetal.API.systemProvisioningFactory[systemProvisioning].base(),
|
|
850
|
+
...UnderpostBaremetal.API.systemProvisioningFactory[systemProvisioning].user(),
|
|
851
|
+
...UnderpostBaremetal.API.systemProvisioningFactory[systemProvisioning].timezone({
|
|
852
|
+
timezone,
|
|
853
|
+
chronyConfPath: chronyc.chronyConfPath,
|
|
854
|
+
}),
|
|
855
|
+
...UnderpostBaremetal.API.systemProvisioningFactory[systemProvisioning].keyboard(keyboard.layout),
|
|
856
|
+
],
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
if (options.rockyToolsTest)
|
|
861
|
+
UnderpostBaremetal.API.crossArchRunner({
|
|
862
|
+
nfsHostPath,
|
|
863
|
+
bootstrapArch,
|
|
864
|
+
callbackMetaData,
|
|
865
|
+
steps: [
|
|
866
|
+
`node --version`,
|
|
867
|
+
`npm --version`,
|
|
868
|
+
`underpost --version`,
|
|
869
|
+
`timedatectl status`,
|
|
870
|
+
`localectl status`,
|
|
871
|
+
`id root`,
|
|
872
|
+
`ls -la /home/root/.ssh/`,
|
|
873
|
+
`cat /home/root/.ssh/authorized_keys`,
|
|
874
|
+
'underpost test',
|
|
875
|
+
],
|
|
876
|
+
});
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
if (options.cloudInit || options.cloudInitUpdate) {
|
|
880
|
+
const { chronyc, networkInterfaceName } = workflowsConfig[workflowId];
|
|
881
|
+
const { timezone, chronyConfPath } = chronyc;
|
|
882
|
+
const authCredentials = UnderpostCloudInit.API.authCredentialsFactory();
|
|
883
|
+
const { cloudConfigSrc } = UnderpostCloudInit.API.configFactory(
|
|
884
|
+
{
|
|
885
|
+
controlServerIp: callbackMetaData.runnerHost.ip,
|
|
886
|
+
hostname,
|
|
887
|
+
commissioningDeviceIp: ipAddress,
|
|
888
|
+
gatewayip: callbackMetaData.runnerHost.ip,
|
|
889
|
+
mac: macAddress,
|
|
890
|
+
timezone,
|
|
891
|
+
chronyConfPath,
|
|
892
|
+
networkInterfaceName,
|
|
893
|
+
ubuntuToolsBuild: options.ubuntuToolsBuild,
|
|
894
|
+
bootcmd: options.bootcmd,
|
|
895
|
+
runcmd: options.runcmd,
|
|
896
|
+
},
|
|
897
|
+
authCredentials,
|
|
898
|
+
);
|
|
899
|
+
|
|
900
|
+
UnderpostBaremetal.API.httpBootstrapServerStaticFactory({
|
|
901
|
+
bootstrapHttpServerPath,
|
|
902
|
+
hostname,
|
|
903
|
+
cloudConfigSrc,
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
// Rebuild NFS server configuration.
|
|
908
|
+
if (
|
|
909
|
+
(options.nfsBuildServer === true || options.commission === true) &&
|
|
910
|
+
(workflowsConfig[workflowId].type === 'iso-nfs' ||
|
|
911
|
+
workflowsConfig[workflowId].type === 'chroot-debootstrap' ||
|
|
912
|
+
workflowsConfig[workflowId].type === 'chroot-container')
|
|
913
|
+
) {
|
|
914
|
+
shellExec(`${underpostRoot}/scripts/nat-iptables.sh`, { silent: true });
|
|
915
|
+
UnderpostBaremetal.API.rebuildNfsServer({
|
|
916
|
+
nfsHostPath,
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
// Handle commissioning tasks
|
|
548
920
|
if (options.commission === true) {
|
|
549
|
-
|
|
921
|
+
let { firmwares, networkInterfaceName, maas, menuentryStr, type } = workflowsConfig[workflowId];
|
|
922
|
+
|
|
923
|
+
// Use commissioning config (Ubuntu ephemeral) for PXE boot resources
|
|
924
|
+
const commissioningImage = maas?.commissioning || {
|
|
925
|
+
architecture: 'arm64/generic',
|
|
926
|
+
name: 'ubuntu/noble',
|
|
927
|
+
};
|
|
550
928
|
const resource = resources.find(
|
|
551
|
-
(o) => o.architecture ===
|
|
929
|
+
(o) => o.architecture === commissioningImage.architecture && o.name === commissioningImage.name,
|
|
552
930
|
);
|
|
553
931
|
logger.info('Commissioning resource', resource);
|
|
554
932
|
|
|
933
|
+
if (type === 'iso-nfs') {
|
|
934
|
+
// Prepare NFS casper path if using NFS boot.
|
|
935
|
+
shellExec(`sudo rm -rf ${nfsHostPath}`);
|
|
936
|
+
shellExec(`mkdir -p ${nfsHostPath}/casper`);
|
|
937
|
+
}
|
|
938
|
+
|
|
555
939
|
// Clean and create TFTP root path.
|
|
556
940
|
shellExec(`sudo rm -rf ${tftpRootPath}`);
|
|
557
941
|
shellExec(`mkdir -p ${tftpRootPath}/pxe`);
|
|
558
942
|
|
|
943
|
+
// Restore iPXE build from cache if available and not forcing rebuild
|
|
944
|
+
if (fs.existsSync(`${ipxeCacheDir}/ipxe.efi`) && !options.ipxeRebuild) {
|
|
945
|
+
shellExec(`cp ${ipxeCacheDir}/ipxe.efi ${tftpRootPath}/ipxe.efi`);
|
|
946
|
+
}
|
|
947
|
+
|
|
559
948
|
// Process firmwares for TFTP.
|
|
560
949
|
for (const firmware of firmwares) {
|
|
561
950
|
const { url, gateway, subnet } = firmware;
|
|
@@ -569,221 +958,1235 @@ rm -rf ${artifacts.join(' ')}`);
|
|
|
569
958
|
shellExec(`sudo cp -a ${path}/* ${tftpRootPath}`); // Copy firmware files to TFTP root.
|
|
570
959
|
|
|
571
960
|
if (gateway && subnet) {
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
}),
|
|
583
|
-
'utf8',
|
|
584
|
-
);
|
|
961
|
+
const bootConfSrc = UnderpostBaremetal.API.bootConfFactory({
|
|
962
|
+
workflowId,
|
|
963
|
+
tftpIp: callbackMetaData.runnerHost.ip,
|
|
964
|
+
tftpPrefixStr: tftpPrefix,
|
|
965
|
+
macAddress,
|
|
966
|
+
clientIp: ipAddress,
|
|
967
|
+
subnet,
|
|
968
|
+
gateway,
|
|
969
|
+
});
|
|
970
|
+
if (bootConfSrc) fs.writeFileSync(`${tftpRootPath}/boot_${name}.conf`, bootConfSrc, 'utf8');
|
|
585
971
|
}
|
|
586
972
|
}
|
|
587
973
|
}
|
|
588
974
|
|
|
589
|
-
//
|
|
590
|
-
|
|
591
|
-
|
|
975
|
+
// Configure GRUB for PXE boot.
|
|
976
|
+
{
|
|
977
|
+
// Fetch kernel and initrd paths from MAAS boot resource.
|
|
978
|
+
// Both NFS and disk-based commissioning use MAAS boot resources.
|
|
979
|
+
let kernelFilesPaths, resourcesPath;
|
|
980
|
+
if (workflowsConfig[workflowId].type === 'chroot-container') {
|
|
981
|
+
const arch = commissioningImage.architecture.split('/')[0];
|
|
982
|
+
resourcesPath = `/var/snap/maas/common/maas/image-storage/bootloaders/uefi/${arch}`;
|
|
983
|
+
kernelFilesPaths = {
|
|
984
|
+
'vmlinuz-efi': `${nfsHostPath}/boot/vmlinuz-efi`,
|
|
985
|
+
'initrd.img': `${nfsHostPath}/boot/initrd.img`,
|
|
986
|
+
};
|
|
987
|
+
} else {
|
|
988
|
+
const kf = UnderpostBaremetal.API.kernelFactory({
|
|
989
|
+
resource,
|
|
990
|
+
type,
|
|
991
|
+
nfsHostPath,
|
|
992
|
+
isoUrl: options.isoUrl || workflowsConfig[workflowId].isoUrl,
|
|
993
|
+
workflowId,
|
|
994
|
+
});
|
|
995
|
+
kernelFilesPaths = kf.kernelFilesPaths;
|
|
996
|
+
resourcesPath = kf.resourcesPath;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
const { cmd } = UnderpostBaremetal.API.kernelCmdBootParamsFactory({
|
|
1000
|
+
ipClient: ipAddress,
|
|
1001
|
+
ipDhcpServer: callbackMetaData.runnerHost.ip,
|
|
1002
|
+
ipConfig,
|
|
1003
|
+
ipFileServer,
|
|
1004
|
+
netmask,
|
|
1005
|
+
hostname,
|
|
1006
|
+
dnsServer,
|
|
1007
|
+
networkInterfaceName,
|
|
1008
|
+
fileSystemUrl: kernelFilesPaths.isoUrl,
|
|
1009
|
+
bootstrapHttpServerPort:
|
|
1010
|
+
options.bootstrapHttpServerPort || workflowsConfig[workflowId].bootstrapHttpServerPort || 8888,
|
|
1011
|
+
type,
|
|
1012
|
+
macAddress,
|
|
1013
|
+
cloudInit: options.cloudInit,
|
|
1014
|
+
machine,
|
|
1015
|
+
dev: options.dev,
|
|
1016
|
+
osIdLike: workflowsConfig[workflowId].osIdLike || '',
|
|
1017
|
+
});
|
|
1018
|
+
|
|
1019
|
+
// Check if iPXE mode is enabled AND the iPXE EFI binary exists
|
|
1020
|
+
let useIpxe = options.ipxe;
|
|
1021
|
+
if (options.ipxe) {
|
|
1022
|
+
const arch = commissioningImage.architecture.split('/')[0];
|
|
1023
|
+
const ipxeScript = UnderpostBaremetal.API.ipxeScriptFactory({
|
|
1024
|
+
maasIp: callbackMetaData.runnerHost.ip,
|
|
1025
|
+
macAddress,
|
|
1026
|
+
architecture: arch,
|
|
1027
|
+
tftpPrefix,
|
|
1028
|
+
kernelCmd: cmd,
|
|
1029
|
+
});
|
|
1030
|
+
fs.writeFileSync(`${tftpRootPath}/stable-id.ipxe`, ipxeScript, 'utf8');
|
|
1031
|
+
|
|
1032
|
+
// Create embedded boot script that does DHCP and chains to the main script
|
|
1033
|
+
const embeddedScript = UnderpostBaremetal.API.ipxeEmbeddedScriptFactory({
|
|
1034
|
+
tftpServer: callbackMetaData.runnerHost.ip,
|
|
1035
|
+
scriptPath: `/${tftpPrefix}/stable-id.ipxe`,
|
|
1036
|
+
macAddress: macAddress,
|
|
1037
|
+
});
|
|
1038
|
+
fs.writeFileSync(`${tftpRootPath}/boot.ipxe`, embeddedScript, 'utf8');
|
|
1039
|
+
|
|
1040
|
+
logger.info('✓ iPXE script generated for MAAS commissioning', {
|
|
1041
|
+
registeredMAC: macAddress,
|
|
1042
|
+
path: `${tftpRootPath}/stable-id.ipxe`,
|
|
1043
|
+
embeddedPath: `${tftpRootPath}/boot.ipxe`,
|
|
1044
|
+
});
|
|
1045
|
+
|
|
1046
|
+
UnderpostBaremetal.API.ipxeEfiFactory({
|
|
1047
|
+
tftpRootPath,
|
|
1048
|
+
ipxeCacheDir,
|
|
1049
|
+
arch,
|
|
1050
|
+
underpostRoot,
|
|
1051
|
+
embeddedScriptPath: `${tftpRootPath}/boot.ipxe`,
|
|
1052
|
+
forceRebuild: options.ipxeRebuild,
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
const { grubCfgSrc } = UnderpostBaremetal.API.grubFactory({
|
|
1057
|
+
menuentryStr,
|
|
1058
|
+
kernelPath: `/${tftpPrefix}/pxe/vmlinuz-efi`,
|
|
1059
|
+
initrdPath: `/${tftpPrefix}/pxe/initrd.img`,
|
|
1060
|
+
cmd,
|
|
1061
|
+
tftpIp: callbackMetaData.runnerHost.ip,
|
|
1062
|
+
ipxe: useIpxe,
|
|
1063
|
+
ipxePath: `/${tftpPrefix}/ipxe.efi`,
|
|
1064
|
+
});
|
|
1065
|
+
UnderpostBaremetal.API.writeGrubConfigToFile({
|
|
1066
|
+
grubCfgSrc: machine ? grubCfgSrc.replaceAll('system-id', machine.system_id) : grubCfgSrc,
|
|
1067
|
+
});
|
|
1068
|
+
if (machine) {
|
|
1069
|
+
logger.info('✓ GRUB config written with system_id', { system_id: machine.system_id });
|
|
1070
|
+
}
|
|
1071
|
+
UnderpostBaremetal.API.updateKernelFiles({
|
|
1072
|
+
commissioningImage,
|
|
1073
|
+
resourcesPath,
|
|
1074
|
+
tftpRootPath,
|
|
1075
|
+
kernelFilesPaths,
|
|
1076
|
+
});
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
// Pass architecture from commissioning or deployment config
|
|
1080
|
+
const grubArch = commissioningImage.architecture;
|
|
1081
|
+
UnderpostBaremetal.API.efiGrubModulesFactory({ image: { architecture: grubArch } });
|
|
1082
|
+
|
|
1083
|
+
// Set ownership and permissions for TFTP root.
|
|
1084
|
+
shellExec(`sudo chown -R $(whoami):$(whoami) ${process.env.TFTP_ROOT}`);
|
|
1085
|
+
shellExec(`sudo sudo chmod 755 ${process.env.TFTP_ROOT}`);
|
|
1086
|
+
|
|
1087
|
+
UnderpostBaremetal.API.httpBootstrapServerRunnerFactory({
|
|
1088
|
+
hostname,
|
|
1089
|
+
bootstrapHttpServerPath,
|
|
1090
|
+
bootstrapHttpServerPort:
|
|
1091
|
+
options.bootstrapHttpServerPort || workflowsConfig[workflowId].bootstrapHttpServerPort,
|
|
592
1092
|
});
|
|
593
1093
|
|
|
594
|
-
|
|
1094
|
+
if (type === 'chroot-debootstrap' || type === 'chroot-container')
|
|
1095
|
+
await UnderpostBaremetal.API.nfsMountCallback({
|
|
1096
|
+
hostname,
|
|
1097
|
+
nfsHostPath,
|
|
1098
|
+
workflowId,
|
|
1099
|
+
mount: true,
|
|
1100
|
+
});
|
|
1101
|
+
|
|
1102
|
+
const commissionMonitorPayload = {
|
|
1103
|
+
macAddress,
|
|
1104
|
+
ipAddress,
|
|
1105
|
+
hostname,
|
|
1106
|
+
architecture:
|
|
1107
|
+
workflowsConfig[workflowId].maas?.commissioning?.architecture ||
|
|
1108
|
+
workflowsConfig[workflowId].container?.architecture ||
|
|
1109
|
+
workflowsConfig[workflowId].debootstrap?.image?.architecture ||
|
|
1110
|
+
'arm64/generic',
|
|
1111
|
+
machine,
|
|
1112
|
+
};
|
|
1113
|
+
logger.info('Waiting for commissioning...', {
|
|
1114
|
+
...commissionMonitorPayload,
|
|
1115
|
+
machine: machine ? machine.system_id : null,
|
|
1116
|
+
});
|
|
1117
|
+
|
|
1118
|
+
const { discovery } = await UnderpostBaremetal.API.commissionMonitor(commissionMonitorPayload);
|
|
1119
|
+
|
|
1120
|
+
if ((type === 'chroot-debootstrap' || type === 'chroot-container') && options.cloudInit === true) {
|
|
1121
|
+
openTerminal(`node ${underpostRoot}/bin baremetal ${workflowId} ${ipAddress} ${hostname} --logs cloud-init`);
|
|
1122
|
+
openTerminal(
|
|
1123
|
+
`node ${underpostRoot}/bin baremetal ${workflowId} ${ipAddress} ${hostname} --logs cloud-init-machine`,
|
|
1124
|
+
);
|
|
1125
|
+
shellExec(
|
|
1126
|
+
`node ${underpostRoot}/bin baremetal ${workflowId} ${ipAddress} ${hostname} --logs cloud-init-config`,
|
|
1127
|
+
);
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
},
|
|
1131
|
+
|
|
1132
|
+
/**
|
|
1133
|
+
* @method macAddressFactory
|
|
1134
|
+
* @description Generates or returns a MAC address based on options.
|
|
1135
|
+
* @param {object} options - Options for MAC address generation.
|
|
1136
|
+
* @param {string} options.mac - 'random' for random MAC, 'hardware' to use device's actual MAC, specific MAC string, or empty for default.
|
|
1137
|
+
* @returns {object} Object with mac property - null for 'hardware', generated/specified MAC otherwise.
|
|
1138
|
+
* @memberof UnderpostBaremetal
|
|
1139
|
+
*/
|
|
1140
|
+
macAddressFactory(options = { mac: '' }) {
|
|
1141
|
+
const len = 6;
|
|
1142
|
+
const defaultMac = range(1, len)
|
|
1143
|
+
.map(() => '00')
|
|
1144
|
+
.join(':');
|
|
1145
|
+
if (options) {
|
|
1146
|
+
if (!options.mac) options.mac = defaultMac;
|
|
1147
|
+
if (options.mac === 'hardware') {
|
|
1148
|
+
// Return null to indicate hardware MAC should be used (no spoofing)
|
|
1149
|
+
options.mac = null;
|
|
1150
|
+
} else if (options.mac === 'random') {
|
|
1151
|
+
options.mac = range(1, len)
|
|
1152
|
+
.map(() => s4().toLowerCase().substring(0, 2))
|
|
1153
|
+
.join(':');
|
|
1154
|
+
}
|
|
1155
|
+
} else options = { mac: defaultMac };
|
|
1156
|
+
return options;
|
|
1157
|
+
},
|
|
1158
|
+
|
|
1159
|
+
/**
|
|
1160
|
+
* @method downloadISO
|
|
1161
|
+
* @description Downloads a generic ISO and extracts kernel boot files.
|
|
1162
|
+
* @param {object} params - Parameters for the method.
|
|
1163
|
+
* @param {object} params.resource - The MAAS boot resource object.
|
|
1164
|
+
* @param {string} params.architecture - The architecture (arm64 or amd64).
|
|
1165
|
+
* @param {string} params.nfsHostPath - The NFS host path to store the ISO and extracted files.
|
|
1166
|
+
* @param {string} params.isoUrl - The full URL to the ISO file to download.
|
|
1167
|
+
* @param {string} params.osIdLike - OS family identifier (e.g., 'debian ubuntu' or 'rhel centos fedora').
|
|
1168
|
+
* @returns {object} An object containing paths to the extracted kernel, initrd, and optionally squashfs.
|
|
1169
|
+
* @memberof UnderpostBaremetal
|
|
1170
|
+
*/
|
|
1171
|
+
downloadISO({ resource, architecture, nfsHostPath, isoUrl, osIdLike }) {
|
|
1172
|
+
const arch = architecture || resource.architecture.split('/')[0];
|
|
1173
|
+
|
|
1174
|
+
// Validate that isoUrl is provided
|
|
1175
|
+
if (!isoUrl) {
|
|
1176
|
+
throw new Error('isoUrl parameter is required. Please specify the full ISO URL in the workflow configuration.');
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
// Extract ISO filename from URL
|
|
1180
|
+
const isoFilename = isoUrl.split('/').pop();
|
|
1181
|
+
|
|
1182
|
+
// Determine OS family from osIdLike
|
|
1183
|
+
const isDebianBased = osIdLike && osIdLike.match(/debian|ubuntu/i);
|
|
1184
|
+
const isRhelBased = osIdLike && osIdLike.match(/rhel|centos|fedora|alma|rocky/i);
|
|
1185
|
+
|
|
1186
|
+
// Set extraction directory based on OS family
|
|
1187
|
+
const extractDirName = isDebianBased ? 'casper' : 'iso-extract';
|
|
1188
|
+
shellExec(`mkdir -p ${nfsHostPath}/${extractDirName}`);
|
|
1189
|
+
|
|
1190
|
+
const isoPath = `/var/tmp/live-iso/${isoFilename}`;
|
|
1191
|
+
const extractDir = `${nfsHostPath}/${extractDirName}`;
|
|
1192
|
+
|
|
1193
|
+
if (!fs.existsSync(isoPath)) {
|
|
1194
|
+
logger.info(`Downloading ISO for ${arch}...`);
|
|
1195
|
+
logger.info(`URL: ${isoUrl}`);
|
|
1196
|
+
shellExec(`mkdir -p /var/tmp/live-iso`);
|
|
1197
|
+
shellExec(`wget --progress=bar:force -O ${isoPath} "${isoUrl}"`);
|
|
1198
|
+
// Verify download by checking file existence and size (not exit code, which can be unreliable)
|
|
1199
|
+
if (!fs.existsSync(isoPath)) {
|
|
1200
|
+
throw new Error(`Failed to download ISO from ${isoUrl} - file not created`);
|
|
1201
|
+
}
|
|
1202
|
+
const stats = fs.statSync(isoPath);
|
|
1203
|
+
if (stats.size < 100 * 1024 * 1024) {
|
|
1204
|
+
shellExec(`rm -f ${isoPath}`);
|
|
1205
|
+
throw new Error(`Downloaded ISO is too small (${stats.size} bytes), download may have failed`);
|
|
1206
|
+
}
|
|
1207
|
+
logger.info(`Downloaded ISO to ${isoPath} (${(stats.size / 1024 / 1024 / 1024).toFixed(2)} GB)`);
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
// Mount ISO and extract boot files
|
|
1211
|
+
const mountPoint = `${nfsHostPath}/mnt-iso-${arch}`;
|
|
1212
|
+
shellExec(`mkdir -p ${mountPoint}`);
|
|
1213
|
+
|
|
1214
|
+
// Ensure mount point is not already mounted
|
|
1215
|
+
shellExec(`sudo umount ${mountPoint} 2>/dev/null`, { silent: true });
|
|
1216
|
+
|
|
1217
|
+
try {
|
|
1218
|
+
// Mount the ISO
|
|
1219
|
+
shellExec(`sudo mount -o loop,ro ${isoPath} ${mountPoint}`, { silent: false });
|
|
1220
|
+
logger.info(`Mounted ISO at ${mountPoint}`);
|
|
1221
|
+
|
|
1222
|
+
// Distribution-specific extraction logic
|
|
1223
|
+
if (isDebianBased) {
|
|
1224
|
+
// Ubuntu/Debian: Extract from casper directory
|
|
1225
|
+
if (!fs.existsSync(`${mountPoint}/casper`)) {
|
|
1226
|
+
throw new Error(`Failed to mount ISO or casper directory not found: ${isoPath}`);
|
|
1227
|
+
}
|
|
1228
|
+
logger.info(`Checking casper directory contents...`);
|
|
1229
|
+
shellExec(`ls -la ${mountPoint}/casper/ 2>/dev/null || echo "casper directory not found"`);
|
|
1230
|
+
shellExec(`sudo cp -a ${mountPoint}/casper/* ${extractDir}/`);
|
|
1231
|
+
}
|
|
1232
|
+
} finally {
|
|
1233
|
+
shellExec(`ls -la ${mountPoint}/`);
|
|
1234
|
+
|
|
1235
|
+
shellExec(`sudo chown -R $(whoami):$(whoami) ${extractDir}`);
|
|
1236
|
+
// Unmount ISO
|
|
1237
|
+
shellExec(`sudo umount ${mountPoint}`, { silent: true });
|
|
1238
|
+
logger.info(`Unmounted ISO`);
|
|
1239
|
+
// Clean up temporary mount point
|
|
1240
|
+
shellExec(`rmdir ${mountPoint}`, { silent: true });
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
return {
|
|
1244
|
+
'vmlinuz-efi': `${extractDir}/vmlinuz`,
|
|
1245
|
+
'initrd.img': `${extractDir}/initrd`,
|
|
1246
|
+
isoUrl,
|
|
1247
|
+
};
|
|
1248
|
+
},
|
|
1249
|
+
|
|
1250
|
+
/**
|
|
1251
|
+
* @method machineFactory
|
|
1252
|
+
* @description Creates a new machine in MAAS with specified options.
|
|
1253
|
+
* @param {object} options - Options for creating the machine.
|
|
1254
|
+
* @param {string} options.macAddress - The MAC address of the machine.
|
|
1255
|
+
* @param {string} options.hostname - The hostname for the machine.
|
|
1256
|
+
* @param {string} options.ipAddress - The IP address for the machine.
|
|
1257
|
+
* @param {string} options.powerType - The power type for the machine (default is 'manual').
|
|
1258
|
+
* @param {object} options.maas - Additional MAAS-specific options.
|
|
1259
|
+
* @returns {object} An object containing the created machine details.
|
|
1260
|
+
* @memberof UnderpostBaremetal
|
|
1261
|
+
*/
|
|
1262
|
+
machineFactory(
|
|
1263
|
+
options = {
|
|
1264
|
+
macAddress: '',
|
|
1265
|
+
hostname: '',
|
|
1266
|
+
ipAddress: '',
|
|
1267
|
+
powerType: 'manual',
|
|
1268
|
+
architecture: 'arm64/generic',
|
|
1269
|
+
},
|
|
1270
|
+
) {
|
|
1271
|
+
if (!options.powerType) options.powerType = 'manual';
|
|
1272
|
+
const payload = {
|
|
1273
|
+
architecture: (options.architecture || 'arm64/generic').match('arm') ? 'arm64/generic' : 'amd64/generic',
|
|
1274
|
+
mac_address: options.macAddress,
|
|
1275
|
+
mac_addresses: options.macAddress,
|
|
1276
|
+
hostname: options.hostname,
|
|
1277
|
+
power_type: options.powerType,
|
|
1278
|
+
ip: options.ipAddress,
|
|
1279
|
+
};
|
|
1280
|
+
logger.info('Creating MAAS machine', payload);
|
|
1281
|
+
const machine = shellExec(
|
|
1282
|
+
`maas ${process.env.MAAS_ADMIN_USERNAME} machines create ${Object.keys(payload)
|
|
1283
|
+
.map((k) => `${k}="${payload[k]}"`)
|
|
1284
|
+
.join(' ')}`,
|
|
595
1285
|
{
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
1286
|
+
silent: true,
|
|
1287
|
+
stdout: true,
|
|
1288
|
+
},
|
|
1289
|
+
);
|
|
1290
|
+
// console.log(machine);
|
|
1291
|
+
try {
|
|
1292
|
+
return { machine: JSON.parse(machine) };
|
|
1293
|
+
} catch (error) {
|
|
1294
|
+
console.log(error);
|
|
1295
|
+
logger.error(error);
|
|
1296
|
+
throw new Error(`Failed to create MAAS machine. Output:\n${machine}`);
|
|
1297
|
+
}
|
|
1298
|
+
},
|
|
1299
|
+
|
|
1300
|
+
/**
|
|
1301
|
+
* @method kernelFactory
|
|
1302
|
+
* @description Retrieves kernel, initrd, and root filesystem paths from a MAAS boot resource.
|
|
1303
|
+
* @param {object} params - Parameters for the method.
|
|
1304
|
+
* @param {object} params.resource - The MAAS boot resource object.
|
|
1305
|
+
* @param {string} params.type - The type of boot (e.g., 'iso-ram', 'iso-nfs', etc.).
|
|
1306
|
+
* @param {string} params.nfsHostPath - The NFS host path (used for ISO types).
|
|
1307
|
+
* @param {string} params.isoUrl - The ISO URL (used for ISO types).
|
|
1308
|
+
* @param {string} params.workflowId - The workflow identifier.
|
|
1309
|
+
* @returns {object} An object containing paths to the kernel, initrd, and root filesystem.
|
|
1310
|
+
* @memberof UnderpostBaremetal
|
|
1311
|
+
*/
|
|
1312
|
+
kernelFactory({ resource, type, nfsHostPath, isoUrl, workflowId }) {
|
|
1313
|
+
// For disk-based commissioning (casper/iso), use live ISO files
|
|
1314
|
+
if (type === 'iso-ram' || type === 'iso-nfs') {
|
|
1315
|
+
logger.info('Using live ISO for boot (disk-based commissioning)');
|
|
1316
|
+
const arch = resource.architecture.split('/')[0];
|
|
1317
|
+
const workflowsConfig = UnderpostBaremetal.API.loadWorkflowsConfig();
|
|
1318
|
+
const kernelFilesPaths = UnderpostBaremetal.API.downloadISO({
|
|
1319
|
+
resource,
|
|
1320
|
+
architecture: arch,
|
|
1321
|
+
nfsHostPath,
|
|
1322
|
+
isoUrl,
|
|
1323
|
+
osIdLike: workflowsConfig[workflowId].osIdLike || '',
|
|
1324
|
+
});
|
|
1325
|
+
const resourcesPath = `/var/snap/maas/common/maas/image-storage/bootloaders/uefi/${arch}`;
|
|
1326
|
+
return { kernelFilesPaths, resourcesPath };
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
const resourceData = JSON.parse(
|
|
1330
|
+
shellExec(`maas ${process.env.MAAS_ADMIN_USERNAME} boot-resource read ${resource.id}`, {
|
|
1331
|
+
stdout: true,
|
|
1332
|
+
silent: true,
|
|
1333
|
+
disableLog: true,
|
|
1334
|
+
}),
|
|
1335
|
+
);
|
|
1336
|
+
let kernelFilesPaths = {};
|
|
1337
|
+
const bootFiles = resourceData.sets[Object.keys(resourceData.sets)[0]].files;
|
|
1338
|
+
const arch = resource.architecture.split('/')[0];
|
|
1339
|
+
const resourcesPath = `/var/snap/maas/common/maas/image-storage/bootloaders/uefi/${arch}`;
|
|
1340
|
+
const kernelPath = `/var/snap/maas/common/maas/image-storage`;
|
|
1341
|
+
|
|
1342
|
+
logger.info('Available boot files', Object.keys(bootFiles));
|
|
1343
|
+
logger.info('Boot files info', {
|
|
1344
|
+
id: resourceData.id,
|
|
1345
|
+
type: resourceData.type,
|
|
1346
|
+
name: resourceData.name,
|
|
1347
|
+
architecture: resourceData.architecture,
|
|
1348
|
+
bootFiles,
|
|
1349
|
+
arch,
|
|
1350
|
+
resourcesPath,
|
|
1351
|
+
kernelPath,
|
|
1352
|
+
});
|
|
1353
|
+
|
|
1354
|
+
// Try standard synced image structure (Ubuntu, CentOS from MAAS repos)
|
|
1355
|
+
const _suffix = resource.architecture.match('xgene') ? '.xgene' : '';
|
|
1356
|
+
if (bootFiles['boot-kernel' + _suffix] && bootFiles['boot-initrd' + _suffix] && bootFiles['squashfs']) {
|
|
1357
|
+
kernelFilesPaths = {
|
|
1358
|
+
'vmlinuz-efi': `${kernelPath}/${bootFiles['boot-kernel' + _suffix].filename_on_disk}`,
|
|
1359
|
+
'initrd.img': `${kernelPath}/${bootFiles['boot-initrd' + _suffix].filename_on_disk}`,
|
|
1360
|
+
squashfs: `${kernelPath}/${bootFiles['squashfs'].filename_on_disk}`,
|
|
1361
|
+
};
|
|
1362
|
+
}
|
|
1363
|
+
// Try uploaded image structure (Packer-built images, custom uploads)
|
|
1364
|
+
else if (bootFiles['boot-kernel'] && bootFiles['boot-initrd'] && bootFiles['root-tgz']) {
|
|
1365
|
+
kernelFilesPaths = {
|
|
1366
|
+
'vmlinuz-efi': `${kernelPath}/${bootFiles['boot-kernel'].filename_on_disk}`,
|
|
1367
|
+
'initrd.img': `${kernelPath}/${bootFiles['boot-initrd'].filename_on_disk}`,
|
|
1368
|
+
squashfs: `${kernelPath}/${bootFiles['root-tgz'].filename_on_disk}`,
|
|
1369
|
+
};
|
|
1370
|
+
}
|
|
1371
|
+
// Try alternative uploaded structure with root-image-xz
|
|
1372
|
+
else if (bootFiles['boot-kernel'] && bootFiles['boot-initrd'] && bootFiles['root-image-xz']) {
|
|
1373
|
+
kernelFilesPaths = {
|
|
1374
|
+
'vmlinuz-efi': `${kernelPath}/${bootFiles['boot-kernel'].filename_on_disk}`,
|
|
1375
|
+
'initrd.img': `${kernelPath}/${bootFiles['boot-initrd'].filename_on_disk}`,
|
|
1376
|
+
squashfs: `${kernelPath}/${bootFiles['root-image-xz'].filename_on_disk}`,
|
|
1377
|
+
};
|
|
1378
|
+
}
|
|
1379
|
+
// Fallback: try to find any kernel, initrd, and root image
|
|
1380
|
+
else {
|
|
1381
|
+
logger.warn('Non-standard boot file structure detected. Available files', Object.keys(bootFiles));
|
|
1382
|
+
|
|
1383
|
+
const rootArchiveKey = Object.keys(bootFiles).find(
|
|
1384
|
+
(k) => k.includes('root') && (k.includes('tgz') || k.includes('tar.gz')),
|
|
1385
|
+
);
|
|
1386
|
+
const explicitKernel = Object.keys(bootFiles).find((k) => k.includes('kernel'));
|
|
1387
|
+
const explicitInitrd = Object.keys(bootFiles).find((k) => k.includes('initrd'));
|
|
1388
|
+
|
|
1389
|
+
if (rootArchiveKey && (!explicitKernel || !explicitInitrd)) {
|
|
1390
|
+
logger.info(`Root archive found (${rootArchiveKey}) and missing kernel/initrd. Attempting to extract.`);
|
|
1391
|
+
const rootArchivePath = `${kernelPath}/${bootFiles[rootArchiveKey].filename_on_disk}`;
|
|
1392
|
+
const tempExtractDir = `/tmp/maas-extract-${resource.id}`;
|
|
1393
|
+
shellExec(`mkdir -p ${tempExtractDir}`);
|
|
1394
|
+
|
|
1395
|
+
// List files in archive to find kernel and initrd
|
|
1396
|
+
const tarList = shellExec(`tar -tf ${rootArchivePath}`, { silent: true }).stdout.split('\n');
|
|
1397
|
+
|
|
1398
|
+
// Look for boot/vmlinuz* and boot/initrd* (handling potential leading ./)
|
|
1399
|
+
// Skip rescue, kdump, and other special images
|
|
1400
|
+
const vmlinuzPaths = tarList.filter(
|
|
1401
|
+
(f) => f.match(/(\.\/)?boot\/vmlinuz-[0-9]/) && !f.includes('rescue') && !f.includes('kdump'),
|
|
602
1402
|
);
|
|
603
|
-
const
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
};
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
//
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
'intr',
|
|
634
|
-
'rsize=32768',
|
|
635
|
-
'wsize=32768',
|
|
636
|
-
'acregmin=0',
|
|
637
|
-
'acregmax=0',
|
|
638
|
-
'acdirmin=0',
|
|
639
|
-
'acdirmax=0',
|
|
640
|
-
'noac',
|
|
641
|
-
// 'nodev',
|
|
642
|
-
// 'nosuid',
|
|
643
|
-
]}`,
|
|
644
|
-
`ip=${ipAddress}:${callbackMetaData.runnerHost.ip}:${callbackMetaData.runnerHost.ip}:${netmask}:${hostname}:${networkInterfaceName}:static`,
|
|
645
|
-
`rootfstype=nfs`,
|
|
646
|
-
`rw`,
|
|
647
|
-
`rootwait`,
|
|
648
|
-
`fixrtc`,
|
|
649
|
-
'initrd=initrd.img',
|
|
650
|
-
// 'boot=casper',
|
|
651
|
-
// 'ro',
|
|
652
|
-
'netboot=nfs',
|
|
653
|
-
`init=/sbin/init`,
|
|
654
|
-
// `cloud-config-url=/dev/null`,
|
|
655
|
-
// 'ip=dhcp',
|
|
656
|
-
// 'ip=dfcp',
|
|
657
|
-
// 'autoinstall',
|
|
658
|
-
// 'rd.break',
|
|
659
|
-
|
|
660
|
-
// Disable services that not apply over nfs
|
|
661
|
-
`systemd.mask=systemd-network-generator.service`,
|
|
662
|
-
`systemd.mask=systemd-networkd.service`,
|
|
663
|
-
`systemd.mask=systemd-fsck-root.service`,
|
|
664
|
-
`systemd.mask=systemd-udev-trigger.service`,
|
|
665
|
-
];
|
|
666
|
-
const nfsConnectStr = cmd.join(' ');
|
|
1403
|
+
const initrdPaths = tarList.filter(
|
|
1404
|
+
(f) =>
|
|
1405
|
+
f.match(/(\.\/)?boot\/(initrd|initramfs)-[0-9]/) &&
|
|
1406
|
+
!f.includes('rescue') &&
|
|
1407
|
+
!f.includes('kdump') &&
|
|
1408
|
+
f.includes('.img'),
|
|
1409
|
+
);
|
|
1410
|
+
|
|
1411
|
+
logger.info(`Found kernel candidates:`, { vmlinuzPaths, initrdPaths });
|
|
1412
|
+
|
|
1413
|
+
// Try to match kernel and initrd by version number
|
|
1414
|
+
let vmlinuzPath = null;
|
|
1415
|
+
let initrdPath = null;
|
|
1416
|
+
|
|
1417
|
+
if (vmlinuzPaths.length > 0 && initrdPaths.length > 0) {
|
|
1418
|
+
// Extract version from kernel filename (e.g., "5.14.0-611.11.1.el9_7.aarch64")
|
|
1419
|
+
for (const kernelPath of vmlinuzPaths.sort().reverse()) {
|
|
1420
|
+
const kernelVersion = kernelPath.match(/vmlinuz-(.+)$/)?.[1];
|
|
1421
|
+
if (kernelVersion) {
|
|
1422
|
+
// Look for matching initrd
|
|
1423
|
+
const matchingInitrd = initrdPaths.find((p) => p.includes(kernelVersion));
|
|
1424
|
+
if (matchingInitrd) {
|
|
1425
|
+
vmlinuzPath = kernelPath;
|
|
1426
|
+
initrdPath = matchingInitrd;
|
|
1427
|
+
logger.info(`Matched kernel and initrd by version: ${kernelVersion}`);
|
|
1428
|
+
break;
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
667
1433
|
|
|
668
|
-
//
|
|
669
|
-
|
|
670
|
-
|
|
1434
|
+
// Fallback: use newest versions if no match found
|
|
1435
|
+
if (!vmlinuzPath && vmlinuzPaths.length > 0) {
|
|
1436
|
+
vmlinuzPath = vmlinuzPaths.sort().pop();
|
|
671
1437
|
}
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
shellExec(`sudo cp -a ${kernelFilesPaths[file]} ${tftpRootPath}/pxe/${file}`);
|
|
1438
|
+
if (!initrdPath && initrdPaths.length > 0) {
|
|
1439
|
+
initrdPath = initrdPaths.sort().pop();
|
|
675
1440
|
}
|
|
676
1441
|
|
|
677
|
-
|
|
678
|
-
fs.writeFileSync(
|
|
679
|
-
`${process.env.TFTP_ROOT}/grub/grub.cfg`,
|
|
680
|
-
`
|
|
681
|
-
insmod gzio
|
|
682
|
-
insmod http
|
|
683
|
-
insmod nfs
|
|
684
|
-
set timeout=5
|
|
685
|
-
set default=0
|
|
686
|
-
|
|
687
|
-
menuentry '${menuentryStr}' {
|
|
688
|
-
set root=(tftp,${callbackMetaData.runnerHost.ip})
|
|
689
|
-
linux /${hostname}/pxe/vmlinuz-efi ${nfsConnectStr}
|
|
690
|
-
initrd /${hostname}/pxe/initrd.img
|
|
691
|
-
boot
|
|
692
|
-
}
|
|
1442
|
+
logger.info(`Selected kernel: ${vmlinuzPath}, initrd: ${initrdPath}`);
|
|
693
1443
|
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
1444
|
+
if (vmlinuzPath && initrdPath) {
|
|
1445
|
+
// Extract specific files
|
|
1446
|
+
// Extract all files in boot/ to ensure symlinks resolve
|
|
1447
|
+
shellExec(`tar -xf ${rootArchivePath} -C ${tempExtractDir} --wildcards '*boot/*'`);
|
|
1448
|
+
|
|
1449
|
+
kernelFilesPaths = {
|
|
1450
|
+
'vmlinuz-efi': `${tempExtractDir}/${vmlinuzPath}`,
|
|
1451
|
+
'initrd.img': `${tempExtractDir}/${initrdPath}`,
|
|
1452
|
+
squashfs: rootArchivePath,
|
|
1453
|
+
};
|
|
1454
|
+
logger.info('Extracted kernel and initrd from root archive.');
|
|
1455
|
+
} else {
|
|
1456
|
+
logger.error(
|
|
1457
|
+
`Failed to find kernel/initrd in archive. Contents of boot/ directory:`,
|
|
1458
|
+
tarList.filter((f) => f.includes('boot/')),
|
|
1459
|
+
);
|
|
1460
|
+
throw new Error(`Could not find kernel or initrd in ${rootArchiveKey}`);
|
|
1461
|
+
}
|
|
1462
|
+
} else {
|
|
1463
|
+
const kernelFile = Object.keys(bootFiles).find((k) => k.includes('kernel')) || Object.keys(bootFiles)[0];
|
|
1464
|
+
const initrdFile = Object.keys(bootFiles).find((k) => k.includes('initrd')) || Object.keys(bootFiles)[1];
|
|
1465
|
+
const rootFile =
|
|
1466
|
+
Object.keys(bootFiles).find(
|
|
1467
|
+
(k) => k.includes('root') || k.includes('squashfs') || k.includes('tgz') || k.includes('xz'),
|
|
1468
|
+
) || Object.keys(bootFiles)[2];
|
|
1469
|
+
|
|
1470
|
+
if (kernelFile && initrdFile && rootFile) {
|
|
1471
|
+
kernelFilesPaths = {
|
|
1472
|
+
'vmlinuz-efi': `${kernelPath}/${bootFiles[kernelFile].filename_on_disk}`,
|
|
1473
|
+
'initrd.img': `${kernelPath}/${bootFiles[initrdFile].filename_on_disk}`,
|
|
1474
|
+
squashfs: `${kernelPath}/${bootFiles[rootFile].filename_on_disk}`,
|
|
1475
|
+
};
|
|
1476
|
+
logger.info('Using detected files', { kernel: kernelFile, initrd: initrdFile, root: rootFile });
|
|
1477
|
+
} else {
|
|
1478
|
+
throw new Error(`Cannot identify boot files. Available: ${Object.keys(bootFiles).join(', ')}`);
|
|
1479
|
+
}
|
|
697
1480
|
}
|
|
1481
|
+
}
|
|
1482
|
+
return {
|
|
1483
|
+
resource,
|
|
1484
|
+
bootFiles,
|
|
1485
|
+
arch,
|
|
1486
|
+
resourcesPath,
|
|
1487
|
+
kernelPath,
|
|
1488
|
+
resourceData,
|
|
1489
|
+
kernelFilesPaths,
|
|
1490
|
+
};
|
|
1491
|
+
},
|
|
1492
|
+
|
|
1493
|
+
/**
|
|
1494
|
+
* @method writeGrubConfigToFile
|
|
1495
|
+
* @description Writes the GRUB configuration content to the grub.cfg file in the TFTP root.
|
|
1496
|
+
* @param {object} params - Parameters for the method.
|
|
1497
|
+
* @param {string} params.grubCfgSrc - The GRUB configuration content to write.
|
|
1498
|
+
* @memberof UnderpostBaremetal
|
|
1499
|
+
* @returns {void}
|
|
1500
|
+
*/
|
|
1501
|
+
writeGrubConfigToFile({ grubCfgSrc = '' }) {
|
|
1502
|
+
shellExec(`mkdir -p ${process.env.TFTP_ROOT}/grub`, {
|
|
1503
|
+
disableLog: true,
|
|
1504
|
+
});
|
|
1505
|
+
return fs.writeFileSync(`${process.env.TFTP_ROOT}/grub/grub.cfg`, grubCfgSrc, 'utf8');
|
|
1506
|
+
},
|
|
698
1507
|
|
|
1508
|
+
/**
|
|
1509
|
+
* @method getGrubConfigFromFile
|
|
1510
|
+
* @description Reads the GRUB configuration content from the grub.cfg file in the TFTP root.
|
|
1511
|
+
* @memberof UnderpostBaremetal
|
|
1512
|
+
* @returns {string} The GRUB configuration content.
|
|
1513
|
+
*/
|
|
1514
|
+
getGrubConfigFromFile() {
|
|
1515
|
+
const grubCfgPath = `${process.env.TFTP_ROOT}/grub/grub.cfg`;
|
|
1516
|
+
const grubCfgSrc = fs.readFileSync(grubCfgPath, 'utf8');
|
|
1517
|
+
return { grubCfgPath, grubCfgSrc };
|
|
1518
|
+
},
|
|
1519
|
+
|
|
1520
|
+
/**
|
|
1521
|
+
* @method removeDiscoveredMachines
|
|
1522
|
+
* @description Removes all machines in the 'discovered' status from MAAS.
|
|
1523
|
+
* @memberof UnderpostBaremetal
|
|
1524
|
+
* @returns {void}
|
|
1525
|
+
*/
|
|
1526
|
+
removeDiscoveredMachines() {
|
|
1527
|
+
logger.info('Removing all discovered machines from MAAS...');
|
|
1528
|
+
shellExec(`maas ${process.env.MAAS_ADMIN_USERNAME} discoveries clear all=true`);
|
|
1529
|
+
},
|
|
1530
|
+
|
|
1531
|
+
/**
|
|
1532
|
+
* @method efiGrubModulesFactory
|
|
1533
|
+
* @description Copies the appropriate EFI GRUB modules to the TFTP root based on the image architecture.
|
|
1534
|
+
* @param {object} options - Options for determining which GRUB modules to copy.
|
|
1535
|
+
* @param {object} options.image - Image configuration object.
|
|
1536
|
+
* @param {string} options.image.architecture - The architecture of the image ('amd64' or 'arm64').
|
|
1537
|
+
* @memberof UnderpostBaremetal
|
|
1538
|
+
* @returns {void}
|
|
1539
|
+
*/
|
|
1540
|
+
efiGrubModulesFactory(options = { image: { architecture: 'amd64' } }) {
|
|
1541
|
+
if (options.image.architecture.match('arm64')) {
|
|
699
1542
|
// Copy ARM64 EFI GRUB modules.
|
|
700
1543
|
const arm64EfiPath = `${process.env.TFTP_ROOT}/grub/arm64-efi`;
|
|
701
1544
|
if (fs.existsSync(arm64EfiPath)) shellExec(`sudo rm -rf ${arm64EfiPath}`);
|
|
702
1545
|
shellExec(`sudo cp -a /usr/lib/grub/arm64-efi ${arm64EfiPath}`);
|
|
703
|
-
|
|
704
|
-
//
|
|
705
|
-
|
|
706
|
-
shellExec(`sudo
|
|
1546
|
+
} else {
|
|
1547
|
+
// Copy AMD64 EFI GRUB modules.
|
|
1548
|
+
const amd64EfiPath = `${process.env.TFTP_ROOT}/grub/x86_64-efi`;
|
|
1549
|
+
if (fs.existsSync(amd64EfiPath)) shellExec(`sudo rm -rf ${amd64EfiPath}`);
|
|
1550
|
+
shellExec(`sudo cp -a /usr/lib/grub/x86_64-efi ${amd64EfiPath}`);
|
|
707
1551
|
}
|
|
1552
|
+
},
|
|
708
1553
|
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
1554
|
+
/**
|
|
1555
|
+
* @method ipxeEmbeddedScriptFactory
|
|
1556
|
+
* @description Generates the embedded iPXE boot script that performs DHCP and chains to the main script.
|
|
1557
|
+
* This script is embedded into the iPXE binary or loaded first by GRUB.
|
|
1558
|
+
* Supports MAC address spoofing for baremetal commissioning workflows.
|
|
1559
|
+
* @param {object} params - The parameters for generating the script.
|
|
1560
|
+
* @param {string} params.tftpServer - The IP address of the TFTP server.
|
|
1561
|
+
* @param {string} params.scriptPath - The path to the main iPXE script on TFTP server.
|
|
1562
|
+
* @param {string} [params.macAddress] - Optional MAC address to spoof for MAAS registration.
|
|
1563
|
+
* @returns {string} The embedded iPXE script content.
|
|
1564
|
+
* @memberof UnderpostBaremetal
|
|
1565
|
+
*/
|
|
1566
|
+
ipxeEmbeddedScriptFactory({ tftpServer, scriptPath, macAddress = null }) {
|
|
1567
|
+
const macSpoofingBlock =
|
|
1568
|
+
macAddress && macAddress !== '00:00:00:00:00:00'
|
|
1569
|
+
? `
|
|
1570
|
+
# MAC Address Information
|
|
1571
|
+
echo ========================================
|
|
1572
|
+
echo Target MAC for MAAS: ${macAddress}
|
|
1573
|
+
echo Hardware MAC: \${net0/mac}
|
|
1574
|
+
echo ========================================
|
|
1575
|
+
echo NOTE: iPXE MAC spoofing does NOT persist to kernel
|
|
1576
|
+
echo The kernel will receive MAC via ifname= parameter
|
|
1577
|
+
echo ========================================
|
|
1578
|
+
`
|
|
1579
|
+
: `
|
|
1580
|
+
# Using hardware MAC address
|
|
1581
|
+
echo ========================================
|
|
1582
|
+
echo Using device hardware MAC address
|
|
1583
|
+
echo Hardware MAC: \${net0/mac}
|
|
1584
|
+
echo MAC spoofing disabled - device uses actual MAC
|
|
1585
|
+
echo ========================================
|
|
1586
|
+
`;
|
|
1587
|
+
|
|
1588
|
+
return `#!ipxe
|
|
1589
|
+
# Embedded iPXE Boot Script
|
|
1590
|
+
# This script performs DHCP configuration and chains to the main boot script
|
|
1591
|
+
|
|
1592
|
+
echo ========================================
|
|
1593
|
+
echo iPXE Embedded Boot Loader
|
|
1594
|
+
echo ========================================
|
|
1595
|
+
echo TFTP Server: ${tftpServer}
|
|
1596
|
+
echo Script Path: ${scriptPath}
|
|
1597
|
+
echo ========================================
|
|
1598
|
+
|
|
1599
|
+
${macSpoofingBlock}
|
|
1600
|
+
|
|
1601
|
+
# Show network interface info before DHCP
|
|
1602
|
+
echo Network Interface Info (before DHCP):
|
|
1603
|
+
echo Interface: net0
|
|
1604
|
+
echo MAC: \${net0/mac}
|
|
1605
|
+
ifstat
|
|
1606
|
+
|
|
1607
|
+
# Perform DHCP to get network configuration
|
|
1608
|
+
echo ========================================
|
|
1609
|
+
echo Performing DHCP configuration...
|
|
1610
|
+
echo ========================================
|
|
1611
|
+
dhcp net0 || goto dhcp_retry
|
|
1612
|
+
|
|
1613
|
+
echo DHCP configuration successful
|
|
1614
|
+
echo IP Address: \${net0/ip}
|
|
1615
|
+
echo Netmask: \${net0/netmask}
|
|
1616
|
+
echo Gateway: \${net0/gateway}
|
|
1617
|
+
echo DNS: \${net0/dns}
|
|
1618
|
+
echo TFTP Server: \${next-server}
|
|
1619
|
+
echo MAC used by DHCP: \${net0/mac}
|
|
1620
|
+
|
|
1621
|
+
# Chain to the main iPXE script
|
|
1622
|
+
echo ========================================
|
|
1623
|
+
echo Chainloading main boot script...
|
|
1624
|
+
echo Script: tftp://${tftpServer}${scriptPath}
|
|
1625
|
+
echo ========================================
|
|
1626
|
+
chain tftp://${tftpServer}${scriptPath} || goto chain_failed
|
|
1627
|
+
|
|
1628
|
+
:dhcp_retry
|
|
1629
|
+
echo DHCP failed, retrying in 5 seconds...
|
|
1630
|
+
sleep 5
|
|
1631
|
+
dhcp net0 || goto dhcp_retry
|
|
1632
|
+
goto dhcp_success
|
|
1633
|
+
|
|
1634
|
+
:dhcp_success
|
|
1635
|
+
echo DHCP retry successful
|
|
1636
|
+
echo IP Address: \${net0/ip}
|
|
1637
|
+
echo MAC: \${net0/mac}
|
|
1638
|
+
chain tftp://${tftpServer}${scriptPath} || goto chain_failed
|
|
1639
|
+
|
|
1640
|
+
:chain_failed
|
|
1641
|
+
echo ========================================
|
|
1642
|
+
echo ERROR: Failed to chain to main script
|
|
1643
|
+
echo TFTP Server: ${tftpServer}
|
|
1644
|
+
echo Script Path: ${scriptPath}
|
|
1645
|
+
echo ========================================
|
|
1646
|
+
echo Retrying in 10 seconds...
|
|
1647
|
+
sleep 10
|
|
1648
|
+
chain tftp://${tftpServer}${scriptPath} || goto shell_debug
|
|
1649
|
+
|
|
1650
|
+
:shell_debug
|
|
1651
|
+
echo Dropping to iPXE shell for manual debugging
|
|
1652
|
+
echo Try: chain tftp://${tftpServer}${scriptPath}
|
|
1653
|
+
shell
|
|
1654
|
+
`;
|
|
1655
|
+
},
|
|
713
1656
|
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
1657
|
+
/**
|
|
1658
|
+
* @method ipxeScriptFactory
|
|
1659
|
+
* @description Generates the iPXE script content for stable identity.
|
|
1660
|
+
* This iPXE script uses directly boots kernel/initrd via TFTP.
|
|
1661
|
+
* @param {object} params - The parameters for generating the script.
|
|
1662
|
+
* @param {string} params.maasIp - The IP address of the MAAS server.
|
|
1663
|
+
* @param {string} [params.macAddress] - The MAC address registered in MAAS (for display only).
|
|
1664
|
+
* @param {string} params.architecture - The architecture (arm64/amd64).
|
|
1665
|
+
* @param {string} params.tftpPrefix - The TFTP prefix path (e.g., 'rpi4mb').
|
|
1666
|
+
* @param {string} params.kernelCmd - The kernel command line parameters.
|
|
1667
|
+
* @returns {string} The iPXE script content.
|
|
1668
|
+
* @memberof UnderpostBaremetal
|
|
1669
|
+
*/
|
|
1670
|
+
ipxeScriptFactory({ maasIp, macAddress, architecture, tftpPrefix, kernelCmd }) {
|
|
1671
|
+
const macInfo =
|
|
1672
|
+
macAddress && macAddress !== '00:00:00:00:00:00'
|
|
1673
|
+
? `echo Registered MAC: ${macAddress}`
|
|
1674
|
+
: `echo Using hardware MAC address`;
|
|
1675
|
+
|
|
1676
|
+
// Construct the full TFTP paths for kernel and initrd
|
|
1677
|
+
const kernelPath = `${tftpPrefix}/pxe/vmlinuz-efi`;
|
|
1678
|
+
const initrdPath = `${tftpPrefix}/pxe/initrd.img`;
|
|
1679
|
+
const grubBootloader = architecture === 'arm64' ? 'grubaa64.efi' : 'grubx64.efi';
|
|
1680
|
+
const grubPath = `${tftpPrefix}/pxe/${grubBootloader}`;
|
|
1681
|
+
|
|
1682
|
+
return `#!ipxe
|
|
1683
|
+
echo ========================================
|
|
1684
|
+
echo iPXE Network Boot
|
|
1685
|
+
echo ========================================
|
|
1686
|
+
echo MAAS Server: ${maasIp}
|
|
1687
|
+
echo Architecture: ${architecture}
|
|
1688
|
+
${macInfo}
|
|
1689
|
+
echo ========================================
|
|
1690
|
+
|
|
1691
|
+
# Show current network configuration
|
|
1692
|
+
echo Current Network Configuration:
|
|
1693
|
+
ifstat
|
|
1694
|
+
|
|
1695
|
+
# Display MAC address information
|
|
1696
|
+
echo MAC Address: \${net0/mac}
|
|
1697
|
+
echo IP Address: \${net0/ip}
|
|
1698
|
+
echo Gateway: \${net0/gateway}
|
|
1699
|
+
echo DNS: \${net0/dns}
|
|
1700
|
+
${macAddress && macAddress !== '00:00:00:00:00:00' ? `echo Target MAC for kernel: ${macAddress}` : ''}
|
|
1701
|
+
|
|
1702
|
+
# Direct kernel/initrd boot via TFTP
|
|
1703
|
+
# Modern simplified approach: Direct kernel/initrd boot via TFTP
|
|
1704
|
+
echo ========================================
|
|
1705
|
+
echo Loading kernel and initrd via TFTP...
|
|
1706
|
+
echo Kernel: tftp://${maasIp}/${kernelPath}
|
|
1707
|
+
echo Initrd: tftp://${maasIp}/${initrdPath}
|
|
1708
|
+
${macAddress && macAddress !== '00:00:00:00:00:00' ? `echo Kernel will use MAC: ${macAddress} (via ifname parameter)` : 'echo Kernel will use hardware MAC'}
|
|
1709
|
+
echo ========================================
|
|
1710
|
+
|
|
1711
|
+
# Load kernel via TFTP
|
|
1712
|
+
kernel tftp://${maasIp}/${kernelPath} ${kernelCmd || 'console=ttyS0,115200'} || goto grub_fallback
|
|
1713
|
+
echo Kernel loaded successfully
|
|
1714
|
+
|
|
1715
|
+
# Load initrd via TFTP
|
|
1716
|
+
initrd tftp://${maasIp}/${initrdPath} || goto grub_fallback
|
|
1717
|
+
echo Initrd loaded successfully
|
|
1718
|
+
|
|
1719
|
+
# Boot the kernel
|
|
1720
|
+
echo Booting kernel...
|
|
1721
|
+
boot
|
|
1722
|
+
|
|
1723
|
+
:grub_fallback
|
|
1724
|
+
echo ========================================
|
|
1725
|
+
echo Direct kernel boot failed, falling back to GRUB chainload...
|
|
1726
|
+
echo TFTP Path: tftp://${maasIp}/${grubPath}
|
|
1727
|
+
echo ========================================
|
|
1728
|
+
|
|
1729
|
+
# Fallback: Chain to GRUB via TFTP (avoids malformed HTTP bootloader issues)
|
|
1730
|
+
chain tftp://${maasIp}/${grubPath} || goto http_fallback
|
|
1731
|
+
|
|
1732
|
+
:http_fallback
|
|
1733
|
+
echo TFTP GRUB chainload failed, trying HTTP fallback...
|
|
1734
|
+
echo ========================================
|
|
1735
|
+
|
|
1736
|
+
# Fallback: Try MAAS HTTP bootloader (may have certificate issues)
|
|
1737
|
+
set boot-url http://${maasIp}:5248/images/bootloader
|
|
1738
|
+
echo Boot URL: \${boot-url}
|
|
1739
|
+
chain \${boot-url}/uefi/${architecture}/${grubBootloader === 'grubaa64.efi' ? 'bootaa64.efi' : 'bootx64.efi'} || goto error
|
|
1740
|
+
|
|
1741
|
+
:error
|
|
1742
|
+
echo ========================================
|
|
1743
|
+
echo ERROR: All boot methods failed
|
|
1744
|
+
echo ========================================
|
|
1745
|
+
echo MAAS IP: ${maasIp}
|
|
1746
|
+
echo Architecture: ${architecture}
|
|
1747
|
+
echo MAC: \${net0/mac}
|
|
1748
|
+
echo IP: \${net0/ip}
|
|
1749
|
+
echo ========================================
|
|
1750
|
+
echo Retrying GRUB TFTP in 10 seconds...
|
|
1751
|
+
sleep 10
|
|
1752
|
+
chain tftp://${maasIp}/${grubPath} || goto shell_debug
|
|
1753
|
+
|
|
1754
|
+
:shell_debug
|
|
1755
|
+
echo Dropping to iPXE shell for manual intervention
|
|
1756
|
+
shell
|
|
1757
|
+
`;
|
|
1758
|
+
},
|
|
722
1759
|
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
1760
|
+
/**
|
|
1761
|
+
* @method ipxeEfiFactory
|
|
1762
|
+
* @description Manages iPXE EFI binary build with cache support.
|
|
1763
|
+
* Checks cache, builds only if needed, saves to cache after build.
|
|
1764
|
+
* @param {object} params - The parameters for iPXE build.
|
|
1765
|
+
* @param {string} params.tftpRootPath - TFTP root directory path.
|
|
1766
|
+
* @param {string} params.ipxeCacheDir - iPXE cache directory path.
|
|
1767
|
+
* @param {string} params.arch - Target architecture (arm64/amd64).
|
|
1768
|
+
* @param {string} params.underpostRoot - Underpost root directory.
|
|
1769
|
+
* @param {string} [params.embeddedScriptPath] - Path to embedded boot script.
|
|
1770
|
+
* @param {boolean} [params.forceRebuild=false] - Force rebuild regardless of cache.
|
|
1771
|
+
* @returns {void}
|
|
1772
|
+
* @memberof UnderpostBaremetal
|
|
1773
|
+
*/
|
|
1774
|
+
ipxeEfiFactory({ tftpRootPath, ipxeCacheDir, arch, underpostRoot, embeddedScriptPath, forceRebuild = false }) {
|
|
1775
|
+
const shouldRebuild =
|
|
1776
|
+
forceRebuild || (!fs.existsSync(`${tftpRootPath}/ipxe.efi`) && !fs.existsSync(`${ipxeCacheDir}/ipxe.efi`));
|
|
1777
|
+
|
|
1778
|
+
if (!shouldRebuild) return;
|
|
1779
|
+
|
|
1780
|
+
if (embeddedScriptPath && fs.existsSync(embeddedScriptPath)) {
|
|
1781
|
+
logger.info('Rebuilding iPXE with embedded boot script...', {
|
|
1782
|
+
embeddedScriptPath,
|
|
1783
|
+
forced: forceRebuild,
|
|
742
1784
|
});
|
|
1785
|
+
shellExec(
|
|
1786
|
+
`${underpostRoot}/scripts/ipxe-setup.sh ${tftpRootPath} --target-arch ${arch} --embed-script ${embeddedScriptPath} --rebuild`,
|
|
1787
|
+
);
|
|
1788
|
+
} else if (shouldRebuild) {
|
|
1789
|
+
shellExec(`${underpostRoot}/scripts/ipxe-setup.sh ${tftpRootPath} --target-arch ${arch}`);
|
|
1790
|
+
}
|
|
743
1791
|
|
|
744
|
-
|
|
1792
|
+
shellExec(`mkdir -p ${ipxeCacheDir}`);
|
|
1793
|
+
shellExec(`cp ${tftpRootPath}/ipxe.efi ${ipxeCacheDir}/ipxe.efi`);
|
|
1794
|
+
},
|
|
745
1795
|
|
|
746
|
-
|
|
747
|
-
|
|
1796
|
+
/**
|
|
1797
|
+
* @method grubFactory
|
|
1798
|
+
* @description Generates the GRUB configuration file content.
|
|
1799
|
+
* @param {object} params - The parameters for generating the configuration.
|
|
1800
|
+
* @param {string} params.menuentryStr - The title of the menu entry.
|
|
1801
|
+
* @param {string} params.kernelPath - The path to the kernel file (relative to TFTP root).
|
|
1802
|
+
* @param {string} params.initrdPath - The path to the initrd file (relative to TFTP root).
|
|
1803
|
+
* @param {string} params.cmd - The kernel command line parameters.
|
|
1804
|
+
* @param {string} params.tftpIp - The IP address of the TFTP server.
|
|
1805
|
+
* @param {boolean} [params.ipxe] - Flag to enable iPXE chainloading.
|
|
1806
|
+
* @param {string} [params.ipxePath] - The path to the iPXE binary.
|
|
1807
|
+
* @returns {object} An object containing 'grubCfgSrc' the GRUB configuration source string.
|
|
1808
|
+
* @memberof UnderpostBaremetal
|
|
1809
|
+
*/
|
|
1810
|
+
grubFactory({ menuentryStr, kernelPath, initrdPath, cmd, tftpIp, ipxe, ipxePath }) {
|
|
1811
|
+
if (ipxe) {
|
|
1812
|
+
return {
|
|
1813
|
+
grubCfgSrc: `
|
|
1814
|
+
set default="0"
|
|
1815
|
+
set timeout=10
|
|
1816
|
+
insmod tftp
|
|
1817
|
+
set root=(tftp,${tftpIp})
|
|
1818
|
+
|
|
1819
|
+
menuentry 'iPXE ${menuentryStr}' {
|
|
1820
|
+
echo "Loading iPXE with embedded script..."
|
|
1821
|
+
echo "[INFO] TFTP Server: ${tftpIp}"
|
|
1822
|
+
echo "[INFO] iPXE Binary: ${ipxePath}"
|
|
1823
|
+
echo "[INFO] iPXE will execute embedded script (dhcp + chain)"
|
|
1824
|
+
chainloader ${ipxePath}
|
|
1825
|
+
boot
|
|
1826
|
+
}
|
|
1827
|
+
`,
|
|
1828
|
+
};
|
|
1829
|
+
}
|
|
1830
|
+
return {
|
|
1831
|
+
grubCfgSrc: `
|
|
1832
|
+
set default="0"
|
|
1833
|
+
set timeout=10
|
|
1834
|
+
insmod nfs
|
|
1835
|
+
insmod gzio
|
|
1836
|
+
insmod http
|
|
1837
|
+
insmod tftp
|
|
1838
|
+
set root=(tftp,${tftpIp})
|
|
1839
|
+
|
|
1840
|
+
menuentry '${menuentryStr}' {
|
|
1841
|
+
echo "${menuentryStr}"
|
|
1842
|
+
echo " ${Underpost.version}"
|
|
1843
|
+
echo "Date: ${new Date().toISOString()}"
|
|
1844
|
+
${cmd.match('/MAAS/metadata/by-id/') ? `echo "System ID: ${cmd.split('/MAAS/metadata/by-id/')[1].split('/')[0]}"` : ''}
|
|
1845
|
+
echo "TFTP server: ${tftpIp}"
|
|
1846
|
+
echo "Kernel path: ${kernelPath}"
|
|
1847
|
+
echo "Initrd path: ${initrdPath}"
|
|
1848
|
+
echo "Starting boot process..."
|
|
1849
|
+
echo "Loading kernel..."
|
|
1850
|
+
linux /${kernelPath} ${cmd}
|
|
1851
|
+
echo "Loading initrd..."
|
|
1852
|
+
initrd /${initrdPath}
|
|
1853
|
+
echo "Booting..."
|
|
1854
|
+
boot
|
|
1855
|
+
}
|
|
1856
|
+
`,
|
|
1857
|
+
};
|
|
1858
|
+
},
|
|
748
1859
|
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
1860
|
+
/**
|
|
1861
|
+
* @method httpBootstrapServerStaticFactory
|
|
1862
|
+
* @description Creates static files for the bootstrap HTTP server including cloud-init configuration.
|
|
1863
|
+
* @param {object} params - Parameters for creating static files.
|
|
1864
|
+
* @param {string} params.bootstrapHttpServerPath - The path where static files will be created.
|
|
1865
|
+
* @param {string} params.hostname - The hostname of the client machine.
|
|
1866
|
+
* @param {string} params.cloudConfigSrc - The cloud-init configuration YAML source.
|
|
1867
|
+
* @param {object} [params.metadata] - Optional metadata to include in meta-data file.
|
|
1868
|
+
* @param {string} [params.vendorData] - Optional vendor-data content (default: empty string).
|
|
1869
|
+
* @memberof UnderpostBaremetal
|
|
1870
|
+
* @returns {void}
|
|
1871
|
+
*/
|
|
1872
|
+
httpBootstrapServerStaticFactory({
|
|
1873
|
+
bootstrapHttpServerPath,
|
|
1874
|
+
hostname,
|
|
1875
|
+
cloudConfigSrc,
|
|
1876
|
+
metadata = {},
|
|
1877
|
+
vendorData = '',
|
|
1878
|
+
}) {
|
|
1879
|
+
// Create directory structure
|
|
1880
|
+
shellExec(`mkdir -p ${bootstrapHttpServerPath}/${hostname}/cloud-init`);
|
|
1881
|
+
|
|
1882
|
+
// Write user-data file
|
|
1883
|
+
fs.writeFileSync(
|
|
1884
|
+
`${bootstrapHttpServerPath}/${hostname}/cloud-init/user-data`,
|
|
1885
|
+
`#cloud-config\n${cloudConfigSrc}`,
|
|
1886
|
+
'utf8',
|
|
1887
|
+
);
|
|
754
1888
|
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
debootstrapArch: debootstrap.image.architecture,
|
|
759
|
-
callbackMetaData,
|
|
760
|
-
steps: [
|
|
761
|
-
UnderpostCloudInit.API.configFactory({
|
|
762
|
-
controlServerIp: callbackMetaData.runnerHost.ip,
|
|
763
|
-
hostname,
|
|
764
|
-
commissioningDeviceIp: ipAddress,
|
|
765
|
-
gatewayip: callbackMetaData.runnerHost.ip,
|
|
766
|
-
mac: macAddress, // Updated MAC address.
|
|
767
|
-
timezone,
|
|
768
|
-
chronyConfPath,
|
|
769
|
-
networkInterfaceName,
|
|
770
|
-
}),
|
|
771
|
-
],
|
|
772
|
-
});
|
|
1889
|
+
// Write meta-data file
|
|
1890
|
+
const metaDataContent = `instance-id: ${metadata.instanceId || hostname}\nlocal-hostname: ${metadata.localHostname || hostname}`;
|
|
1891
|
+
fs.writeFileSync(`${bootstrapHttpServerPath}/${hostname}/cloud-init/meta-data`, metaDataContent, 'utf8');
|
|
773
1892
|
|
|
774
|
-
|
|
775
|
-
|
|
1893
|
+
// Write vendor-data file
|
|
1894
|
+
fs.writeFileSync(`${bootstrapHttpServerPath}/${hostname}/cloud-init/vendor-data`, vendorData, 'utf8');
|
|
776
1895
|
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
1896
|
+
logger.info(`Cloud-init files written to ${bootstrapHttpServerPath}/${hostname}/cloud-init`);
|
|
1897
|
+
},
|
|
1898
|
+
|
|
1899
|
+
/**
|
|
1900
|
+
* @method httpBootstrapServerRunnerFactory
|
|
1901
|
+
* @description Starts a simple HTTP server to serve boot files for network booting.
|
|
1902
|
+
* @param {object} options - Options for the HTTP server.
|
|
1903
|
+
* @param {string} hostname - The hostname of the client machine.
|
|
1904
|
+
* @param {string} options.bootstrapHttpServerPath - The path to serve files from (default: './public/localhost').
|
|
1905
|
+
* @param {number} options.bootstrapHttpServerPort - The port on which to start the HTTP server (default: 8888).
|
|
1906
|
+
* @memberof UnderpostBaremetal
|
|
1907
|
+
* @returns {void}
|
|
1908
|
+
*/
|
|
1909
|
+
httpBootstrapServerRunnerFactory(
|
|
1910
|
+
options = { hostname: 'localhost', bootstrapHttpServerPath: './public/localhost', bootstrapHttpServerPort: 8888 },
|
|
1911
|
+
) {
|
|
1912
|
+
const port = options.bootstrapHttpServerPort || 8888;
|
|
1913
|
+
const bootstrapHttpServerPath = options.bootstrapHttpServerPath || './public/localhost';
|
|
1914
|
+
const hostname = options.hostname || 'localhost';
|
|
1915
|
+
|
|
1916
|
+
shellExec(`mkdir -p ${bootstrapHttpServerPath}/${hostname}/cloud-init`);
|
|
1917
|
+
|
|
1918
|
+
// Kill any existing HTTP server
|
|
1919
|
+
shellExec(`sudo pkill -f 'python3 -m http.server ${port}'`, { silent: true });
|
|
1920
|
+
|
|
1921
|
+
shellExec(
|
|
1922
|
+
`cd ${bootstrapHttpServerPath} && nohup python3 -m http.server ${port} --bind 0.0.0.0 > /tmp/http-boot-server.log 2>&1 &`,
|
|
1923
|
+
{ silent: true, async: true },
|
|
1924
|
+
);
|
|
1925
|
+
|
|
1926
|
+
// Configure iptables to allow incoming LAN connections
|
|
1927
|
+
shellExec(
|
|
1928
|
+
`sudo iptables -I INPUT 1 -p tcp -s 192.168.1.0/24 --dport ${port} -m conntrack --ctstate NEW -j ACCEPT`,
|
|
1929
|
+
);
|
|
1930
|
+
// Option for any host:
|
|
1931
|
+
// sudo iptables -I INPUT 1 -p tcp --dport ${port} -m conntrack --ctstate NEW -j ACCEPT
|
|
1932
|
+
shellExec(`sudo chown -R $(whoami):$(whoami) ${bootstrapHttpServerPath}`);
|
|
1933
|
+
shellExec(`sudo sudo chmod 755 ${bootstrapHttpServerPath}`);
|
|
1934
|
+
|
|
1935
|
+
logger.info(`Started Bootstrap Http Server on port ${port}`);
|
|
1936
|
+
},
|
|
1937
|
+
|
|
1938
|
+
/**
|
|
1939
|
+
* @method updateKernelFiles
|
|
1940
|
+
* @description Copies EFI bootloaders, kernel, and initrd images to the TFTP root path.
|
|
1941
|
+
* It also handles decompression of the kernel if necessary for ARM64 compatibility,
|
|
1942
|
+
* and extracts raw kernel images from PE32+ EFI wrappers (common in Rocky Linux ARM64).
|
|
1943
|
+
* @param {object} params - The parameters for the function.
|
|
1944
|
+
* @param {object} params.commissioningImage - The commissioning image configuration.
|
|
1945
|
+
* @param {string} params.resourcesPath - The path where resources are located.
|
|
1946
|
+
* @param {string} params.tftpRootPath - The TFTP root path.
|
|
1947
|
+
* @param {object} params.kernelFilesPaths - Paths to kernel files.
|
|
1948
|
+
* @memberof UnderpostBaremetal
|
|
1949
|
+
* @returns {void}
|
|
1950
|
+
*/
|
|
1951
|
+
updateKernelFiles({ commissioningImage, resourcesPath, tftpRootPath, kernelFilesPaths }) {
|
|
1952
|
+
// Copy EFI bootloaders to TFTP path.
|
|
1953
|
+
const efiFiles = commissioningImage.architecture.match('arm64')
|
|
1954
|
+
? ['bootaa64.efi', 'grubaa64.efi']
|
|
1955
|
+
: ['bootx64.efi', 'grubx64.efi'];
|
|
1956
|
+
for (const file of efiFiles) {
|
|
1957
|
+
shellExec(`sudo cp -a ${resourcesPath}/${file} ${tftpRootPath}/pxe/${file}`);
|
|
1958
|
+
}
|
|
1959
|
+
// Copy kernel and initrd images to TFTP path.
|
|
1960
|
+
for (const file of Object.keys(kernelFilesPaths)) {
|
|
1961
|
+
if (file == 'isoUrl') continue; // Skip URL entries
|
|
1962
|
+
shellExec(`sudo cp -a ${kernelFilesPaths[file]} ${tftpRootPath}/pxe/${file}`);
|
|
1963
|
+
// If the file is a kernel (vmlinuz-efi) and is gzipped, unzip it for GRUB compatibility on ARM64.
|
|
1964
|
+
// GRUB on ARM64 often crashes with synchronous exception (0x200) if handling large compressed kernels directly.
|
|
1965
|
+
if (file === 'vmlinuz-efi') {
|
|
1966
|
+
const kernelDest = `${tftpRootPath}/pxe/${file}`;
|
|
1967
|
+
const fileType = shellExec(`file ${kernelDest}`, { silent: true }).stdout;
|
|
1968
|
+
|
|
1969
|
+
// Handle gzip compressed kernels
|
|
1970
|
+
if (fileType.includes('gzip compressed data')) {
|
|
1971
|
+
logger.info(`Decompressing kernel ${file} for ARM64 UEFI compatibility...`);
|
|
1972
|
+
shellExec(`sudo mv ${kernelDest} ${kernelDest}.gz`);
|
|
1973
|
+
shellExec(`sudo gunzip ${kernelDest}.gz`);
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
// Handle PE32+ EFI wrapped kernels (common in Rocky Linux ARM64)
|
|
1977
|
+
// Rocky Linux ARM64 kernels are distributed as PE32+ EFI executables, which
|
|
1978
|
+
// are bootable directly via UEFI firmware. However, GRUB's 'linux' command
|
|
1979
|
+
// expects a raw ARM64 Linux kernel Image format, not a PE32+ wrapper.
|
|
1980
|
+
if (fileType.includes('PE32+') || fileType.includes('EFI application')) {
|
|
1981
|
+
logger.warn('Detected PE32+ EFI wrapped kernel. Need to extract raw kernel image for GRUB.');
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
},
|
|
1986
|
+
|
|
1987
|
+
/**
|
|
1988
|
+
* @method kernelCmdBootParamsFactory
|
|
1989
|
+
* @description Constructs kernel command line parameters for NFS booting.
|
|
1990
|
+
* @param {object} options - Options for constructing the command line.
|
|
1991
|
+
* @param {string} options.ipClient - The IP address of the client.
|
|
1992
|
+
* @param {string} options.ipDhcpServer - The IP address of the DHCP server.
|
|
1993
|
+
* @param {string} options.ipFileServer - The IP address of the file server.
|
|
1994
|
+
* @param {string} options.ipConfig - The IP configuration method (e.g., 'dhcp').
|
|
1995
|
+
* @param {string} options.netmask - The network mask.
|
|
1996
|
+
* @param {string} options.hostname - The hostname of the client.
|
|
1997
|
+
* @param {string} options.dnsServer - The DNS server address.
|
|
1998
|
+
* @param {string} options.networkInterfaceName - The name of the network interface.
|
|
1999
|
+
* @param {string} options.fileSystemUrl - The URL of the root filesystem.
|
|
2000
|
+
* @param {number} options.bootstrapHttpServerPort - The port of the bootstrap HTTP server.
|
|
2001
|
+
* @param {string} options.type - The type of boot ('iso-ram', 'chroot-debootstrap', 'chroot-container', 'iso-nfs', etc.).
|
|
2002
|
+
* @param {string} options.macAddress - The MAC address of the client.
|
|
2003
|
+
* @param {boolean} options.cloudInit - Whether to include cloud-init parameters.
|
|
2004
|
+
* @param {object} options.machine - The machine object containing system_id.
|
|
2005
|
+
* @param {boolean} [options.dev=false] - Whether to enable dev mode with dracut debugging parameters.
|
|
2006
|
+
* @param {string} [options.osIdLike=''] - OS family identifier (e.g., 'rhel centos fedora' or 'debian ubuntu').
|
|
2007
|
+
* @returns {object} An object containing the constructed command line string.
|
|
2008
|
+
* @memberof UnderpostBaremetal
|
|
2009
|
+
*/
|
|
2010
|
+
kernelCmdBootParamsFactory(
|
|
2011
|
+
options = {
|
|
2012
|
+
ipClient: '',
|
|
2013
|
+
ipDhcpServer: '',
|
|
2014
|
+
ipFileServer: '',
|
|
2015
|
+
ipConfig: '',
|
|
2016
|
+
netmask: '',
|
|
2017
|
+
hostname: '',
|
|
2018
|
+
dnsServer: '',
|
|
2019
|
+
networkInterfaceName: '',
|
|
2020
|
+
fileSystemUrl: '',
|
|
2021
|
+
bootstrapHttpServerPort: 8888,
|
|
2022
|
+
type: '',
|
|
2023
|
+
macAddress: '',
|
|
2024
|
+
cloudInit: false,
|
|
2025
|
+
machine: { system_id: '' },
|
|
2026
|
+
dev: false,
|
|
2027
|
+
osIdLike: '',
|
|
2028
|
+
},
|
|
2029
|
+
) {
|
|
2030
|
+
// Construct kernel command line arguments for NFS boot.
|
|
2031
|
+
const {
|
|
2032
|
+
ipClient,
|
|
2033
|
+
ipDhcpServer,
|
|
2034
|
+
ipFileServer,
|
|
2035
|
+
ipConfig,
|
|
2036
|
+
netmask,
|
|
2037
|
+
hostname,
|
|
2038
|
+
dnsServer,
|
|
2039
|
+
networkInterfaceName,
|
|
2040
|
+
fileSystemUrl,
|
|
2041
|
+
bootstrapHttpServerPort,
|
|
2042
|
+
type,
|
|
2043
|
+
macAddress,
|
|
2044
|
+
cloudInit,
|
|
2045
|
+
osIdLike,
|
|
2046
|
+
} = options;
|
|
2047
|
+
|
|
2048
|
+
const ipParam = true
|
|
2049
|
+
? `ip=${ipClient}:${ipFileServer}:${ipDhcpServer}:${netmask}:${hostname}` +
|
|
2050
|
+
`:${networkInterfaceName ? networkInterfaceName : 'eth0'}:${ipConfig}:${dnsServer}`
|
|
2051
|
+
: 'ip=dhcp';
|
|
2052
|
+
|
|
2053
|
+
const nfsOptions = `${
|
|
2054
|
+
type === 'chroot-debootstrap' || type === 'chroot-container'
|
|
2055
|
+
? [
|
|
2056
|
+
'tcp',
|
|
2057
|
+
'nfsvers=3',
|
|
2058
|
+
'nolock',
|
|
2059
|
+
// 'protocol=tcp',
|
|
2060
|
+
// 'hard=true',
|
|
2061
|
+
'port=2049',
|
|
2062
|
+
// 'sec=none',
|
|
2063
|
+
'hard',
|
|
2064
|
+
'intr',
|
|
2065
|
+
'rsize=32768',
|
|
2066
|
+
'wsize=32768',
|
|
2067
|
+
'acregmin=0',
|
|
2068
|
+
'acregmax=0',
|
|
2069
|
+
'acdirmin=0',
|
|
2070
|
+
'acdirmax=0',
|
|
2071
|
+
'noac',
|
|
2072
|
+
// 'nodev',
|
|
2073
|
+
// 'nosuid',
|
|
2074
|
+
]
|
|
2075
|
+
: []
|
|
2076
|
+
}`;
|
|
2077
|
+
|
|
2078
|
+
const nfsRootParam = `nfsroot=${ipFileServer}:${process.env.NFS_EXPORT_PATH}/${hostname}${nfsOptions ? `,${nfsOptions}` : ''}`;
|
|
2079
|
+
|
|
2080
|
+
const permissionsParams = [
|
|
2081
|
+
`rw`,
|
|
2082
|
+
// `ro`
|
|
2083
|
+
];
|
|
2084
|
+
|
|
2085
|
+
const kernelParams = [
|
|
2086
|
+
...permissionsParams,
|
|
2087
|
+
`ignore_uuid`,
|
|
2088
|
+
`rootwait`,
|
|
2089
|
+
`ipv6.disable=1`,
|
|
2090
|
+
`fixrtc`,
|
|
2091
|
+
// `console=serial0,115200`,
|
|
2092
|
+
// `console=tty1`,
|
|
2093
|
+
// `layerfs-path=filesystem.squashfs`,
|
|
2094
|
+
// `root=/dev/ram0`,
|
|
2095
|
+
// `toram`,
|
|
2096
|
+
'nomodeset',
|
|
2097
|
+
`editable_rootfs=tmpfs`,
|
|
2098
|
+
`ramdisk_size=3550000`,
|
|
2099
|
+
// `root=/dev/sda1`, // rpi4 usb port unit
|
|
2100
|
+
'apparmor=0', // Disable AppArmor security
|
|
2101
|
+
...(networkInterfaceName === 'eth0'
|
|
2102
|
+
? [
|
|
2103
|
+
'net.ifnames=0', // Disable predictable network interface names
|
|
2104
|
+
'biosdevname=0', // Disable BIOS device naming
|
|
2105
|
+
]
|
|
2106
|
+
: []),
|
|
2107
|
+
];
|
|
2108
|
+
|
|
2109
|
+
const performanceParams = [
|
|
2110
|
+
// --- Boot Automation & Stability ---
|
|
2111
|
+
'auto=true', // Enable automated installation/configuration
|
|
2112
|
+
'noeject', // Do not attempt to eject boot media on reboot
|
|
2113
|
+
`casper-getty`, // Enable console login for live sessions
|
|
2114
|
+
'nowatchdog', // Disable watchdog timers to prevent unexpected reboots
|
|
2115
|
+
'noprompt', // Don't wait for "Press Enter" during boot/reboot
|
|
2116
|
+
|
|
2117
|
+
// --- CPU & System Performance ---
|
|
2118
|
+
'mitigations=off', // Disable CPU security mitigations for maximum speed
|
|
2119
|
+
'clocksource=tsc', // Use fastest available hardware clock
|
|
2120
|
+
'tsc=reliable', // Trust CPU clock without extra verification
|
|
2121
|
+
'hpet=disable', // Disable legacy slow timer
|
|
2122
|
+
'nohz=on', // Reduce overhead by disabling timer ticks on idle CPUs
|
|
2123
|
+
|
|
2124
|
+
// --- Memory & Hardware Optimization ---
|
|
2125
|
+
'cma=40M', // Reserve contiguous RAM for RPi hardware/video
|
|
2126
|
+
'zswap.enabled=1', // Use compressed RAM cache (vital for NFS/SD)
|
|
2127
|
+
'zswap.compressor=zstd', // Best balance of speed and compression
|
|
2128
|
+
'zswap.max_pool_percent=30', // Use max 30% of RAM as compressed storage
|
|
2129
|
+
'zswap.zpool=zsmalloc', // Efficient memory management for zswap
|
|
2130
|
+
'fsck.mode=skip', // Skip disk checks to accelerate boot
|
|
2131
|
+
'max_loop=255', // Ensure enough loop devices for squashfs/snaps
|
|
2132
|
+
|
|
2133
|
+
// --- Immutable Filesystem ---
|
|
2134
|
+
'overlayroot=tmpfs', // Run entire OS in RAM to protect storage
|
|
2135
|
+
'overlayroot_cfgdisk=disabled', // Ignore external overlay configurations
|
|
2136
|
+
];
|
|
2137
|
+
|
|
2138
|
+
let cmd = [];
|
|
2139
|
+
if (type === 'iso-ram') {
|
|
2140
|
+
const netBootParams = [`netboot=url`];
|
|
2141
|
+
if (fileSystemUrl) netBootParams.push(`url=${fileSystemUrl.replace('https', 'http')}`);
|
|
2142
|
+
cmd = [ipParam, `boot=casper`, ...netBootParams, ...kernelParams];
|
|
2143
|
+
} else if (type === 'chroot-debootstrap' || type === 'chroot-container') {
|
|
2144
|
+
let qemuNfsRootParams = [`root=/dev/nfs`, `rootfstype=nfs`];
|
|
2145
|
+
|
|
2146
|
+
// Determine OS family from osIdLike configuration
|
|
2147
|
+
const isRhelBased = osIdLike && osIdLike.match(/rhel|centos|fedora|alma|rocky/i);
|
|
2148
|
+
const isDebianBased = osIdLike && osIdLike.match(/debian|ubuntu/i);
|
|
2149
|
+
|
|
2150
|
+
// Add RHEL/Rocky/Fedora based images specific parameters
|
|
2151
|
+
if (isRhelBased) {
|
|
2152
|
+
qemuNfsRootParams = qemuNfsRootParams.concat([`rd.neednet=1`, `rd.timeout=180`, `selinux=0`, `enforcing=0`]);
|
|
2153
|
+
}
|
|
2154
|
+
// Add Debian/Ubuntu based images specific parameters
|
|
2155
|
+
else if (isDebianBased) {
|
|
2156
|
+
qemuNfsRootParams = qemuNfsRootParams.concat([`initrd=initrd.img`, `init=/sbin/init`]);
|
|
2157
|
+
}
|
|
2158
|
+
|
|
2159
|
+
// Add debugging parameters in dev mode for dracut troubleshooting
|
|
2160
|
+
if (options.dev) {
|
|
2161
|
+
// qemuNfsRootParams = qemuNfsRootParams.concat([`rd.shell`, `rd.debug`]);
|
|
2162
|
+
}
|
|
2163
|
+
|
|
2164
|
+
cmd = [ipParam, ...qemuNfsRootParams, nfsRootParam, ...kernelParams];
|
|
2165
|
+
} else {
|
|
2166
|
+
// 'iso-nfs'
|
|
2167
|
+
cmd = [ipParam, `netboot=nfs`, nfsRootParam, ...kernelParams, ...performanceParams];
|
|
2168
|
+
|
|
2169
|
+
cmd.push(`ifname=${networkInterfaceName}:${macAddress}`);
|
|
2170
|
+
|
|
2171
|
+
if (cloudInit) {
|
|
2172
|
+
const cloudInitPreseedUrl = `http://${ipDhcpServer}:5248/MAAS/metadata/by-id/${options.machine?.system_id ? options.machine.system_id : 'system-id'}/?op=get_preseed`;
|
|
2173
|
+
cmd = cmd.concat([
|
|
2174
|
+
`cloud-init=enabled`,
|
|
2175
|
+
'autoinstall',
|
|
2176
|
+
`cloud-config-url=${cloudInitPreseedUrl}`,
|
|
2177
|
+
`ds=nocloud-net;s=${cloudInitPreseedUrl}`,
|
|
2178
|
+
`log_host=${ipDhcpServer}`,
|
|
2179
|
+
`log_port=5247`,
|
|
2180
|
+
// `BOOTIF=${macAddress}`,
|
|
2181
|
+
// `cc:{'datasource_list': ['MAAS']}end_cc`,
|
|
2182
|
+
]);
|
|
2183
|
+
}
|
|
786
2184
|
}
|
|
2185
|
+
// cmd.push('---');
|
|
2186
|
+
const cmdStr = cmd.join(' ');
|
|
2187
|
+
logger.info('Constructed kernel command line');
|
|
2188
|
+
console.log(newInstance(cmdStr).bgRed.bold.black);
|
|
2189
|
+
return { cmd: cmdStr };
|
|
787
2190
|
},
|
|
788
2191
|
|
|
789
2192
|
/**
|
|
@@ -792,25 +2195,15 @@ menuentry '${menuentryStr}' {
|
|
|
792
2195
|
* once a matching MAC address is found. It also opens terminal windows for live logs.
|
|
793
2196
|
* @param {object} params - The parameters for the function.
|
|
794
2197
|
* @param {string} params.macAddress - The MAC address to monitor for.
|
|
795
|
-
* @param {string} params.
|
|
796
|
-
* @param {string} params.
|
|
797
|
-
* @param {string} params.
|
|
798
|
-
* @param {object} params.
|
|
799
|
-
* @
|
|
800
|
-
* @returns {Promise<void>} A promise that resolves when commissioning is initiated or after a delay.
|
|
2198
|
+
* @param {string} params.ipAddress - The IP address of the machine (used if MAC is all zeros).
|
|
2199
|
+
* @param {string} [params.hostname] - The hostname for the machine (optional).
|
|
2200
|
+
* @param {string} [params.architecture] - The architecture of the machine (optional).
|
|
2201
|
+
* @param {object} [params.machine] - Existing machine payload to use (optional).
|
|
2202
|
+
* @returns {Promise<void>} A promise object with machine and discovery details.
|
|
801
2203
|
* @memberof UnderpostBaremetal
|
|
802
2204
|
*/
|
|
803
|
-
async commissionMonitor({ macAddress,
|
|
2205
|
+
async commissionMonitor({ macAddress, ipAddress, hostname, architecture, machine }) {
|
|
804
2206
|
{
|
|
805
|
-
logger.info('Waiting for commissioning...', {
|
|
806
|
-
macAddress,
|
|
807
|
-
nfsHostPath,
|
|
808
|
-
underpostRoot,
|
|
809
|
-
hostname,
|
|
810
|
-
maas,
|
|
811
|
-
networkInterfaceName,
|
|
812
|
-
});
|
|
813
|
-
|
|
814
2207
|
// Query observed discoveries from MAAS.
|
|
815
2208
|
const discoveries = JSON.parse(
|
|
816
2209
|
shellExec(`maas ${process.env.MAAS_ADMIN_USERNAME} discoveries read`, {
|
|
@@ -819,130 +2212,92 @@ menuentry '${menuentryStr}' {
|
|
|
819
2212
|
}),
|
|
820
2213
|
);
|
|
821
2214
|
|
|
822
|
-
// {
|
|
823
|
-
// "discovery_id": "",
|
|
824
|
-
// "ip": "192.168.1.189",
|
|
825
|
-
// "mac_address": "00:00:00:00:00:00",
|
|
826
|
-
// "last_seen": "2025-05-05T14:17:37.354",
|
|
827
|
-
// "hostname": null,
|
|
828
|
-
// "fabric_name": "",
|
|
829
|
-
// "vid": null,
|
|
830
|
-
// "mac_organization": "",
|
|
831
|
-
// "observer": {
|
|
832
|
-
// "system_id": "",
|
|
833
|
-
// "hostname": "",
|
|
834
|
-
// "interface_id": 1,
|
|
835
|
-
// "interface_name": ""
|
|
836
|
-
// },
|
|
837
|
-
// "resource_uri": "/MAAS/api/2.0/discovery/MTkyLjE2OC4xLjE4OSwwMDowMDowMDowMDowMDowMA==/"
|
|
838
|
-
// },
|
|
839
|
-
|
|
840
|
-
// Log discovered IPs for visibility.
|
|
841
|
-
console.log(discoveries.map((d) => d.ip).join(' | '));
|
|
842
|
-
|
|
843
|
-
// Iterate through discoveries to find a matching MAC address.
|
|
844
2215
|
for (const discovery of discoveries) {
|
|
845
|
-
const
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
2216
|
+
const dicoverHostname = discovery.hostname
|
|
2217
|
+
? discovery.hostname
|
|
2218
|
+
: discovery.mac_organization
|
|
2219
|
+
? discovery.mac_organization
|
|
2220
|
+
: discovery.domain
|
|
2221
|
+
? discovery.domain
|
|
2222
|
+
: `generic-host-${s4()}${s4()}`;
|
|
2223
|
+
|
|
2224
|
+
console.log(dicoverHostname.bgBlue.bold.white);
|
|
2225
|
+
console.log('ip target:'.green + ipAddress, 'ip discovered:'.green + discovery.ip);
|
|
2226
|
+
console.log('mac target:'.green + macAddress, 'mac discovered:'.green + discovery.mac_address);
|
|
2227
|
+
|
|
2228
|
+
if (discovery.ip === ipAddress) {
|
|
2229
|
+
logger.info('Machine discovered!', discovery);
|
|
2230
|
+
if (!machine) {
|
|
2231
|
+
logger.info('Creating new machine with discovered hardware MAC...', {
|
|
2232
|
+
discoveredMAC: discovery.mac_address,
|
|
2233
|
+
ipAddress,
|
|
2234
|
+
hostname,
|
|
2235
|
+
});
|
|
2236
|
+
machine = UnderpostBaremetal.API.machineFactory({
|
|
2237
|
+
ipAddress,
|
|
2238
|
+
macAddress: discovery.mac_address,
|
|
2239
|
+
hostname,
|
|
2240
|
+
architecture,
|
|
2241
|
+
}).machine;
|
|
2242
|
+
console.log('New machine system id:', machine.system_id.bgYellow.bold.black);
|
|
2243
|
+
UnderpostBaremetal.API.writeGrubConfigToFile({
|
|
2244
|
+
grubCfgSrc: UnderpostBaremetal.API.getGrubConfigFromFile().grubCfgSrc.replaceAll(
|
|
2245
|
+
'system-id',
|
|
2246
|
+
machine.system_id,
|
|
2247
|
+
),
|
|
2248
|
+
});
|
|
2249
|
+
} else {
|
|
2250
|
+
const systemId = machine.system_id;
|
|
2251
|
+
console.log('Using pre-registered machine system_id:', systemId.bgYellow.bold.black);
|
|
2252
|
+
|
|
2253
|
+
// Update the boot interface MAC if hardware MAC differs from pre-registered MAC
|
|
2254
|
+
// This handles both hardware mode (macAddress is null) and MAC mismatch scenarios
|
|
2255
|
+
if (macAddress === null || macAddress !== discovery.mac_address) {
|
|
2256
|
+
logger.info('Updating machine interface with discovered hardware MAC...', {
|
|
2257
|
+
preRegisteredMAC: macAddress || 'none (hardware mode)',
|
|
2258
|
+
discoveredMAC: discovery.mac_address,
|
|
2259
|
+
});
|
|
2260
|
+
|
|
2261
|
+
shellExec(`maas ${process.env.MAAS_ADMIN_USERNAME} machine mark-broken ${systemId}`, {
|
|
871
2262
|
silent: true,
|
|
872
|
-
|
|
873
|
-
},
|
|
874
|
-
);
|
|
875
|
-
newMachine = { discovery, machine: JSON.parse(newMachine) };
|
|
876
|
-
console.log(newMachine);
|
|
877
|
-
|
|
878
|
-
const discoverInterfaceName = 'eth0'; // Default interface name for discovery.
|
|
2263
|
+
});
|
|
879
2264
|
|
|
880
|
-
// Read interface data.
|
|
881
|
-
const interfaceData = JSON.parse(
|
|
882
2265
|
shellExec(
|
|
883
|
-
|
|
2266
|
+
// name=${networkInterfaceName}
|
|
2267
|
+
`maas ${process.env.MAAS_ADMIN_USERNAME} interface update ${systemId} ${machine.boot_interface.id}` +
|
|
2268
|
+
` mac_address=${discovery.mac_address}`,
|
|
884
2269
|
{
|
|
885
2270
|
silent: true,
|
|
886
|
-
stdout: true,
|
|
887
2271
|
},
|
|
888
|
-
)
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
// commissioning_scripts=90-verify-user.sh
|
|
907
|
-
// shellExec(
|
|
908
|
-
// `maas ${process.env.MAAS_ADMIN_USERNAME} machine commission --debug --insecure ${newMachine.machine.boot_interface.system_id} enable_ssh=1 skip_bmc_config=1 skip_networking=1 skip_storage=1`,
|
|
909
|
-
// {
|
|
910
|
-
// silent: true,
|
|
911
|
-
// },
|
|
912
|
-
// );
|
|
913
|
-
|
|
914
|
-
// Save system-id for enlistment.
|
|
915
|
-
logger.info('system-id', newMachine.machine.boot_interface.system_id);
|
|
916
|
-
fs.writeFileSync(
|
|
917
|
-
`${nfsHostPath}/underpost/system-id`,
|
|
918
|
-
newMachine.machine.boot_interface.system_id,
|
|
919
|
-
'utf8',
|
|
920
|
-
);
|
|
921
|
-
|
|
922
|
-
// Get and save MAAS authentication credentials.
|
|
923
|
-
const { consumer_key, token_key, token_secret } = UnderpostCloudInit.API.authCredentialsFactory();
|
|
924
|
-
|
|
925
|
-
fs.writeFileSync(`${nfsHostPath}/underpost/consumer-key`, consumer_key, 'utf8');
|
|
926
|
-
fs.writeFileSync(`${nfsHostPath}/underpost/token-key`, token_key, 'utf8');
|
|
927
|
-
fs.writeFileSync(`${nfsHostPath}/underpost/token-secret`, token_secret, 'utf8');
|
|
928
|
-
|
|
929
|
-
// Open new terminals for live cloud-init logs.
|
|
930
|
-
openTerminal(`node ${underpostRoot}/bin baremetal --logs cloud`);
|
|
931
|
-
openTerminal(`node ${underpostRoot}/bin baremetal --logs machine`);
|
|
932
|
-
} catch (error) {
|
|
933
|
-
logger.error(error, error.stack);
|
|
934
|
-
} finally {
|
|
935
|
-
process.exit(0);
|
|
2272
|
+
);
|
|
2273
|
+
|
|
2274
|
+
shellExec(`maas ${process.env.MAAS_ADMIN_USERNAME} machine mark-fixed ${systemId}`, {
|
|
2275
|
+
silent: true,
|
|
2276
|
+
});
|
|
2277
|
+
|
|
2278
|
+
logger.info('✓ Machine interface MAC address updated successfully');
|
|
2279
|
+
|
|
2280
|
+
// commissioning_scripts=90-verify-user.sh
|
|
2281
|
+
}
|
|
2282
|
+
logger.info('Machine resource uri', machine.resource_uri);
|
|
2283
|
+
for (const iface of machine.interface_set)
|
|
2284
|
+
logger.info('Interface info', {
|
|
2285
|
+
name: iface.name,
|
|
2286
|
+
mac_address: iface.mac_address,
|
|
2287
|
+
resource_uri: iface.resource_uri,
|
|
2288
|
+
});
|
|
936
2289
|
}
|
|
2290
|
+
|
|
2291
|
+
return { discovery, machine };
|
|
2292
|
+
}
|
|
937
2293
|
}
|
|
938
2294
|
await timer(1000);
|
|
939
|
-
UnderpostBaremetal.API.commissionMonitor({
|
|
2295
|
+
return await UnderpostBaremetal.API.commissionMonitor({
|
|
940
2296
|
macAddress,
|
|
941
|
-
|
|
942
|
-
underpostRoot,
|
|
2297
|
+
ipAddress,
|
|
943
2298
|
hostname,
|
|
944
|
-
|
|
945
|
-
|
|
2299
|
+
architecture,
|
|
2300
|
+
machine,
|
|
946
2301
|
});
|
|
947
2302
|
}
|
|
948
2303
|
},
|
|
@@ -952,11 +2307,10 @@ menuentry '${menuentryStr}' {
|
|
|
952
2307
|
* @description Mounts the binfmt_misc filesystem to enable QEMU user-static binfmt support.
|
|
953
2308
|
* This is necessary for cross-architecture execution within a chroot environment.
|
|
954
2309
|
* @param {object} params - The parameters for the function.
|
|
955
|
-
* @param {string} params.nfsHostPath - The path to the NFS root filesystem on the host.
|
|
956
2310
|
* @memberof UnderpostBaremetal
|
|
957
2311
|
* @returns {void}
|
|
958
2312
|
*/
|
|
959
|
-
mountBinfmtMisc(
|
|
2313
|
+
mountBinfmtMisc() {
|
|
960
2314
|
// Install necessary packages for debootstrap and QEMU.
|
|
961
2315
|
shellExec(`sudo dnf install -y iptables-legacy`);
|
|
962
2316
|
shellExec(`sudo dnf install -y debootstrap`);
|
|
@@ -966,9 +2320,6 @@ menuentry '${menuentryStr}' {
|
|
|
966
2320
|
// Mount binfmt_misc filesystem.
|
|
967
2321
|
shellExec(`sudo modprobe binfmt_misc`);
|
|
968
2322
|
shellExec(`sudo mount -t binfmt_misc binfmt_misc /proc/sys/fs/binfmt_misc`);
|
|
969
|
-
// Set ownership and permissions for the NFS host path.
|
|
970
|
-
shellExec(`sudo chown -R root:root ${nfsHostPath}`);
|
|
971
|
-
shellExec(`sudo chmod 755 ${nfsHostPath}`);
|
|
972
2323
|
},
|
|
973
2324
|
|
|
974
2325
|
/**
|
|
@@ -976,12 +2327,17 @@ menuentry '${menuentryStr}' {
|
|
|
976
2327
|
* @description Deletes all specified machines from MAAS.
|
|
977
2328
|
* @param {object} params - The parameters for the function.
|
|
978
2329
|
* @param {Array<object>} params.machines - An array of machine objects, each with a `system_id`.
|
|
2330
|
+
* @param {Array<string>} [params.ignore] - An optional array of system IDs to ignore during deletion.
|
|
979
2331
|
* @memberof UnderpostBaremetal
|
|
980
2332
|
* @returns {Array<object>} An empty array after machines are removed.
|
|
981
2333
|
*/
|
|
982
|
-
removeMachines({ machines }) {
|
|
2334
|
+
removeMachines({ machines, ignore }) {
|
|
983
2335
|
for (const machine of machines) {
|
|
984
|
-
|
|
2336
|
+
// Handle both string system_ids and machine objects
|
|
2337
|
+
const systemId = typeof machine === 'string' ? machine : machine.system_id;
|
|
2338
|
+
if (ignore && ignore.find((mId) => mId === systemId)) continue;
|
|
2339
|
+
logger.info(`Removing machine: ${systemId}`);
|
|
2340
|
+
shellExec(`maas ${process.env.MAAS_ADMIN_USERNAME} machine delete ${systemId}`);
|
|
985
2341
|
}
|
|
986
2342
|
return [];
|
|
987
2343
|
},
|
|
@@ -1030,8 +2386,8 @@ menuentry '${menuentryStr}' {
|
|
|
1030
2386
|
* @param {'arm64'|'amd64'} params.debootstrapArch - The target architecture of the debootstrap environment.
|
|
1031
2387
|
* @returns {void}
|
|
1032
2388
|
*/
|
|
1033
|
-
crossArchBinFactory({ nfsHostPath,
|
|
1034
|
-
switch (
|
|
2389
|
+
crossArchBinFactory({ nfsHostPath, bootstrapArch }) {
|
|
2390
|
+
switch (bootstrapArch) {
|
|
1035
2391
|
case 'arm64':
|
|
1036
2392
|
// Copy QEMU static binary for ARM64.
|
|
1037
2393
|
shellExec(`sudo podman cp extract:/usr/bin/qemu-aarch64-static ${nfsHostPath}/usr/bin/`);
|
|
@@ -1042,7 +2398,7 @@ menuentry '${menuentryStr}' {
|
|
|
1042
2398
|
break;
|
|
1043
2399
|
default:
|
|
1044
2400
|
// Log a warning or throw an error for unsupported architectures.
|
|
1045
|
-
logger.warn(`Unsupported
|
|
2401
|
+
logger.warn(`Unsupported bootstrap architecture: ${bootstrapArch}`);
|
|
1046
2402
|
break;
|
|
1047
2403
|
}
|
|
1048
2404
|
// Install GRUB EFI modules for both architectures to ensure compatibility.
|
|
@@ -1062,14 +2418,14 @@ menuentry '${menuentryStr}' {
|
|
|
1062
2418
|
* @param {string[]} params.steps - An array of shell commands to execute.
|
|
1063
2419
|
* @returns {void}
|
|
1064
2420
|
*/
|
|
1065
|
-
crossArchRunner({ nfsHostPath,
|
|
2421
|
+
crossArchRunner({ nfsHostPath, bootstrapArch, callbackMetaData, steps }) {
|
|
1066
2422
|
// Render the steps with logging for better visibility during execution.
|
|
1067
2423
|
steps = UnderpostBaremetal.API.stepsRender(steps, false);
|
|
1068
2424
|
|
|
1069
2425
|
let qemuCrossArchBash = '';
|
|
1070
2426
|
// Determine if QEMU is needed for cross-architecture execution.
|
|
1071
|
-
if (
|
|
1072
|
-
switch (
|
|
2427
|
+
if (bootstrapArch !== callbackMetaData.runnerHost.architecture)
|
|
2428
|
+
switch (bootstrapArch) {
|
|
1073
2429
|
case 'arm64':
|
|
1074
2430
|
qemuCrossArchBash = '/usr/bin/qemu-aarch64-static ';
|
|
1075
2431
|
break;
|
|
@@ -1124,45 +2480,80 @@ EOF`);
|
|
|
1124
2480
|
* It checks the mount status and performs mount/unmount operations as requested.
|
|
1125
2481
|
* @param {object} params - The parameters for the function.
|
|
1126
2482
|
* @param {string} params.hostname - The hostname of the target machine.
|
|
2483
|
+
* @param {string} params.nfsHostPath - The NFS host path for the target machine.
|
|
1127
2484
|
* @param {string} params.workflowId - The identifier for the workflow configuration.
|
|
1128
2485
|
* @param {boolean} [params.mount] - If true, attempts to mount the NFS paths.
|
|
1129
2486
|
* @param {boolean} [params.unmount] - If true, attempts to unmount the NFS paths.
|
|
2487
|
+
* @param {number} [currentRecall=0] - The current recall attempt count for retries.
|
|
2488
|
+
* @param {number} [maxRecalls=5] - The maximum number of recall attempts allowed.
|
|
1130
2489
|
* @memberof UnderpostBaremetal
|
|
1131
|
-
* @returns {
|
|
2490
|
+
* @returns {Promise<void>} A promise that resolves when the mount/unmount operations are complete.
|
|
1132
2491
|
*/
|
|
1133
|
-
nfsMountCallback({ hostname, workflowId, mount, unmount }) {
|
|
1134
|
-
|
|
2492
|
+
async nfsMountCallback({ hostname, nfsHostPath, workflowId, mount, unmount }, currentRecall = 0, maxRecalls = 5) {
|
|
2493
|
+
// Mount binfmt_misc filesystem.
|
|
2494
|
+
if (mount) UnderpostBaremetal.API.mountBinfmtMisc();
|
|
2495
|
+
const unMountCmds = [];
|
|
2496
|
+
const mountCmds = [];
|
|
1135
2497
|
const workflowsConfig = UnderpostBaremetal.API.loadWorkflowsConfig();
|
|
2498
|
+
let recall = false;
|
|
1136
2499
|
if (!workflowsConfig[workflowId]) {
|
|
1137
2500
|
throw new Error(`Workflow configuration not found for ID: ${workflowId}`);
|
|
1138
2501
|
}
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
2502
|
+
if (
|
|
2503
|
+
workflowsConfig[workflowId].type === 'chroot-debootstrap' ||
|
|
2504
|
+
workflowsConfig[workflowId].type === 'chroot-container'
|
|
2505
|
+
) {
|
|
2506
|
+
const mounts = {
|
|
2507
|
+
bind: ['/proc', '/sys', '/run'],
|
|
2508
|
+
rbind: ['/dev'],
|
|
2509
|
+
};
|
|
2510
|
+
|
|
2511
|
+
for (const mountCmd of Object.keys(mounts)) {
|
|
2512
|
+
for (const mountPath of mounts[mountCmd]) {
|
|
2513
|
+
const hostMountPath = `${process.env.NFS_EXPORT_PATH}/${hostname}${mountPath}`;
|
|
2514
|
+
// Check if the path is already mounted using `mountpoint` command.
|
|
2515
|
+
const isPathMounted = !shellExec(`mountpoint ${hostMountPath}`, { silent: true, stdout: true }).match(
|
|
2516
|
+
'not a mountpoint',
|
|
2517
|
+
);
|
|
1147
2518
|
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
} else {
|
|
1156
|
-
if (mount === true) {
|
|
1157
|
-
// Mount if requested and not already mounted.
|
|
1158
|
-
shellExec(`sudo mount --${mountCmd} ${mountPath} ${hostMountPath}`);
|
|
2519
|
+
if (isPathMounted) {
|
|
2520
|
+
logger.warn('Nfs path already mounted', mountPath);
|
|
2521
|
+
if (unmount === true) {
|
|
2522
|
+
// Unmount if requested.
|
|
2523
|
+
unMountCmds.push(`sudo umount -Rfl ${hostMountPath}`);
|
|
2524
|
+
if (!recall) recall = true;
|
|
2525
|
+
}
|
|
1159
2526
|
} else {
|
|
1160
|
-
|
|
2527
|
+
if (mount === true) {
|
|
2528
|
+
// Mount if requested and not already mounted.
|
|
2529
|
+
mountCmds.push(`sudo mount --${mountCmd} ${mountPath} ${hostMountPath}`);
|
|
2530
|
+
} else {
|
|
2531
|
+
logger.warn('Nfs path not mounted', mountPath);
|
|
2532
|
+
}
|
|
1161
2533
|
}
|
|
1162
2534
|
}
|
|
1163
2535
|
}
|
|
2536
|
+
for (const unMountCmd of unMountCmds) shellExec(unMountCmd);
|
|
2537
|
+
if (recall) {
|
|
2538
|
+
if (currentRecall >= maxRecalls) {
|
|
2539
|
+
throw new Error(
|
|
2540
|
+
`Maximum recall attempts (${maxRecalls}) reached for nfsMountCallback. Hostname: ${hostname}`,
|
|
2541
|
+
);
|
|
2542
|
+
}
|
|
2543
|
+
logger.info(`nfsMountCallback recall attempt ${currentRecall + 1}/${maxRecalls} for hostname: ${hostname}`);
|
|
2544
|
+
await timer(1000);
|
|
2545
|
+
return await UnderpostBaremetal.API.nfsMountCallback(
|
|
2546
|
+
{ hostname, nfsHostPath, workflowId, mount, unmount },
|
|
2547
|
+
currentRecall + 1,
|
|
2548
|
+
maxRecalls,
|
|
2549
|
+
);
|
|
2550
|
+
}
|
|
2551
|
+
if (mountCmds.length > 0) {
|
|
2552
|
+
shellExec(`sudo chown -R $(whoami):$(whoami) ${nfsHostPath}`);
|
|
2553
|
+
shellExec(`sudo chmod -R 755 ${nfsHostPath}`);
|
|
2554
|
+
for (const mountCmd of mountCmds) shellExec(mountCmd);
|
|
2555
|
+
}
|
|
1164
2556
|
}
|
|
1165
|
-
return { isMounted };
|
|
1166
2557
|
},
|
|
1167
2558
|
|
|
1168
2559
|
/**
|
|
@@ -1203,35 +2594,29 @@ EOF`);
|
|
|
1203
2594
|
* This includes updating package lists, installing essential build tools,
|
|
1204
2595
|
* kernel modules, cloud-init, SSH server, and other core utilities.
|
|
1205
2596
|
* @param {object} params - The parameters for the function.
|
|
1206
|
-
* @param {string} params.kernelLibVersion - The specific kernel library version to install.
|
|
1207
2597
|
* @memberof UnderpostBaremetal.systemProvisioningFactory.ubuntu
|
|
1208
2598
|
* @returns {string[]} An array of shell commands.
|
|
1209
2599
|
*/
|
|
1210
|
-
base: (
|
|
1211
|
-
// Configure APT sources for Ubuntu ports
|
|
2600
|
+
base: () => [
|
|
2601
|
+
// Configure APT sources for Ubuntu ports
|
|
1212
2602
|
`cat <<SOURCES | tee /etc/apt/sources.list
|
|
1213
2603
|
deb http://ports.ubuntu.com/ubuntu-ports noble main restricted universe multiverse
|
|
1214
2604
|
deb http://ports.ubuntu.com/ubuntu-ports noble-updates main restricted universe multiverse
|
|
1215
2605
|
deb http://ports.ubuntu.com/ubuntu-ports noble-security main restricted universe multiverse
|
|
1216
2606
|
SOURCES`,
|
|
1217
2607
|
|
|
1218
|
-
// Update package lists and perform a full system upgrade
|
|
2608
|
+
// Update package lists and perform a full system upgrade
|
|
1219
2609
|
`apt update -qq`,
|
|
1220
2610
|
`apt -y full-upgrade`,
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
//
|
|
1229
|
-
`apt
|
|
1230
|
-
`ln -sf /lib/systemd/systemd /sbin/init`, // Ensure systemd is the init system.
|
|
1231
|
-
|
|
1232
|
-
`apt-get update`,
|
|
1233
|
-
`DEBIAN_FRONTEND=noninteractive apt-get install -y apt-utils`, // Install apt-utils non-interactively.
|
|
1234
|
-
`DEBIAN_FRONTEND=noninteractive apt-get install -y tzdata kmod keyboard-configuration console-setup iputils-ping`, // Install timezone data, kernel modules, and network tools.
|
|
2611
|
+
|
|
2612
|
+
// Install all essential packages in one consolidated step
|
|
2613
|
+
`DEBIAN_FRONTEND=noninteractive apt install -y build-essential xinput x11-xkb-utils usbutils uuid-runtime linux-image-generic systemd-sysv openssh-server sudo locales udev util-linux iproute2 netplan.io ca-certificates curl wget chrony apt-utils tzdata kmod keyboard-configuration console-setup iputils-ping`,
|
|
2614
|
+
|
|
2615
|
+
// Ensure systemd is the init system
|
|
2616
|
+
`ln -sf /lib/systemd/systemd /sbin/init`,
|
|
2617
|
+
|
|
2618
|
+
// Clean up
|
|
2619
|
+
`apt-get clean`,
|
|
1235
2620
|
],
|
|
1236
2621
|
/**
|
|
1237
2622
|
* @method user
|
|
@@ -1336,15 +2721,167 @@ logdir /var/log/chrony
|
|
|
1336
2721
|
* @method keyboard
|
|
1337
2722
|
* @description Generates shell commands for configuring the keyboard layout.
|
|
1338
2723
|
* This ensures correct input behavior on the provisioned system.
|
|
2724
|
+
* @param {string} [keyCode='en'] - The keyboard layout code (e.g., 'en', 'es').
|
|
1339
2725
|
* @memberof UnderpostBaremetal.systemProvisioningFactory.ubuntu
|
|
1340
2726
|
* @returns {string[]} An array of shell commands.
|
|
1341
2727
|
*/
|
|
1342
|
-
keyboard: () => [
|
|
1343
|
-
`sudo locale-gen en_US.UTF-8`,
|
|
1344
|
-
`sudo update-locale LANG=en_US.UTF-8`,
|
|
1345
|
-
`sudo sed -i 's/XKBLAYOUT="us"/XKBLAYOUT="
|
|
1346
|
-
`sudo dpkg-reconfigure --frontend noninteractive keyboard-configuration`,
|
|
1347
|
-
`sudo systemctl restart keyboard-setup.service`,
|
|
2728
|
+
keyboard: (keyCode = 'en') => [
|
|
2729
|
+
`sudo locale-gen en_US.UTF-8`,
|
|
2730
|
+
`sudo update-locale LANG=en_US.UTF-8`,
|
|
2731
|
+
`sudo sed -i 's/XKBLAYOUT="us"/XKBLAYOUT="${keyCode}"/' /etc/default/keyboard`,
|
|
2732
|
+
`sudo dpkg-reconfigure --frontend noninteractive keyboard-configuration`,
|
|
2733
|
+
`sudo systemctl restart keyboard-setup.service`,
|
|
2734
|
+
],
|
|
2735
|
+
},
|
|
2736
|
+
/**
|
|
2737
|
+
* @property {object} rocky
|
|
2738
|
+
* @description Provisioning steps for Rocky Linux-based systems.
|
|
2739
|
+
* @memberof UnderpostBaremetal.systemProvisioningFactory
|
|
2740
|
+
* @namespace UnderpostBaremetal.systemProvisioningFactory.rocky
|
|
2741
|
+
*/
|
|
2742
|
+
rocky: {
|
|
2743
|
+
/**
|
|
2744
|
+
* @method base
|
|
2745
|
+
* @description Generates shell commands for basic Rocky Linux system provisioning.
|
|
2746
|
+
* This includes installing Node.js, npm, and underpost CLI tools.
|
|
2747
|
+
* @param {object} params - The parameters for the function.
|
|
2748
|
+
* @memberof UnderpostBaremetal.systemProvisioningFactory.rocky
|
|
2749
|
+
* @returns {string[]} An array of shell commands.
|
|
2750
|
+
*/
|
|
2751
|
+
base: () => [
|
|
2752
|
+
// Update system and install EPEL repository
|
|
2753
|
+
`dnf -y update`,
|
|
2754
|
+
`dnf -y install epel-release`,
|
|
2755
|
+
|
|
2756
|
+
// Install essential system tools (avoiding duplicates from container packages)
|
|
2757
|
+
`dnf -y install --allowerasing bzip2 openssh-server nano vim-enhanced less openssl-devel git gnupg2 libnsl perl`,
|
|
2758
|
+
`dnf clean all`,
|
|
2759
|
+
|
|
2760
|
+
// Install Node.js
|
|
2761
|
+
`curl -fsSL https://rpm.nodesource.com/setup_24.x | bash -`,
|
|
2762
|
+
`dnf install -y nodejs`,
|
|
2763
|
+
`dnf clean all`,
|
|
2764
|
+
|
|
2765
|
+
// Verify Node.js and npm versions
|
|
2766
|
+
`node --version`,
|
|
2767
|
+
`npm --version`,
|
|
2768
|
+
|
|
2769
|
+
// Install underpost ci/cd cli
|
|
2770
|
+
`npm install -g underpost`,
|
|
2771
|
+
`underpost --version`,
|
|
2772
|
+
],
|
|
2773
|
+
/**
|
|
2774
|
+
* @method user
|
|
2775
|
+
* @description Generates shell commands for creating a root user and configuring SSH access on Rocky Linux.
|
|
2776
|
+
* This is a critical security step for initial access to the provisioned system.
|
|
2777
|
+
* @memberof UnderpostBaremetal.systemProvisioningFactory.rocky
|
|
2778
|
+
* @returns {string[]} An array of shell commands.
|
|
2779
|
+
*/
|
|
2780
|
+
user: () => [
|
|
2781
|
+
`useradd -m -s /bin/bash -G wheel root`, // Create a root user with bash shell and wheel group (sudo on RHEL)
|
|
2782
|
+
`echo 'root:root' | chpasswd`, // Set a default password for the root user
|
|
2783
|
+
`mkdir -p /home/root/.ssh`, // Create .ssh directory for authorized keys
|
|
2784
|
+
// Add the public SSH key to authorized_keys for passwordless login
|
|
2785
|
+
`echo '${fs.readFileSync(
|
|
2786
|
+
`/home/dd/engine/engine-private/deploy/id_rsa.pub`,
|
|
2787
|
+
'utf8',
|
|
2788
|
+
)}' > /home/root/.ssh/authorized_keys`,
|
|
2789
|
+
`chown -R root:root /home/root/.ssh`, // Set ownership for security
|
|
2790
|
+
`chmod 700 /home/root/.ssh`, // Set permissions for the .ssh directory
|
|
2791
|
+
`chmod 600 /home/root/.ssh/authorized_keys`, // Set permissions for authorized_keys
|
|
2792
|
+
],
|
|
2793
|
+
/**
|
|
2794
|
+
* @method timezone
|
|
2795
|
+
* @description Generates shell commands for configuring the system timezone on Rocky Linux.
|
|
2796
|
+
* @param {object} params - The parameters for the function.
|
|
2797
|
+
* @param {string} params.timezone - The timezone string (e.g., 'America/Santiago').
|
|
2798
|
+
* @param {string} params.chronyConfPath - The path to the Chrony configuration file (optional).
|
|
2799
|
+
* @memberof UnderpostBaremetal.systemProvisioningFactory.rocky
|
|
2800
|
+
* @returns {string[]} An array of shell commands.
|
|
2801
|
+
*/
|
|
2802
|
+
timezone: ({ timezone, chronyConfPath = '/etc/chrony.conf' }) => [
|
|
2803
|
+
// Set system timezone using both methods (for chroot and running system)
|
|
2804
|
+
`ln -sf /usr/share/zoneinfo/${timezone} /etc/localtime`,
|
|
2805
|
+
`echo '${timezone}' > /etc/timezone`,
|
|
2806
|
+
`timedatectl set-timezone ${timezone} 2>/dev/null`,
|
|
2807
|
+
|
|
2808
|
+
// Configure chrony with local NTP server and common NTP pools
|
|
2809
|
+
`echo '# Local NTP server' > ${chronyConfPath}`,
|
|
2810
|
+
`echo 'server 192.168.1.1 iburst prefer' >> ${chronyConfPath}`,
|
|
2811
|
+
`echo '' >> ${chronyConfPath}`,
|
|
2812
|
+
`echo '# Fallback public NTP servers' >> ${chronyConfPath}`,
|
|
2813
|
+
`echo 'server 0.pool.ntp.org iburst' >> ${chronyConfPath}`,
|
|
2814
|
+
`echo 'server 1.pool.ntp.org iburst' >> ${chronyConfPath}`,
|
|
2815
|
+
`echo 'server 2.pool.ntp.org iburst' >> ${chronyConfPath}`,
|
|
2816
|
+
`echo 'server 3.pool.ntp.org iburst' >> ${chronyConfPath}`,
|
|
2817
|
+
`echo '' >> ${chronyConfPath}`,
|
|
2818
|
+
`echo '# Configuration' >> ${chronyConfPath}`,
|
|
2819
|
+
`echo 'driftfile /var/lib/chrony/drift' >> ${chronyConfPath}`,
|
|
2820
|
+
`echo 'makestep 1.0 3' >> ${chronyConfPath}`,
|
|
2821
|
+
`echo 'rtcsync' >> ${chronyConfPath}`,
|
|
2822
|
+
`echo 'logdir /var/log/chrony' >> ${chronyConfPath}`,
|
|
2823
|
+
|
|
2824
|
+
// Enable chronyd to start on boot
|
|
2825
|
+
`systemctl enable chronyd 2>/dev/null`,
|
|
2826
|
+
|
|
2827
|
+
// Create systemd link for boot (works in chroot)
|
|
2828
|
+
`mkdir -p /etc/systemd/system/multi-user.target.wants`,
|
|
2829
|
+
`ln -sf /usr/lib/systemd/system/chronyd.service /etc/systemd/system/multi-user.target.wants/chronyd.service 2>/dev/null`,
|
|
2830
|
+
|
|
2831
|
+
// Start chronyd if systemd is running
|
|
2832
|
+
`systemctl start chronyd 2>/dev/null`,
|
|
2833
|
+
|
|
2834
|
+
// Restart chronyd to apply configuration
|
|
2835
|
+
`systemctl restart chronyd 2>/dev/null`,
|
|
2836
|
+
|
|
2837
|
+
// Force immediate time synchronization (only if chronyd is running)
|
|
2838
|
+
`chronyc makestep 2>/dev/null`,
|
|
2839
|
+
|
|
2840
|
+
// Verify timezone configuration
|
|
2841
|
+
`ls -l /etc/localtime`,
|
|
2842
|
+
`cat /etc/timezone || echo 'No /etc/timezone file'`,
|
|
2843
|
+
`timedatectl status 2>/dev/null || echo 'Timezone set to ${timezone} (timedatectl not available in chroot)'`,
|
|
2844
|
+
`chronyc tracking 2>/dev/null || echo 'Chrony configured but not running (will start on boot)'`,
|
|
2845
|
+
],
|
|
2846
|
+
/**
|
|
2847
|
+
* @method keyboard
|
|
2848
|
+
* @description Generates shell commands for configuring the keyboard layout on Rocky Linux.
|
|
2849
|
+
* This uses localectl to set the keyboard layout for both console and X11.
|
|
2850
|
+
* @param {string} [keyCode='us'] - The keyboard layout code (e.g., 'us', 'es').
|
|
2851
|
+
* @memberof UnderpostBaremetal.systemProvisioningFactory.rocky
|
|
2852
|
+
* @returns {string[]} An array of shell commands.
|
|
2853
|
+
*/
|
|
2854
|
+
keyboard: (keyCode = 'us') => [
|
|
2855
|
+
// Configure vconsole.conf for console keyboard layout (persistent)
|
|
2856
|
+
`echo 'KEYMAP=${keyCode}' > /etc/vconsole.conf`,
|
|
2857
|
+
`echo 'FONT=latarcyrheb-sun16' >> /etc/vconsole.conf`,
|
|
2858
|
+
|
|
2859
|
+
// Configure locale.conf for system locale
|
|
2860
|
+
`echo 'LANG=en_US.UTF-8' > /etc/locale.conf`,
|
|
2861
|
+
`echo 'LC_ALL=en_US.UTF-8' >> /etc/locale.conf`,
|
|
2862
|
+
|
|
2863
|
+
// Set keyboard layout using localectl (works if systemd is running)
|
|
2864
|
+
`localectl set-locale LANG=en_US.UTF-8 2>/dev/null`,
|
|
2865
|
+
`localectl set-keymap ${keyCode} 2>/dev/null`,
|
|
2866
|
+
`localectl set-x11-keymap ${keyCode} 2>/dev/null`,
|
|
2867
|
+
|
|
2868
|
+
// Configure X11 keyboard layout file directly
|
|
2869
|
+
`mkdir -p /etc/X11/xorg.conf.d`,
|
|
2870
|
+
`echo 'Section "InputClass"' > /etc/X11/xorg.conf.d/00-keyboard.conf`,
|
|
2871
|
+
`echo ' Identifier "system-keyboard"' >> /etc/X11/xorg.conf.d/00-keyboard.conf`,
|
|
2872
|
+
`echo ' MatchIsKeyboard "on"' >> /etc/X11/xorg.conf.d/00-keyboard.conf`,
|
|
2873
|
+
`echo ' Option "XkbLayout" "${keyCode}"' >> /etc/X11/xorg.conf.d/00-keyboard.conf`,
|
|
2874
|
+
`echo 'EndSection' >> /etc/X11/xorg.conf.d/00-keyboard.conf`,
|
|
2875
|
+
|
|
2876
|
+
// Load the keymap immediately (if not in chroot)
|
|
2877
|
+
`loadkeys ${keyCode} 2>/dev/null || echo 'Keymap ${keyCode} configured (loadkeys not available in chroot)'`,
|
|
2878
|
+
|
|
2879
|
+
// Verify configuration
|
|
2880
|
+
`echo 'Keyboard configuration files:'`,
|
|
2881
|
+
`cat /etc/vconsole.conf`,
|
|
2882
|
+
`cat /etc/locale.conf`,
|
|
2883
|
+
`cat /etc/X11/xorg.conf.d/00-keyboard.conf 2>/dev/null || echo 'X11 config created'`,
|
|
2884
|
+
`localectl status 2>/dev/null || echo 'Keyboard layout set to ${keyCode} (localectl not available in chroot)'`,
|
|
1348
2885
|
],
|
|
1349
2886
|
},
|
|
1350
2887
|
},
|
|
@@ -1354,7 +2891,7 @@ logdir /var/log/chrony
|
|
|
1354
2891
|
* @description Configures and restarts the NFS server to export the specified path.
|
|
1355
2892
|
* This is crucial for allowing baremetal machines to boot via NFS.
|
|
1356
2893
|
* @param {object} params - The parameters for the function.
|
|
1357
|
-
* @param {string} params.nfsHostPath - The path to
|
|
2894
|
+
* @param {string} params.nfsHostPath - The path to the NFS server export.
|
|
1358
2895
|
* @memberof UnderpostBaremetal
|
|
1359
2896
|
* @param {string} [params.subnet='192.168.1.0/24'] - The subnet allowed to access the NFS export.
|
|
1360
2897
|
* @returns {void}
|
|
@@ -1403,7 +2940,7 @@ udp-port = 32766
|
|
|
1403
2940
|
shellExec(`sudo exportfs -rav`);
|
|
1404
2941
|
|
|
1405
2942
|
// Display the currently active NFS exports for verification.
|
|
1406
|
-
logger.info('Displaying active NFS exports
|
|
2943
|
+
logger.info('Displaying active NFS exports');
|
|
1407
2944
|
shellExec(`sudo exportfs -s`);
|
|
1408
2945
|
|
|
1409
2946
|
// Restart the nfs-server service to apply all configuration changes,
|
|
@@ -1505,9 +3042,14 @@ udp-port = 32766
|
|
|
1505
3042
|
* @throws {Error} If an invalid workflow ID is provided.
|
|
1506
3043
|
*/
|
|
1507
3044
|
bootConfFactory({ workflowId, tftpIp, tftpPrefixStr, macAddress, clientIp, subnet, gateway }) {
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
3045
|
+
if (workflowId.startsWith('rpi4mb')) {
|
|
3046
|
+
// Instructions: Flash sd with Raspberry Pi OS lite and update:
|
|
3047
|
+
// EEPROM (Electrically Erasable Programmable Read-Only Memory) like microcontrollers
|
|
3048
|
+
// sudo rpi-eeprom-config --apply /boot/firmware/boot.conf
|
|
3049
|
+
// sudo reboot
|
|
3050
|
+
// vcgencmd bootloader_config
|
|
3051
|
+
// shutdown -h now
|
|
3052
|
+
return `[all]
|
|
1511
3053
|
BOOT_UART=0
|
|
1512
3054
|
WAKE_ON_GPIO=1
|
|
1513
3055
|
POWER_OFF_ON_HALT=0
|
|
@@ -1540,7 +3082,7 @@ TFTP_PREFIX_STR=${tftpPrefixStr}/
|
|
|
1540
3082
|
# Manually override Ethernet MAC address
|
|
1541
3083
|
# ─────────────────────────────────────────────────────────────
|
|
1542
3084
|
|
|
1543
|
-
MAC_ADDRESS=${macAddress}
|
|
3085
|
+
#MAC_ADDRESS=${macAddress}
|
|
1544
3086
|
|
|
1545
3087
|
# OTP MAC address override
|
|
1546
3088
|
#MAC_ADDRESS_OTP=0,1
|
|
@@ -1548,18 +3090,14 @@ MAC_ADDRESS=${macAddress}
|
|
|
1548
3090
|
# ─────────────────────────────────────────────────────────────
|
|
1549
3091
|
# Static IP configuration (bypasses DHCP completely)
|
|
1550
3092
|
# ─────────────────────────────────────────────────────────────
|
|
1551
|
-
CLIENT_IP=${clientIp}
|
|
3093
|
+
#CLIENT_IP=${clientIp}
|
|
1552
3094
|
SUBNET=${subnet}
|
|
1553
3095
|
GATEWAY=${gateway}`;
|
|
1554
|
-
|
|
1555
|
-
default:
|
|
1556
|
-
throw new Error('Boot conf factory invalid workflow ID:' + workflowId);
|
|
1557
|
-
}
|
|
3096
|
+
} else logger.warn(`No boot configuration factory defined for workflow ID: ${workflowId}`);
|
|
1558
3097
|
},
|
|
1559
3098
|
|
|
1560
3099
|
/**
|
|
1561
3100
|
* @method loadWorkflowsConfig
|
|
1562
|
-
* @namespace UnderpostBaremetal.API
|
|
1563
3101
|
* @description Loads the commission workflows configuration from commission-workflows.json.
|
|
1564
3102
|
* Each workflow defines specific parameters like system provisioning type,
|
|
1565
3103
|
* kernel version, Chrony settings, debootstrap image details, and NFS mounts. *
|