underpost 2.8.817 → 2.8.821
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/README.md +2 -2
- package/bin/deploy.js +2 -1252
- package/cli.md +24 -16
- package/docker-compose.yml +1 -1
- package/manifests/deployment/dd-template-development/deployment.yaml +2 -2
- package/manifests/maas/device-scan.sh +43 -0
- package/manifests/maas/maas-setup.sh +81 -26
- package/manifests/maas/nat-iptables.sh +26 -0
- package/package.json +1 -1
- package/src/cli/baremetal.js +1233 -46
- package/src/cli/cloud-init.js +537 -0
- package/src/cli/index.js +15 -10
- package/src/index.js +26 -16
- package/src/server/runtime.js +0 -5
- package/src/server/ssl.js +1 -12
package/src/cli/baremetal.js
CHANGED
|
@@ -1,98 +1,1285 @@
|
|
|
1
1
|
import { getNpmRootPath, getUnderpostRootPath } from '../server/conf.js';
|
|
2
|
-
import { shellExec } from '../server/process.js';
|
|
2
|
+
import { pbcopy, shellExec } from '../server/process.js';
|
|
3
3
|
import dotenv from 'dotenv';
|
|
4
4
|
import { loggerFactory } from '../server/logger.js';
|
|
5
5
|
import { getLocalIPv4Address } from '../server/dns.js';
|
|
6
|
+
import fs from 'fs-extra';
|
|
7
|
+
import { Downloader } from '../server/downloader.js';
|
|
8
|
+
import UnderpostCloudInit from './cloud-init.js';
|
|
9
|
+
import { s4, timer } from '../client/components/core/CommonJs.js';
|
|
6
10
|
|
|
7
11
|
const logger = loggerFactory(import.meta);
|
|
8
12
|
|
|
13
|
+
/**
|
|
14
|
+
* @class UnderpostBaremetal
|
|
15
|
+
* @description Manages baremetal provisioning and configuration tasks.
|
|
16
|
+
* This class provides a set of static methods to automate various
|
|
17
|
+
* infrastructure operations, including NFS management, control server setup,
|
|
18
|
+
* and system provisioning for different architectures.
|
|
19
|
+
*/
|
|
9
20
|
class UnderpostBaremetal {
|
|
10
21
|
static API = {
|
|
11
|
-
|
|
22
|
+
/**
|
|
23
|
+
* @method callback
|
|
24
|
+
* @description Initiates a baremetal provisioning workflow based on the provided options.
|
|
25
|
+
* This is the primary entry point for orchestrating baremetal operations.
|
|
26
|
+
* It handles NFS root filesystem building, control server installation/uninstallation,
|
|
27
|
+
* and system-level provisioning tasks like timezone and keyboard configuration.
|
|
28
|
+
* @param {string} [workflowId='rpi4mb'] - Identifier for the specific workflow configuration to use.
|
|
29
|
+
* @param {string} [hostname=workflowId] - The hostname of the target baremetal machine.
|
|
30
|
+
* @param {string} [ipAddress=getLocalIPv4Address()] - The IP address of the control server or the local machine.
|
|
31
|
+
* @param {object} [options] - An object containing boolean flags for various operations.
|
|
32
|
+
* @param {boolean} [options.dev=false] - Development mode flag.
|
|
33
|
+
* @param {boolean} [options.controlServerInstall=false] - Flag to install the control server (e.g., MAAS).
|
|
34
|
+
* @param {boolean} [options.controlServerUninstall=false] - Flag to uninstall the control server.
|
|
35
|
+
* @param {boolean} [options.controlServerDbInstall=false] - Flag to install the control server's database.
|
|
36
|
+
* @param {boolean} [options.controlServerDbUninstall=false] - Flag to uninstall the control server's database.
|
|
37
|
+
* @param {boolean} [options.commission=false] - Flag to commission the baremetal machine.
|
|
38
|
+
* @param {boolean} [options.nfsBuild=false] - Flag to build the NFS root filesystem.
|
|
39
|
+
* @param {boolean} [options.nfsMount=false] - Flag to mount the NFS root filesystem.
|
|
40
|
+
* @param {boolean} [options.nfsUnmount=false] - Flag to unmount the NFS root filesystem.
|
|
41
|
+
* @param {boolean} [options.nfsSh=false] - Flag to chroot into the NFS environment for shell access.
|
|
42
|
+
* @param {string} [options.logs=''] - Specifies which logs to display ('dhcp', 'cloud', 'machine', 'cloud-config').
|
|
43
|
+
* @returns {void}
|
|
44
|
+
*/
|
|
45
|
+
async callback(
|
|
46
|
+
workflowId,
|
|
47
|
+
hostname,
|
|
48
|
+
ipAddress,
|
|
12
49
|
options = {
|
|
13
50
|
dev: false,
|
|
14
51
|
controlServerInstall: false,
|
|
15
|
-
controlServerDbInit: false,
|
|
16
|
-
controlServerDbUninstall: false,
|
|
17
|
-
controlServerInit: false,
|
|
18
52
|
controlServerUninstall: false,
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
53
|
+
controlServerDbInstall: false,
|
|
54
|
+
controlServerDbUninstall: false,
|
|
55
|
+
commission: false,
|
|
56
|
+
nfsBuild: false,
|
|
57
|
+
nfsMount: false,
|
|
58
|
+
nfsUnmount: false,
|
|
59
|
+
nfsSh: false,
|
|
60
|
+
logs: '',
|
|
24
61
|
},
|
|
25
62
|
) {
|
|
63
|
+
// Load environment variables from .env file, overriding existing ones if present.
|
|
26
64
|
dotenv.config({ path: `${getUnderpostRootPath()}/.env`, override: true });
|
|
65
|
+
|
|
66
|
+
// Determine the root path for npm and underpost.
|
|
27
67
|
const npmRoot = getNpmRootPath();
|
|
28
68
|
const underpostRoot = options?.dev === true ? '.' : `${npmRoot}/underpost`;
|
|
69
|
+
|
|
70
|
+
// Set default values if not provided.
|
|
71
|
+
workflowId = workflowId ?? 'rpi4mb';
|
|
72
|
+
hostname = hostname ?? workflowId;
|
|
73
|
+
ipAddress = ipAddress ?? '192.168.1.192';
|
|
74
|
+
|
|
75
|
+
// Set default MAC address
|
|
76
|
+
let macAddress = '00:00:00:00:00:00';
|
|
77
|
+
|
|
78
|
+
// Define the debootstrap architecture.
|
|
79
|
+
let debootstrapArch;
|
|
80
|
+
|
|
81
|
+
// Define the database provider ID.
|
|
29
82
|
const dbProviderId = 'postgresql-17';
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
83
|
+
|
|
84
|
+
// Define the NFS host path based on the environment variable and hostname.
|
|
85
|
+
const nfsHostPath = `${process.env.NFS_EXPORT_PATH}/${hostname}`;
|
|
86
|
+
|
|
87
|
+
// Define the TFTP root path based on the environment variable and hostname.
|
|
88
|
+
const tftpRootPath = `${process.env.TFTP_ROOT}/${hostname}`;
|
|
89
|
+
|
|
90
|
+
// Capture metadata for the callback execution, useful for logging and auditing.
|
|
91
|
+
const callbackMetaData = {
|
|
92
|
+
args: { hostname, ipAddress, workflowId },
|
|
93
|
+
options,
|
|
94
|
+
runnerHost: { architecture: UnderpostBaremetal.API.getHostArch(), ip: getLocalIPv4Address() },
|
|
95
|
+
nfsHostPath,
|
|
96
|
+
tftpRootPath,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// Log the initiation of the baremetal callback with relevant metadata.
|
|
100
|
+
logger.info('Baremetal callback', callbackMetaData);
|
|
101
|
+
|
|
102
|
+
// Handle various log display options.
|
|
103
|
+
if (options.logs === 'dhcp') {
|
|
104
|
+
shellExec(`journalctl -f -t dhcpd -u snap.maas.pebble.service`);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (options.logs === 'cloud') {
|
|
109
|
+
shellExec(`tail -f -n 900 ${nfsHostPath}/var/log/cloud-init.log`);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (options.logs === 'machine') {
|
|
114
|
+
shellExec(`tail -f -n 900 ${nfsHostPath}/var/log/cloud-init-output.log`);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (options.logs === 'cloud-config') {
|
|
119
|
+
shellExec(`cat ${nfsHostPath}/etc/cloud/cloud.cfg.d/90_maas.cfg`);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Handle NFS shell access option.
|
|
124
|
+
if (options.nfsSh === true) {
|
|
125
|
+
const { debootstrap } = UnderpostBaremetal.API.workflowsConfig[workflowId];
|
|
126
|
+
// Copy the chroot command to the clipboard for easy execution.
|
|
127
|
+
if (debootstrap.image.architecture !== callbackMetaData.runnerHost.architecture)
|
|
128
|
+
switch (debootstrap.image.architecture) {
|
|
129
|
+
case 'arm64':
|
|
130
|
+
pbcopy(`sudo chroot ${nfsHostPath} /usr/bin/qemu-aarch64-static /bin/bash`);
|
|
131
|
+
break;
|
|
132
|
+
|
|
133
|
+
case 'amd64':
|
|
134
|
+
pbcopy(`sudo chroot ${nfsHostPath} /usr/bin/qemu-x86_64-static /bin/bash`);
|
|
135
|
+
break;
|
|
136
|
+
|
|
137
|
+
default:
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
else pbcopy(`sudo chroot ${nfsHostPath} /bin/bash`);
|
|
141
|
+
|
|
142
|
+
return; // Exit early as this is a specific interactive operation.
|
|
34
143
|
}
|
|
144
|
+
|
|
145
|
+
// Handle control server installation.
|
|
146
|
+
if (options.controlServerInstall === true) {
|
|
147
|
+
// Ensure scripts are executable and then run them.
|
|
148
|
+
shellExec(`chmod +x ${underpostRoot}/manifests/maas/maas-setup.sh`);
|
|
149
|
+
shellExec(`chmod +x ${underpostRoot}/manifests/maas/nat-iptables.sh`);
|
|
150
|
+
shellExec(`${underpostRoot}/manifests/maas/maas-setup.sh`);
|
|
151
|
+
shellExec(`${underpostRoot}/manifests/maas/nat-iptables.sh`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Handle control server uninstallation.
|
|
35
155
|
if (options.controlServerUninstall === true) {
|
|
36
|
-
// Stop MAAS services
|
|
156
|
+
// Stop and remove MAAS services, handling potential errors gracefully.
|
|
37
157
|
shellExec(`sudo snap stop maas.pebble || true`);
|
|
38
158
|
shellExec(`sudo snap stop maas`);
|
|
39
159
|
shellExec(`sudo snap remove maas --purge || true`);
|
|
40
160
|
|
|
41
|
-
// Remove
|
|
161
|
+
// Remove residual snap data to ensure a clean uninstall.
|
|
42
162
|
shellExec(`sudo rm -rf /var/snap/maas`);
|
|
43
163
|
shellExec(`sudo rm -rf ~/snap/maas`);
|
|
44
164
|
|
|
45
|
-
// Remove MAAS
|
|
165
|
+
// Remove MAAS configuration and data directories.
|
|
46
166
|
shellExec(`sudo rm -rf /etc/maas`);
|
|
47
167
|
shellExec(`sudo rm -rf /var/lib/maas`);
|
|
48
168
|
shellExec(`sudo rm -rf /var/log/maas`);
|
|
49
169
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
shellExec(`sudo snap stop maas`);
|
|
55
|
-
}
|
|
56
|
-
if (options.controlServerDbInit === true) {
|
|
170
|
+
|
|
171
|
+
// Handle control server database installation.
|
|
172
|
+
if (options.controlServerDbInstall === true) {
|
|
173
|
+
// Deploy the database provider and manage MAAS database.
|
|
57
174
|
shellExec(`node ${underpostRoot}/bin/deploy ${dbProviderId} install`);
|
|
58
175
|
shellExec(
|
|
59
176
|
`node ${underpostRoot}/bin/deploy pg-drop-db ${process.env.DB_PG_MAAS_NAME} ${process.env.DB_PG_MAAS_USER}`,
|
|
60
177
|
);
|
|
61
178
|
shellExec(`node ${underpostRoot}/bin/deploy maas db`);
|
|
62
179
|
}
|
|
180
|
+
|
|
181
|
+
// Handle control server database uninstallation.
|
|
63
182
|
if (options.controlServerDbUninstall === true) {
|
|
64
183
|
shellExec(`node ${underpostRoot}/bin/deploy ${dbProviderId} uninstall`);
|
|
65
184
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
185
|
+
|
|
186
|
+
// Set debootstrap architecture.
|
|
187
|
+
{
|
|
188
|
+
const { architecture } = UnderpostBaremetal.API.workflowsConfig[workflowId].debootstrap.image;
|
|
189
|
+
debootstrapArch = architecture;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Handle NFS mount operation.
|
|
193
|
+
if (options.nfsMount === true) {
|
|
194
|
+
// Mount binfmt_misc filesystem.
|
|
195
|
+
UnderpostBaremetal.API.mountBinfmtMisc({ nfsHostPath });
|
|
196
|
+
UnderpostBaremetal.API.nfsMountCallback({ hostname, workflowId, mount: true });
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Handle NFS unmount operation.
|
|
200
|
+
if (options.nfsUnmount === true) {
|
|
201
|
+
UnderpostBaremetal.API.nfsMountCallback({ hostname, workflowId, unmount: true });
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Handle NFS root filesystem build operation.
|
|
205
|
+
if (options.nfsBuild === true) {
|
|
206
|
+
// Check if NFS is already mounted to avoid redundant builds.
|
|
207
|
+
const { isMounted } = UnderpostBaremetal.API.nfsMountCallback({ hostname, workflowId });
|
|
208
|
+
if (isMounted) {
|
|
209
|
+
logger.warn('NFS root filesystem is mounted, skipping build.');
|
|
210
|
+
return; // Exit if already mounted.
|
|
211
|
+
}
|
|
212
|
+
logger.info('NFS root filesystem is not mounted, building...');
|
|
213
|
+
|
|
214
|
+
// Clean and create the NFS host path.
|
|
215
|
+
shellExec(`sudo rm -rf ${nfsHostPath}/*`);
|
|
216
|
+
shellExec(`mkdir -p ${nfsHostPath}`);
|
|
217
|
+
|
|
218
|
+
// Mount binfmt_misc filesystem.
|
|
219
|
+
UnderpostBaremetal.API.mountBinfmtMisc({ nfsHostPath });
|
|
220
|
+
|
|
221
|
+
// Perform the first stage of debootstrap.
|
|
222
|
+
{
|
|
223
|
+
const { architecture, name } = UnderpostBaremetal.API.workflowsConfig[workflowId].debootstrap.image;
|
|
224
|
+
shellExec(
|
|
225
|
+
[
|
|
226
|
+
`sudo debootstrap`,
|
|
227
|
+
`--arch=${architecture}`,
|
|
228
|
+
`--variant=minbase`,
|
|
229
|
+
`--foreign`, // Indicates a two-stage debootstrap.
|
|
230
|
+
name,
|
|
231
|
+
nfsHostPath,
|
|
232
|
+
`http://ports.ubuntu.com/ubuntu-ports/`,
|
|
233
|
+
].join(' '),
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Create a podman container to extract QEMU static binaries.
|
|
238
|
+
shellExec(`sudo podman create --name extract multiarch/qemu-user-static`);
|
|
239
|
+
shellExec(`podman ps -a`); // List all podman containers for verification.
|
|
240
|
+
|
|
241
|
+
// If cross-architecture, copy the QEMU static binary into the chroot.
|
|
242
|
+
if (debootstrapArch !== callbackMetaData.runnerHost.architecture)
|
|
243
|
+
UnderpostBaremetal.API.crossArchBinFactory({
|
|
244
|
+
nfsHostPath,
|
|
245
|
+
debootstrapArch,
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// Clean up the temporary podman container.
|
|
249
|
+
shellExec(`sudo podman rm extract`);
|
|
250
|
+
shellExec(`podman ps -a`);
|
|
251
|
+
shellExec(`file ${nfsHostPath}/bin/bash`); // Verify the bash executable in the chroot.
|
|
252
|
+
|
|
253
|
+
// Perform the second stage of debootstrap within the chroot environment.
|
|
254
|
+
UnderpostBaremetal.API.crossArchRunner({
|
|
255
|
+
nfsHostPath,
|
|
256
|
+
debootstrapArch,
|
|
257
|
+
callbackMetaData,
|
|
258
|
+
steps: [`/debootstrap/debootstrap --second-stage`],
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
// Mount NFS if it's not already mounted after the build.
|
|
262
|
+
if (!isMounted) {
|
|
263
|
+
UnderpostBaremetal.API.nfsMountCallback({ hostname, workflowId, mount: true });
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Apply system provisioning steps (base, user, timezone, keyboard).
|
|
267
|
+
{
|
|
268
|
+
const { systemProvisioning, kernelLibVersion, chronyc } = UnderpostBaremetal.API.workflowsConfig[workflowId];
|
|
269
|
+
const { timezone, chronyConfPath } = chronyc;
|
|
270
|
+
|
|
271
|
+
UnderpostBaremetal.API.crossArchRunner({
|
|
272
|
+
nfsHostPath,
|
|
273
|
+
debootstrapArch,
|
|
274
|
+
callbackMetaData,
|
|
275
|
+
steps: [
|
|
276
|
+
...UnderpostBaremetal.API.systemProvisioningFactory[systemProvisioning].base({
|
|
277
|
+
kernelLibVersion,
|
|
278
|
+
}),
|
|
279
|
+
...UnderpostBaremetal.API.systemProvisioningFactory[systemProvisioning].user(),
|
|
280
|
+
...UnderpostBaremetal.API.systemProvisioningFactory[systemProvisioning].timezone({
|
|
281
|
+
timezone,
|
|
282
|
+
chronyConfPath,
|
|
283
|
+
}),
|
|
284
|
+
...UnderpostBaremetal.API.systemProvisioningFactory[systemProvisioning].keyboard(),
|
|
285
|
+
],
|
|
286
|
+
});
|
|
287
|
+
}
|
|
69
288
|
}
|
|
70
|
-
|
|
71
|
-
|
|
289
|
+
|
|
290
|
+
// Fetch boot resources and machines if commissioning or listing.
|
|
291
|
+
|
|
292
|
+
let resources = JSON.parse(
|
|
293
|
+
shellExec(`maas ${process.env.MAAS_ADMIN_USERNAME} boot-resources read`, {
|
|
294
|
+
silent: true,
|
|
295
|
+
stdout: true,
|
|
296
|
+
}),
|
|
297
|
+
).map((o) => ({
|
|
298
|
+
id: o.id,
|
|
299
|
+
name: o.name,
|
|
300
|
+
architecture: o.architecture,
|
|
301
|
+
}));
|
|
302
|
+
if (options.ls === true) {
|
|
303
|
+
console.table(resources);
|
|
72
304
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const MAAS_API_TOKEN = shellExec(`maas apikey --username ${process.env.MAAS_ADMIN_USERNAME}`, {
|
|
305
|
+
let machines = JSON.parse(
|
|
306
|
+
shellExec(`maas ${process.env.MAAS_ADMIN_USERNAME} machines read`, {
|
|
76
307
|
stdout: true,
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
308
|
+
silent: true,
|
|
309
|
+
}),
|
|
310
|
+
).map((m) => ({
|
|
311
|
+
system_id: m.interface_set[0].system_id,
|
|
312
|
+
mac_address: m.interface_set[0].mac_address,
|
|
313
|
+
hostname: m.hostname,
|
|
314
|
+
status_name: m.status_name,
|
|
315
|
+
}));
|
|
316
|
+
if (options.ls === true) {
|
|
317
|
+
console.table(machines);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Handle commissioning tasks (placeholder for future implementation).
|
|
321
|
+
if (options.commission === true) {
|
|
322
|
+
const { firmwares, networkInterfaceName, maas, netmask, menuentryStr } =
|
|
323
|
+
UnderpostBaremetal.API.workflowsConfig[workflowId];
|
|
324
|
+
const resource = resources.find(
|
|
325
|
+
(o) => o.architecture === maas.image.architecture && o.name === maas.image.name,
|
|
326
|
+
);
|
|
327
|
+
logger.info('Commissioning resource', resource);
|
|
328
|
+
|
|
329
|
+
// Clean and create TFTP root path.
|
|
330
|
+
shellExec(`sudo rm -rf ${tftpRootPath}`);
|
|
331
|
+
shellExec(`mkdir -p ${tftpRootPath}/pxe`);
|
|
332
|
+
|
|
333
|
+
// Process firmwares for TFTP.
|
|
334
|
+
for (const firmware of firmwares) {
|
|
335
|
+
const { url, gateway, subnet } = firmware;
|
|
336
|
+
if (url.match('.zip')) {
|
|
337
|
+
const name = url.split('/').pop().replace('.zip', '');
|
|
338
|
+
const path = `../${name}`;
|
|
339
|
+
if (!fs.existsSync(path)) {
|
|
340
|
+
await Downloader(url, `../${name}.zip`); // Download firmware if not exists.
|
|
341
|
+
shellExec(`cd .. && mkdir ${name} && cd ${name} && unzip ../${name}.zip`); // Unzip firmware.
|
|
342
|
+
}
|
|
343
|
+
shellExec(`sudo cp -a ${path}/* ${tftpRootPath}`); // Copy firmware files to TFTP root.
|
|
344
|
+
|
|
345
|
+
if (gateway && subnet) {
|
|
346
|
+
fs.writeFileSync(
|
|
347
|
+
`${tftpRootPath}/boot_${name}.conf`,
|
|
348
|
+
UnderpostBaremetal.API.bootConfFactory({
|
|
349
|
+
workflowId,
|
|
350
|
+
tftpIp: callbackMetaData.runnerHost.ip,
|
|
351
|
+
tftpPrefixStr: hostname,
|
|
352
|
+
macAddress,
|
|
353
|
+
clientIp: ipAddress,
|
|
354
|
+
subnet,
|
|
355
|
+
gateway,
|
|
356
|
+
}),
|
|
357
|
+
'utf8',
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Rebuild NFS server configuration.
|
|
364
|
+
UnderpostBaremetal.API.rebuildNfsServer({
|
|
365
|
+
nfsHostPath,
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
// Configure GRUB for PXE boot.
|
|
369
|
+
{
|
|
370
|
+
const resourceData = JSON.parse(
|
|
371
|
+
shellExec(`maas ${process.env.MAAS_ADMIN_USERNAME} boot-resource read ${resource.id}`, {
|
|
372
|
+
stdout: true,
|
|
373
|
+
silent: true,
|
|
374
|
+
disableLog: true,
|
|
375
|
+
}),
|
|
376
|
+
);
|
|
377
|
+
const bootFiles = resourceData.sets[Object.keys(resourceData.sets)[0]].files;
|
|
378
|
+
const suffix = resource.architecture.match('xgene') ? '.xgene' : '';
|
|
379
|
+
const resourcesPath = `/var/snap/maas/common/maas/image-storage/bootloaders/uefi/arm64`;
|
|
380
|
+
const kernelPath = `/var/snap/maas/common/maas/image-storage`;
|
|
381
|
+
const kernelFilesPaths = {
|
|
382
|
+
'vmlinuz-efi': `${kernelPath}/${bootFiles['boot-kernel' + suffix].filename_on_disk}`,
|
|
383
|
+
'initrd.img': `${kernelPath}/${bootFiles['boot-initrd' + suffix].filename_on_disk}`,
|
|
384
|
+
squashfs: `${kernelPath}/${bootFiles['squashfs'].filename_on_disk}`,
|
|
385
|
+
};
|
|
386
|
+
// Construct kernel command line arguments for NFS boot.
|
|
387
|
+
const cmd = [
|
|
388
|
+
`console=serial0,115200`,
|
|
389
|
+
// `console=ttyAMA0,115200`,
|
|
390
|
+
`console=tty1`,
|
|
391
|
+
// `initrd=-1`,
|
|
392
|
+
// `net.ifnames=0`,
|
|
393
|
+
// `dwc_otg.lpm_enable=0`,
|
|
394
|
+
// `elevator=deadline`,
|
|
395
|
+
`root=/dev/nfs`,
|
|
396
|
+
`nfsroot=${callbackMetaData.runnerHost.ip}:${process.env.NFS_EXPORT_PATH}/rpi4mb,${[
|
|
397
|
+
'tcp',
|
|
398
|
+
'vers=3',
|
|
399
|
+
'nfsvers=3',
|
|
400
|
+
'nolock',
|
|
401
|
+
// 'protocol=tcp',
|
|
402
|
+
// 'hard=true',
|
|
403
|
+
'port=2049',
|
|
404
|
+
// 'sec=none',
|
|
405
|
+
'rw',
|
|
406
|
+
'hard',
|
|
407
|
+
'intr',
|
|
408
|
+
'rsize=32768',
|
|
409
|
+
'wsize=32768',
|
|
410
|
+
'acregmin=0',
|
|
411
|
+
'acregmax=0',
|
|
412
|
+
'acdirmin=0',
|
|
413
|
+
'acdirmax=0',
|
|
414
|
+
'noac',
|
|
415
|
+
// 'nodev',
|
|
416
|
+
// 'nosuid',
|
|
417
|
+
]}`,
|
|
418
|
+
`ip=${ipAddress}:${callbackMetaData.runnerHost.ip}:${callbackMetaData.runnerHost.ip}:${netmask}:${hostname}:${networkInterfaceName}:static`,
|
|
419
|
+
`rootfstype=nfs`,
|
|
420
|
+
`rw`,
|
|
421
|
+
`rootwait`,
|
|
422
|
+
`fixrtc`,
|
|
423
|
+
'initrd=initrd.img',
|
|
424
|
+
// 'boot=casper',
|
|
425
|
+
// 'ro',
|
|
426
|
+
'netboot=nfs',
|
|
427
|
+
`init=/sbin/init`,
|
|
428
|
+
// `cloud-config-url=/dev/null`,
|
|
429
|
+
// 'ip=dhcp',
|
|
430
|
+
// 'ip=dfcp',
|
|
431
|
+
// 'autoinstall',
|
|
432
|
+
// 'rd.break',
|
|
433
|
+
|
|
434
|
+
// Disable services that not apply over nfs
|
|
435
|
+
`systemd.mask=systemd-network-generator.service`,
|
|
436
|
+
`systemd.mask=systemd-networkd.service`,
|
|
437
|
+
`systemd.mask=systemd-fsck-root.service`,
|
|
438
|
+
`systemd.mask=systemd-udev-trigger.service`,
|
|
439
|
+
];
|
|
440
|
+
const nfsConnectStr = cmd.join(' ');
|
|
441
|
+
|
|
442
|
+
// Copy EFI bootloaders to TFTP path.
|
|
443
|
+
for (const file of ['bootaa64.efi', 'grubaa64.efi']) {
|
|
444
|
+
shellExec(`sudo cp -a ${resourcesPath}/${file} ${tftpRootPath}/pxe/${file}`);
|
|
445
|
+
}
|
|
446
|
+
// Copy kernel and initrd images to TFTP path.
|
|
447
|
+
for (const file of Object.keys(kernelFilesPaths)) {
|
|
448
|
+
shellExec(`sudo cp -a ${kernelFilesPaths[file]} ${tftpRootPath}/pxe/${file}`);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Write GRUB configuration file.
|
|
452
|
+
fs.writeFileSync(
|
|
453
|
+
`${process.env.TFTP_ROOT}/grub/grub.cfg`,
|
|
454
|
+
`
|
|
455
|
+
insmod gzio
|
|
456
|
+
insmod http
|
|
457
|
+
insmod nfs
|
|
458
|
+
set timeout=5
|
|
459
|
+
set default=0
|
|
460
|
+
|
|
461
|
+
menuentry '${menuentryStr}' {
|
|
462
|
+
set root=(tftp,${callbackMetaData.runnerHost.ip})
|
|
463
|
+
linux /${hostname}/pxe/vmlinuz-efi ${nfsConnectStr}
|
|
464
|
+
initrd /${hostname}/pxe/initrd.img
|
|
465
|
+
boot
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
`,
|
|
469
|
+
'utf8',
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// Copy ARM64 EFI GRUB modules.
|
|
474
|
+
const arm64EfiPath = `${process.env.TFTP_ROOT}/grub/arm64-efi`;
|
|
475
|
+
if (fs.existsSync(arm64EfiPath)) shellExec(`sudo rm -rf ${arm64EfiPath}`);
|
|
476
|
+
shellExec(`sudo cp -a /usr/lib/grub/arm64-efi ${arm64EfiPath}`);
|
|
477
|
+
|
|
478
|
+
// Set ownership and permissions for TFTP root.
|
|
479
|
+
shellExec(`sudo chown -R root:root ${process.env.TFTP_ROOT}`);
|
|
480
|
+
shellExec(`sudo sudo chmod 755 ${process.env.TFTP_ROOT}`);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Final commissioning steps.
|
|
484
|
+
if (options.commission === true || options.cloudInitUpdate === true) {
|
|
485
|
+
const { debootstrap, networkInterfaceName, chronyc, maas } = UnderpostBaremetal.API.workflowsConfig[workflowId];
|
|
486
|
+
const { timezone, chronyConfPath } = chronyc;
|
|
487
|
+
|
|
488
|
+
// Build cloud-init tools.
|
|
489
|
+
UnderpostCloudInit.API.buildTools({
|
|
490
|
+
workflowId,
|
|
491
|
+
nfsHostPath,
|
|
492
|
+
hostname,
|
|
493
|
+
callbackMetaData,
|
|
494
|
+
dev: options.dev,
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
// Run cloud-init reset and configure cloud-init.
|
|
498
|
+
UnderpostBaremetal.API.crossArchRunner({
|
|
499
|
+
nfsHostPath,
|
|
500
|
+
debootstrapArch: debootstrap.image.architecture,
|
|
501
|
+
callbackMetaData,
|
|
502
|
+
steps: [
|
|
503
|
+
options.cloudInitUpdate === true ? '' : `/underpost/reset.sh`,
|
|
504
|
+
`chown root:root /usr/bin/sudo && chmod 4755 /usr/bin/sudo`,
|
|
505
|
+
UnderpostCloudInit.API.configFactory({
|
|
506
|
+
controlServerIp: callbackMetaData.runnerHost.ip,
|
|
507
|
+
hostname,
|
|
508
|
+
commissioningDeviceIp: ipAddress,
|
|
509
|
+
gatewayip: callbackMetaData.runnerHost.ip,
|
|
510
|
+
mac: macAddress, // Initial MAC, will be updated.
|
|
511
|
+
timezone,
|
|
512
|
+
chronyConfPath,
|
|
513
|
+
networkInterfaceName,
|
|
514
|
+
}),
|
|
515
|
+
],
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
if (options.cloudInitUpdate === true) return;
|
|
519
|
+
|
|
520
|
+
// Apply NAT iptables rules.
|
|
521
|
+
shellExec(`${underpostRoot}/manifests/maas/nat-iptables.sh`, { silent: true });
|
|
522
|
+
|
|
523
|
+
// Wait for MAC address assignment.
|
|
524
|
+
logger.info('Waiting for MAC assignment...');
|
|
525
|
+
fs.removeSync(`${nfsHostPath}/underpost/mac`); // Clear previous MAC.
|
|
526
|
+
await UnderpostBaremetal.API.macMonitor({ nfsHostPath }); // Monitor for MAC file.
|
|
527
|
+
macAddress = fs.readFileSync(`${nfsHostPath}/underpost/mac`, 'utf8').trim(); // Read assigned MAC.
|
|
528
|
+
|
|
529
|
+
// Re-run cloud-init config factory with the newly assigned MAC address.
|
|
530
|
+
UnderpostBaremetal.API.crossArchRunner({
|
|
531
|
+
nfsHostPath,
|
|
532
|
+
debootstrapArch: debootstrap.image.architecture,
|
|
533
|
+
callbackMetaData,
|
|
534
|
+
steps: [
|
|
535
|
+
UnderpostCloudInit.API.configFactory({
|
|
536
|
+
controlServerIp: callbackMetaData.runnerHost.ip,
|
|
537
|
+
hostname,
|
|
538
|
+
commissioningDeviceIp: ipAddress,
|
|
539
|
+
gatewayip: callbackMetaData.runnerHost.ip,
|
|
540
|
+
mac: macAddress, // Updated MAC address.
|
|
541
|
+
timezone,
|
|
542
|
+
chronyConfPath,
|
|
543
|
+
networkInterfaceName,
|
|
544
|
+
}),
|
|
545
|
+
],
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
// Remove existing machines from MAAS.
|
|
549
|
+
machines = UnderpostBaremetal.API.removeMachines({ machines });
|
|
550
|
+
|
|
551
|
+
// Monitor commissioning process.
|
|
552
|
+
UnderpostBaremetal.API.commissionMonitor({
|
|
553
|
+
macAddress,
|
|
554
|
+
nfsHostPath,
|
|
555
|
+
underpostRoot,
|
|
556
|
+
hostname,
|
|
557
|
+
maas,
|
|
558
|
+
networkInterfaceName,
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
},
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* @method commissionMonitor
|
|
565
|
+
* @description Monitors the MAAS discoveries and initiates machine creation and commissioning
|
|
566
|
+
* once a matching MAC address is found. It also opens terminal windows for live logs.
|
|
567
|
+
* @param {object} params - The parameters for the function.
|
|
568
|
+
* @param {string} params.macAddress - The MAC address to monitor for.
|
|
569
|
+
* @param {string} params.nfsHostPath - The NFS host path for storing system-id and auth tokens.
|
|
570
|
+
* @param {string} params.underpostRoot - The root directory of the Underpost project.
|
|
571
|
+
* @param {string} params.hostname - The desired hostname for the new machine.
|
|
572
|
+
* @param {object} params.maas - MAAS configuration details.
|
|
573
|
+
* @param {string} params.networkInterfaceName - The name of the network interface.
|
|
574
|
+
* @returns {Promise<void>} A promise that resolves when commissioning is initiated or after a delay.
|
|
575
|
+
*/
|
|
576
|
+
async commissionMonitor({ macAddress, nfsHostPath, underpostRoot, hostname, maas, networkInterfaceName }) {
|
|
577
|
+
{
|
|
578
|
+
logger.info('Waiting for commissioning...', {
|
|
579
|
+
macAddress,
|
|
580
|
+
nfsHostPath,
|
|
581
|
+
underpostRoot,
|
|
582
|
+
hostname,
|
|
583
|
+
maas,
|
|
584
|
+
networkInterfaceName,
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
// Query observed discoveries from MAAS.
|
|
588
|
+
const discoveries = JSON.parse(
|
|
589
|
+
shellExec(`maas ${process.env.MAAS_ADMIN_USERNAME} discoveries read`, {
|
|
83
590
|
silent: true,
|
|
84
|
-
|
|
591
|
+
stdout: true,
|
|
592
|
+
}),
|
|
85
593
|
);
|
|
86
|
-
|
|
594
|
+
|
|
595
|
+
// {
|
|
596
|
+
// "discovery_id": "",
|
|
597
|
+
// "ip": "192.168.1.189",
|
|
598
|
+
// "mac_address": "00:00:00:00:00:00",
|
|
599
|
+
// "last_seen": "2025-05-05T14:17:37.354",
|
|
600
|
+
// "hostname": null,
|
|
601
|
+
// "fabric_name": "",
|
|
602
|
+
// "vid": null,
|
|
603
|
+
// "mac_organization": "",
|
|
604
|
+
// "observer": {
|
|
605
|
+
// "system_id": "",
|
|
606
|
+
// "hostname": "",
|
|
607
|
+
// "interface_id": 1,
|
|
608
|
+
// "interface_name": ""
|
|
609
|
+
// },
|
|
610
|
+
// "resource_uri": "/MAAS/api/2.0/discovery/MTkyLjE2OC4xLjE4OSwwMDowMDowMDowMDowMDowMA==/"
|
|
611
|
+
// },
|
|
612
|
+
|
|
613
|
+
// Log discovered IPs for visibility.
|
|
614
|
+
console.log(discoveries.map((d) => d.ip).join(' | '));
|
|
615
|
+
|
|
616
|
+
// Iterate through discoveries to find a matching MAC address.
|
|
617
|
+
for (const discovery of discoveries) {
|
|
618
|
+
const machine = {
|
|
619
|
+
architecture: maas.image.architecture.match('amd') ? 'amd64/generic' : 'arm64/generic',
|
|
620
|
+
mac_address: discovery.mac_address,
|
|
621
|
+
hostname:
|
|
622
|
+
discovery.hostname ?? discovery.mac_organization ?? discovery.domain ?? `generic-host-${s4()}${s4()}`,
|
|
623
|
+
power_type: 'manual',
|
|
624
|
+
mac_addresses: discovery.mac_address,
|
|
625
|
+
ip: discovery.ip,
|
|
626
|
+
};
|
|
627
|
+
machine.hostname = machine.hostname.replaceAll(' ', '').replaceAll('.', ''); // Sanitize hostname.
|
|
628
|
+
|
|
629
|
+
if (machine.mac_addresses === macAddress)
|
|
630
|
+
try {
|
|
631
|
+
machine.hostname = hostname;
|
|
632
|
+
machine.mac_address = macAddress;
|
|
633
|
+
// Create a new machine in MAAS.
|
|
634
|
+
let newMachine = shellExec(
|
|
635
|
+
`maas ${process.env.MAAS_ADMIN_USERNAME} machines create ${Object.keys(machine)
|
|
636
|
+
.map((k) => `${k}="${machine[k]}"`)
|
|
637
|
+
.join(' ')}`,
|
|
638
|
+
{
|
|
639
|
+
silent: true,
|
|
640
|
+
stdout: true,
|
|
641
|
+
},
|
|
642
|
+
);
|
|
643
|
+
newMachine = { discovery, machine: JSON.parse(newMachine) };
|
|
644
|
+
console.log(newMachine);
|
|
645
|
+
|
|
646
|
+
const discoverInterfaceName = 'eth0'; // Default interface name for discovery.
|
|
647
|
+
|
|
648
|
+
// Read interface data.
|
|
649
|
+
const interfaceData = JSON.parse(
|
|
650
|
+
shellExec(
|
|
651
|
+
`maas ${process.env.MAAS_ADMIN_USERNAME} interface read ${newMachine.machine.boot_interface.system_id} ${discoverInterfaceName}`,
|
|
652
|
+
{
|
|
653
|
+
silent: true,
|
|
654
|
+
stdout: true,
|
|
655
|
+
},
|
|
656
|
+
),
|
|
657
|
+
);
|
|
658
|
+
|
|
659
|
+
logger.info('Interface', interfaceData);
|
|
660
|
+
|
|
661
|
+
// Mark machine as broken, update interface name, then mark as fixed.
|
|
662
|
+
shellExec(
|
|
663
|
+
`maas ${process.env.MAAS_ADMIN_USERNAME} machine mark-broken ${newMachine.machine.boot_interface.system_id}`,
|
|
664
|
+
);
|
|
665
|
+
|
|
666
|
+
shellExec(
|
|
667
|
+
`maas ${process.env.MAAS_ADMIN_USERNAME} interface update ${newMachine.machine.boot_interface.system_id} ${interfaceData.id} name=${networkInterfaceName}`,
|
|
668
|
+
);
|
|
669
|
+
|
|
670
|
+
shellExec(
|
|
671
|
+
`maas ${process.env.MAAS_ADMIN_USERNAME} machine mark-fixed ${newMachine.machine.boot_interface.system_id}`,
|
|
672
|
+
);
|
|
673
|
+
|
|
674
|
+
// commissioning_scripts=90-verify-user.sh
|
|
675
|
+
// shellExec(
|
|
676
|
+
// `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`,
|
|
677
|
+
// {
|
|
678
|
+
// silent: true,
|
|
679
|
+
// },
|
|
680
|
+
// );
|
|
681
|
+
|
|
682
|
+
// Save system-id for enlistment.
|
|
683
|
+
logger.info('system-id', newMachine.machine.boot_interface.system_id);
|
|
684
|
+
fs.writeFileSync(
|
|
685
|
+
`${nfsHostPath}/underpost/system-id`,
|
|
686
|
+
newMachine.machine.boot_interface.system_id,
|
|
687
|
+
'utf8',
|
|
688
|
+
);
|
|
689
|
+
|
|
690
|
+
// Get and save MAAS authentication credentials.
|
|
691
|
+
const { consumer_key, token_key, token_secret } = UnderpostCloudInit.API.authCredentialsFactory();
|
|
692
|
+
|
|
693
|
+
fs.writeFileSync(`${nfsHostPath}/underpost/consumer-key`, consumer_key, 'utf8');
|
|
694
|
+
fs.writeFileSync(`${nfsHostPath}/underpost/token-key`, token_key, 'utf8');
|
|
695
|
+
fs.writeFileSync(`${nfsHostPath}/underpost/token-secret`, token_secret, 'utf8');
|
|
696
|
+
|
|
697
|
+
// Open new terminals for live cloud-init logs.
|
|
698
|
+
shellExec(
|
|
699
|
+
`gnome-terminal -- bash -c "node ${underpostRoot}/bin baremetal --logs cloud; exec bash" & disown`,
|
|
700
|
+
{
|
|
701
|
+
async: true,
|
|
702
|
+
},
|
|
703
|
+
);
|
|
704
|
+
shellExec(
|
|
705
|
+
`gnome-terminal -- bash -c "node ${underpostRoot}/bin baremetal --logs machine; exec bash" & disown`,
|
|
706
|
+
{
|
|
707
|
+
async: true,
|
|
708
|
+
},
|
|
709
|
+
);
|
|
710
|
+
} catch (error) {
|
|
711
|
+
logger.error(error, error.stack);
|
|
712
|
+
} finally {
|
|
713
|
+
process.exit(0);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
await timer(1000);
|
|
717
|
+
UnderpostBaremetal.API.commissionMonitor({
|
|
718
|
+
macAddress,
|
|
719
|
+
nfsHostPath,
|
|
720
|
+
underpostRoot,
|
|
721
|
+
hostname,
|
|
722
|
+
maas,
|
|
723
|
+
networkInterfaceName,
|
|
724
|
+
});
|
|
87
725
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
726
|
+
},
|
|
727
|
+
|
|
728
|
+
/**
|
|
729
|
+
* @method mountBinfmtMisc
|
|
730
|
+
* @description Mounts the binfmt_misc filesystem to enable QEMU user-static binfmt support.
|
|
731
|
+
* This is necessary for cross-architecture execution within a chroot environment.
|
|
732
|
+
* @param {object} params - The parameters for the function.
|
|
733
|
+
* @param {string} params.nfsHostPath - The path to the NFS root filesystem on the host.
|
|
734
|
+
* @returns {void}
|
|
735
|
+
*/
|
|
736
|
+
mountBinfmtMisc({ nfsHostPath }) {
|
|
737
|
+
// Install necessary packages for debootstrap and QEMU.
|
|
738
|
+
shellExec(`sudo dnf install -y iptables-legacy`);
|
|
739
|
+
shellExec(`sudo dnf install -y debootstrap`);
|
|
740
|
+
shellExec(`sudo dnf install kernel-modules-extra-$(uname -r)`);
|
|
741
|
+
// Reset QEMU user-static binfmt for proper cross-architecture execution.
|
|
742
|
+
shellExec(`sudo podman run --rm --privileged multiarch/qemu-user-static --reset -p yes`);
|
|
743
|
+
// Mount binfmt_misc filesystem.
|
|
744
|
+
shellExec(`sudo modprobe binfmt_misc`);
|
|
745
|
+
shellExec(`sudo mount -t binfmt_misc binfmt_misc /proc/sys/fs/binfmt_misc`);
|
|
746
|
+
// Set ownership and permissions for the NFS host path.
|
|
747
|
+
shellExec(`sudo chown -R root:root ${nfsHostPath}`);
|
|
748
|
+
shellExec(`sudo chmod 755 ${nfsHostPath}`);
|
|
749
|
+
},
|
|
750
|
+
|
|
751
|
+
/**
|
|
752
|
+
* @method removeMachines
|
|
753
|
+
* @description Deletes all specified machines from MAAS.
|
|
754
|
+
* @param {object} params - The parameters for the function.
|
|
755
|
+
* @param {Array<object>} params.machines - An array of machine objects, each with a `system_id`.
|
|
756
|
+
* @returns {Array<object>} An empty array after machines are removed.
|
|
757
|
+
*/
|
|
758
|
+
removeMachines({ machines }) {
|
|
759
|
+
for (const machine of machines) {
|
|
760
|
+
shellExec(`maas ${process.env.MAAS_ADMIN_USERNAME} machine delete ${machine.system_id}`);
|
|
761
|
+
}
|
|
762
|
+
return [];
|
|
763
|
+
},
|
|
764
|
+
|
|
765
|
+
/**
|
|
766
|
+
* @method clearDiscoveries
|
|
767
|
+
* @description Clears all observed discoveries in MAAS and optionally forces a new scan.
|
|
768
|
+
* @param {object} params - The parameters for the function.
|
|
769
|
+
* @param {boolean} params.force - If true, forces a new discovery scan after clearing.
|
|
770
|
+
* @returns {void}
|
|
771
|
+
*/
|
|
772
|
+
clearDiscoveries({ force }) {
|
|
773
|
+
shellExec(`maas ${process.env.MAAS_ADMIN_USERNAME} discoveries clear all=true`);
|
|
774
|
+
if (force === true) {
|
|
775
|
+
shellExec(`maas ${process.env.MAAS_ADMIN_USERNAME} discoveries scan force=true`);
|
|
776
|
+
}
|
|
777
|
+
},
|
|
778
|
+
|
|
779
|
+
/**
|
|
780
|
+
* @method macMonitor
|
|
781
|
+
* @description Monitors for the presence of a MAC address file in the NFS host path.
|
|
782
|
+
* This is used to wait for the target machine to report its MAC address.
|
|
783
|
+
* @param {object} params - The parameters for the function.
|
|
784
|
+
* @param {string} params.nfsHostPath - The NFS host path where the MAC file is expected.
|
|
785
|
+
* @returns {Promise<void>} A promise that resolves when the MAC file is found or after a delay.
|
|
786
|
+
*/
|
|
787
|
+
async macMonitor({ nfsHostPath }) {
|
|
788
|
+
if (fs.existsSync(`${nfsHostPath}/underpost/mac`)) {
|
|
789
|
+
const mac = fs.readFileSync(`${nfsHostPath}/underpost/mac`, 'utf8').trim();
|
|
790
|
+
logger.info('Commissioning MAC', mac);
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
await timer(1000);
|
|
794
|
+
await UnderpostBaremetal.API.macMonitor({ nfsHostPath });
|
|
795
|
+
},
|
|
796
|
+
|
|
797
|
+
/**
|
|
798
|
+
* @method crossArchBinFactory
|
|
799
|
+
* @description Copies the appropriate QEMU static binary into the NFS root filesystem
|
|
800
|
+
* for cross-architecture execution within a chroot environment.
|
|
801
|
+
* @param {object} params - The parameters for the function.
|
|
802
|
+
* @param {string} params.nfsHostPath - The path to the NFS root filesystem on the host.
|
|
803
|
+
* @param {'arm64'|'amd64'} params.debootstrapArch - The target architecture of the debootstrap environment.
|
|
804
|
+
* @returns {void}
|
|
805
|
+
*/
|
|
806
|
+
crossArchBinFactory({ nfsHostPath, debootstrapArch }) {
|
|
807
|
+
switch (debootstrapArch) {
|
|
808
|
+
case 'arm64':
|
|
809
|
+
// Copy QEMU static binary for ARM64.
|
|
810
|
+
shellExec(`sudo podman cp extract:/usr/bin/qemu-aarch64-static ${nfsHostPath}/usr/bin/`);
|
|
811
|
+
break;
|
|
812
|
+
case 'amd64':
|
|
813
|
+
// Copy QEMU static binary for AMD64.
|
|
814
|
+
shellExec(`sudo podman cp extract:/usr/bin/qemu-x86_64-static ${nfsHostPath}/usr/bin/`);
|
|
815
|
+
break;
|
|
816
|
+
default:
|
|
817
|
+
// Log a warning or throw an error for unsupported architectures.
|
|
818
|
+
logger.warn(`Unsupported debootstrap architecture: ${debootstrapArch}`);
|
|
819
|
+
break;
|
|
820
|
+
}
|
|
821
|
+
// Install GRUB EFI modules for both architectures to ensure compatibility.
|
|
822
|
+
shellExec(`sudo dnf install grub2-efi-aa64-modules`);
|
|
823
|
+
shellExec(`sudo dnf install grub2-efi-x64-modules`);
|
|
824
|
+
},
|
|
825
|
+
|
|
826
|
+
/**
|
|
827
|
+
* @method crossArchRunner
|
|
828
|
+
* @description Executes a series of shell commands within a chroot environment,
|
|
829
|
+
* optionally using QEMU for cross-architecture execution.
|
|
830
|
+
* @param {object} params - The parameters for the function.
|
|
831
|
+
* @param {string} params.nfsHostPath - The path to the NFS root filesystem on the host.
|
|
832
|
+
* @param {'arm64'|'amd64'} params.debootstrapArch - The target architecture of the debootstrap environment.
|
|
833
|
+
* @param {object} params.callbackMetaData - Metadata about the callback, including runner host architecture.
|
|
834
|
+
* @param {string[]} params.steps - An array of shell commands to execute.
|
|
835
|
+
* @returns {void}
|
|
836
|
+
*/
|
|
837
|
+
crossArchRunner({ nfsHostPath, debootstrapArch, callbackMetaData, steps }) {
|
|
838
|
+
// Render the steps with logging for better visibility during execution.
|
|
839
|
+
steps = UnderpostBaremetal.API.stepsRender(steps, false);
|
|
840
|
+
|
|
841
|
+
let qemuCrossArchBash = '';
|
|
842
|
+
// Determine if QEMU is needed for cross-architecture execution.
|
|
843
|
+
if (debootstrapArch !== callbackMetaData.runnerHost.architecture)
|
|
844
|
+
switch (debootstrapArch) {
|
|
845
|
+
case 'arm64':
|
|
846
|
+
qemuCrossArchBash = '/usr/bin/qemu-aarch64-static ';
|
|
847
|
+
break;
|
|
848
|
+
case 'amd64':
|
|
849
|
+
qemuCrossArchBash = '/usr/bin/qemu-x86_64-static ';
|
|
850
|
+
break;
|
|
851
|
+
default:
|
|
852
|
+
// No QEMU prefix for unsupported or native architectures.
|
|
853
|
+
break;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
// Execute the commands within the chroot environment using a heredoc.
|
|
857
|
+
shellExec(`sudo chroot ${nfsHostPath} ${qemuCrossArchBash}/bin/bash <<'EOF'
|
|
858
|
+
${steps}
|
|
859
|
+
EOF`);
|
|
860
|
+
},
|
|
861
|
+
|
|
862
|
+
/**
|
|
863
|
+
* @method stepsRender
|
|
864
|
+
* @description Renders an array of shell commands into a formatted string,
|
|
865
|
+
* optionally including YAML-style formatting and execution logging.
|
|
866
|
+
* This helps in visualizing and debugging the execution flow of provisioning steps.
|
|
867
|
+
* @param {string[]} [steps=[]] - An array of shell commands.
|
|
868
|
+
* @param {boolean} [yaml=true] - If true, formats the output as YAML list items.
|
|
869
|
+
* @returns {string} The formatted string of commands.
|
|
870
|
+
*/
|
|
871
|
+
stepsRender(steps = [], yaml = true) {
|
|
872
|
+
return steps
|
|
873
|
+
.map(
|
|
874
|
+
(step, i, a) =>
|
|
875
|
+
// Add a timestamp and step counter for better logging and traceability.
|
|
876
|
+
(yaml ? ' - ' : '') +
|
|
877
|
+
'echo "' +
|
|
878
|
+
(yaml ? '\\' : '') +
|
|
879
|
+
'$(date) | ' +
|
|
880
|
+
(i + 1) +
|
|
881
|
+
'/' +
|
|
882
|
+
a.length +
|
|
883
|
+
' - ' +
|
|
884
|
+
step.split('\n')[0] +
|
|
885
|
+
'"' +
|
|
886
|
+
`\n` +
|
|
887
|
+
`${yaml ? ' - ' : ''}${step}`,
|
|
888
|
+
)
|
|
889
|
+
.join('\n');
|
|
890
|
+
},
|
|
891
|
+
|
|
892
|
+
/**
|
|
893
|
+
* @method nfsMountCallback
|
|
894
|
+
* @description Manages NFS mounts and unmounts for the baremetal provisioning process.
|
|
895
|
+
* It checks the mount status and performs mount/unmount operations as requested.
|
|
896
|
+
* @param {object} params - The parameters for the function.
|
|
897
|
+
* @param {string} params.hostname - The hostname of the target machine.
|
|
898
|
+
* @param {string} params.workflowId - The identifier for the workflow configuration.
|
|
899
|
+
* @param {boolean} [params.mount] - If true, attempts to mount the NFS paths.
|
|
900
|
+
* @param {boolean} [params.unmount] - If true, attempts to unmount the NFS paths.
|
|
901
|
+
* @returns {{isMounted: boolean}} An object indicating whether any NFS path is currently mounted.
|
|
902
|
+
*/
|
|
903
|
+
nfsMountCallback({ hostname, workflowId, mount, unmount }) {
|
|
904
|
+
let isMounted = false;
|
|
905
|
+
// Iterate through defined NFS mounts in the workflow configuration.
|
|
906
|
+
for (const mountCmd of Object.keys(UnderpostBaremetal.API.workflowsConfig[workflowId].nfs.mounts)) {
|
|
907
|
+
for (const mountPath of UnderpostBaremetal.API.workflowsConfig[workflowId].nfs.mounts[mountCmd]) {
|
|
908
|
+
const hostMountPath = `${process.env.NFS_EXPORT_PATH}/${hostname}${mountPath}`;
|
|
909
|
+
// Check if the path is already mounted using `mountpoint` command.
|
|
910
|
+
const isPathMounted = !shellExec(`mountpoint ${hostMountPath}`, { silent: true, stdout: true }).match(
|
|
911
|
+
'not a mountpoint',
|
|
912
|
+
);
|
|
913
|
+
|
|
914
|
+
if (isPathMounted) {
|
|
915
|
+
if (!isMounted) isMounted = true; // Set overall mounted status.
|
|
916
|
+
logger.warn('Nfs path already mounted', mountPath);
|
|
917
|
+
if (unmount === true) {
|
|
918
|
+
// Unmount if requested.
|
|
919
|
+
shellExec(`sudo umount ${hostMountPath}`);
|
|
920
|
+
}
|
|
921
|
+
} else {
|
|
922
|
+
if (mount === true) {
|
|
923
|
+
// Mount if requested and not already mounted.
|
|
924
|
+
shellExec(`sudo mount --${mountCmd} ${mountPath} ${hostMountPath}`);
|
|
925
|
+
} else {
|
|
926
|
+
logger.warn('Nfs path not mounted', mountPath);
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
return { isMounted };
|
|
932
|
+
},
|
|
933
|
+
|
|
934
|
+
/**
|
|
935
|
+
* @method getHostArch
|
|
936
|
+
* @description Determines the architecture of the host machine.
|
|
937
|
+
* This is crucial for cross-compilation and selecting the correct QEMU binaries.
|
|
938
|
+
* @returns {'amd64'|'arm64'} The host architecture.
|
|
939
|
+
* @throws {Error} If the host architecture is unsupported.
|
|
940
|
+
*/
|
|
941
|
+
getHostArch() {
|
|
942
|
+
// `uname -m` returns e.g. 'x86_64' or 'aarch64'
|
|
943
|
+
const machine = shellExec('uname -m', { stdout: true }).trim();
|
|
944
|
+
if (machine === 'x86_64') return 'amd64';
|
|
945
|
+
if (machine === 'aarch64') return 'arm64';
|
|
946
|
+
throw new Error(`Unsupported host architecture: ${machine}`);
|
|
947
|
+
},
|
|
948
|
+
|
|
949
|
+
/**
|
|
950
|
+
* @property {object} systemProvisioningFactory
|
|
951
|
+
* @description A factory object containing functions for system provisioning based on OS type.
|
|
952
|
+
* Each OS type (e.g., 'ubuntu') provides methods for base system setup, user creation,
|
|
953
|
+
* timezone configuration, and keyboard layout settings.
|
|
954
|
+
*/
|
|
955
|
+
systemProvisioningFactory: {
|
|
956
|
+
/**
|
|
957
|
+
* @property {object} ubuntu
|
|
958
|
+
* @description Provisioning steps for Ubuntu-based systems.
|
|
959
|
+
*/
|
|
960
|
+
ubuntu: {
|
|
961
|
+
/**
|
|
962
|
+
* @method base
|
|
963
|
+
* @description Generates shell commands for basic Ubuntu system provisioning.
|
|
964
|
+
* This includes updating package lists, installing essential build tools,
|
|
965
|
+
* kernel modules, cloud-init, SSH server, and other core utilities.
|
|
966
|
+
* @param {object} params - The parameters for the function.
|
|
967
|
+
* @param {string} params.kernelLibVersion - The specific kernel library version to install.
|
|
968
|
+
* @returns {string[]} An array of shell commands.
|
|
969
|
+
*/
|
|
970
|
+
base: ({ kernelLibVersion }) => [
|
|
971
|
+
// Configure APT sources for Ubuntu ports.
|
|
972
|
+
`cat <<SOURCES | tee /etc/apt/sources.list
|
|
973
|
+
deb http://ports.ubuntu.com/ubuntu-ports noble main restricted universe multiverse
|
|
974
|
+
deb http://ports.ubuntu.com/ubuntu-ports noble-updates main restricted universe multiverse
|
|
975
|
+
deb http://ports.ubuntu.com/ubuntu-ports noble-security main restricted universe multiverse
|
|
976
|
+
SOURCES`,
|
|
977
|
+
|
|
978
|
+
// Update package lists and perform a full system upgrade.
|
|
979
|
+
`apt update -qq`,
|
|
980
|
+
`apt -y full-upgrade`,
|
|
981
|
+
// Install essential development and system utilities.
|
|
982
|
+
`apt install -y build-essential xinput x11-xkb-utils usbutils uuid-runtime`,
|
|
983
|
+
'apt install -y linux-image-generic',
|
|
984
|
+
// Install specific kernel modules.
|
|
985
|
+
`apt install -y linux-modules-${kernelLibVersion} linux-modules-extra-${kernelLibVersion}`,
|
|
986
|
+
|
|
987
|
+
`depmod -a ${kernelLibVersion}`, // Update kernel module dependencies.
|
|
988
|
+
// Install cloud-init, systemd, SSH, sudo, locales, udev, and networking tools.
|
|
989
|
+
`apt install -y cloud-init systemd-sysv openssh-server sudo locales udev util-linux systemd-sysv iproute2 netplan.io ca-certificates curl wget chrony`,
|
|
990
|
+
`ln -sf /lib/systemd/systemd /sbin/init`, // Ensure systemd is the init system.
|
|
991
|
+
|
|
992
|
+
`apt-get update`,
|
|
993
|
+
`DEBIAN_FRONTEND=noninteractive apt-get install -y apt-utils`, // Install apt-utils non-interactively.
|
|
994
|
+
`DEBIAN_FRONTEND=noninteractive apt-get install -y tzdata kmod keyboard-configuration console-setup iputils-ping`, // Install timezone data, kernel modules, and network tools.
|
|
995
|
+
],
|
|
996
|
+
/**
|
|
997
|
+
* @method user
|
|
998
|
+
* @description Generates shell commands for creating a root user and configuring SSH access.
|
|
999
|
+
* This is a critical security step for initial access to the provisioned system.
|
|
1000
|
+
* @returns {string[]} An array of shell commands.
|
|
1001
|
+
*/
|
|
1002
|
+
user: () => [
|
|
1003
|
+
`useradd -m -s /bin/bash -G sudo root`, // Create a root user with bash shell and sudo privileges.
|
|
1004
|
+
`echo 'root:root' | chpasswd`, // Set a default password for the root user (consider more secure methods for production).
|
|
1005
|
+
`mkdir -p /home/root/.ssh`, // Create .ssh directory for authorized keys.
|
|
1006
|
+
// Add the public SSH key to authorized_keys for passwordless login.
|
|
1007
|
+
`echo '${fs.readFileSync(
|
|
1008
|
+
`/home/dd/engine/engine-private/deploy/id_rsa.pub`,
|
|
1009
|
+
'utf8',
|
|
1010
|
+
)}' > /home/root/.ssh/authorized_keys`,
|
|
1011
|
+
`chown -R root /home/root/.ssh`, // Set ownership for security.
|
|
1012
|
+
`chmod 700 /home/root/.ssh`, // Set permissions for the .ssh directory.
|
|
1013
|
+
`chmod 600 /home/root/.ssh/authorized_keys`, // Set permissions for authorized_keys.
|
|
1014
|
+
],
|
|
1015
|
+
/**
|
|
1016
|
+
* @method timezone
|
|
1017
|
+
* @description Generates shell commands for configuring the system timezone and Chrony (NTP client).
|
|
1018
|
+
* Accurate time synchronization is essential for logging, security, and distributed systems.
|
|
1019
|
+
* @param {object} params - The parameters for the function.
|
|
1020
|
+
* @param {string} params.timezone - The timezone string (e.g., 'America/New_York').
|
|
1021
|
+
* @param {string} params.chronyConfPath - The path to the Chrony configuration file.
|
|
1022
|
+
* @param {string} [alias='chrony'] - The alias for the chrony service.
|
|
1023
|
+
* @returns {string[]} An array of shell commands.
|
|
1024
|
+
*/
|
|
1025
|
+
timezone: ({ timezone, chronyConfPath }, alias = 'chrony') => [
|
|
1026
|
+
`export DEBIAN_FRONTEND=noninteractive`, // Set non-interactive mode for Debian packages.
|
|
1027
|
+
`ln -fs /usr/share/zoneinfo/${timezone} /etc/localtime`, // Symlink timezone.
|
|
1028
|
+
`sudo dpkg-reconfigure --frontend noninteractive tzdata`, // Reconfigure timezone data.
|
|
1029
|
+
|
|
1030
|
+
// Write the Chrony configuration file.
|
|
1031
|
+
`echo '
|
|
1032
|
+
# Use public servers from the pool.ntp.org project.
|
|
1033
|
+
# Please consider joining the pool (http://www.pool.ntp.org/join.html).
|
|
1034
|
+
# pool 2.pool.ntp.org iburst
|
|
1035
|
+
server ${process.env.MAAS_NTP_SERVER} iburst
|
|
1036
|
+
|
|
1037
|
+
# Record the rate at which the system clock gains/losses time.
|
|
1038
|
+
driftfile /var/lib/chrony/drift
|
|
1039
|
+
|
|
1040
|
+
# Allow the system clock to be stepped in the first three updates
|
|
1041
|
+
# if its offset is larger than 1 second.
|
|
1042
|
+
makestep 1.0 3
|
|
1043
|
+
|
|
1044
|
+
# Enable kernel synchronization of the real-time clock (RTC).
|
|
1045
|
+
rtcsync
|
|
1046
|
+
|
|
1047
|
+
# Enable hardware timestamping on all interfaces that support it.
|
|
1048
|
+
#hwtimestamp *
|
|
1049
|
+
|
|
1050
|
+
# Increase the minimum number of selectable sources required to adjust
|
|
1051
|
+
# the system clock.
|
|
1052
|
+
#minsources 2
|
|
1053
|
+
|
|
1054
|
+
# Allow NTP client access from local network.
|
|
1055
|
+
#allow 192.168.0.0/16
|
|
1056
|
+
|
|
1057
|
+
# Serve time even if not synchronized to a time source.
|
|
1058
|
+
#local stratum 10
|
|
1059
|
+
|
|
1060
|
+
# Specify file containing keys for NTP authentication.
|
|
1061
|
+
keyfile /etc/chrony.keys
|
|
1062
|
+
|
|
1063
|
+
# Get TAI-UTC offset and leap seconds from the system tz database.
|
|
1064
|
+
leapsectz right/UTC
|
|
1065
|
+
|
|
1066
|
+
# Specify directory for log files.
|
|
1067
|
+
logdir /var/log/chrony
|
|
1068
|
+
|
|
1069
|
+
# Select which information is logged.
|
|
1070
|
+
#log measurements statistics tracking
|
|
1071
|
+
' > ${chronyConfPath}`,
|
|
1072
|
+
`systemctl stop ${alias}`, // Stop Chrony service before reconfiguring.
|
|
1073
|
+
|
|
1074
|
+
// Enable, restart, and check status of Chrony service.
|
|
1075
|
+
`sudo systemctl enable --now ${alias}`,
|
|
1076
|
+
`sudo systemctl restart ${alias}`,
|
|
1077
|
+
`sudo systemctl status ${alias}`,
|
|
1078
|
+
|
|
1079
|
+
// Verify Chrony synchronization.
|
|
1080
|
+
`chronyc sources`,
|
|
1081
|
+
`chronyc tracking`,
|
|
1082
|
+
|
|
1083
|
+
`chronyc sourcestats -v`, // Display source statistics.
|
|
1084
|
+
`timedatectl status`, // Display current time and date settings.
|
|
1085
|
+
],
|
|
1086
|
+
/**
|
|
1087
|
+
* @method keyboard
|
|
1088
|
+
* @description Generates shell commands for configuring the keyboard layout.
|
|
1089
|
+
* This ensures correct input behavior on the provisioned system.
|
|
1090
|
+
* @returns {string[]} An array of shell commands.
|
|
1091
|
+
*/
|
|
1092
|
+
keyboard: () => [
|
|
1093
|
+
`sudo locale-gen en_US.UTF-8`, // Generate the specified locale.
|
|
1094
|
+
`sudo update-locale LANG=en_US.UTF-8`, // Update system locale.
|
|
1095
|
+
`sudo sed -i 's/XKBLAYOUT="us"/XKBLAYOUT="es"/' /etc/default/keyboard`, // Change keyboard layout to Spanish.
|
|
1096
|
+
`sudo dpkg-reconfigure --frontend noninteractive keyboard-configuration`, // Reconfigure keyboard non-interactively.
|
|
1097
|
+
`sudo systemctl restart keyboard-setup.service`, // Restart keyboard setup service.
|
|
1098
|
+
],
|
|
1099
|
+
},
|
|
1100
|
+
},
|
|
1101
|
+
|
|
1102
|
+
/**
|
|
1103
|
+
* @method rebuildNfsServer
|
|
1104
|
+
* @description Configures and restarts the NFS server to export the specified path.
|
|
1105
|
+
* This is crucial for allowing baremetal machines to boot via NFS.
|
|
1106
|
+
* @param {object} params - The parameters for the function.
|
|
1107
|
+
* @param {string} params.nfsHostPath - The path to be exported by the NFS server.
|
|
1108
|
+
* @param {string} [params.subnet='192.168.1.0/24'] - The subnet allowed to access the NFS export.
|
|
1109
|
+
* @returns {void}
|
|
1110
|
+
*/
|
|
1111
|
+
rebuildNfsServer({ nfsHostPath, subnet }) {
|
|
1112
|
+
if (!subnet) subnet = '192.168.1.0/24'; // Default subnet if not provided.
|
|
1113
|
+
// Write the NFS exports configuration to /etc/exports.
|
|
1114
|
+
fs.writeFileSync(
|
|
1115
|
+
`/etc/exports`,
|
|
1116
|
+
`${nfsHostPath} ${subnet}(${[
|
|
1117
|
+
'rw', // Read-write access.
|
|
1118
|
+
// 'all_squash', // Squash all client UIDs/GIDs to anonymous.
|
|
1119
|
+
'sync', // Synchronous writes.
|
|
1120
|
+
'no_root_squash', // Do not squash root user.
|
|
1121
|
+
'no_subtree_check', // Disable subtree checking.
|
|
1122
|
+
'insecure', // Allow connections from non-privileged ports.
|
|
1123
|
+
]})`,
|
|
1124
|
+
'utf8',
|
|
1125
|
+
);
|
|
1126
|
+
|
|
1127
|
+
logger.info('Writing NFS server configuration to /etc/nfs.conf...');
|
|
1128
|
+
// Write NFS daemon configuration, including port settings.
|
|
1129
|
+
fs.writeFileSync(
|
|
1130
|
+
`/etc/nfs.conf`,
|
|
1131
|
+
`[mountd]
|
|
1132
|
+
port = 20048
|
|
1133
|
+
|
|
1134
|
+
[statd]
|
|
1135
|
+
port = 32765
|
|
1136
|
+
outgoing-port = 32765
|
|
1137
|
+
|
|
1138
|
+
[nfsd]
|
|
1139
|
+
# Enable RDMA support if desired and hardware supports it.
|
|
1140
|
+
rdma=y
|
|
1141
|
+
rdma-port=20049
|
|
1142
|
+
|
|
1143
|
+
[lockd]
|
|
1144
|
+
port = 32766
|
|
1145
|
+
udp-port = 32766
|
|
1146
|
+
`,
|
|
1147
|
+
'utf8',
|
|
1148
|
+
);
|
|
1149
|
+
logger.info('NFS configuration written.');
|
|
1150
|
+
|
|
1151
|
+
logger.info('Reloading NFS exports...');
|
|
1152
|
+
shellExec(`sudo exportfs -rav`);
|
|
1153
|
+
|
|
1154
|
+
// Display the currently active NFS exports for verification.
|
|
1155
|
+
logger.info('Displaying active NFS exports:');
|
|
1156
|
+
shellExec(`sudo exportfs -s`);
|
|
1157
|
+
|
|
1158
|
+
// Restart the nfs-server service to apply all configuration changes,
|
|
1159
|
+
// including port settings from /etc/nfs.conf and export changes.
|
|
1160
|
+
logger.info('Restarting nfs-server service...');
|
|
1161
|
+
shellExec(`sudo systemctl restart nfs-server`);
|
|
1162
|
+
logger.info('NFS server restarted.');
|
|
1163
|
+
},
|
|
1164
|
+
|
|
1165
|
+
/**
|
|
1166
|
+
* @method bootConfFactory
|
|
1167
|
+
* @description Generates the boot configuration file for specific workflows,
|
|
1168
|
+
* primarily for Raspberry Pi 4 Model B. This configuration includes TFTP settings,
|
|
1169
|
+
* MAC address override, and static IP configuration.
|
|
1170
|
+
* @param {object} params - The parameters for the function.
|
|
1171
|
+
* @param {string} params.workflowId - The identifier for the specific workflow.
|
|
1172
|
+
* @param {string} params.tftpIp - The IP address of the TFTP server.
|
|
1173
|
+
* @param {string} params.tftpPrefixStr - The TFTP prefix string for boot files.
|
|
1174
|
+
* @param {string} params.macAddress - The MAC address to be set for the device.
|
|
1175
|
+
* @param {string} params.clientIp - The static IP address for the client device.
|
|
1176
|
+
* @param {string} params.subnet - The subnet mask for the client device.
|
|
1177
|
+
* @param {string} params.gateway - The gateway IP address for the client device.
|
|
1178
|
+
* @returns {string} The generated boot configuration content.
|
|
1179
|
+
* @throws {Error} If an invalid workflow ID is provided.
|
|
1180
|
+
*/
|
|
1181
|
+
bootConfFactory({ workflowId, tftpIp, tftpPrefixStr, macAddress, clientIp, subnet, gateway }) {
|
|
1182
|
+
switch (workflowId) {
|
|
1183
|
+
case 'rpi4mb':
|
|
1184
|
+
return `[all]
|
|
1185
|
+
BOOT_UART=0
|
|
1186
|
+
WAKE_ON_GPIO=1
|
|
1187
|
+
POWER_OFF_ON_HALT=0
|
|
1188
|
+
ENABLE_SELF_UPDATE=1
|
|
1189
|
+
DISABLE_HDMI=0
|
|
1190
|
+
NET_INSTALL_ENABLED=1
|
|
1191
|
+
DHCP_TIMEOUT=45000
|
|
1192
|
+
DHCP_REQ_TIMEOUT=4000
|
|
1193
|
+
TFTP_FILE_TIMEOUT=30000
|
|
1194
|
+
BOOT_ORDER=0x21
|
|
1195
|
+
|
|
1196
|
+
# ─────────────────────────────────────────────────────────────
|
|
1197
|
+
# TFTP configuration
|
|
1198
|
+
# ─────────────────────────────────────────────────────────────
|
|
1199
|
+
|
|
1200
|
+
# Custom TFTP prefix string (e.g., based on MAC address, no colons)
|
|
1201
|
+
#TFTP_PREFIX_STR=AA-BB-CC-DD-EE-FF/
|
|
1202
|
+
|
|
1203
|
+
# Optional PXE Option43 override (leave commented if unused)
|
|
1204
|
+
#PXE_OPTION43="Raspberry Pi Boot"
|
|
1205
|
+
|
|
1206
|
+
# DHCP client GUID (Option 97); 0x34695052 is the FourCC for Raspberry Pi 4
|
|
1207
|
+
#DHCP_OPTION97=0x34695052
|
|
1208
|
+
|
|
1209
|
+
TFTP_IP=${tftpIp}
|
|
1210
|
+
TFTP_PREFIX=1
|
|
1211
|
+
TFTP_PREFIX_STR=${tftpPrefixStr}/
|
|
1212
|
+
|
|
1213
|
+
# ─────────────────────────────────────────────────────────────
|
|
1214
|
+
# Manually override Ethernet MAC address
|
|
1215
|
+
# ─────────────────────────────────────────────────────────────
|
|
1216
|
+
|
|
1217
|
+
MAC_ADDRESS=${macAddress}
|
|
1218
|
+
|
|
1219
|
+
# OTP MAC address override
|
|
1220
|
+
#MAC_ADDRESS_OTP=0,1
|
|
1221
|
+
|
|
1222
|
+
# ─────────────────────────────────────────────────────────────
|
|
1223
|
+
# Static IP configuration (bypasses DHCP completely)
|
|
1224
|
+
# ─────────────────────────────────────────────────────────────
|
|
1225
|
+
CLIENT_IP=${clientIp}
|
|
1226
|
+
SUBNET=${subnet}
|
|
1227
|
+
GATEWAY=${gateway}`;
|
|
1228
|
+
|
|
1229
|
+
default:
|
|
1230
|
+
throw new Error('Boot conf factory invalid workflow ID:' + workflowId);
|
|
94
1231
|
}
|
|
95
1232
|
},
|
|
1233
|
+
|
|
1234
|
+
/**
|
|
1235
|
+
* @property {object} workflowsConfig
|
|
1236
|
+
* @description Configuration for different baremetal provisioning workflows.
|
|
1237
|
+
* Each workflow defines specific parameters like system provisioning type,
|
|
1238
|
+
* kernel version, Chrony settings, debootstrap image details, and NFS mounts.
|
|
1239
|
+
*/
|
|
1240
|
+
workflowsConfig: {
|
|
1241
|
+
/**
|
|
1242
|
+
* @property {object} rpi4mb
|
|
1243
|
+
* @description Configuration for the Raspberry Pi 4 Model B workflow.
|
|
1244
|
+
*/
|
|
1245
|
+
rpi4mb: {
|
|
1246
|
+
menuentryStr: 'UNDERPOST.NET UEFI/GRUB/MAAS RPi4 commissioning (ARM64)',
|
|
1247
|
+
systemProvisioning: 'ubuntu', // Specifies the system provisioning factory to use.
|
|
1248
|
+
kernelLibVersion: `6.8.0-41-generic`, // The kernel library version for this workflow.
|
|
1249
|
+
networkInterfaceName: 'enabcm6e4ei0', // The name of the primary network interface on the RPi4.
|
|
1250
|
+
netmask: '255.255.255.0', // Subnet mask for the network.
|
|
1251
|
+
firmwares: [
|
|
1252
|
+
{
|
|
1253
|
+
url: 'https://github.com/pftf/RPi4/releases/download/v1.41/RPi4_UEFI_Firmware_v1.41.zip',
|
|
1254
|
+
gateway: '192.168.1.1',
|
|
1255
|
+
subnet: '255.255.255.0',
|
|
1256
|
+
},
|
|
1257
|
+
],
|
|
1258
|
+
chronyc: {
|
|
1259
|
+
timezone: 'America/New_York', // Timezone for Chrony configuration.
|
|
1260
|
+
chronyConfPath: `/etc/chrony/chrony.conf`, // Path to Chrony configuration file.
|
|
1261
|
+
},
|
|
1262
|
+
debootstrap: {
|
|
1263
|
+
image: {
|
|
1264
|
+
architecture: 'arm64', // Architecture for the debootstrap image.
|
|
1265
|
+
name: 'noble', // Codename of the Ubuntu release (e.g., 'noble' for 24.04 LTS).
|
|
1266
|
+
},
|
|
1267
|
+
},
|
|
1268
|
+
maas: {
|
|
1269
|
+
image: {
|
|
1270
|
+
architecture: 'arm64/ga-24.04', // Architecture for MAAS image.
|
|
1271
|
+
name: 'ubuntu/noble', // Name of the MAAS Ubuntu image.
|
|
1272
|
+
},
|
|
1273
|
+
},
|
|
1274
|
+
nfs: {
|
|
1275
|
+
mounts: {
|
|
1276
|
+
// Define NFS mount points and their types (bind, rbind).
|
|
1277
|
+
bind: ['/proc', '/sys', '/run'], // Standard bind mounts.
|
|
1278
|
+
rbind: ['/dev'], // Recursive bind mount for /dev.
|
|
1279
|
+
},
|
|
1280
|
+
},
|
|
1281
|
+
},
|
|
1282
|
+
},
|
|
96
1283
|
};
|
|
97
1284
|
}
|
|
98
1285
|
|