@underpostnet/underpost 2.96.0 → 2.97.0
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 +64 -17
- package/baremetal/packer-workflows.json +11 -0
- package/cli.md +72 -40
- 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/images/Rocky9Amd64/rocky9.pkr.hcl +6 -2
- package/packer/images/Rocky9Arm64/Makefile +69 -0
- package/packer/images/Rocky9Arm64/README.md +122 -0
- package/packer/images/Rocky9Arm64/http/rocky9.ks.pkrtpl.hcl +114 -0
- package/packer/images/Rocky9Arm64/rocky9.pkr.hcl +171 -0
- package/scripts/disk-clean.sh +128 -187
- package/scripts/ipxe-setup.sh +197 -0
- package/scripts/packer-init-vars-file.sh +16 -6
- package/scripts/packer-setup.sh +270 -33
- package/scripts/ports-ls.sh +31 -0
- package/scripts/quick-tftp.sh +19 -0
- package/src/api/document/document.controller.js +15 -0
- package/src/api/document/document.model.js +14 -0
- package/src/api/document/document.router.js +1 -0
- package/src/api/document/document.service.js +61 -3
- package/src/cli/baremetal.js +1716 -439
- package/src/cli/cloud-init.js +354 -231
- package/src/cli/cluster.js +1 -1
- package/src/cli/db.js +22 -0
- package/src/cli/deploy.js +6 -2
- package/src/cli/image.js +1 -0
- package/src/cli/index.js +40 -36
- package/src/cli/run.js +77 -11
- package/src/cli/ssh.js +1 -1
- package/src/client/components/core/Input.js +3 -1
- package/src/client/components/core/Panel.js +161 -15
- package/src/client/components/core/PanelForm.js +198 -35
- package/src/client/components/core/Translate.js +11 -0
- package/src/client/services/document/document.service.js +19 -0
- package/src/index.js +2 -1
- package/src/server/dns.js +8 -2
- package/src/server/start.js +14 -6
- package/manifests/mariadb/config.yaml +0 -10
- package/manifests/mariadb/secret.yaml +0 -8
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,21 +50,40 @@ 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 {string} [options.bootcmd=''] - Comma-separated list of boot commands to execute.
|
|
86
|
+
* @param {string} [options.runcmd=''] - Comma-separated list of run commands to execute.
|
|
67
87
|
* @param {boolean} [options.nfsBuild=false] - Flag to build the NFS root filesystem.
|
|
68
88
|
* @param {boolean} [options.nfsMount=false] - Flag to mount the NFS root filesystem.
|
|
69
89
|
* @param {boolean} [options.nfsUnmount=false] - Flag to unmount the NFS root filesystem.
|
|
@@ -74,21 +94,41 @@ class UnderpostBaremetal {
|
|
|
74
94
|
*/
|
|
75
95
|
async callback(
|
|
76
96
|
workflowId,
|
|
77
|
-
hostname,
|
|
78
97
|
ipAddress,
|
|
98
|
+
hostname,
|
|
99
|
+
ipFileServer,
|
|
100
|
+
ipConfig,
|
|
101
|
+
netmask,
|
|
102
|
+
dnsServer,
|
|
79
103
|
options = {
|
|
80
104
|
dev: false,
|
|
81
105
|
controlServerInstall: false,
|
|
82
106
|
controlServerUninstall: false,
|
|
107
|
+
controlServerRestart: false,
|
|
83
108
|
controlServerDbInstall: false,
|
|
84
109
|
controlServerDbUninstall: false,
|
|
110
|
+
createMachine: false,
|
|
111
|
+
mac: '',
|
|
112
|
+
ipxe: false,
|
|
113
|
+
ipxeRebuild: false,
|
|
85
114
|
installPacker: false,
|
|
86
115
|
packerMaasImageTemplate: false,
|
|
87
116
|
packerWorkflowId: '',
|
|
88
117
|
packerMaasImageBuild: false,
|
|
89
118
|
packerMaasImageUpload: false,
|
|
119
|
+
packerMaasImageCached: false,
|
|
120
|
+
removeMachines: '',
|
|
121
|
+
clearDiscovered: false,
|
|
90
122
|
cloudInitUpdate: false,
|
|
123
|
+
cloudInit: false,
|
|
91
124
|
commission: false,
|
|
125
|
+
bootstrapHttpServerPort: 8888,
|
|
126
|
+
bootstrapHttpServerPath: './public/localhost',
|
|
127
|
+
isoUrl: '',
|
|
128
|
+
ubuntuToolsBuild: false,
|
|
129
|
+
ubuntuToolsTest: false,
|
|
130
|
+
bootcmd: '',
|
|
131
|
+
runcmd: '',
|
|
92
132
|
nfsBuild: false,
|
|
93
133
|
nfsMount: false,
|
|
94
134
|
nfsUnmount: false,
|
|
@@ -104,45 +144,132 @@ class UnderpostBaremetal {
|
|
|
104
144
|
const underpostRoot = options?.dev === true ? '.' : `${npmRoot}/underpost`;
|
|
105
145
|
|
|
106
146
|
// Set default values if not provided.
|
|
107
|
-
workflowId = workflowId ? workflowId : '
|
|
147
|
+
workflowId = workflowId ? workflowId : 'rpi4mbarm64-iso-ram';
|
|
108
148
|
hostname = hostname ? hostname : workflowId;
|
|
109
|
-
ipAddress = ipAddress ? ipAddress : '192.168.1.
|
|
149
|
+
ipAddress = ipAddress ? ipAddress : '192.168.1.191';
|
|
150
|
+
ipFileServer = ipFileServer ? ipFileServer : getLocalIPv4Address();
|
|
151
|
+
netmask = netmask ? netmask : '255.255.255.0';
|
|
152
|
+
dnsServer = dnsServer ? dnsServer : '8.8.8.8';
|
|
153
|
+
|
|
154
|
+
// IpConfig options:
|
|
155
|
+
// dhcp - DHCP configuration
|
|
156
|
+
// dhpc6 - DHCP IPv6 configuration
|
|
157
|
+
// auto6 - automatic IPv6 configuration
|
|
158
|
+
// on, any - any protocol available in the kernel (default)
|
|
159
|
+
// none, off - no autoconfiguration, static network configuration
|
|
160
|
+
ipConfig = ipConfig ? ipConfig : 'none';
|
|
110
161
|
|
|
111
162
|
// Set default MAC address
|
|
112
|
-
let macAddress =
|
|
163
|
+
let macAddress = UnderpostBaremetal.API.macAddressFactory(options).mac;
|
|
164
|
+
const workflowsConfig = UnderpostBaremetal.API.loadWorkflowsConfig();
|
|
165
|
+
|
|
166
|
+
if (!workflowsConfig[workflowId]) {
|
|
167
|
+
throw new Error(`Workflow configuration not found for ID: ${workflowId}`);
|
|
168
|
+
}
|
|
113
169
|
|
|
170
|
+
const tftpPrefix = workflowsConfig[workflowId].tftpPrefix || 'rpi4mb';
|
|
114
171
|
// Define the debootstrap architecture.
|
|
115
172
|
let debootstrapArch;
|
|
116
173
|
|
|
174
|
+
// Set debootstrap architecture.
|
|
175
|
+
if (workflowsConfig[workflowId].type === 'chroot') {
|
|
176
|
+
const { architecture } = workflowsConfig[workflowId].debootstrap.image;
|
|
177
|
+
debootstrapArch = architecture;
|
|
178
|
+
}
|
|
179
|
+
|
|
117
180
|
// Define the database provider ID.
|
|
118
181
|
const dbProviderId = 'postgresql-17';
|
|
119
182
|
|
|
120
183
|
// Define the NFS host path based on the environment variable and hostname.
|
|
121
184
|
const nfsHostPath = `${process.env.NFS_EXPORT_PATH}/${hostname}`;
|
|
122
185
|
|
|
123
|
-
// Define the TFTP root path based
|
|
124
|
-
const tftpRootPath = `${process.env.TFTP_ROOT}/${
|
|
186
|
+
// Define the TFTP root prefix path based
|
|
187
|
+
const tftpRootPath = `${process.env.TFTP_ROOT}/${tftpPrefix}`;
|
|
188
|
+
|
|
189
|
+
// Define the bootstrap HTTP server path.
|
|
190
|
+
const bootstrapHttpServerPath = options.bootstrapHttpServerPath
|
|
191
|
+
? options.bootstrapHttpServerPath
|
|
192
|
+
: `/tmp/bootstrap-http-server/${workflowId}`;
|
|
125
193
|
|
|
126
194
|
// Capture metadata for the callback execution, useful for logging and auditing.
|
|
127
195
|
const callbackMetaData = {
|
|
128
|
-
args: {
|
|
196
|
+
args: { workflowId, ipAddress, hostname, ipFileServer, ipConfig, netmask, dnsServer },
|
|
129
197
|
options,
|
|
130
198
|
runnerHost: { architecture: UnderpostBaremetal.API.getHostArch().alias, ip: getLocalIPv4Address() },
|
|
131
199
|
nfsHostPath,
|
|
132
200
|
tftpRootPath,
|
|
201
|
+
bootstrapHttpServerPath,
|
|
133
202
|
};
|
|
134
203
|
|
|
135
204
|
// Log the initiation of the baremetal callback with relevant metadata.
|
|
136
205
|
logger.info('Baremetal callback', callbackMetaData);
|
|
137
206
|
|
|
207
|
+
// Create a new machine in MAAS if the option is set.
|
|
208
|
+
let machine;
|
|
209
|
+
if (options.createMachine === true) {
|
|
210
|
+
const [searhMachine] = JSON.parse(
|
|
211
|
+
shellExec(`maas maas machines read hostname=${hostname}`, {
|
|
212
|
+
stdout: true,
|
|
213
|
+
silent: true,
|
|
214
|
+
}),
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
if (searhMachine) {
|
|
218
|
+
// Check if existing machine's MAC matches the specified MAC
|
|
219
|
+
const existingMac = searhMachine.boot_interface?.mac_address || searhMachine.mac_address;
|
|
220
|
+
|
|
221
|
+
// If using hardware MAC (macAddress is null), skip MAC validation and use existing machine
|
|
222
|
+
if (macAddress === null) {
|
|
223
|
+
logger.info(`Using hardware MAC mode - keeping existing machine ${hostname} with MAC ${existingMac}`);
|
|
224
|
+
machine = searhMachine;
|
|
225
|
+
} else if (existingMac && existingMac !== macAddress) {
|
|
226
|
+
logger.warn(`⚠ Machine ${hostname} exists with MAC ${existingMac}, but --mac specified ${macAddress}`);
|
|
227
|
+
logger.info(`Deleting existing machine ${searhMachine.system_id} to recreate with correct MAC...`);
|
|
228
|
+
|
|
229
|
+
// Delete the existing machine
|
|
230
|
+
shellExec(`maas maas machine delete ${searhMachine.system_id}`, {
|
|
231
|
+
silent: true,
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// Create new machine with correct MAC
|
|
235
|
+
machine = UnderpostBaremetal.API.machineFactory({
|
|
236
|
+
hostname,
|
|
237
|
+
ipAddress,
|
|
238
|
+
macAddress,
|
|
239
|
+
maas: workflowsConfig[workflowId].maas,
|
|
240
|
+
}).machine;
|
|
241
|
+
|
|
242
|
+
logger.info(`✓ Machine recreated with MAC ${macAddress}`);
|
|
243
|
+
} else {
|
|
244
|
+
logger.info(`Using existing machine ${hostname} with MAC ${existingMac}`);
|
|
245
|
+
machine = searhMachine;
|
|
246
|
+
}
|
|
247
|
+
} else {
|
|
248
|
+
// No existing machine found, create new one
|
|
249
|
+
// For hardware MAC mode (macAddress is null), we'll create machine after discovery
|
|
250
|
+
if (macAddress === null) {
|
|
251
|
+
logger.info(`Hardware MAC mode - machine will be created after discovery`);
|
|
252
|
+
machine = null;
|
|
253
|
+
} else {
|
|
254
|
+
machine = UnderpostBaremetal.API.machineFactory({
|
|
255
|
+
hostname,
|
|
256
|
+
ipAddress,
|
|
257
|
+
macAddress,
|
|
258
|
+
maas: workflowsConfig[workflowId].maas,
|
|
259
|
+
}).machine;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
138
264
|
if (options.installPacker) {
|
|
139
265
|
await UnderpostBaremetal.API.installPacker(underpostRoot);
|
|
140
266
|
return;
|
|
141
267
|
}
|
|
142
268
|
|
|
143
269
|
if (options.packerMaasImageTemplate) {
|
|
270
|
+
workflowId = options.packerWorkflowId;
|
|
144
271
|
if (!workflowId) {
|
|
145
|
-
throw new Error('workflow-id is required when using --packer-maas-image-template');
|
|
272
|
+
throw new Error('--packer-workflow-id is required when using --packer-maas-image-template');
|
|
146
273
|
}
|
|
147
274
|
|
|
148
275
|
const templatePath = options.packerMaasImageTemplate;
|
|
@@ -181,13 +308,13 @@ class UnderpostBaremetal {
|
|
|
181
308
|
workflows[workflowId] = workflowConfig;
|
|
182
309
|
UnderpostBaremetal.API.writePackerMaasImageBuildWorkflows(workflows);
|
|
183
310
|
|
|
184
|
-
logger.info('
|
|
185
|
-
logger.info(
|
|
186
|
-
logger.info('
|
|
311
|
+
logger.info('Template extracted successfully!');
|
|
312
|
+
logger.info(`Added configuration for ${workflowId} to engine/baremetal/packer-workflows.json`);
|
|
313
|
+
logger.info('Next steps');
|
|
187
314
|
logger.info(`1. Review and customize the Packer template files in: ${targetDir}`);
|
|
188
315
|
logger.info(`2. Review the workflow configuration in engine/baremetal/packer-workflows.json`);
|
|
189
316
|
logger.info(
|
|
190
|
-
`3. Build the image with: underpost baremetal ${workflowId} --packer-maas-image-build
|
|
317
|
+
`3. Build the image with: underpost baremetal --packer-workflow-id ${workflowId} --packer-maas-image-build`,
|
|
191
318
|
);
|
|
192
319
|
} catch (error) {
|
|
193
320
|
throw new Error(`Failed to extract template: ${error.message}`);
|
|
@@ -217,10 +344,26 @@ class UnderpostBaremetal {
|
|
|
217
344
|
throw new Error('Packer is not installed. Please install Packer to proceed.');
|
|
218
345
|
}
|
|
219
346
|
|
|
347
|
+
// Check for QEMU support if building for a different architecture (validator bots case)
|
|
348
|
+
UnderpostBaremetal.API.checkQemuCrossArchSupport(workflow);
|
|
349
|
+
|
|
220
350
|
logger.info(`Building Packer image for ${workflowId} in ${packerDir}...`);
|
|
221
|
-
|
|
222
|
-
|
|
351
|
+
|
|
352
|
+
// Only remove artifacts if not using cached mode
|
|
353
|
+
if (!options.packerMaasImageCached) {
|
|
354
|
+
const artifacts = [
|
|
355
|
+
'output-rocky9',
|
|
356
|
+
'packer_cache',
|
|
357
|
+
'x86_64_VARS.fd',
|
|
358
|
+
'aarch64_VARS.fd',
|
|
359
|
+
workflow.maas.content,
|
|
360
|
+
];
|
|
361
|
+
shellExec(`cd packer/images/${workflowId}
|
|
223
362
|
rm -rf ${artifacts.join(' ')}`);
|
|
363
|
+
logger.info('Removed previous build artifacts');
|
|
364
|
+
} else {
|
|
365
|
+
logger.info('Cached mode: Keeping existing artifacts for incremental build');
|
|
366
|
+
}
|
|
224
367
|
shellExec(`chmod +x ${underpostRoot}/scripts/packer-init-vars-file.sh`);
|
|
225
368
|
shellExec(`${underpostRoot}/scripts/packer-init-vars-file.sh`);
|
|
226
369
|
|
|
@@ -229,10 +372,17 @@ rm -rf ${artifacts.join(' ')}`);
|
|
|
229
372
|
throw new Error('Packer init failed');
|
|
230
373
|
}
|
|
231
374
|
|
|
232
|
-
const
|
|
375
|
+
const isArm = process.arch === 'arm64';
|
|
376
|
+
// Add /usr/local/bin to PATH so Packer can find compiled QEMU binaries
|
|
377
|
+
const packerEnv = {
|
|
378
|
+
...process.env,
|
|
379
|
+
PACKER_LOG: '1',
|
|
380
|
+
PATH: `/usr/local/bin:${process.env.PATH || '/usr/bin:/bin'}`,
|
|
381
|
+
};
|
|
382
|
+
const build = spawnSync('packer', ['build', '-var', `host_is_arm=${isArm}`, '.'], {
|
|
233
383
|
stdio: 'inherit',
|
|
234
384
|
cwd: packerDir,
|
|
235
|
-
env:
|
|
385
|
+
env: packerEnv,
|
|
236
386
|
});
|
|
237
387
|
|
|
238
388
|
if (build.status !== 0) {
|
|
@@ -244,7 +394,7 @@ rm -rf ${artifacts.join(' ')}`);
|
|
|
244
394
|
if (!fs.existsSync(tarballPath)) {
|
|
245
395
|
throw new Error(
|
|
246
396
|
`Build artifact not found: ${tarballPath}\n` +
|
|
247
|
-
`Please build first with: --packer-
|
|
397
|
+
`Please build first with: --packer-workflow-id ${workflowId} --packer-maas-image-build`,
|
|
248
398
|
);
|
|
249
399
|
}
|
|
250
400
|
const stats = fs.statSync(tarballPath);
|
|
@@ -299,18 +449,31 @@ rm -rf ${artifacts.join(' ')}`);
|
|
|
299
449
|
return;
|
|
300
450
|
}
|
|
301
451
|
|
|
302
|
-
if (options.logs === '
|
|
452
|
+
if (options.logs === 'dhcp-lease') {
|
|
453
|
+
shellExec(`cat /var/snap/maas/common/maas/dhcp/dhcpd.leases`);
|
|
454
|
+
shellExec(`cat /var/snap/maas/common/maas/dhcp/dhcpd.pid`);
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
if (options.logs === 'dhcp-lan') {
|
|
459
|
+
shellExec(`sudo tcpdump -l -n -i any -s0 -vv 'udp and (port 67 or 68)'`);
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (options.logs === 'cloud-init') {
|
|
303
464
|
shellExec(`tail -f -n 900 ${nfsHostPath}/var/log/cloud-init.log`);
|
|
304
465
|
return;
|
|
305
466
|
}
|
|
306
467
|
|
|
307
|
-
if (options.logs === 'machine') {
|
|
468
|
+
if (options.logs === 'cloud-init-machine') {
|
|
308
469
|
shellExec(`tail -f -n 900 ${nfsHostPath}/var/log/cloud-init-output.log`);
|
|
309
470
|
return;
|
|
310
471
|
}
|
|
311
472
|
|
|
312
|
-
if (options.logs === 'cloud-config') {
|
|
313
|
-
shellExec(`cat ${
|
|
473
|
+
if (options.logs === 'cloud-init-config') {
|
|
474
|
+
shellExec(`cat ${bootstrapHttpServerPath}/${hostname}/cloud-init/user-data`);
|
|
475
|
+
shellExec(`cat ${bootstrapHttpServerPath}/${hostname}/cloud-init/meta-data`);
|
|
476
|
+
shellExec(`cat ${bootstrapHttpServerPath}/${hostname}/cloud-init/vendor-data`);
|
|
314
477
|
return;
|
|
315
478
|
}
|
|
316
479
|
|
|
@@ -346,7 +509,6 @@ rm -rf ${artifacts.join(' ')}`);
|
|
|
346
509
|
shellExec(`chmod +x ${underpostRoot}/scripts/maas-setup.sh`);
|
|
347
510
|
shellExec(`chmod +x ${underpostRoot}/scripts/nat-iptables.sh`);
|
|
348
511
|
shellExec(`${underpostRoot}/scripts/maas-setup.sh`);
|
|
349
|
-
shellExec(`${underpostRoot}/scripts/nat-iptables.sh`);
|
|
350
512
|
return;
|
|
351
513
|
}
|
|
352
514
|
|
|
@@ -368,6 +530,12 @@ rm -rf ${artifacts.join(' ')}`);
|
|
|
368
530
|
return;
|
|
369
531
|
}
|
|
370
532
|
|
|
533
|
+
// Handle control server restart.
|
|
534
|
+
if (options.controlServerRestart === true) {
|
|
535
|
+
shellExec(`sudo snap restart maas`);
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
|
|
371
539
|
// Handle control server database installation.
|
|
372
540
|
if (options.controlServerDbInstall === true) {
|
|
373
541
|
// Deploy the database provider and manage MAAS database.
|
|
@@ -385,41 +553,43 @@ rm -rf ${artifacts.join(' ')}`);
|
|
|
385
553
|
return;
|
|
386
554
|
}
|
|
387
555
|
|
|
388
|
-
const workflowsConfig = UnderpostBaremetal.API.loadWorkflowsConfig();
|
|
389
|
-
if (!workflowsConfig[workflowId]) {
|
|
390
|
-
throw new Error(`Workflow configuration not found for ID: ${workflowId}`);
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
// Set debootstrap architecture.
|
|
394
|
-
{
|
|
395
|
-
const { architecture } = workflowsConfig[workflowId].debootstrap.image;
|
|
396
|
-
debootstrapArch = architecture;
|
|
397
|
-
}
|
|
398
|
-
|
|
399
556
|
// Handle NFS mount operation.
|
|
400
557
|
if (options.nfsMount === true) {
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
558
|
+
const { isMounted } = UnderpostBaremetal.API.nfsMountCallback({
|
|
559
|
+
hostname,
|
|
560
|
+
nfsHostPath,
|
|
561
|
+
workflowId,
|
|
562
|
+
mount: true,
|
|
563
|
+
});
|
|
564
|
+
return;
|
|
404
565
|
}
|
|
405
566
|
|
|
406
567
|
// Handle NFS unmount operation.
|
|
407
568
|
if (options.nfsUnmount === true) {
|
|
408
|
-
UnderpostBaremetal.API.nfsMountCallback({
|
|
569
|
+
const { isMounted } = UnderpostBaremetal.API.nfsMountCallback({
|
|
570
|
+
hostname,
|
|
571
|
+
nfsHostPath,
|
|
572
|
+
workflowId,
|
|
573
|
+
unmount: true,
|
|
574
|
+
});
|
|
575
|
+
return;
|
|
409
576
|
}
|
|
410
577
|
|
|
411
578
|
// Handle NFS root filesystem build operation.
|
|
412
579
|
if (options.nfsBuild === true) {
|
|
413
|
-
|
|
414
|
-
|
|
580
|
+
const { isMounted } = UnderpostBaremetal.API.nfsMountCallback({
|
|
581
|
+
hostname,
|
|
582
|
+
nfsHostPath,
|
|
583
|
+
workflowId,
|
|
584
|
+
unmount: true,
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
if (isMounted) throw new Error(`NFS path ${nfsHostPath} is currently mounted. Please unmount before building.`);
|
|
415
588
|
|
|
416
589
|
// Clean and create the NFS host path.
|
|
417
590
|
shellExec(`sudo rm -rf ${nfsHostPath}/*`);
|
|
418
591
|
shellExec(`mkdir -p ${nfsHostPath}`);
|
|
419
592
|
|
|
420
|
-
// Mount binfmt_misc filesystem.
|
|
421
|
-
UnderpostBaremetal.API.mountBinfmtMisc({ nfsHostPath });
|
|
422
|
-
|
|
423
593
|
// Perform the first stage of debootstrap.
|
|
424
594
|
{
|
|
425
595
|
const { architecture, name } = workflowsConfig[workflowId].debootstrap.image;
|
|
@@ -452,6 +622,14 @@ rm -rf ${artifacts.join(' ')}`);
|
|
|
452
622
|
shellExec(`podman ps -a`);
|
|
453
623
|
shellExec(`file ${nfsHostPath}/bin/bash`); // Verify the bash executable in the chroot.
|
|
454
624
|
|
|
625
|
+
// Mount necessary filesystems and register binfmt for the second stage.
|
|
626
|
+
UnderpostBaremetal.API.nfsMountCallback({
|
|
627
|
+
hostname,
|
|
628
|
+
nfsHostPath,
|
|
629
|
+
workflowId,
|
|
630
|
+
mount: true,
|
|
631
|
+
});
|
|
632
|
+
|
|
455
633
|
// Perform the second stage of debootstrap within the chroot environment.
|
|
456
634
|
UnderpostBaremetal.API.crossArchRunner({
|
|
457
635
|
nfsHostPath,
|
|
@@ -459,34 +637,7 @@ rm -rf ${artifacts.join(' ')}`);
|
|
|
459
637
|
callbackMetaData,
|
|
460
638
|
steps: [`/debootstrap/debootstrap --second-stage`],
|
|
461
639
|
});
|
|
462
|
-
|
|
463
|
-
// Mount NFS if it's not already mounted after the build.
|
|
464
|
-
if (!isMounted) {
|
|
465
|
-
UnderpostBaremetal.API.nfsMountCallback({ hostname, workflowId, mount: true });
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
// Apply system provisioning steps (base, user, timezone, keyboard).
|
|
469
|
-
{
|
|
470
|
-
const { systemProvisioning, kernelLibVersion, chronyc } = workflowsConfig[workflowId];
|
|
471
|
-
const { timezone, chronyConfPath } = chronyc;
|
|
472
|
-
|
|
473
|
-
UnderpostBaremetal.API.crossArchRunner({
|
|
474
|
-
nfsHostPath,
|
|
475
|
-
debootstrapArch,
|
|
476
|
-
callbackMetaData,
|
|
477
|
-
steps: [
|
|
478
|
-
...UnderpostBaremetal.API.systemProvisioningFactory[systemProvisioning].base({
|
|
479
|
-
kernelLibVersion,
|
|
480
|
-
}),
|
|
481
|
-
...UnderpostBaremetal.API.systemProvisioningFactory[systemProvisioning].user(),
|
|
482
|
-
...UnderpostBaremetal.API.systemProvisioningFactory[systemProvisioning].timezone({
|
|
483
|
-
timezone,
|
|
484
|
-
chronyConfPath,
|
|
485
|
-
}),
|
|
486
|
-
...UnderpostBaremetal.API.systemProvisioningFactory[systemProvisioning].keyboard(),
|
|
487
|
-
],
|
|
488
|
-
});
|
|
489
|
-
}
|
|
640
|
+
return;
|
|
490
641
|
}
|
|
491
642
|
|
|
492
643
|
// Fetch boot resources and machines if commissioning or listing.
|
|
@@ -519,14 +670,33 @@ rm -rf ${artifacts.join(' ')}`);
|
|
|
519
670
|
console.table(machines);
|
|
520
671
|
}
|
|
521
672
|
|
|
673
|
+
if (options.clearDiscovered) UnderpostBaremetal.API.removeDiscoveredMachines();
|
|
674
|
+
|
|
675
|
+
// Handle remove existing machines from MAAS.
|
|
676
|
+
if (options.removeMachines)
|
|
677
|
+
machines = UnderpostBaremetal.API.removeMachines({
|
|
678
|
+
machines: options.removeMachines === 'all' ? machines : options.removeMachines.split(','),
|
|
679
|
+
ignore: machine ? [machine.system_id] : [],
|
|
680
|
+
});
|
|
681
|
+
|
|
522
682
|
// Handle commissioning tasks (placeholder for future implementation).
|
|
683
|
+
|
|
523
684
|
if (options.commission === true) {
|
|
524
|
-
|
|
685
|
+
let { firmwares, networkInterfaceName, maas, menuentryStr, type } = workflowsConfig[workflowId];
|
|
686
|
+
|
|
687
|
+
// Use commissioning config (Ubuntu ephemeral) for PXE boot resources
|
|
688
|
+
const commissioningImage = maas.commissioning;
|
|
525
689
|
const resource = resources.find(
|
|
526
|
-
(o) => o.architecture ===
|
|
690
|
+
(o) => o.architecture === commissioningImage.architecture && o.name === commissioningImage.name,
|
|
527
691
|
);
|
|
528
692
|
logger.info('Commissioning resource', resource);
|
|
529
693
|
|
|
694
|
+
if (type === 'iso-nfs') {
|
|
695
|
+
// Prepare NFS casper path if using NFS boot.
|
|
696
|
+
shellExec(`sudo rm -rf ${nfsHostPath}`);
|
|
697
|
+
shellExec(`mkdir -p ${nfsHostPath}/casper`);
|
|
698
|
+
}
|
|
699
|
+
|
|
530
700
|
// Clean and create TFTP root path.
|
|
531
701
|
shellExec(`sudo rm -rf ${tftpRootPath}`);
|
|
532
702
|
shellExec(`mkdir -p ${tftpRootPath}/pxe`);
|
|
@@ -544,221 +714,1280 @@ rm -rf ${artifacts.join(' ')}`);
|
|
|
544
714
|
shellExec(`sudo cp -a ${path}/* ${tftpRootPath}`); // Copy firmware files to TFTP root.
|
|
545
715
|
|
|
546
716
|
if (gateway && subnet) {
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
717
|
+
const bootConfSrc = UnderpostBaremetal.API.bootConfFactory({
|
|
718
|
+
workflowId,
|
|
719
|
+
tftpIp: callbackMetaData.runnerHost.ip,
|
|
720
|
+
tftpPrefixStr: hostname,
|
|
721
|
+
macAddress,
|
|
722
|
+
clientIp: ipAddress,
|
|
723
|
+
subnet,
|
|
724
|
+
gateway,
|
|
725
|
+
});
|
|
726
|
+
if (bootConfSrc) fs.writeFileSync(`${tftpRootPath}/boot_${name}.conf`, bootConfSrc, 'utf8');
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// Configure GRUB for PXE boot.
|
|
732
|
+
{
|
|
733
|
+
// Fetch kernel and initrd paths from MAAS boot resource.
|
|
734
|
+
// Both NFS and disk-based commissioning use MAAS boot resources.
|
|
735
|
+
const { kernelFilesPaths, resourcesPath } = UnderpostBaremetal.API.kernelFactory({
|
|
736
|
+
resource,
|
|
737
|
+
type,
|
|
738
|
+
nfsHostPath,
|
|
739
|
+
isoUrl: options.isoUrl || workflowsConfig[workflowId].isoUrl,
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
const { cmd } = UnderpostBaremetal.API.kernelCmdBootParamsFactory({
|
|
743
|
+
ipClient: ipAddress,
|
|
744
|
+
ipDhcpServer: callbackMetaData.runnerHost.ip,
|
|
745
|
+
ipConfig,
|
|
746
|
+
ipFileServer,
|
|
747
|
+
netmask,
|
|
748
|
+
hostname,
|
|
749
|
+
dnsServer,
|
|
750
|
+
networkInterfaceName,
|
|
751
|
+
fileSystemUrl: kernelFilesPaths.isoUrl,
|
|
752
|
+
bootstrapHttpServerPort:
|
|
753
|
+
options.bootstrapHttpServerPort || workflowsConfig[workflowId].bootstrapHttpServerPort || 8888,
|
|
754
|
+
type,
|
|
755
|
+
macAddress,
|
|
756
|
+
cloudInit: options.cloudInit,
|
|
757
|
+
machine,
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
// Check if iPXE mode is enabled AND the iPXE EFI binary exists
|
|
761
|
+
let useIpxe = options.ipxe;
|
|
762
|
+
if (options.ipxe) {
|
|
763
|
+
const arch = commissioningImage.architecture.split('/')[0];
|
|
764
|
+
const ipxeScript = UnderpostBaremetal.API.ipxeScriptFactory({
|
|
765
|
+
maasIp: callbackMetaData.runnerHost.ip,
|
|
766
|
+
macAddress,
|
|
767
|
+
architecture: arch,
|
|
768
|
+
tftpPrefix,
|
|
769
|
+
kernelCmd: cmd,
|
|
770
|
+
});
|
|
771
|
+
fs.writeFileSync(`${tftpRootPath}/stable-id.ipxe`, ipxeScript, 'utf8');
|
|
772
|
+
|
|
773
|
+
// Create embedded boot script that does DHCP and chains to the main script
|
|
774
|
+
const embeddedScript = UnderpostBaremetal.API.ipxeEmbeddedScriptFactory({
|
|
775
|
+
tftpServer: callbackMetaData.runnerHost.ip,
|
|
776
|
+
scriptPath: `/${tftpPrefix}/stable-id.ipxe`,
|
|
777
|
+
macAddress: macAddress,
|
|
778
|
+
});
|
|
779
|
+
fs.writeFileSync(`${tftpRootPath}/boot.ipxe`, embeddedScript, 'utf8');
|
|
780
|
+
|
|
781
|
+
logger.info('✓ iPXE script generated for MAAS commissioning', {
|
|
782
|
+
registeredMAC: macAddress,
|
|
783
|
+
path: `${tftpRootPath}/stable-id.ipxe`,
|
|
784
|
+
embeddedPath: `${tftpRootPath}/boot.ipxe`,
|
|
785
|
+
});
|
|
786
|
+
if (macAddress === null) {
|
|
787
|
+
logger.info('ℹ Hardware MAC mode - device will use actual hardware MAC address');
|
|
788
|
+
logger.info('ℹ MAAS will identify the machine by its hardware MAC after discovery');
|
|
789
|
+
} else {
|
|
790
|
+
logger.info('ℹ Machine registered in MAAS with MAC:', macAddress);
|
|
791
|
+
if (macAddress !== '00:00:00:00:00:00') {
|
|
792
|
+
logger.info('ℹ MAAS will identify the machine by MAC:', macAddress);
|
|
793
|
+
} else {
|
|
794
|
+
logger.info('ℹ Device will boot with its actual hardware MAC address');
|
|
795
|
+
logger.info('ℹ MAAS will identify the machine by its hardware MAC');
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// Rebuild iPXE with embedded boot script if requested or if binary doesn't exist
|
|
800
|
+
const embeddedScriptPath = `${tftpRootPath}/boot.ipxe`;
|
|
801
|
+
const shouldRebuild = options.ipxeRebuild || !fs.existsSync(`${tftpRootPath}/ipxe.efi`);
|
|
802
|
+
|
|
803
|
+
if (shouldRebuild && fs.existsSync(embeddedScriptPath)) {
|
|
804
|
+
logger.info('Rebuilding iPXE with embedded boot script...', {
|
|
805
|
+
embeddedScriptPath,
|
|
806
|
+
forced: options.ipxeRebuild,
|
|
807
|
+
});
|
|
808
|
+
shellExec(
|
|
809
|
+
`${underpostRoot}/scripts/ipxe-setup.sh ${tftpRootPath} --target-arch ${arch} --embed-script ${embeddedScriptPath} --rebuild`,
|
|
559
810
|
);
|
|
811
|
+
} else if (shouldRebuild) {
|
|
812
|
+
logger.warn('⚠ Embedded script not found, building without embedded script');
|
|
813
|
+
shellExec(`${underpostRoot}/scripts/ipxe-setup.sh ${tftpRootPath} --target-arch ${arch}`);
|
|
814
|
+
} else {
|
|
815
|
+
logger.info('ℹ Using existing iPXE binary (use --ipxe-rebuild to force rebuild)');
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// Only use iPXE chainloading if the binary exists
|
|
819
|
+
if (fs.existsSync(`${tftpRootPath}/ipxe.efi`)) {
|
|
820
|
+
logger.info('✓ iPXE EFI binary found');
|
|
821
|
+
} else {
|
|
822
|
+
useIpxe = false;
|
|
823
|
+
logger.warn(`⚠ iPXE EFI binary not found at ${tftpRootPath}/ipxe.efi - falling back to direct GRUB boot`);
|
|
824
|
+
logger.warn(' The iPXE script was generated but cannot be used without the iPXE EFI binary.');
|
|
825
|
+
logger.warn(' Consider booting without --ipxe flag or providing the iPXE EFI binary.');
|
|
560
826
|
}
|
|
561
827
|
}
|
|
828
|
+
|
|
829
|
+
const { grubCfgSrc } = UnderpostBaremetal.API.grubFactory({
|
|
830
|
+
menuentryStr,
|
|
831
|
+
kernelPath: `/${tftpPrefix}/pxe/vmlinuz-efi`,
|
|
832
|
+
initrdPath: `/${tftpPrefix}/pxe/initrd.img`,
|
|
833
|
+
cmd,
|
|
834
|
+
tftpIp: callbackMetaData.runnerHost.ip,
|
|
835
|
+
ipxe: useIpxe,
|
|
836
|
+
ipxePath: `/${tftpPrefix}/ipxe.efi`,
|
|
837
|
+
});
|
|
838
|
+
UnderpostBaremetal.API.writeGrubConfigToFile({
|
|
839
|
+
grubCfgSrc: machine ? grubCfgSrc.replaceAll('system-id', machine.system_id) : grubCfgSrc,
|
|
840
|
+
});
|
|
841
|
+
if (machine) {
|
|
842
|
+
logger.info('✓ GRUB config written with system_id', { system_id: machine.system_id });
|
|
843
|
+
}
|
|
844
|
+
UnderpostBaremetal.API.updateKernelFiles({
|
|
845
|
+
commissioningImage,
|
|
846
|
+
resourcesPath,
|
|
847
|
+
tftpRootPath,
|
|
848
|
+
kernelFilesPaths,
|
|
849
|
+
});
|
|
562
850
|
}
|
|
563
851
|
|
|
564
|
-
//
|
|
852
|
+
// Pass architecture from commissioning or deployment config
|
|
853
|
+
const grubArch = maas.commissioning.architecture;
|
|
854
|
+
UnderpostBaremetal.API.efiGrubModulesFactory({ image: { architecture: grubArch } });
|
|
855
|
+
|
|
856
|
+
// Set ownership and permissions for TFTP root.
|
|
857
|
+
shellExec(`sudo chown -R $(whoami):$(whoami) ${process.env.TFTP_ROOT}`);
|
|
858
|
+
shellExec(`sudo sudo chmod 755 ${process.env.TFTP_ROOT}`);
|
|
859
|
+
|
|
860
|
+
UnderpostBaremetal.API.httpBootstrapServerRunnerFactory({
|
|
861
|
+
hostname,
|
|
862
|
+
bootstrapHttpServerPath,
|
|
863
|
+
bootstrapHttpServerPort:
|
|
864
|
+
options.bootstrapHttpServerPort || workflowsConfig[workflowId].bootstrapHttpServerPort,
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
if (options.cloudInit || options.cloudInitUpdate) {
|
|
869
|
+
const { chronyc, networkInterfaceName } = workflowsConfig[workflowId];
|
|
870
|
+
const { timezone, chronyConfPath } = chronyc;
|
|
871
|
+
const authCredentials = UnderpostCloudInit.API.authCredentialsFactory();
|
|
872
|
+
const { cloudConfigSrc } = UnderpostCloudInit.API.configFactory(
|
|
873
|
+
{
|
|
874
|
+
controlServerIp: callbackMetaData.runnerHost.ip,
|
|
875
|
+
hostname,
|
|
876
|
+
commissioningDeviceIp: ipAddress,
|
|
877
|
+
gatewayip: callbackMetaData.runnerHost.ip,
|
|
878
|
+
mac: macAddress,
|
|
879
|
+
timezone,
|
|
880
|
+
chronyConfPath,
|
|
881
|
+
networkInterfaceName,
|
|
882
|
+
ubuntuToolsBuild: options.ubuntuToolsBuild,
|
|
883
|
+
bootcmd: options.bootcmd,
|
|
884
|
+
runcmd: options.runcmd,
|
|
885
|
+
},
|
|
886
|
+
authCredentials,
|
|
887
|
+
);
|
|
888
|
+
|
|
889
|
+
shellExec(`mkdir -p ${bootstrapHttpServerPath}`);
|
|
890
|
+
fs.writeFileSync(
|
|
891
|
+
`${bootstrapHttpServerPath}/${hostname}/cloud-init/user-data`,
|
|
892
|
+
`#cloud-config\n${cloudConfigSrc}`,
|
|
893
|
+
'utf8',
|
|
894
|
+
);
|
|
895
|
+
fs.writeFileSync(
|
|
896
|
+
`${bootstrapHttpServerPath}/${hostname}/cloud-init/meta-data`,
|
|
897
|
+
`instance-id: ${hostname}\nlocal-hostname: ${hostname}`,
|
|
898
|
+
'utf8',
|
|
899
|
+
);
|
|
900
|
+
fs.writeFileSync(`${bootstrapHttpServerPath}/${hostname}/cloud-init/vendor-data`, ``, 'utf8');
|
|
901
|
+
|
|
902
|
+
logger.info(`Cloud-init files written to ${bootstrapHttpServerPath}`);
|
|
903
|
+
if (options.cloudInitUpdate) return;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
if (workflowsConfig[workflowId].type === 'chroot') {
|
|
907
|
+
if (options.ubuntuToolsBuild) {
|
|
908
|
+
UnderpostCloudInit.API.buildTools({
|
|
909
|
+
workflowId,
|
|
910
|
+
nfsHostPath,
|
|
911
|
+
hostname,
|
|
912
|
+
callbackMetaData,
|
|
913
|
+
dev: options.dev,
|
|
914
|
+
});
|
|
915
|
+
|
|
916
|
+
const { chronyc, keyboard } = workflowsConfig[workflowId];
|
|
917
|
+
const { timezone, chronyConfPath } = chronyc;
|
|
918
|
+
const systemProvisioning = 'ubuntu';
|
|
919
|
+
|
|
920
|
+
UnderpostBaremetal.API.crossArchRunner({
|
|
921
|
+
nfsHostPath,
|
|
922
|
+
debootstrapArch,
|
|
923
|
+
callbackMetaData,
|
|
924
|
+
steps: [
|
|
925
|
+
...UnderpostBaremetal.API.systemProvisioningFactory[systemProvisioning].base(),
|
|
926
|
+
...UnderpostBaremetal.API.systemProvisioningFactory[systemProvisioning].user(),
|
|
927
|
+
...UnderpostBaremetal.API.systemProvisioningFactory[systemProvisioning].timezone({
|
|
928
|
+
timezone,
|
|
929
|
+
chronyConfPath,
|
|
930
|
+
}),
|
|
931
|
+
...UnderpostBaremetal.API.systemProvisioningFactory[systemProvisioning].keyboard(keyboard.layout),
|
|
932
|
+
],
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
if (options.ubuntuToolsTest)
|
|
937
|
+
UnderpostBaremetal.API.crossArchRunner({
|
|
938
|
+
nfsHostPath,
|
|
939
|
+
debootstrapArch,
|
|
940
|
+
callbackMetaData,
|
|
941
|
+
steps: [
|
|
942
|
+
`chmod +x /underpost/date.sh`,
|
|
943
|
+
`chmod +x /underpost/keyboard.sh`,
|
|
944
|
+
`chmod +x /underpost/dns.sh`,
|
|
945
|
+
`chmod +x /underpost/help.sh`,
|
|
946
|
+
`chmod +x /underpost/host.sh`,
|
|
947
|
+
`chmod +x /underpost/test.sh`,
|
|
948
|
+
`chmod +x /underpost/start.sh`,
|
|
949
|
+
`chmod +x /underpost/reset.sh`,
|
|
950
|
+
`chmod +x /underpost/shutdown.sh`,
|
|
951
|
+
`chmod +x /underpost/device_scan.sh`,
|
|
952
|
+
`chmod +x /underpost/mac.sh`,
|
|
953
|
+
`chmod +x /underpost/enlistment.sh`,
|
|
954
|
+
`sudo chmod 700 ~/.ssh/`, // Set secure permissions for .ssh directory.
|
|
955
|
+
`sudo chmod 600 ~/.ssh/authorized_keys`, // Set secure permissions for authorized_keys.
|
|
956
|
+
`sudo chmod 644 ~/.ssh/known_hosts`, // Set permissions for known_hosts.
|
|
957
|
+
`sudo chmod 600 ~/.ssh/id_rsa`, // Set secure permissions for private key.
|
|
958
|
+
`sudo chmod 600 /etc/ssh/ssh_host_ed25519_key`, // Set secure permissions for host key.
|
|
959
|
+
`chown -R root:root ~/.ssh`, // Ensure root owns the .ssh directory.
|
|
960
|
+
`/underpost/test.sh`,
|
|
961
|
+
],
|
|
962
|
+
});
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
shellExec(`${underpostRoot}/scripts/nat-iptables.sh`, { silent: true });
|
|
966
|
+
// Rebuild NFS server configuration.
|
|
967
|
+
if (workflowsConfig[workflowId].type === 'iso-nfs' || workflowsConfig[workflowId].type === 'chroot')
|
|
565
968
|
UnderpostBaremetal.API.rebuildNfsServer({
|
|
566
969
|
nfsHostPath,
|
|
567
970
|
});
|
|
568
971
|
|
|
569
|
-
|
|
972
|
+
// Final commissioning steps.
|
|
973
|
+
if (options.commission === true) {
|
|
974
|
+
const { type } = workflowsConfig[workflowId];
|
|
975
|
+
|
|
976
|
+
if (type === 'chroot') {
|
|
977
|
+
const { isMounted } = UnderpostBaremetal.API.nfsMountCallback({
|
|
978
|
+
hostname,
|
|
979
|
+
nfsHostPath,
|
|
980
|
+
workflowId,
|
|
981
|
+
mount: true,
|
|
982
|
+
});
|
|
983
|
+
if (!isMounted) throw new Error('NFS root filesystem is not mounted');
|
|
984
|
+
}
|
|
985
|
+
const commissionMonitorPayload = {
|
|
986
|
+
macAddress,
|
|
987
|
+
ipAddress,
|
|
988
|
+
hostname,
|
|
989
|
+
maas: workflowsConfig[workflowId].maas,
|
|
990
|
+
machine,
|
|
991
|
+
};
|
|
992
|
+
logger.info('Waiting for commissioning...', {
|
|
993
|
+
...commissionMonitorPayload,
|
|
994
|
+
machine: machine ? machine.system_id : null,
|
|
995
|
+
});
|
|
996
|
+
|
|
997
|
+
const { discovery } = await UnderpostBaremetal.API.commissionMonitor(commissionMonitorPayload);
|
|
998
|
+
|
|
999
|
+
if (type === 'chroot' && options.cloudInit === true) {
|
|
1000
|
+
openTerminal(`node ${underpostRoot}/bin baremetal ${workflowId} ${ipAddress} ${hostname} --logs cloud-init`);
|
|
1001
|
+
openTerminal(
|
|
1002
|
+
`node ${underpostRoot}/bin baremetal ${workflowId} ${ipAddress} ${hostname} --logs cloud-init-machine`,
|
|
1003
|
+
);
|
|
1004
|
+
shellExec(
|
|
1005
|
+
`node ${underpostRoot}/bin baremetal ${workflowId} ${ipAddress} ${hostname} --logs cloud-init-config`,
|
|
1006
|
+
);
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
},
|
|
1010
|
+
|
|
1011
|
+
/**
|
|
1012
|
+
* @method macAddressFactory
|
|
1013
|
+
* @description Generates or returns a MAC address based on options.
|
|
1014
|
+
* @param {object} options - Options for MAC address generation.
|
|
1015
|
+
* @param {string} options.mac - 'random' for random MAC, 'hardware' to use device's actual MAC, specific MAC string, or empty for default.
|
|
1016
|
+
* @returns {object} Object with mac property - null for 'hardware', generated/specified MAC otherwise.
|
|
1017
|
+
* @memberof UnderpostBaremetal
|
|
1018
|
+
*/
|
|
1019
|
+
macAddressFactory(options = { mac: '' }) {
|
|
1020
|
+
const len = 6;
|
|
1021
|
+
const defaultMac = range(1, len)
|
|
1022
|
+
.map(() => '00')
|
|
1023
|
+
.join(':');
|
|
1024
|
+
if (options) {
|
|
1025
|
+
if (!options.mac) options.mac = defaultMac;
|
|
1026
|
+
if (options.mac === 'hardware') {
|
|
1027
|
+
// Return null to indicate hardware MAC should be used (no spoofing)
|
|
1028
|
+
options.mac = null;
|
|
1029
|
+
} else if (options.mac === 'random') {
|
|
1030
|
+
options.mac = range(1, len)
|
|
1031
|
+
.map(() => s4().toLowerCase().substring(0, 2))
|
|
1032
|
+
.join(':');
|
|
1033
|
+
}
|
|
1034
|
+
} else options = { mac: defaultMac };
|
|
1035
|
+
return options;
|
|
1036
|
+
},
|
|
1037
|
+
|
|
1038
|
+
/**
|
|
1039
|
+
* @method downloadUbuntuLiveISO
|
|
1040
|
+
* @description Downloads Ubuntu live ISO and extracts casper boot files for live boot.
|
|
1041
|
+
* @param {object} params - Parameters for the method.
|
|
1042
|
+
* @param {object} params.resource - The MAAS boot resource object.
|
|
1043
|
+
* @param {string} params.architecture - The architecture (arm64 or amd64).
|
|
1044
|
+
* @param {string} params.nfsHostPath - The NFS host path to store the ISO and extracted files.
|
|
1045
|
+
* @returns {object} An object containing paths to the extracted kernel, initrd, and squashfs.
|
|
1046
|
+
* @memberof UnderpostBaremetal
|
|
1047
|
+
*/
|
|
1048
|
+
downloadUbuntuLiveISO({ resource, architecture, nfsHostPath, isoUrl }) {
|
|
1049
|
+
const arch = architecture || resource.architecture.split('/')[0];
|
|
1050
|
+
const osName = resource.name.split('/')[1]; // e.g., "focal", "jammy", "noble"
|
|
1051
|
+
|
|
1052
|
+
// Map Ubuntu codenames to versions - different versions available for different architectures
|
|
1053
|
+
// ARM64 ISOs are hosted on cdimage.ubuntu.com, AMD64 on releases.ubuntu.com
|
|
1054
|
+
const versionMap = {
|
|
1055
|
+
arm64: {
|
|
1056
|
+
focal: '20.04.5', // ARM64 focal only up to 20.04.5 on cdimage
|
|
1057
|
+
jammy: '22.04.5',
|
|
1058
|
+
noble: '24.04.3', // ubuntu-24.04.3-live-server-arm64+largemem.iso
|
|
1059
|
+
bionic: '18.04.6',
|
|
1060
|
+
},
|
|
1061
|
+
amd64: {
|
|
1062
|
+
focal: '20.04.6',
|
|
1063
|
+
jammy: '22.04.5',
|
|
1064
|
+
noble: '24.04.1',
|
|
1065
|
+
bionic: '18.04.6',
|
|
1066
|
+
},
|
|
1067
|
+
};
|
|
1068
|
+
|
|
1069
|
+
shellExec(`mkdir -p ${nfsHostPath}/casper`);
|
|
1070
|
+
|
|
1071
|
+
const version = (versionMap[arch] && versionMap[arch][osName]) || '20.04.5';
|
|
1072
|
+
const majorVersion = version.split('.').slice(0, 2).join('.');
|
|
1073
|
+
|
|
1074
|
+
// Determine ISO filename and URL based on architecture
|
|
1075
|
+
// ARM64 ISOs are on cdimage.ubuntu.com, AMD64 on releases.ubuntu.com
|
|
1076
|
+
let isoFilename;
|
|
1077
|
+
if (arch === 'arm64') {
|
|
1078
|
+
isoFilename = `ubuntu-${version}-live-server-arm64${osName === 'noble' ? '+largemem' : ''}.iso`;
|
|
1079
|
+
} else {
|
|
1080
|
+
isoFilename = `ubuntu-${version}-live-server-amd64.iso`;
|
|
1081
|
+
}
|
|
1082
|
+
if (!isoUrl) isoUrl = `https://cdimage.ubuntu.com/releases/${majorVersion}/release/${isoFilename}`;
|
|
1083
|
+
else isoFilename = isoUrl.split('/').pop();
|
|
1084
|
+
|
|
1085
|
+
const isoPath = `/var/tmp/ubuntu-live-iso/${isoFilename}`;
|
|
1086
|
+
const extractDir = `${nfsHostPath}/casper`;
|
|
1087
|
+
|
|
1088
|
+
if (!fs.existsSync(isoPath)) {
|
|
1089
|
+
logger.info(`Downloading Ubuntu ${version} live ISO for ${arch}...`);
|
|
1090
|
+
logger.info(`URL: ${isoUrl}`);
|
|
1091
|
+
logger.info(`This may take a while (typically 1-2 GB)...`);
|
|
1092
|
+
shellExec(`wget --progress=bar:force -O ${isoPath} "${isoUrl}"`, { silent: false });
|
|
1093
|
+
// Verify download by checking file existence and size (not exit code, which can be unreliable)
|
|
1094
|
+
if (!fs.existsSync(isoPath)) {
|
|
1095
|
+
throw new Error(`Failed to download ISO from ${isoUrl} - file not created`);
|
|
1096
|
+
}
|
|
1097
|
+
const stats = fs.statSync(isoPath);
|
|
1098
|
+
if (stats.size < 100 * 1024 * 1024) {
|
|
1099
|
+
shellExec(`rm -f ${isoPath}`);
|
|
1100
|
+
throw new Error(`Downloaded ISO is too small (${stats.size} bytes), download may have failed`);
|
|
1101
|
+
}
|
|
1102
|
+
logger.info(`Downloaded ISO to ${isoPath} (${(stats.size / 1024 / 1024 / 1024).toFixed(2)} GB)`);
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
// Mount ISO and extract casper files
|
|
1106
|
+
const mountPoint = `${nfsHostPath}/mnt-${osName}-${arch}`;
|
|
1107
|
+
shellExec(`mkdir -p ${mountPoint}`);
|
|
1108
|
+
|
|
1109
|
+
// Ensure mount point is not already mounted
|
|
1110
|
+
shellExec(`sudo umount ${mountPoint} 2>/dev/null || true`, { silent: true });
|
|
1111
|
+
|
|
1112
|
+
try {
|
|
1113
|
+
// Mount the ISO
|
|
1114
|
+
shellExec(`sudo mount -o loop,ro ${isoPath} ${mountPoint}`, { silent: false });
|
|
1115
|
+
// Verify mount succeeded by checking if casper directory exists
|
|
1116
|
+
if (!fs.existsSync(`${mountPoint}/casper`)) {
|
|
1117
|
+
throw new Error(`Failed to mount ISO or casper directory not found: ${isoPath}`);
|
|
1118
|
+
}
|
|
1119
|
+
logger.info(`Mounted ISO at ${mountPoint}`);
|
|
1120
|
+
|
|
1121
|
+
// List casper directory to see what's available
|
|
1122
|
+
logger.info(`Checking casper directory contents...`);
|
|
1123
|
+
shellExec(`ls -la ${mountPoint}/casper/ 2>/dev/null || echo "casper directory not found"`, { silent: false });
|
|
1124
|
+
|
|
1125
|
+
// Extract casper files
|
|
1126
|
+
shellExec(`sudo cp -a ${mountPoint}/casper/* ${extractDir}/`);
|
|
1127
|
+
shellExec(`sudo chown -R $(whoami):$(whoami) ${extractDir}`);
|
|
1128
|
+
logger.info(`Extracted casper files from ISO`);
|
|
1129
|
+
|
|
1130
|
+
// Rename kernel and initrd to standard names if needed
|
|
1131
|
+
if (!fs.existsSync(`${extractDir}/vmlinuz`)) {
|
|
1132
|
+
const vmlinuz = shellExec(`ls ${extractDir}/vmlinuz* | head -1`, {
|
|
1133
|
+
silent: true,
|
|
1134
|
+
stdout: true,
|
|
1135
|
+
}).stdout.trim();
|
|
1136
|
+
if (vmlinuz) shellExec(`mv ${vmlinuz} ${extractDir}/vmlinuz`);
|
|
1137
|
+
}
|
|
1138
|
+
if (!fs.existsSync(`${extractDir}/initrd`)) {
|
|
1139
|
+
const initrd = shellExec(`ls ${extractDir}/initrd* | head -1`, { silent: true, stdout: true }).stdout.trim();
|
|
1140
|
+
if (initrd) shellExec(`mv ${initrd} ${extractDir}/initrd`);
|
|
1141
|
+
}
|
|
1142
|
+
} finally {
|
|
1143
|
+
shellExec(`ls -la ${mountPoint}/`);
|
|
1144
|
+
|
|
1145
|
+
// Unmount ISO
|
|
1146
|
+
shellExec(`sudo umount ${mountPoint}`, { silent: true });
|
|
1147
|
+
logger.info(`Unmounted ISO`);
|
|
1148
|
+
// Clean up temporary mount point
|
|
1149
|
+
shellExec(`rmdir ${mountPoint}`, { silent: true });
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
return {
|
|
1153
|
+
'vmlinuz-efi': `${extractDir}/vmlinuz`,
|
|
1154
|
+
'initrd.img': `${extractDir}/initrd`,
|
|
1155
|
+
isoUrl,
|
|
1156
|
+
};
|
|
1157
|
+
},
|
|
1158
|
+
|
|
1159
|
+
/**
|
|
1160
|
+
* @method machineFactory
|
|
1161
|
+
* @description Creates a new machine in MAAS with specified options.
|
|
1162
|
+
* @param {object} options - Options for creating the machine.
|
|
1163
|
+
* @param {string} options.macAddress - The MAC address of the machine.
|
|
1164
|
+
* @param {string} options.hostname - The hostname for the machine.
|
|
1165
|
+
* @param {string} options.ipAddress - The IP address for the machine.
|
|
1166
|
+
* @param {string} options.powerType - The power type for the machine (default is 'manual').
|
|
1167
|
+
* @param {object} options.maas - Additional MAAS-specific options.
|
|
1168
|
+
* @returns {object} An object containing the created machine details.
|
|
1169
|
+
* @memberof UnderpostBaremetal
|
|
1170
|
+
*/
|
|
1171
|
+
machineFactory(
|
|
1172
|
+
options = {
|
|
1173
|
+
macAddress: '',
|
|
1174
|
+
hostname: '',
|
|
1175
|
+
ipAddress: '',
|
|
1176
|
+
powerType: 'manual',
|
|
1177
|
+
maas: {},
|
|
1178
|
+
},
|
|
1179
|
+
) {
|
|
1180
|
+
if (!options.powerType) options.powerType = 'manual';
|
|
1181
|
+
const maas = options.maas || {};
|
|
1182
|
+
const payload = {
|
|
1183
|
+
architecture: (maas.commissioning?.architecture || 'arm64/generic').match('arm')
|
|
1184
|
+
? 'arm64/generic'
|
|
1185
|
+
: 'amd64/generic',
|
|
1186
|
+
mac_address: options.macAddress,
|
|
1187
|
+
mac_addresses: options.macAddress,
|
|
1188
|
+
hostname: options.hostname,
|
|
1189
|
+
power_type: options.powerType,
|
|
1190
|
+
ip: options.ipAddress,
|
|
1191
|
+
};
|
|
1192
|
+
logger.info('Creating MAAS machine', payload);
|
|
1193
|
+
const machine = shellExec(
|
|
1194
|
+
`maas ${process.env.MAAS_ADMIN_USERNAME} machines create ${Object.keys(payload)
|
|
1195
|
+
.map((k) => `${k}="${payload[k]}"`)
|
|
1196
|
+
.join(' ')}`,
|
|
570
1197
|
{
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
1198
|
+
silent: true,
|
|
1199
|
+
stdout: true,
|
|
1200
|
+
},
|
|
1201
|
+
);
|
|
1202
|
+
// console.log(machine);
|
|
1203
|
+
try {
|
|
1204
|
+
return { machine: JSON.parse(machine) };
|
|
1205
|
+
} catch (error) {
|
|
1206
|
+
console.log(error);
|
|
1207
|
+
logger.error(error);
|
|
1208
|
+
throw new Error(`Failed to create MAAS machine. Output:\n${machine}`);
|
|
1209
|
+
}
|
|
1210
|
+
},
|
|
1211
|
+
|
|
1212
|
+
/**
|
|
1213
|
+
* @method kernelFactory
|
|
1214
|
+
* @description Retrieves kernel, initrd, and root filesystem paths from a MAAS boot resource.
|
|
1215
|
+
* @param {object} params - Parameters for the method.
|
|
1216
|
+
* @param {object} params.resource - The MAAS boot resource object.
|
|
1217
|
+
* @param {boolean} params.useLiveIso - Whether to use Ubuntu live ISO instead of MAAS boot resources.
|
|
1218
|
+
* @param {string} params.nfsHostPath - The NFS host path for storing extracted files.
|
|
1219
|
+
* @returns {object} An object containing paths to the kernel, initrd, and root filesystem.
|
|
1220
|
+
* @memberof UnderpostBaremetal
|
|
1221
|
+
*/
|
|
1222
|
+
kernelFactory({ resource, type, nfsHostPath, isoUrl }) {
|
|
1223
|
+
// For disk-based commissioning (casper), use Ubuntu live ISO files
|
|
1224
|
+
if (type === 'iso-ram' || type === 'iso-nfs') {
|
|
1225
|
+
logger.info('Using Ubuntu live ISO for casper boot (disk-based commissioning)');
|
|
1226
|
+
const arch = resource.architecture.split('/')[0];
|
|
1227
|
+
const kernelFilesPaths = UnderpostBaremetal.API.downloadUbuntuLiveISO({
|
|
1228
|
+
resource,
|
|
1229
|
+
architecture: arch,
|
|
1230
|
+
nfsHostPath,
|
|
1231
|
+
isoUrl,
|
|
1232
|
+
});
|
|
1233
|
+
const resourcesPath = `/var/snap/maas/common/maas/image-storage/bootloaders/uefi/${arch}`;
|
|
1234
|
+
return { kernelFilesPaths, resourcesPath };
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
const resourceData = JSON.parse(
|
|
1238
|
+
shellExec(`maas ${process.env.MAAS_ADMIN_USERNAME} boot-resource read ${resource.id}`, {
|
|
1239
|
+
stdout: true,
|
|
1240
|
+
silent: true,
|
|
1241
|
+
disableLog: true,
|
|
1242
|
+
}),
|
|
1243
|
+
);
|
|
1244
|
+
let kernelFilesPaths = {};
|
|
1245
|
+
const bootFiles = resourceData.sets[Object.keys(resourceData.sets)[0]].files;
|
|
1246
|
+
const arch = resource.architecture.split('/')[0];
|
|
1247
|
+
const resourcesPath = `/var/snap/maas/common/maas/image-storage/bootloaders/uefi/${arch}`;
|
|
1248
|
+
const kernelPath = `/var/snap/maas/common/maas/image-storage`;
|
|
1249
|
+
|
|
1250
|
+
logger.info('Available boot files', Object.keys(bootFiles));
|
|
1251
|
+
logger.info('Boot files info', {
|
|
1252
|
+
id: resourceData.id,
|
|
1253
|
+
type: resourceData.type,
|
|
1254
|
+
name: resourceData.name,
|
|
1255
|
+
architecture: resourceData.architecture,
|
|
1256
|
+
bootFiles,
|
|
1257
|
+
arch,
|
|
1258
|
+
resourcesPath,
|
|
1259
|
+
kernelPath,
|
|
1260
|
+
});
|
|
1261
|
+
|
|
1262
|
+
// Try standard synced image structure (Ubuntu, CentOS from MAAS repos)
|
|
1263
|
+
const _suffix = resource.architecture.match('xgene') ? '.xgene' : '';
|
|
1264
|
+
if (bootFiles['boot-kernel' + _suffix] && bootFiles['boot-initrd' + _suffix] && bootFiles['squashfs']) {
|
|
1265
|
+
kernelFilesPaths = {
|
|
1266
|
+
'vmlinuz-efi': `${kernelPath}/${bootFiles['boot-kernel' + _suffix].filename_on_disk}`,
|
|
1267
|
+
'initrd.img': `${kernelPath}/${bootFiles['boot-initrd' + _suffix].filename_on_disk}`,
|
|
1268
|
+
squashfs: `${kernelPath}/${bootFiles['squashfs'].filename_on_disk}`,
|
|
1269
|
+
};
|
|
1270
|
+
}
|
|
1271
|
+
// Try uploaded image structure (Packer-built images, custom uploads)
|
|
1272
|
+
else if (bootFiles['boot-kernel'] && bootFiles['boot-initrd'] && bootFiles['root-tgz']) {
|
|
1273
|
+
kernelFilesPaths = {
|
|
1274
|
+
'vmlinuz-efi': `${kernelPath}/${bootFiles['boot-kernel'].filename_on_disk}`,
|
|
1275
|
+
'initrd.img': `${kernelPath}/${bootFiles['boot-initrd'].filename_on_disk}`,
|
|
1276
|
+
squashfs: `${kernelPath}/${bootFiles['root-tgz'].filename_on_disk}`,
|
|
1277
|
+
};
|
|
1278
|
+
}
|
|
1279
|
+
// Try alternative uploaded structure with root-image-xz
|
|
1280
|
+
else if (bootFiles['boot-kernel'] && bootFiles['boot-initrd'] && bootFiles['root-image-xz']) {
|
|
1281
|
+
kernelFilesPaths = {
|
|
1282
|
+
'vmlinuz-efi': `${kernelPath}/${bootFiles['boot-kernel'].filename_on_disk}`,
|
|
1283
|
+
'initrd.img': `${kernelPath}/${bootFiles['boot-initrd'].filename_on_disk}`,
|
|
1284
|
+
squashfs: `${kernelPath}/${bootFiles['root-image-xz'].filename_on_disk}`,
|
|
1285
|
+
};
|
|
1286
|
+
}
|
|
1287
|
+
// Fallback: try to find any kernel, initrd, and root image
|
|
1288
|
+
else {
|
|
1289
|
+
logger.warn('Non-standard boot file structure detected. Available files', Object.keys(bootFiles));
|
|
1290
|
+
|
|
1291
|
+
const rootArchiveKey = Object.keys(bootFiles).find(
|
|
1292
|
+
(k) => k.includes('root') && (k.includes('tgz') || k.includes('tar.gz')),
|
|
1293
|
+
);
|
|
1294
|
+
const explicitKernel = Object.keys(bootFiles).find((k) => k.includes('kernel'));
|
|
1295
|
+
const explicitInitrd = Object.keys(bootFiles).find((k) => k.includes('initrd'));
|
|
1296
|
+
|
|
1297
|
+
if (rootArchiveKey && (!explicitKernel || !explicitInitrd)) {
|
|
1298
|
+
logger.info(`Root archive found (${rootArchiveKey}) and missing kernel/initrd. Attempting to extract.`);
|
|
1299
|
+
const rootArchivePath = `${kernelPath}/${bootFiles[rootArchiveKey].filename_on_disk}`;
|
|
1300
|
+
const tempExtractDir = `/tmp/maas-extract-${resource.id}`;
|
|
1301
|
+
shellExec(`mkdir -p ${tempExtractDir}`);
|
|
1302
|
+
|
|
1303
|
+
// List files in archive to find kernel and initrd
|
|
1304
|
+
const tarList = shellExec(`tar -tf ${rootArchivePath}`, { silent: true }).stdout.split('\n');
|
|
1305
|
+
|
|
1306
|
+
// Look for boot/vmlinuz* and boot/initrd* (handling potential leading ./)
|
|
1307
|
+
// Skip rescue, kdump, and other special images
|
|
1308
|
+
const vmlinuzPaths = tarList.filter(
|
|
1309
|
+
(f) => f.match(/(\.\/)?boot\/vmlinuz-[0-9]/) && !f.includes('rescue') && !f.includes('kdump'),
|
|
577
1310
|
);
|
|
578
|
-
const
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
};
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
//
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
'acregmax=0',
|
|
613
|
-
'acdirmin=0',
|
|
614
|
-
'acdirmax=0',
|
|
615
|
-
'noac',
|
|
616
|
-
// 'nodev',
|
|
617
|
-
// 'nosuid',
|
|
618
|
-
]}`,
|
|
619
|
-
`ip=${ipAddress}:${callbackMetaData.runnerHost.ip}:${callbackMetaData.runnerHost.ip}:${netmask}:${hostname}:${networkInterfaceName}:static`,
|
|
620
|
-
`rootfstype=nfs`,
|
|
621
|
-
`rw`,
|
|
622
|
-
`rootwait`,
|
|
623
|
-
`fixrtc`,
|
|
624
|
-
'initrd=initrd.img',
|
|
625
|
-
// 'boot=casper',
|
|
626
|
-
// 'ro',
|
|
627
|
-
'netboot=nfs',
|
|
628
|
-
`init=/sbin/init`,
|
|
629
|
-
// `cloud-config-url=/dev/null`,
|
|
630
|
-
// 'ip=dhcp',
|
|
631
|
-
// 'ip=dfcp',
|
|
632
|
-
// 'autoinstall',
|
|
633
|
-
// 'rd.break',
|
|
634
|
-
|
|
635
|
-
// Disable services that not apply over nfs
|
|
636
|
-
`systemd.mask=systemd-network-generator.service`,
|
|
637
|
-
`systemd.mask=systemd-networkd.service`,
|
|
638
|
-
`systemd.mask=systemd-fsck-root.service`,
|
|
639
|
-
`systemd.mask=systemd-udev-trigger.service`,
|
|
640
|
-
];
|
|
641
|
-
const nfsConnectStr = cmd.join(' ');
|
|
642
|
-
|
|
643
|
-
// Copy EFI bootloaders to TFTP path.
|
|
644
|
-
for (const file of ['bootaa64.efi', 'grubaa64.efi']) {
|
|
645
|
-
shellExec(`sudo cp -a ${resourcesPath}/${file} ${tftpRootPath}/pxe/${file}`);
|
|
1311
|
+
const initrdPaths = tarList.filter(
|
|
1312
|
+
(f) =>
|
|
1313
|
+
f.match(/(\.\/)?boot\/(initrd|initramfs)-[0-9]/) &&
|
|
1314
|
+
!f.includes('rescue') &&
|
|
1315
|
+
!f.includes('kdump') &&
|
|
1316
|
+
f.includes('.img'),
|
|
1317
|
+
);
|
|
1318
|
+
|
|
1319
|
+
logger.info(`Found kernel candidates:`, { vmlinuzPaths, initrdPaths });
|
|
1320
|
+
|
|
1321
|
+
// Try to match kernel and initrd by version number
|
|
1322
|
+
let vmlinuzPath = null;
|
|
1323
|
+
let initrdPath = null;
|
|
1324
|
+
|
|
1325
|
+
if (vmlinuzPaths.length > 0 && initrdPaths.length > 0) {
|
|
1326
|
+
// Extract version from kernel filename (e.g., "5.14.0-611.11.1.el9_7.aarch64")
|
|
1327
|
+
for (const kernelPath of vmlinuzPaths.sort().reverse()) {
|
|
1328
|
+
const kernelVersion = kernelPath.match(/vmlinuz-(.+)$/)?.[1];
|
|
1329
|
+
if (kernelVersion) {
|
|
1330
|
+
// Look for matching initrd
|
|
1331
|
+
const matchingInitrd = initrdPaths.find((p) => p.includes(kernelVersion));
|
|
1332
|
+
if (matchingInitrd) {
|
|
1333
|
+
vmlinuzPath = kernelPath;
|
|
1334
|
+
initrdPath = matchingInitrd;
|
|
1335
|
+
logger.info(`Matched kernel and initrd by version: ${kernelVersion}`);
|
|
1336
|
+
break;
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
// Fallback: use newest versions if no match found
|
|
1343
|
+
if (!vmlinuzPath && vmlinuzPaths.length > 0) {
|
|
1344
|
+
vmlinuzPath = vmlinuzPaths.sort().pop();
|
|
646
1345
|
}
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
shellExec(`sudo cp -a ${kernelFilesPaths[file]} ${tftpRootPath}/pxe/${file}`);
|
|
1346
|
+
if (!initrdPath && initrdPaths.length > 0) {
|
|
1347
|
+
initrdPath = initrdPaths.sort().pop();
|
|
650
1348
|
}
|
|
651
1349
|
|
|
652
|
-
|
|
653
|
-
fs.writeFileSync(
|
|
654
|
-
`${process.env.TFTP_ROOT}/grub/grub.cfg`,
|
|
655
|
-
`
|
|
656
|
-
insmod gzio
|
|
657
|
-
insmod http
|
|
658
|
-
insmod nfs
|
|
659
|
-
set timeout=5
|
|
660
|
-
set default=0
|
|
661
|
-
|
|
662
|
-
menuentry '${menuentryStr}' {
|
|
663
|
-
set root=(tftp,${callbackMetaData.runnerHost.ip})
|
|
664
|
-
linux /${hostname}/pxe/vmlinuz-efi ${nfsConnectStr}
|
|
665
|
-
initrd /${hostname}/pxe/initrd.img
|
|
666
|
-
boot
|
|
667
|
-
}
|
|
1350
|
+
logger.info(`Selected kernel: ${vmlinuzPath}, initrd: ${initrdPath}`);
|
|
668
1351
|
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
1352
|
+
if (vmlinuzPath && initrdPath) {
|
|
1353
|
+
// Extract specific files
|
|
1354
|
+
// Extract all files in boot/ to ensure symlinks resolve
|
|
1355
|
+
shellExec(`tar -xf ${rootArchivePath} -C ${tempExtractDir} --wildcards '*boot/*'`);
|
|
1356
|
+
|
|
1357
|
+
kernelFilesPaths = {
|
|
1358
|
+
'vmlinuz-efi': `${tempExtractDir}/${vmlinuzPath}`,
|
|
1359
|
+
'initrd.img': `${tempExtractDir}/${initrdPath}`,
|
|
1360
|
+
squashfs: rootArchivePath,
|
|
1361
|
+
};
|
|
1362
|
+
logger.info('Extracted kernel and initrd from root archive.');
|
|
1363
|
+
} else {
|
|
1364
|
+
logger.error(
|
|
1365
|
+
`Failed to find kernel/initrd in archive. Contents of boot/ directory:`,
|
|
1366
|
+
tarList.filter((f) => f.includes('boot/')),
|
|
1367
|
+
);
|
|
1368
|
+
throw new Error(`Could not find kernel or initrd in ${rootArchiveKey}`);
|
|
1369
|
+
}
|
|
1370
|
+
} else {
|
|
1371
|
+
const kernelFile = Object.keys(bootFiles).find((k) => k.includes('kernel')) || Object.keys(bootFiles)[0];
|
|
1372
|
+
const initrdFile = Object.keys(bootFiles).find((k) => k.includes('initrd')) || Object.keys(bootFiles)[1];
|
|
1373
|
+
const rootFile =
|
|
1374
|
+
Object.keys(bootFiles).find(
|
|
1375
|
+
(k) => k.includes('root') || k.includes('squashfs') || k.includes('tgz') || k.includes('xz'),
|
|
1376
|
+
) || Object.keys(bootFiles)[2];
|
|
1377
|
+
|
|
1378
|
+
if (kernelFile && initrdFile && rootFile) {
|
|
1379
|
+
kernelFilesPaths = {
|
|
1380
|
+
'vmlinuz-efi': `${kernelPath}/${bootFiles[kernelFile].filename_on_disk}`,
|
|
1381
|
+
'initrd.img': `${kernelPath}/${bootFiles[initrdFile].filename_on_disk}`,
|
|
1382
|
+
squashfs: `${kernelPath}/${bootFiles[rootFile].filename_on_disk}`,
|
|
1383
|
+
};
|
|
1384
|
+
logger.info('Using detected files', { kernel: kernelFile, initrd: initrdFile, root: rootFile });
|
|
1385
|
+
} else {
|
|
1386
|
+
throw new Error(`Cannot identify boot files. Available: ${Object.keys(bootFiles).join(', ')}`);
|
|
1387
|
+
}
|
|
672
1388
|
}
|
|
1389
|
+
}
|
|
1390
|
+
return {
|
|
1391
|
+
resource,
|
|
1392
|
+
bootFiles,
|
|
1393
|
+
arch,
|
|
1394
|
+
resourcesPath,
|
|
1395
|
+
kernelPath,
|
|
1396
|
+
resourceData,
|
|
1397
|
+
kernelFilesPaths,
|
|
1398
|
+
};
|
|
1399
|
+
},
|
|
673
1400
|
|
|
1401
|
+
/**
|
|
1402
|
+
* @method writeGrubConfigToFile
|
|
1403
|
+
* @description Writes the GRUB configuration content to the grub.cfg file in the TFTP root.
|
|
1404
|
+
* @param {object} params - Parameters for the method.
|
|
1405
|
+
* @param {string} params.grubCfgSrc - The GRUB configuration content to write.
|
|
1406
|
+
* @memberof UnderpostBaremetal
|
|
1407
|
+
* @returns {void}
|
|
1408
|
+
*/
|
|
1409
|
+
writeGrubConfigToFile({ grubCfgSrc = '' }) {
|
|
1410
|
+
shellExec(`mkdir -p ${process.env.TFTP_ROOT}/grub`, {
|
|
1411
|
+
disableLog: true,
|
|
1412
|
+
});
|
|
1413
|
+
return fs.writeFileSync(`${process.env.TFTP_ROOT}/grub/grub.cfg`, grubCfgSrc, 'utf8');
|
|
1414
|
+
},
|
|
1415
|
+
|
|
1416
|
+
/**
|
|
1417
|
+
* @method getGrubConfigFromFile
|
|
1418
|
+
* @description Reads the GRUB configuration content from the grub.cfg file in the TFTP root.
|
|
1419
|
+
* @memberof UnderpostBaremetal
|
|
1420
|
+
* @returns {string} The GRUB configuration content.
|
|
1421
|
+
*/
|
|
1422
|
+
getGrubConfigFromFile() {
|
|
1423
|
+
const grubCfgPath = `${process.env.TFTP_ROOT}/grub/grub.cfg`;
|
|
1424
|
+
const grubCfgSrc = fs.readFileSync(grubCfgPath, 'utf8');
|
|
1425
|
+
return { grubCfgPath, grubCfgSrc };
|
|
1426
|
+
},
|
|
1427
|
+
|
|
1428
|
+
/**
|
|
1429
|
+
* @method removeDiscoveredMachines
|
|
1430
|
+
* @description Removes all machines in the 'discovered' status from MAAS.
|
|
1431
|
+
* @memberof UnderpostBaremetal
|
|
1432
|
+
* @returns {void}
|
|
1433
|
+
*/
|
|
1434
|
+
removeDiscoveredMachines() {
|
|
1435
|
+
logger.info('Removing all discovered machines from MAAS...');
|
|
1436
|
+
shellExec(`maas ${process.env.MAAS_ADMIN_USERNAME} discoveries clear all=true`);
|
|
1437
|
+
},
|
|
1438
|
+
|
|
1439
|
+
/**
|
|
1440
|
+
* @method efiGrubModulesFactory
|
|
1441
|
+
* @description Copies the appropriate EFI GRUB modules to the TFTP root based on the image architecture.
|
|
1442
|
+
* @param {object} options - Options for determining which GRUB modules to copy.
|
|
1443
|
+
* @param {object} options.image - Image configuration object.
|
|
1444
|
+
* @param {string} options.image.architecture - The architecture of the image ('amd64' or 'arm64').
|
|
1445
|
+
* @memberof UnderpostBaremetal
|
|
1446
|
+
* @returns {void}
|
|
1447
|
+
*/
|
|
1448
|
+
efiGrubModulesFactory(options = { image: { architecture: 'amd64' } }) {
|
|
1449
|
+
if (options.image.architecture.match('arm64')) {
|
|
674
1450
|
// Copy ARM64 EFI GRUB modules.
|
|
675
1451
|
const arm64EfiPath = `${process.env.TFTP_ROOT}/grub/arm64-efi`;
|
|
676
1452
|
if (fs.existsSync(arm64EfiPath)) shellExec(`sudo rm -rf ${arm64EfiPath}`);
|
|
677
1453
|
shellExec(`sudo cp -a /usr/lib/grub/arm64-efi ${arm64EfiPath}`);
|
|
678
|
-
|
|
679
|
-
//
|
|
680
|
-
|
|
681
|
-
shellExec(`sudo
|
|
1454
|
+
} else {
|
|
1455
|
+
// Copy AMD64 EFI GRUB modules.
|
|
1456
|
+
const amd64EfiPath = `${process.env.TFTP_ROOT}/grub/x86_64-efi`;
|
|
1457
|
+
if (fs.existsSync(amd64EfiPath)) shellExec(`sudo rm -rf ${amd64EfiPath}`);
|
|
1458
|
+
shellExec(`sudo cp -a /usr/lib/grub/x86_64-efi ${amd64EfiPath}`);
|
|
682
1459
|
}
|
|
1460
|
+
},
|
|
683
1461
|
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
1462
|
+
/**
|
|
1463
|
+
* @method ipxeEmbeddedScriptFactory
|
|
1464
|
+
* @description Generates the embedded iPXE boot script that performs DHCP and chains to the main script.
|
|
1465
|
+
* This script is embedded into the iPXE binary or loaded first by GRUB.
|
|
1466
|
+
* Supports MAC address spoofing for baremetal commissioning workflows.
|
|
1467
|
+
* @param {object} params - The parameters for generating the script.
|
|
1468
|
+
* @param {string} params.tftpServer - The IP address of the TFTP server.
|
|
1469
|
+
* @param {string} params.scriptPath - The path to the main iPXE script on TFTP server.
|
|
1470
|
+
* @param {string} [params.macAddress] - Optional MAC address to spoof for MAAS registration.
|
|
1471
|
+
* @returns {string} The embedded iPXE script content.
|
|
1472
|
+
* @memberof UnderpostBaremetal
|
|
1473
|
+
*/
|
|
1474
|
+
ipxeEmbeddedScriptFactory({ tftpServer, scriptPath, macAddress = null }) {
|
|
1475
|
+
const macSpoofingBlock =
|
|
1476
|
+
macAddress && macAddress !== '00:00:00:00:00:00'
|
|
1477
|
+
? `
|
|
1478
|
+
# MAC Address Information
|
|
1479
|
+
echo ========================================
|
|
1480
|
+
echo Target MAC for MAAS: ${macAddress}
|
|
1481
|
+
echo Hardware MAC: \${net0/mac}
|
|
1482
|
+
echo ========================================
|
|
1483
|
+
echo NOTE: iPXE MAC spoofing does NOT persist to kernel
|
|
1484
|
+
echo The kernel will receive MAC via ifname= parameter
|
|
1485
|
+
echo ========================================
|
|
1486
|
+
`
|
|
1487
|
+
: `
|
|
1488
|
+
# Using hardware MAC address
|
|
1489
|
+
echo ========================================
|
|
1490
|
+
echo Using device hardware MAC address
|
|
1491
|
+
echo Hardware MAC: \${net0/mac}
|
|
1492
|
+
echo MAC spoofing disabled - device uses actual MAC
|
|
1493
|
+
echo ========================================
|
|
1494
|
+
`;
|
|
1495
|
+
|
|
1496
|
+
return `#!ipxe
|
|
1497
|
+
# Embedded iPXE Boot Script
|
|
1498
|
+
# This script performs DHCP configuration and chains to the main boot script
|
|
1499
|
+
|
|
1500
|
+
echo ========================================
|
|
1501
|
+
echo iPXE Embedded Boot Loader
|
|
1502
|
+
echo ========================================
|
|
1503
|
+
echo TFTP Server: ${tftpServer}
|
|
1504
|
+
echo Script Path: ${scriptPath}
|
|
1505
|
+
echo ========================================
|
|
1506
|
+
|
|
1507
|
+
${macSpoofingBlock}
|
|
1508
|
+
|
|
1509
|
+
# Show network interface info before DHCP
|
|
1510
|
+
echo Network Interface Info (before DHCP):
|
|
1511
|
+
echo Interface: net0
|
|
1512
|
+
echo MAC: \${net0/mac}
|
|
1513
|
+
ifstat
|
|
1514
|
+
|
|
1515
|
+
# Perform DHCP to get network configuration
|
|
1516
|
+
echo ========================================
|
|
1517
|
+
echo Performing DHCP configuration...
|
|
1518
|
+
echo ========================================
|
|
1519
|
+
dhcp net0 || goto dhcp_retry
|
|
1520
|
+
|
|
1521
|
+
echo DHCP configuration successful
|
|
1522
|
+
echo IP Address: \${net0/ip}
|
|
1523
|
+
echo Netmask: \${net0/netmask}
|
|
1524
|
+
echo Gateway: \${net0/gateway}
|
|
1525
|
+
echo DNS: \${net0/dns}
|
|
1526
|
+
echo TFTP Server: \${next-server}
|
|
1527
|
+
echo MAC used by DHCP: \${net0/mac}
|
|
1528
|
+
|
|
1529
|
+
# Chain to the main iPXE script
|
|
1530
|
+
echo ========================================
|
|
1531
|
+
echo Chainloading main boot script...
|
|
1532
|
+
echo Script: tftp://${tftpServer}${scriptPath}
|
|
1533
|
+
echo ========================================
|
|
1534
|
+
chain tftp://${tftpServer}${scriptPath} || goto chain_failed
|
|
1535
|
+
|
|
1536
|
+
:dhcp_retry
|
|
1537
|
+
echo DHCP failed, retrying in 5 seconds...
|
|
1538
|
+
sleep 5
|
|
1539
|
+
dhcp net0 || goto dhcp_retry
|
|
1540
|
+
goto dhcp_success
|
|
1541
|
+
|
|
1542
|
+
:dhcp_success
|
|
1543
|
+
echo DHCP retry successful
|
|
1544
|
+
echo IP Address: \${net0/ip}
|
|
1545
|
+
echo MAC: \${net0/mac}
|
|
1546
|
+
chain tftp://${tftpServer}${scriptPath} || goto chain_failed
|
|
1547
|
+
|
|
1548
|
+
:chain_failed
|
|
1549
|
+
echo ========================================
|
|
1550
|
+
echo ERROR: Failed to chain to main script
|
|
1551
|
+
echo TFTP Server: ${tftpServer}
|
|
1552
|
+
echo Script Path: ${scriptPath}
|
|
1553
|
+
echo ========================================
|
|
1554
|
+
echo Retrying in 10 seconds...
|
|
1555
|
+
sleep 10
|
|
1556
|
+
chain tftp://${tftpServer}${scriptPath} || goto shell_debug
|
|
1557
|
+
|
|
1558
|
+
:shell_debug
|
|
1559
|
+
echo Dropping to iPXE shell for manual debugging
|
|
1560
|
+
echo Try: chain tftp://${tftpServer}${scriptPath}
|
|
1561
|
+
shell
|
|
1562
|
+
`;
|
|
1563
|
+
},
|
|
688
1564
|
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
1565
|
+
/**
|
|
1566
|
+
* @method ipxeScriptFactory
|
|
1567
|
+
* @description Generates the iPXE script content for stable identity.
|
|
1568
|
+
* This iPXE script uses directly boots kernel/initrd via TFTP.
|
|
1569
|
+
* @param {object} params - The parameters for generating the script.
|
|
1570
|
+
* @param {string} params.maasIp - The IP address of the MAAS server.
|
|
1571
|
+
* @param {string} [params.macAddress] - The MAC address registered in MAAS (for display only).
|
|
1572
|
+
* @param {string} params.architecture - The architecture (arm64/amd64).
|
|
1573
|
+
* @param {string} params.tftpPrefix - The TFTP prefix path (e.g., 'rpi4mb').
|
|
1574
|
+
* @param {string} params.kernelCmd - The kernel command line parameters.
|
|
1575
|
+
* @returns {string} The iPXE script content.
|
|
1576
|
+
* @memberof UnderpostBaremetal
|
|
1577
|
+
*/
|
|
1578
|
+
ipxeScriptFactory({ maasIp, macAddress, architecture, tftpPrefix, kernelCmd }) {
|
|
1579
|
+
const macInfo =
|
|
1580
|
+
macAddress && macAddress !== '00:00:00:00:00:00'
|
|
1581
|
+
? `echo Registered MAC: ${macAddress}`
|
|
1582
|
+
: `echo Using hardware MAC address`;
|
|
1583
|
+
|
|
1584
|
+
// Construct the full TFTP paths for kernel and initrd
|
|
1585
|
+
const kernelPath = `${tftpPrefix}/pxe/vmlinuz-efi`;
|
|
1586
|
+
const initrdPath = `${tftpPrefix}/pxe/initrd.img`;
|
|
1587
|
+
const grubBootloader = architecture === 'arm64' ? 'grubaa64.efi' : 'grubx64.efi';
|
|
1588
|
+
const grubPath = `${tftpPrefix}/pxe/${grubBootloader}`;
|
|
1589
|
+
|
|
1590
|
+
return `#!ipxe
|
|
1591
|
+
echo ========================================
|
|
1592
|
+
echo iPXE Network Boot
|
|
1593
|
+
echo ========================================
|
|
1594
|
+
echo MAAS Server: ${maasIp}
|
|
1595
|
+
echo Architecture: ${architecture}
|
|
1596
|
+
${macInfo}
|
|
1597
|
+
echo ========================================
|
|
1598
|
+
|
|
1599
|
+
# Show current network configuration
|
|
1600
|
+
echo Current Network Configuration:
|
|
1601
|
+
ifstat
|
|
1602
|
+
|
|
1603
|
+
# Display MAC address information
|
|
1604
|
+
echo MAC Address: \${net0/mac}
|
|
1605
|
+
echo IP Address: \${net0/ip}
|
|
1606
|
+
echo Gateway: \${net0/gateway}
|
|
1607
|
+
echo DNS: \${net0/dns}
|
|
1608
|
+
${macAddress && macAddress !== '00:00:00:00:00:00' ? `echo Target MAC for kernel: ${macAddress}` : ''}
|
|
1609
|
+
|
|
1610
|
+
# Direct kernel/initrd boot via TFTP
|
|
1611
|
+
# Modern simplified approach: Direct kernel/initrd boot via TFTP
|
|
1612
|
+
echo ========================================
|
|
1613
|
+
echo Loading kernel and initrd via TFTP...
|
|
1614
|
+
echo Kernel: tftp://${maasIp}/${kernelPath}
|
|
1615
|
+
echo Initrd: tftp://${maasIp}/${initrdPath}
|
|
1616
|
+
${macAddress && macAddress !== '00:00:00:00:00:00' ? `echo Kernel will use MAC: ${macAddress} (via ifname parameter)` : 'echo Kernel will use hardware MAC'}
|
|
1617
|
+
echo ========================================
|
|
1618
|
+
|
|
1619
|
+
# Load kernel via TFTP
|
|
1620
|
+
kernel tftp://${maasIp}/${kernelPath} ${kernelCmd || 'console=ttyS0,115200'} || goto grub_fallback
|
|
1621
|
+
echo Kernel loaded successfully
|
|
1622
|
+
|
|
1623
|
+
# Load initrd via TFTP
|
|
1624
|
+
initrd tftp://${maasIp}/${initrdPath} || goto grub_fallback
|
|
1625
|
+
echo Initrd loaded successfully
|
|
1626
|
+
|
|
1627
|
+
# Boot the kernel
|
|
1628
|
+
echo Booting kernel...
|
|
1629
|
+
boot
|
|
1630
|
+
|
|
1631
|
+
:grub_fallback
|
|
1632
|
+
echo ========================================
|
|
1633
|
+
echo Direct kernel boot failed, falling back to GRUB chainload...
|
|
1634
|
+
echo TFTP Path: tftp://${maasIp}/${grubPath}
|
|
1635
|
+
echo ========================================
|
|
1636
|
+
|
|
1637
|
+
# Fallback: Chain to GRUB via TFTP (avoids malformed HTTP bootloader issues)
|
|
1638
|
+
chain tftp://${maasIp}/${grubPath} || goto http_fallback
|
|
1639
|
+
|
|
1640
|
+
:http_fallback
|
|
1641
|
+
echo TFTP GRUB chainload failed, trying HTTP fallback...
|
|
1642
|
+
echo ========================================
|
|
1643
|
+
|
|
1644
|
+
# Fallback: Try MAAS HTTP bootloader (may have certificate issues)
|
|
1645
|
+
set boot-url http://${maasIp}:5248/images/bootloader
|
|
1646
|
+
echo Boot URL: \${boot-url}
|
|
1647
|
+
chain \${boot-url}/uefi/${architecture}/${grubBootloader === 'grubaa64.efi' ? 'bootaa64.efi' : 'bootx64.efi'} || goto error
|
|
1648
|
+
|
|
1649
|
+
:error
|
|
1650
|
+
echo ========================================
|
|
1651
|
+
echo ERROR: All boot methods failed
|
|
1652
|
+
echo ========================================
|
|
1653
|
+
echo MAAS IP: ${maasIp}
|
|
1654
|
+
echo Architecture: ${architecture}
|
|
1655
|
+
echo MAC: \${net0/mac}
|
|
1656
|
+
echo IP: \${net0/ip}
|
|
1657
|
+
echo ========================================
|
|
1658
|
+
echo Retrying GRUB TFTP in 10 seconds...
|
|
1659
|
+
sleep 10
|
|
1660
|
+
chain tftp://${maasIp}/${grubPath} || goto shell_debug
|
|
1661
|
+
|
|
1662
|
+
:shell_debug
|
|
1663
|
+
echo Dropping to iPXE shell for manual intervention
|
|
1664
|
+
shell
|
|
1665
|
+
`;
|
|
1666
|
+
},
|
|
697
1667
|
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
1668
|
+
/**
|
|
1669
|
+
* @method grubFactory
|
|
1670
|
+
* @description Generates the GRUB configuration file content.
|
|
1671
|
+
* @param {object} params - The parameters for generating the configuration.
|
|
1672
|
+
* @param {string} params.menuentryStr - The title of the menu entry.
|
|
1673
|
+
* @param {string} params.kernelPath - The path to the kernel file (relative to TFTP root).
|
|
1674
|
+
* @param {string} params.initrdPath - The path to the initrd file (relative to TFTP root).
|
|
1675
|
+
* @param {string} params.cmd - The kernel command line parameters.
|
|
1676
|
+
* @param {string} params.tftpIp - The IP address of the TFTP server.
|
|
1677
|
+
* @param {boolean} [params.ipxe] - Flag to enable iPXE chainloading.
|
|
1678
|
+
* @param {string} [params.ipxePath] - The path to the iPXE binary.
|
|
1679
|
+
* @returns {object} An object containing 'grubCfgSrc' the GRUB configuration source string.
|
|
1680
|
+
* @memberof UnderpostBaremetal
|
|
1681
|
+
*/
|
|
1682
|
+
grubFactory({ menuentryStr, kernelPath, initrdPath, cmd, tftpIp, ipxe, ipxePath }) {
|
|
1683
|
+
if (ipxe) {
|
|
1684
|
+
return {
|
|
1685
|
+
grubCfgSrc: `
|
|
1686
|
+
set default="0"
|
|
1687
|
+
set timeout=10
|
|
1688
|
+
insmod tftp
|
|
1689
|
+
set root=(tftp,${tftpIp})
|
|
1690
|
+
|
|
1691
|
+
menuentry 'iPXE ${menuentryStr}' {
|
|
1692
|
+
echo "Loading iPXE with embedded script..."
|
|
1693
|
+
echo "[INFO] TFTP Server: ${tftpIp}"
|
|
1694
|
+
echo "[INFO] iPXE Binary: ${ipxePath}"
|
|
1695
|
+
echo "[INFO] iPXE will execute embedded script (dhcp + chain)"
|
|
1696
|
+
chainloader ${ipxePath}
|
|
1697
|
+
boot
|
|
1698
|
+
}
|
|
1699
|
+
`,
|
|
1700
|
+
};
|
|
1701
|
+
}
|
|
1702
|
+
return {
|
|
1703
|
+
grubCfgSrc: `
|
|
1704
|
+
set default="0"
|
|
1705
|
+
set timeout=10
|
|
1706
|
+
insmod nfs
|
|
1707
|
+
insmod gzio
|
|
1708
|
+
insmod http
|
|
1709
|
+
insmod tftp
|
|
1710
|
+
set root=(tftp,${tftpIp})
|
|
1711
|
+
|
|
1712
|
+
menuentry '${menuentryStr}' {
|
|
1713
|
+
echo "${menuentryStr}"
|
|
1714
|
+
echo " ${Underpost.version}"
|
|
1715
|
+
echo "Date: ${new Date().toISOString()}"
|
|
1716
|
+
${cmd.match('/MAAS/metadata/by-id/') ? `echo "System ID: ${cmd.split('/MAAS/metadata/by-id/')[1].split('/')[0]}"` : ''}
|
|
1717
|
+
echo "TFTP server: ${tftpIp}"
|
|
1718
|
+
echo "Kernel path: ${kernelPath}"
|
|
1719
|
+
echo "Initrd path: ${initrdPath}"
|
|
1720
|
+
echo "Starting boot process..."
|
|
1721
|
+
echo "Loading kernel..."
|
|
1722
|
+
linux /${kernelPath} ${cmd}
|
|
1723
|
+
echo "Loading initrd..."
|
|
1724
|
+
initrd /${initrdPath}
|
|
1725
|
+
echo "Booting..."
|
|
1726
|
+
boot
|
|
1727
|
+
}
|
|
1728
|
+
`,
|
|
1729
|
+
};
|
|
1730
|
+
},
|
|
718
1731
|
|
|
719
|
-
|
|
1732
|
+
/**
|
|
1733
|
+
* @method httpBootstrapServerRunnerFactory
|
|
1734
|
+
* @description Starts a simple HTTP server to serve boot files for network booting.
|
|
1735
|
+
* @param {object} options - Options for the HTTP server.
|
|
1736
|
+
* @param {string} hostname - The hostname of the client machine.
|
|
1737
|
+
* @param {string} options.bootstrapHttpServerPath - The path to serve files from (default: './public/localhost').
|
|
1738
|
+
* @param {number} options.bootstrapHttpServerPort - The port on which to start the HTTP server (default: 8888).
|
|
1739
|
+
* @memberof UnderpostBaremetal
|
|
1740
|
+
* @returns {void}
|
|
1741
|
+
*/
|
|
1742
|
+
httpBootstrapServerRunnerFactory(
|
|
1743
|
+
options = { hostname: 'localhost', bootstrapHttpServerPath: './public/localhost', bootstrapHttpServerPort: 8888 },
|
|
1744
|
+
) {
|
|
1745
|
+
const port = options.bootstrapHttpServerPort || 8888;
|
|
1746
|
+
const bootstrapHttpServerPath = options.bootstrapHttpServerPath || './public/localhost';
|
|
1747
|
+
const hostname = options.hostname || 'localhost';
|
|
720
1748
|
|
|
721
|
-
|
|
722
|
-
shellExec(`${underpostRoot}/scripts/nat-iptables.sh`, { silent: true });
|
|
1749
|
+
shellExec(`mkdir -p ${bootstrapHttpServerPath}/${hostname}/cloud-init`);
|
|
723
1750
|
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
fs.removeSync(`${nfsHostPath}/underpost/mac`); // Clear previous MAC.
|
|
727
|
-
await UnderpostBaremetal.API.macMonitor({ nfsHostPath }); // Monitor for MAC file.
|
|
728
|
-
macAddress = fs.readFileSync(`${nfsHostPath}/underpost/mac`, 'utf8').trim(); // Read assigned MAC.
|
|
1751
|
+
// Kill any existing HTTP server
|
|
1752
|
+
shellExec(`sudo pkill -f 'python3 -m http.server ${port}' || true`, { silent: true });
|
|
729
1753
|
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
callbackMetaData,
|
|
735
|
-
steps: [
|
|
736
|
-
UnderpostCloudInit.API.configFactory({
|
|
737
|
-
controlServerIp: callbackMetaData.runnerHost.ip,
|
|
738
|
-
hostname,
|
|
739
|
-
commissioningDeviceIp: ipAddress,
|
|
740
|
-
gatewayip: callbackMetaData.runnerHost.ip,
|
|
741
|
-
mac: macAddress, // Updated MAC address.
|
|
742
|
-
timezone,
|
|
743
|
-
chronyConfPath,
|
|
744
|
-
networkInterfaceName,
|
|
745
|
-
}),
|
|
746
|
-
],
|
|
747
|
-
});
|
|
1754
|
+
shellExec(
|
|
1755
|
+
`cd ${bootstrapHttpServerPath} && nohup python3 -m http.server ${port} --bind 0.0.0.0 > /tmp/http-boot-server.log 2>&1 &`,
|
|
1756
|
+
{ silent: true, async: true },
|
|
1757
|
+
);
|
|
748
1758
|
|
|
749
|
-
|
|
750
|
-
|
|
1759
|
+
// Configure iptables to allow incoming LAN connections
|
|
1760
|
+
shellExec(
|
|
1761
|
+
`sudo iptables -I INPUT 1 -p tcp -s 192.168.1.0/24 --dport ${port} -m conntrack --ctstate NEW -j ACCEPT`,
|
|
1762
|
+
);
|
|
1763
|
+
// Option for any host:
|
|
1764
|
+
// sudo iptables -I INPUT 1 -p tcp --dport ${port} -m conntrack --ctstate NEW -j ACCEPT
|
|
1765
|
+
shellExec(`sudo chown -R $(whoami):$(whoami) ${bootstrapHttpServerPath}`);
|
|
1766
|
+
shellExec(`sudo sudo chmod 755 ${bootstrapHttpServerPath}`);
|
|
751
1767
|
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
1768
|
+
logger.info(`Started Bootstrap Http Server on port ${port}`);
|
|
1769
|
+
},
|
|
1770
|
+
|
|
1771
|
+
/**
|
|
1772
|
+
* @method updateKernelFiles
|
|
1773
|
+
* @description Copies EFI bootloaders, kernel, and initrd images to the TFTP root path.
|
|
1774
|
+
* It also handles decompression of the kernel if necessary for ARM64 compatibility.
|
|
1775
|
+
* @param {object} params - The parameters for the function.
|
|
1776
|
+
* @param {object} params.commissioningImage - The commissioning image configuration.
|
|
1777
|
+
* @param {string} params.resourcesPath - The path where resources are located.
|
|
1778
|
+
* @param {string} params.tftpRootPath - The TFTP root path.
|
|
1779
|
+
* @param {object} params.kernelFilesPaths - Paths to kernel files.
|
|
1780
|
+
* @memberof UnderpostBaremetal
|
|
1781
|
+
* @returns {void}
|
|
1782
|
+
*/
|
|
1783
|
+
updateKernelFiles({ commissioningImage, resourcesPath, tftpRootPath, kernelFilesPaths }) {
|
|
1784
|
+
// Copy EFI bootloaders to TFTP path.
|
|
1785
|
+
const efiFiles = commissioningImage.architecture.match('arm64')
|
|
1786
|
+
? ['bootaa64.efi', 'grubaa64.efi']
|
|
1787
|
+
: ['bootx64.efi', 'grubx64.efi'];
|
|
1788
|
+
for (const file of efiFiles) {
|
|
1789
|
+
shellExec(`sudo cp -a ${resourcesPath}/${file} ${tftpRootPath}/pxe/${file}`);
|
|
1790
|
+
}
|
|
1791
|
+
// Copy kernel and initrd images to TFTP path.
|
|
1792
|
+
for (const file of Object.keys(kernelFilesPaths)) {
|
|
1793
|
+
if (file == 'isoUrl') continue; // Skip URL entries
|
|
1794
|
+
shellExec(`sudo cp -a ${kernelFilesPaths[file]} ${tftpRootPath}/pxe/${file}`);
|
|
1795
|
+
// If the file is a kernel (vmlinuz-efi) and is gzipped, unzip it for GRUB compatibility on ARM64.
|
|
1796
|
+
// GRUB on ARM64 often crashes with synchronous exception (0x200) if handling large compressed kernels directly.
|
|
1797
|
+
if (file === 'vmlinuz-efi') {
|
|
1798
|
+
const kernelDest = `${tftpRootPath}/pxe/${file}`;
|
|
1799
|
+
const fileType = shellExec(`file ${kernelDest}`, { silent: true }).stdout;
|
|
1800
|
+
if (fileType.includes('gzip compressed data')) {
|
|
1801
|
+
logger.info(`Decompressing kernel ${file} for ARM64 UEFI compatibility...`);
|
|
1802
|
+
shellExec(`sudo mv ${kernelDest} ${kernelDest}.gz`);
|
|
1803
|
+
shellExec(`sudo gunzip ${kernelDest}.gz`);
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
},
|
|
1808
|
+
|
|
1809
|
+
/**
|
|
1810
|
+
* @method kernelCmdBootParamsFactory
|
|
1811
|
+
* @description Constructs kernel command line parameters for NFS booting.
|
|
1812
|
+
* @param {object} options - Options for constructing the command line.
|
|
1813
|
+
* @param {string} options.ipClient - The IP address of the client.
|
|
1814
|
+
* @param {string} options.ipDhcpServer - The IP address of the DHCP server.
|
|
1815
|
+
* @param {string} options.ipFileServer - The IP address of the file server.
|
|
1816
|
+
* @param {string} options.ipConfig - The IP configuration method (e.g., 'dhcp').
|
|
1817
|
+
* @param {string} options.netmask - The network mask.
|
|
1818
|
+
* @param {string} options.hostname - The hostname of the client.
|
|
1819
|
+
* @param {string} options.dnsServer - The DNS server address.
|
|
1820
|
+
* @param {string} options.networkInterfaceName - The name of the network interface.
|
|
1821
|
+
* @param {string} options.fileSystemUrl - The URL of the root filesystem.
|
|
1822
|
+
* @param {number} options.bootstrapHttpServerPort - The port of the bootstrap HTTP server.
|
|
1823
|
+
* @param {string} options.type - The type of boot ('iso-ram', 'chroot', 'iso-nfs', etc.).
|
|
1824
|
+
* @param {string} options.macAddress - The MAC address of the client.
|
|
1825
|
+
* @param {boolean} options.cloudInit - Whether to include cloud-init parameters.
|
|
1826
|
+
* @param {object} options.machine - The machine object containing system_id.
|
|
1827
|
+
* @returns {object} An object containing the constructed command line string.
|
|
1828
|
+
* @memberof UnderpostBaremetal
|
|
1829
|
+
*/
|
|
1830
|
+
kernelCmdBootParamsFactory(
|
|
1831
|
+
options = {
|
|
1832
|
+
ipClient: '',
|
|
1833
|
+
ipDhcpServer: '',
|
|
1834
|
+
ipFileServer: '',
|
|
1835
|
+
ipConfig: '',
|
|
1836
|
+
netmask: '',
|
|
1837
|
+
hostname: '',
|
|
1838
|
+
dnsServer: '',
|
|
1839
|
+
networkInterfaceName: '',
|
|
1840
|
+
fileSystemUrl: '',
|
|
1841
|
+
bootstrapHttpServerPort: 8888,
|
|
1842
|
+
type: '',
|
|
1843
|
+
macAddress: '',
|
|
1844
|
+
cloudInit: false,
|
|
1845
|
+
machine: { system_id: '' },
|
|
1846
|
+
},
|
|
1847
|
+
) {
|
|
1848
|
+
// Construct kernel command line arguments for NFS boot.
|
|
1849
|
+
const {
|
|
1850
|
+
ipClient,
|
|
1851
|
+
ipDhcpServer,
|
|
1852
|
+
ipFileServer,
|
|
1853
|
+
ipConfig,
|
|
1854
|
+
netmask,
|
|
1855
|
+
hostname,
|
|
1856
|
+
dnsServer,
|
|
1857
|
+
networkInterfaceName,
|
|
1858
|
+
fileSystemUrl,
|
|
1859
|
+
bootstrapHttpServerPort,
|
|
1860
|
+
type,
|
|
1861
|
+
macAddress,
|
|
1862
|
+
cloudInit,
|
|
1863
|
+
} = options;
|
|
1864
|
+
|
|
1865
|
+
const ipParam = true
|
|
1866
|
+
? `ip=${ipClient}:${ipFileServer}:${ipDhcpServer}:${netmask}:${hostname}` +
|
|
1867
|
+
`:${networkInterfaceName ? networkInterfaceName : 'eth0'}:${ipConfig}:${dnsServer}`
|
|
1868
|
+
: 'ip=dhcp';
|
|
1869
|
+
|
|
1870
|
+
const nfsOptions = `${
|
|
1871
|
+
type === 'chroot'
|
|
1872
|
+
? [
|
|
1873
|
+
'tcp',
|
|
1874
|
+
'nfsvers=3',
|
|
1875
|
+
'nolock',
|
|
1876
|
+
'vers=3',
|
|
1877
|
+
// 'protocol=tcp',
|
|
1878
|
+
// 'hard=true',
|
|
1879
|
+
'port=2049',
|
|
1880
|
+
// 'sec=none',
|
|
1881
|
+
'hard',
|
|
1882
|
+
'intr',
|
|
1883
|
+
'rsize=32768',
|
|
1884
|
+
'wsize=32768',
|
|
1885
|
+
'acregmin=0',
|
|
1886
|
+
'acregmax=0',
|
|
1887
|
+
'acdirmin=0',
|
|
1888
|
+
'acdirmax=0',
|
|
1889
|
+
'noac',
|
|
1890
|
+
// 'nodev',
|
|
1891
|
+
// 'nosuid',
|
|
1892
|
+
]
|
|
1893
|
+
: []
|
|
1894
|
+
}`;
|
|
1895
|
+
|
|
1896
|
+
const nfsRootParam = `nfsroot=${ipFileServer}:${process.env.NFS_EXPORT_PATH}/${hostname}${nfsOptions ? `,${nfsOptions}` : ''}`;
|
|
1897
|
+
|
|
1898
|
+
const permissionsParams = [
|
|
1899
|
+
`rw`,
|
|
1900
|
+
// `ro`
|
|
1901
|
+
];
|
|
1902
|
+
|
|
1903
|
+
const kernelParams = [
|
|
1904
|
+
...permissionsParams,
|
|
1905
|
+
`ignore_uuid`,
|
|
1906
|
+
`rootwait`,
|
|
1907
|
+
`ipv6.disable=1`,
|
|
1908
|
+
`fixrtc`,
|
|
1909
|
+
// `console=serial0,115200`,
|
|
1910
|
+
// `console=tty1`,
|
|
1911
|
+
// `layerfs-path=filesystem.squashfs`,
|
|
1912
|
+
// `root=/dev/ram0`,
|
|
1913
|
+
// `toram`,
|
|
1914
|
+
'nomodeset',
|
|
1915
|
+
`editable_rootfs=tmpfs`,
|
|
1916
|
+
`ramdisk_size=3550000`,
|
|
1917
|
+
// `root=/dev/sda1`, // rpi4 usb port unit
|
|
1918
|
+
'apparmor=0', // Disable AppArmor security
|
|
1919
|
+
...(networkInterfaceName === 'eth0'
|
|
1920
|
+
? [
|
|
1921
|
+
'net.ifnames=0', // Disable predictable network interface names
|
|
1922
|
+
'biosdevname=0', // Disable BIOS device naming
|
|
1923
|
+
]
|
|
1924
|
+
: []),
|
|
1925
|
+
];
|
|
1926
|
+
|
|
1927
|
+
const performanceParams = [
|
|
1928
|
+
// --- Boot Automation & Stability ---
|
|
1929
|
+
'auto=true', // Enable automated installation/configuration
|
|
1930
|
+
'noeject', // Do not attempt to eject boot media on reboot
|
|
1931
|
+
`casper-getty`, // Enable console login for live sessions
|
|
1932
|
+
'nowatchdog', // Disable watchdog timers to prevent unexpected reboots
|
|
1933
|
+
'noprompt', // Don't wait for "Press Enter" during boot/reboot
|
|
1934
|
+
|
|
1935
|
+
// --- CPU & System Performance ---
|
|
1936
|
+
'mitigations=off', // Disable CPU security mitigations for maximum speed
|
|
1937
|
+
'clocksource=tsc', // Use fastest available hardware clock
|
|
1938
|
+
'tsc=reliable', // Trust CPU clock without extra verification
|
|
1939
|
+
'hpet=disable', // Disable legacy slow timer
|
|
1940
|
+
'nohz=on', // Reduce overhead by disabling timer ticks on idle CPUs
|
|
1941
|
+
|
|
1942
|
+
// --- Memory & Hardware Optimization ---
|
|
1943
|
+
'cma=40M', // Reserve contiguous RAM for RPi hardware/video
|
|
1944
|
+
'zswap.enabled=1', // Use compressed RAM cache (vital for NFS/SD)
|
|
1945
|
+
'zswap.compressor=zstd', // Best balance of speed and compression
|
|
1946
|
+
'zswap.max_pool_percent=30', // Use max 30% of RAM as compressed storage
|
|
1947
|
+
'zswap.zpool=zsmalloc', // Efficient memory management for zswap
|
|
1948
|
+
'fsck.mode=skip', // Skip disk checks to accelerate boot
|
|
1949
|
+
'max_loop=255', // Ensure enough loop devices for squashfs/snaps
|
|
1950
|
+
|
|
1951
|
+
// --- Immutable Filesystem ---
|
|
1952
|
+
'overlayroot=tmpfs', // Run entire OS in RAM to protect storage
|
|
1953
|
+
'overlayroot_cfgdisk=disabled', // Ignore external overlay configurations
|
|
1954
|
+
];
|
|
1955
|
+
|
|
1956
|
+
const baseNfsParams = [`netboot=nfs`];
|
|
1957
|
+
|
|
1958
|
+
let cmd = [];
|
|
1959
|
+
if (type === 'iso-ram') {
|
|
1960
|
+
const netBootParams = [`netboot=url`];
|
|
1961
|
+
if (fileSystemUrl) netBootParams.push(`url=${fileSystemUrl.replace('https', 'http')}`);
|
|
1962
|
+
cmd = [ipParam, `boot=casper`, ...netBootParams, ...kernelParams];
|
|
1963
|
+
} else if (type === 'chroot') {
|
|
1964
|
+
const qemuNfsRootParams = [`root=/dev/nfs`, `rootfstype=nfs`, `initrd=initrd.img`, `init=/sbin/init`];
|
|
1965
|
+
cmd = [ipParam, ...baseNfsParams, ...qemuNfsRootParams, nfsRootParam, ...kernelParams];
|
|
1966
|
+
} else {
|
|
1967
|
+
// 'iso-nfs'
|
|
1968
|
+
cmd = [ipParam, ...baseNfsParams, nfsRootParam, ...kernelParams, ...performanceParams];
|
|
1969
|
+
|
|
1970
|
+
cmd.push(`ifname=${networkInterfaceName}:${macAddress}`);
|
|
1971
|
+
|
|
1972
|
+
if (cloudInit) {
|
|
1973
|
+
const cloudInitPreseedUrl = `http://${ipDhcpServer}:5248/MAAS/metadata/by-id/${options.machine?.system_id ? options.machine.system_id : 'system-id'}/?op=get_preseed`;
|
|
1974
|
+
cmd = cmd.concat([
|
|
1975
|
+
`cloud-init=enabled`,
|
|
1976
|
+
'autoinstall',
|
|
1977
|
+
`cloud-config-url=${cloudInitPreseedUrl}`,
|
|
1978
|
+
`ds=nocloud-net;s=${cloudInitPreseedUrl}`,
|
|
1979
|
+
`log_host=${ipDhcpServer}`,
|
|
1980
|
+
`log_port=5247`,
|
|
1981
|
+
// `BOOTIF=${macAddress}`,
|
|
1982
|
+
// `cc:{'datasource_list': ['MAAS']}end_cc`,
|
|
1983
|
+
]);
|
|
1984
|
+
}
|
|
761
1985
|
}
|
|
1986
|
+
// cmd.push('---');
|
|
1987
|
+
const cmdStr = cmd.join(' ');
|
|
1988
|
+
logger.info('Constructed kernel command line');
|
|
1989
|
+
console.log(newInstance(cmdStr).bgRed.bold.black);
|
|
1990
|
+
return { cmd: cmdStr };
|
|
762
1991
|
},
|
|
763
1992
|
|
|
764
1993
|
/**
|
|
@@ -767,25 +1996,15 @@ menuentry '${menuentryStr}' {
|
|
|
767
1996
|
* once a matching MAC address is found. It also opens terminal windows for live logs.
|
|
768
1997
|
* @param {object} params - The parameters for the function.
|
|
769
1998
|
* @param {string} params.macAddress - The MAC address to monitor for.
|
|
770
|
-
* @param {string} params.
|
|
771
|
-
* @param {string} params.
|
|
772
|
-
* @param {
|
|
773
|
-
* @param {object} params.
|
|
774
|
-
* @param {string} params.networkInterfaceName - The name of the network interface.
|
|
1999
|
+
* @param {string} params.ipAddress - The IP address of the machine (used if MAC is all zeros).
|
|
2000
|
+
* @param {string} [params.hostname] - The hostname for the machine (optional).
|
|
2001
|
+
* @param {object} [params.maas] - Additional MAAS-specific options (optional).
|
|
2002
|
+
* @param {object} [params.machine] - Existing machine payload to use (optional).
|
|
775
2003
|
* @returns {Promise<void>} A promise that resolves when commissioning is initiated or after a delay.
|
|
776
2004
|
* @memberof UnderpostBaremetal
|
|
777
2005
|
*/
|
|
778
|
-
async commissionMonitor({ macAddress,
|
|
2006
|
+
async commissionMonitor({ macAddress, ipAddress, hostname, maas, machine }) {
|
|
779
2007
|
{
|
|
780
|
-
logger.info('Waiting for commissioning...', {
|
|
781
|
-
macAddress,
|
|
782
|
-
nfsHostPath,
|
|
783
|
-
underpostRoot,
|
|
784
|
-
hostname,
|
|
785
|
-
maas,
|
|
786
|
-
networkInterfaceName,
|
|
787
|
-
});
|
|
788
|
-
|
|
789
2008
|
// Query observed discoveries from MAAS.
|
|
790
2009
|
const discoveries = JSON.parse(
|
|
791
2010
|
shellExec(`maas ${process.env.MAAS_ADMIN_USERNAME} discoveries read`, {
|
|
@@ -794,131 +2013,95 @@ menuentry '${menuentryStr}' {
|
|
|
794
2013
|
}),
|
|
795
2014
|
);
|
|
796
2015
|
|
|
797
|
-
// {
|
|
798
|
-
// "discovery_id": "",
|
|
799
|
-
// "ip": "192.168.1.189",
|
|
800
|
-
// "mac_address": "00:00:00:00:00:00",
|
|
801
|
-
// "last_seen": "2025-05-05T14:17:37.354",
|
|
802
|
-
// "hostname": null,
|
|
803
|
-
// "fabric_name": "",
|
|
804
|
-
// "vid": null,
|
|
805
|
-
// "mac_organization": "",
|
|
806
|
-
// "observer": {
|
|
807
|
-
// "system_id": "",
|
|
808
|
-
// "hostname": "",
|
|
809
|
-
// "interface_id": 1,
|
|
810
|
-
// "interface_name": ""
|
|
811
|
-
// },
|
|
812
|
-
// "resource_uri": "/MAAS/api/2.0/discovery/MTkyLjE2OC4xLjE4OSwwMDowMDowMDowMDowMDowMA==/"
|
|
813
|
-
// },
|
|
814
|
-
|
|
815
|
-
// Log discovered IPs for visibility.
|
|
816
|
-
console.log(discoveries.map((d) => d.ip).join(' | '));
|
|
817
|
-
|
|
818
|
-
// Iterate through discoveries to find a matching MAC address.
|
|
819
2016
|
for (const discovery of discoveries) {
|
|
820
|
-
const
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
2017
|
+
const dicoverHostname = discovery.hostname
|
|
2018
|
+
? discovery.hostname
|
|
2019
|
+
: discovery.mac_organization
|
|
2020
|
+
? discovery.mac_organization
|
|
2021
|
+
: discovery.domain
|
|
2022
|
+
? discovery.domain
|
|
2023
|
+
: `generic-host-${s4()}${s4()}`;
|
|
2024
|
+
|
|
2025
|
+
console.log(dicoverHostname.bgBlue.bold.white);
|
|
2026
|
+
console.log('ip target:'.green + ipAddress, 'ip discovered:'.green + discovery.ip);
|
|
2027
|
+
console.log('mac target:'.green + macAddress, 'mac discovered:'.green + discovery.mac_address);
|
|
2028
|
+
|
|
2029
|
+
if (discovery.ip === ipAddress) {
|
|
2030
|
+
logger.info('Machine discovered!', discovery);
|
|
2031
|
+
if (!machine) {
|
|
2032
|
+
logger.info('Creating new machine with discovered hardware MAC...', {
|
|
2033
|
+
discoveredMAC: discovery.mac_address,
|
|
2034
|
+
ipAddress,
|
|
2035
|
+
hostname,
|
|
2036
|
+
});
|
|
2037
|
+
machine = UnderpostBaremetal.API.machineFactory({
|
|
2038
|
+
ipAddress,
|
|
2039
|
+
macAddress: discovery.mac_address,
|
|
2040
|
+
hostname,
|
|
2041
|
+
maas,
|
|
2042
|
+
}).machine;
|
|
2043
|
+
console.log('New machine system id:', machine.system_id.bgYellow.bold.black);
|
|
2044
|
+
UnderpostBaremetal.API.writeGrubConfigToFile({
|
|
2045
|
+
grubCfgSrc: UnderpostBaremetal.API.getGrubConfigFromFile().grubCfgSrc.replaceAll(
|
|
2046
|
+
'system-id',
|
|
2047
|
+
machine.system_id,
|
|
2048
|
+
),
|
|
2049
|
+
});
|
|
2050
|
+
} else {
|
|
2051
|
+
const systemId = machine.system_id;
|
|
2052
|
+
console.log('Using pre-registered machine system_id:', systemId.bgYellow.bold.black);
|
|
2053
|
+
|
|
2054
|
+
// Update the boot interface MAC if hardware MAC differs from pre-registered MAC
|
|
2055
|
+
// This handles both hardware mode (macAddress is null) and MAC mismatch scenarios
|
|
2056
|
+
if (macAddress === null || macAddress !== discovery.mac_address) {
|
|
2057
|
+
logger.info('Updating machine interface with discovered hardware MAC...', {
|
|
2058
|
+
preRegisteredMAC: macAddress || 'none (hardware mode)',
|
|
2059
|
+
discoveredMAC: discovery.mac_address,
|
|
2060
|
+
});
|
|
2061
|
+
|
|
2062
|
+
shellExec(`maas ${process.env.MAAS_ADMIN_USERNAME} machine mark-broken ${systemId}`, {
|
|
846
2063
|
silent: true,
|
|
847
|
-
|
|
848
|
-
},
|
|
849
|
-
);
|
|
850
|
-
newMachine = { discovery, machine: JSON.parse(newMachine) };
|
|
851
|
-
console.log(newMachine);
|
|
2064
|
+
});
|
|
852
2065
|
|
|
853
|
-
const discoverInterfaceName = 'eth0'; // Default interface name for discovery.
|
|
854
|
-
|
|
855
|
-
// Read interface data.
|
|
856
|
-
const interfaceData = JSON.parse(
|
|
857
2066
|
shellExec(
|
|
858
|
-
|
|
2067
|
+
// name=${networkInterfaceName}
|
|
2068
|
+
`maas ${process.env.MAAS_ADMIN_USERNAME} interface update ${systemId} ${machine.boot_interface.id}` +
|
|
2069
|
+
` mac_address=${discovery.mac_address}`,
|
|
859
2070
|
{
|
|
860
2071
|
silent: true,
|
|
861
|
-
stdout: true,
|
|
862
2072
|
},
|
|
863
|
-
)
|
|
864
|
-
);
|
|
2073
|
+
);
|
|
865
2074
|
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
// Save system-id for enlistment.
|
|
890
|
-
logger.info('system-id', newMachine.machine.boot_interface.system_id);
|
|
891
|
-
fs.writeFileSync(
|
|
892
|
-
`${nfsHostPath}/underpost/system-id`,
|
|
893
|
-
newMachine.machine.boot_interface.system_id,
|
|
894
|
-
'utf8',
|
|
895
|
-
);
|
|
896
|
-
|
|
897
|
-
// Get and save MAAS authentication credentials.
|
|
898
|
-
const { consumer_key, token_key, token_secret } = UnderpostCloudInit.API.authCredentialsFactory();
|
|
899
|
-
|
|
900
|
-
fs.writeFileSync(`${nfsHostPath}/underpost/consumer-key`, consumer_key, 'utf8');
|
|
901
|
-
fs.writeFileSync(`${nfsHostPath}/underpost/token-key`, token_key, 'utf8');
|
|
902
|
-
fs.writeFileSync(`${nfsHostPath}/underpost/token-secret`, token_secret, 'utf8');
|
|
903
|
-
|
|
904
|
-
// Open new terminals for live cloud-init logs.
|
|
905
|
-
openTerminal(`node ${underpostRoot}/bin baremetal --logs cloud`);
|
|
906
|
-
openTerminal(`node ${underpostRoot}/bin baremetal --logs machine`);
|
|
907
|
-
} catch (error) {
|
|
908
|
-
logger.error(error, error.stack);
|
|
909
|
-
} finally {
|
|
910
|
-
process.exit(0);
|
|
2075
|
+
shellExec(`maas ${process.env.MAAS_ADMIN_USERNAME} machine mark-fixed ${systemId}`, {
|
|
2076
|
+
silent: true,
|
|
2077
|
+
});
|
|
2078
|
+
|
|
2079
|
+
logger.info('✓ Machine interface MAC address updated successfully');
|
|
2080
|
+
|
|
2081
|
+
// commissioning_scripts=90-verify-user.sh
|
|
2082
|
+
machine = JSON.parse(
|
|
2083
|
+
shellExec(
|
|
2084
|
+
`maas ${process.env.MAAS_ADMIN_USERNAME} machine commission --debug --insecure ${systemId} enable_ssh=1 skip_bmc_config=1 skip_networking=1 skip_storage=1`,
|
|
2085
|
+
{
|
|
2086
|
+
silent: true,
|
|
2087
|
+
},
|
|
2088
|
+
),
|
|
2089
|
+
);
|
|
2090
|
+
}
|
|
2091
|
+
logger.info('Machine resource uri', machine.resource_uri);
|
|
2092
|
+
for (const iface of machine.interface_set)
|
|
2093
|
+
logger.info('Interface info', {
|
|
2094
|
+
name: iface.name,
|
|
2095
|
+
mac_address: iface.mac_address,
|
|
2096
|
+
resource_uri: iface.resource_uri,
|
|
2097
|
+
});
|
|
911
2098
|
}
|
|
2099
|
+
|
|
2100
|
+
return { discovery, machine };
|
|
2101
|
+
}
|
|
912
2102
|
}
|
|
913
2103
|
await timer(1000);
|
|
914
|
-
UnderpostBaremetal.API.commissionMonitor({
|
|
915
|
-
macAddress,
|
|
916
|
-
nfsHostPath,
|
|
917
|
-
underpostRoot,
|
|
918
|
-
hostname,
|
|
919
|
-
maas,
|
|
920
|
-
networkInterfaceName,
|
|
921
|
-
});
|
|
2104
|
+
return await UnderpostBaremetal.API.commissionMonitor({ macAddress, ipAddress, hostname, maas, machine });
|
|
922
2105
|
}
|
|
923
2106
|
},
|
|
924
2107
|
|
|
@@ -927,11 +2110,10 @@ menuentry '${menuentryStr}' {
|
|
|
927
2110
|
* @description Mounts the binfmt_misc filesystem to enable QEMU user-static binfmt support.
|
|
928
2111
|
* This is necessary for cross-architecture execution within a chroot environment.
|
|
929
2112
|
* @param {object} params - The parameters for the function.
|
|
930
|
-
* @param {string} params.nfsHostPath - The path to the NFS root filesystem on the host.
|
|
931
2113
|
* @memberof UnderpostBaremetal
|
|
932
2114
|
* @returns {void}
|
|
933
2115
|
*/
|
|
934
|
-
mountBinfmtMisc(
|
|
2116
|
+
mountBinfmtMisc() {
|
|
935
2117
|
// Install necessary packages for debootstrap and QEMU.
|
|
936
2118
|
shellExec(`sudo dnf install -y iptables-legacy`);
|
|
937
2119
|
shellExec(`sudo dnf install -y debootstrap`);
|
|
@@ -941,9 +2123,6 @@ menuentry '${menuentryStr}' {
|
|
|
941
2123
|
// Mount binfmt_misc filesystem.
|
|
942
2124
|
shellExec(`sudo modprobe binfmt_misc`);
|
|
943
2125
|
shellExec(`sudo mount -t binfmt_misc binfmt_misc /proc/sys/fs/binfmt_misc`);
|
|
944
|
-
// Set ownership and permissions for the NFS host path.
|
|
945
|
-
shellExec(`sudo chown -R root:root ${nfsHostPath}`);
|
|
946
|
-
shellExec(`sudo chmod 755 ${nfsHostPath}`);
|
|
947
2126
|
},
|
|
948
2127
|
|
|
949
2128
|
/**
|
|
@@ -951,12 +2130,17 @@ menuentry '${menuentryStr}' {
|
|
|
951
2130
|
* @description Deletes all specified machines from MAAS.
|
|
952
2131
|
* @param {object} params - The parameters for the function.
|
|
953
2132
|
* @param {Array<object>} params.machines - An array of machine objects, each with a `system_id`.
|
|
2133
|
+
* @param {Array<string>} [params.ignore] - An optional array of system IDs to ignore during deletion.
|
|
954
2134
|
* @memberof UnderpostBaremetal
|
|
955
2135
|
* @returns {Array<object>} An empty array after machines are removed.
|
|
956
2136
|
*/
|
|
957
|
-
removeMachines({ machines }) {
|
|
2137
|
+
removeMachines({ machines, ignore }) {
|
|
958
2138
|
for (const machine of machines) {
|
|
959
|
-
|
|
2139
|
+
// Handle both string system_ids and machine objects
|
|
2140
|
+
const systemId = typeof machine === 'string' ? machine : machine.system_id;
|
|
2141
|
+
if (ignore && ignore.find((mId) => mId === systemId)) continue;
|
|
2142
|
+
logger.info(`Removing machine: ${systemId}`);
|
|
2143
|
+
shellExec(`maas ${process.env.MAAS_ADMIN_USERNAME} machine delete ${systemId}`);
|
|
960
2144
|
}
|
|
961
2145
|
return [];
|
|
962
2146
|
},
|
|
@@ -1099,45 +2283,66 @@ EOF`);
|
|
|
1099
2283
|
* It checks the mount status and performs mount/unmount operations as requested.
|
|
1100
2284
|
* @param {object} params - The parameters for the function.
|
|
1101
2285
|
* @param {string} params.hostname - The hostname of the target machine.
|
|
2286
|
+
* @param {string} params.nfsHostPath - The NFS host path for the target machine.
|
|
1102
2287
|
* @param {string} params.workflowId - The identifier for the workflow configuration.
|
|
1103
2288
|
* @param {boolean} [params.mount] - If true, attempts to mount the NFS paths.
|
|
1104
2289
|
* @param {boolean} [params.unmount] - If true, attempts to unmount the NFS paths.
|
|
1105
2290
|
* @memberof UnderpostBaremetal
|
|
1106
2291
|
* @returns {{isMounted: boolean}} An object indicating whether any NFS path is currently mounted.
|
|
1107
2292
|
*/
|
|
1108
|
-
nfsMountCallback({ hostname, workflowId, mount, unmount }) {
|
|
2293
|
+
nfsMountCallback({ hostname, nfsHostPath, workflowId, mount, unmount }) {
|
|
2294
|
+
// Mount binfmt_misc filesystem.
|
|
2295
|
+
if (mount) UnderpostBaremetal.API.mountBinfmtMisc();
|
|
1109
2296
|
let isMounted = false;
|
|
2297
|
+
const mountCmds = [];
|
|
2298
|
+
const currentMounts = [];
|
|
1110
2299
|
const workflowsConfig = UnderpostBaremetal.API.loadWorkflowsConfig();
|
|
1111
2300
|
if (!workflowsConfig[workflowId]) {
|
|
1112
2301
|
throw new Error(`Workflow configuration not found for ID: ${workflowId}`);
|
|
1113
2302
|
}
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
)
|
|
2303
|
+
if (workflowsConfig[workflowId].type === 'chroot') {
|
|
2304
|
+
const mounts = {
|
|
2305
|
+
bind: ['/proc', '/sys', '/run'],
|
|
2306
|
+
rbind: ['/dev'],
|
|
2307
|
+
};
|
|
2308
|
+
|
|
2309
|
+
for (const mountCmd of Object.keys(mounts)) {
|
|
2310
|
+
for (const mountPath of mounts[mountCmd]) {
|
|
2311
|
+
const hostMountPath = `${process.env.NFS_EXPORT_PATH}/${hostname}${mountPath}`;
|
|
2312
|
+
// Check if the path is already mounted using `mountpoint` command.
|
|
2313
|
+
const isPathMounted = !shellExec(`mountpoint ${hostMountPath}`, { silent: true, stdout: true }).match(
|
|
2314
|
+
'not a mountpoint',
|
|
2315
|
+
);
|
|
1122
2316
|
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
if (mount === true) {
|
|
1132
|
-
// Mount if requested and not already mounted.
|
|
1133
|
-
shellExec(`sudo mount --${mountCmd} ${mountPath} ${hostMountPath}`);
|
|
2317
|
+
if (isPathMounted) {
|
|
2318
|
+
currentMounts.push(mountPath);
|
|
2319
|
+
if (!isMounted) isMounted = true; // Set overall mounted status.
|
|
2320
|
+
logger.warn('Nfs path already mounted', mountPath);
|
|
2321
|
+
if (unmount === true) {
|
|
2322
|
+
// Unmount if requested.
|
|
2323
|
+
mountCmds.push(`sudo umount ${hostMountPath}`);
|
|
2324
|
+
}
|
|
1134
2325
|
} else {
|
|
1135
|
-
|
|
2326
|
+
if (mount === true) {
|
|
2327
|
+
// Mount if requested and not already mounted.
|
|
2328
|
+
mountCmds.push(`sudo mount --${mountCmd} ${mountPath} ${hostMountPath}`);
|
|
2329
|
+
} else {
|
|
2330
|
+
logger.warn('Nfs path not mounted', mountPath);
|
|
2331
|
+
}
|
|
1136
2332
|
}
|
|
1137
2333
|
}
|
|
1138
2334
|
}
|
|
2335
|
+
|
|
2336
|
+
if (!isMounted) {
|
|
2337
|
+
// if all path unmounted, set ownership and permissions for the NFS host path.
|
|
2338
|
+
shellExec(`sudo chown -R $(whoami):$(whoami) ${nfsHostPath}`);
|
|
2339
|
+
shellExec(`sudo chmod -R 755 ${nfsHostPath}`);
|
|
2340
|
+
}
|
|
2341
|
+
for (const mountCmd of mountCmds) shellExec(mountCmd);
|
|
2342
|
+
if (mount) isMounted = true;
|
|
2343
|
+
logger.info('Current mounts', currentMounts);
|
|
1139
2344
|
}
|
|
1140
|
-
return { isMounted };
|
|
2345
|
+
return { isMounted, currentMounts };
|
|
1141
2346
|
},
|
|
1142
2347
|
|
|
1143
2348
|
/**
|
|
@@ -1178,11 +2383,10 @@ EOF`);
|
|
|
1178
2383
|
* This includes updating package lists, installing essential build tools,
|
|
1179
2384
|
* kernel modules, cloud-init, SSH server, and other core utilities.
|
|
1180
2385
|
* @param {object} params - The parameters for the function.
|
|
1181
|
-
* @param {string} params.kernelLibVersion - The specific kernel library version to install.
|
|
1182
2386
|
* @memberof UnderpostBaremetal.systemProvisioningFactory.ubuntu
|
|
1183
2387
|
* @returns {string[]} An array of shell commands.
|
|
1184
2388
|
*/
|
|
1185
|
-
base: (
|
|
2389
|
+
base: () => [
|
|
1186
2390
|
// Configure APT sources for Ubuntu ports.
|
|
1187
2391
|
`cat <<SOURCES | tee /etc/apt/sources.list
|
|
1188
2392
|
deb http://ports.ubuntu.com/ubuntu-ports noble main restricted universe multiverse
|
|
@@ -1196,12 +2400,9 @@ SOURCES`,
|
|
|
1196
2400
|
// Install essential development and system utilities.
|
|
1197
2401
|
`apt install -y build-essential xinput x11-xkb-utils usbutils uuid-runtime`,
|
|
1198
2402
|
'apt install -y linux-image-generic',
|
|
1199
|
-
// Install specific kernel modules.
|
|
1200
|
-
`apt install -y linux-modules-${kernelLibVersion} linux-modules-extra-${kernelLibVersion}`,
|
|
1201
2403
|
|
|
1202
|
-
`depmod -a ${kernelLibVersion}`, // Update kernel module dependencies.
|
|
1203
2404
|
// Install cloud-init, systemd, SSH, sudo, locales, udev, and networking tools.
|
|
1204
|
-
`apt install -y
|
|
2405
|
+
`apt install -y systemd-sysv openssh-server sudo locales udev util-linux systemd-sysv iproute2 netplan.io ca-certificates curl wget chrony`,
|
|
1205
2406
|
`ln -sf /lib/systemd/systemd /sbin/init`, // Ensure systemd is the init system.
|
|
1206
2407
|
|
|
1207
2408
|
`apt-get update`,
|
|
@@ -1311,15 +2512,16 @@ logdir /var/log/chrony
|
|
|
1311
2512
|
* @method keyboard
|
|
1312
2513
|
* @description Generates shell commands for configuring the keyboard layout.
|
|
1313
2514
|
* This ensures correct input behavior on the provisioned system.
|
|
2515
|
+
* @param {string} [keyCode='en'] - The keyboard layout code (e.g., 'en', 'es').
|
|
1314
2516
|
* @memberof UnderpostBaremetal.systemProvisioningFactory.ubuntu
|
|
1315
2517
|
* @returns {string[]} An array of shell commands.
|
|
1316
2518
|
*/
|
|
1317
|
-
keyboard: () => [
|
|
1318
|
-
`sudo locale-gen en_US.UTF-8`,
|
|
1319
|
-
`sudo update-locale LANG=en_US.UTF-8`,
|
|
1320
|
-
`sudo sed -i 's/XKBLAYOUT="us"/XKBLAYOUT="
|
|
1321
|
-
`sudo dpkg-reconfigure --frontend noninteractive keyboard-configuration`,
|
|
1322
|
-
`sudo systemctl restart keyboard-setup.service`,
|
|
2519
|
+
keyboard: (keyCode = 'en') => [
|
|
2520
|
+
`sudo locale-gen en_US.UTF-8`,
|
|
2521
|
+
`sudo update-locale LANG=en_US.UTF-8`,
|
|
2522
|
+
`sudo sed -i 's/XKBLAYOUT="us"/XKBLAYOUT="${keyCode}"/' /etc/default/keyboard`,
|
|
2523
|
+
`sudo dpkg-reconfigure --frontend noninteractive keyboard-configuration`,
|
|
2524
|
+
`sudo systemctl restart keyboard-setup.service`,
|
|
1323
2525
|
],
|
|
1324
2526
|
},
|
|
1325
2527
|
},
|
|
@@ -1329,7 +2531,7 @@ logdir /var/log/chrony
|
|
|
1329
2531
|
* @description Configures and restarts the NFS server to export the specified path.
|
|
1330
2532
|
* This is crucial for allowing baremetal machines to boot via NFS.
|
|
1331
2533
|
* @param {object} params - The parameters for the function.
|
|
1332
|
-
* @param {string} params.nfsHostPath - The path to
|
|
2534
|
+
* @param {string} params.nfsHostPath - The path to the NFS server export.
|
|
1333
2535
|
* @memberof UnderpostBaremetal
|
|
1334
2536
|
* @param {string} [params.subnet='192.168.1.0/24'] - The subnet allowed to access the NFS export.
|
|
1335
2537
|
* @returns {void}
|
|
@@ -1378,7 +2580,7 @@ udp-port = 32766
|
|
|
1378
2580
|
shellExec(`sudo exportfs -rav`);
|
|
1379
2581
|
|
|
1380
2582
|
// Display the currently active NFS exports for verification.
|
|
1381
|
-
logger.info('Displaying active NFS exports
|
|
2583
|
+
logger.info('Displaying active NFS exports');
|
|
1382
2584
|
shellExec(`sudo exportfs -s`);
|
|
1383
2585
|
|
|
1384
2586
|
// Restart the nfs-server service to apply all configuration changes,
|
|
@@ -1388,6 +2590,80 @@ udp-port = 32766
|
|
|
1388
2590
|
logger.info('NFS server restarted.');
|
|
1389
2591
|
},
|
|
1390
2592
|
|
|
2593
|
+
/**
|
|
2594
|
+
* @method checkQemuCrossArchSupport
|
|
2595
|
+
* @description Checks for QEMU support when building for a different architecture.
|
|
2596
|
+
* This is essential for validator bots that need to build images for architectures
|
|
2597
|
+
* different from the host system (e.g., building arm64 on x86_64 or vice versa).
|
|
2598
|
+
* @param {object} workflow - The workflow configuration object.
|
|
2599
|
+
* @param {object} workflow.maas - The MAAS configuration.
|
|
2600
|
+
* @param {string} workflow.maas.architecture - Target architecture (e.g., 'arm64/generic', 'amd64/generic').
|
|
2601
|
+
* @memberof UnderpostBaremetal
|
|
2602
|
+
* @throws {Error} If QEMU is not installed or doesn't support required machine types.
|
|
2603
|
+
* @returns {void}
|
|
2604
|
+
*/
|
|
2605
|
+
checkQemuCrossArchSupport(workflow) {
|
|
2606
|
+
// Check for QEMU support if building for a different architecture (validator bots case)
|
|
2607
|
+
if (workflow.maas.architecture.startsWith('arm64') && process.arch !== 'arm64') {
|
|
2608
|
+
// Building arm64/aarch64 on x86_64 host
|
|
2609
|
+
// Check both /usr/local/bin (compiled) and system paths
|
|
2610
|
+
let qemuAarch64Path = null;
|
|
2611
|
+
|
|
2612
|
+
if (shellExec('test -x /usr/local/bin/qemu-system-aarch64', { silent: true }).code === 0) {
|
|
2613
|
+
qemuAarch64Path = '/usr/local/bin/qemu-system-aarch64';
|
|
2614
|
+
} else if (shellExec('which qemu-system-aarch64', { silent: true }).code === 0) {
|
|
2615
|
+
qemuAarch64Path = shellExec('which qemu-system-aarch64', { silent: true }).stdout.trim();
|
|
2616
|
+
}
|
|
2617
|
+
|
|
2618
|
+
if (!qemuAarch64Path) {
|
|
2619
|
+
throw new Error(
|
|
2620
|
+
'qemu-system-aarch64 is not installed. Please install it to build ARM64 images on x86_64 hosts.\n' +
|
|
2621
|
+
'Run: node bin baremetal --dev --install-packer',
|
|
2622
|
+
);
|
|
2623
|
+
}
|
|
2624
|
+
|
|
2625
|
+
logger.info(`Found qemu-system-aarch64 at: ${qemuAarch64Path}`);
|
|
2626
|
+
|
|
2627
|
+
// Verify that the installed qemu supports the 'virt' machine type (required for arm64)
|
|
2628
|
+
const machineHelp = shellExec(`${qemuAarch64Path} -machine help`, { silent: true }).stdout;
|
|
2629
|
+
if (!machineHelp.includes('virt')) {
|
|
2630
|
+
throw new Error(
|
|
2631
|
+
'The installed qemu-system-aarch64 does not support the "virt" machine type.\n' +
|
|
2632
|
+
'This usually happens if qemu-system-aarch64 is a symlink to qemu-kvm on x86_64.\n' +
|
|
2633
|
+
'Run: node bin baremetal --dev --install-packer',
|
|
2634
|
+
);
|
|
2635
|
+
}
|
|
2636
|
+
} else if (workflow.maas.architecture.startsWith('amd64') && process.arch !== 'x64') {
|
|
2637
|
+
// Building amd64/x86_64 on aarch64 host
|
|
2638
|
+
// Check both /usr/local/bin (compiled) and system paths
|
|
2639
|
+
let qemuX86Path = null;
|
|
2640
|
+
|
|
2641
|
+
if (shellExec('test -x /usr/local/bin/qemu-system-x86_64', { silent: true }).code === 0) {
|
|
2642
|
+
qemuX86Path = '/usr/local/bin/qemu-system-x86_64';
|
|
2643
|
+
} else if (shellExec('which qemu-system-x86_64', { silent: true }).code === 0) {
|
|
2644
|
+
qemuX86Path = shellExec('which qemu-system-x86_64', { silent: true }).stdout.trim();
|
|
2645
|
+
}
|
|
2646
|
+
|
|
2647
|
+
if (!qemuX86Path) {
|
|
2648
|
+
throw new Error(
|
|
2649
|
+
'qemu-system-x86_64 is not installed. Please install it to build x86_64 images on aarch64 hosts.\n' +
|
|
2650
|
+
'Run: node bin baremetal --dev --install-packer',
|
|
2651
|
+
);
|
|
2652
|
+
}
|
|
2653
|
+
|
|
2654
|
+
logger.info(`Found qemu-system-x86_64 at: ${qemuX86Path}`);
|
|
2655
|
+
|
|
2656
|
+
// Verify that the installed qemu supports the 'pc' or 'q35' machine type (required for x86_64)
|
|
2657
|
+
const machineHelp = shellExec(`${qemuX86Path} -machine help`, { silent: true }).stdout;
|
|
2658
|
+
if (!machineHelp.includes('pc') && !machineHelp.includes('q35')) {
|
|
2659
|
+
throw new Error(
|
|
2660
|
+
'The installed qemu-system-x86_64 does not support the "pc" or "q35" machine type.\n' +
|
|
2661
|
+
'Run: node bin baremetal --dev --install-packer',
|
|
2662
|
+
);
|
|
2663
|
+
}
|
|
2664
|
+
}
|
|
2665
|
+
},
|
|
2666
|
+
|
|
1391
2667
|
/**
|
|
1392
2668
|
* @method bootConfFactory
|
|
1393
2669
|
* @description Generates the boot configuration file for specific workflows,
|
|
@@ -1406,9 +2682,14 @@ udp-port = 32766
|
|
|
1406
2682
|
* @throws {Error} If an invalid workflow ID is provided.
|
|
1407
2683
|
*/
|
|
1408
2684
|
bootConfFactory({ workflowId, tftpIp, tftpPrefixStr, macAddress, clientIp, subnet, gateway }) {
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
2685
|
+
if (workflowId.startsWith('rpi4mb')) {
|
|
2686
|
+
// Instructions: Flash sd with Raspberry Pi OS lite and update:
|
|
2687
|
+
// EEPROM (Electrically Erasable Programmable Read-Only Memory) like microcontrollers
|
|
2688
|
+
// sudo rpi-eeprom-config --apply /boot/firmware/boot.conf
|
|
2689
|
+
// sudo reboot
|
|
2690
|
+
// vcgencmd bootloader_config
|
|
2691
|
+
// shutdown -h now
|
|
2692
|
+
return `[all]
|
|
1412
2693
|
BOOT_UART=0
|
|
1413
2694
|
WAKE_ON_GPIO=1
|
|
1414
2695
|
POWER_OFF_ON_HALT=0
|
|
@@ -1441,7 +2722,7 @@ TFTP_PREFIX_STR=${tftpPrefixStr}/
|
|
|
1441
2722
|
# Manually override Ethernet MAC address
|
|
1442
2723
|
# ─────────────────────────────────────────────────────────────
|
|
1443
2724
|
|
|
1444
|
-
MAC_ADDRESS=${macAddress}
|
|
2725
|
+
#MAC_ADDRESS=${macAddress}
|
|
1445
2726
|
|
|
1446
2727
|
# OTP MAC address override
|
|
1447
2728
|
#MAC_ADDRESS_OTP=0,1
|
|
@@ -1449,18 +2730,14 @@ MAC_ADDRESS=${macAddress}
|
|
|
1449
2730
|
# ─────────────────────────────────────────────────────────────
|
|
1450
2731
|
# Static IP configuration (bypasses DHCP completely)
|
|
1451
2732
|
# ─────────────────────────────────────────────────────────────
|
|
1452
|
-
CLIENT_IP=${clientIp}
|
|
2733
|
+
#CLIENT_IP=${clientIp}
|
|
1453
2734
|
SUBNET=${subnet}
|
|
1454
2735
|
GATEWAY=${gateway}`;
|
|
1455
|
-
|
|
1456
|
-
default:
|
|
1457
|
-
throw new Error('Boot conf factory invalid workflow ID:' + workflowId);
|
|
1458
|
-
}
|
|
2736
|
+
} else logger.warn(`No boot configuration factory defined for workflow ID: ${workflowId}`);
|
|
1459
2737
|
},
|
|
1460
2738
|
|
|
1461
2739
|
/**
|
|
1462
2740
|
* @method loadWorkflowsConfig
|
|
1463
|
-
* @namespace UnderpostBaremetal.API
|
|
1464
2741
|
* @description Loads the commission workflows configuration from commission-workflows.json.
|
|
1465
2742
|
* Each workflow defines specific parameters like system provisioning type,
|
|
1466
2743
|
* kernel version, Chrony settings, debootstrap image details, and NFS mounts. *
|