@underpostnet/underpost 2.95.8 → 2.96.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/README.md +2 -2
  2. package/baremetal/commission-workflows.json +44 -0
  3. package/baremetal/packer-workflows.json +24 -0
  4. package/cli.md +29 -31
  5. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  6. package/manifests/deployment/dd-test-development/deployment.yaml +2 -2
  7. package/package.json +1 -1
  8. package/packer/images/Rocky9Amd64/Makefile +62 -0
  9. package/packer/images/Rocky9Amd64/QUICKSTART.md +113 -0
  10. package/packer/images/Rocky9Amd64/README.md +122 -0
  11. package/packer/images/Rocky9Amd64/http/rocky9.ks.pkrtpl.hcl +114 -0
  12. package/packer/images/Rocky9Amd64/rocky9.pkr.hcl +164 -0
  13. package/packer/images/Rocky9Arm64/Makefile +69 -0
  14. package/packer/images/Rocky9Arm64/README.md +122 -0
  15. package/packer/images/Rocky9Arm64/http/rocky9.ks.pkrtpl.hcl +114 -0
  16. package/packer/images/Rocky9Arm64/rocky9.pkr.hcl +171 -0
  17. package/packer/scripts/fuse-nbd +64 -0
  18. package/packer/scripts/fuse-tar-root +63 -0
  19. package/scripts/maas-setup.sh +13 -2
  20. package/scripts/maas-upload-boot-resource.sh +183 -0
  21. package/scripts/packer-init-vars-file.sh +40 -0
  22. package/scripts/packer-setup.sh +289 -0
  23. package/src/cli/baremetal.js +342 -55
  24. package/src/cli/cloud-init.js +1 -1
  25. package/src/cli/env.js +24 -3
  26. package/src/cli/index.js +19 -0
  27. package/src/cli/repository.js +164 -0
  28. package/src/index.js +2 -1
  29. package/manifests/mariadb/config.yaml +0 -10
  30. package/manifests/mariadb/secret.yaml +0 -8
  31. package/src/client/ssr/pages/404.js +0 -12
  32. package/src/client/ssr/pages/500.js +0 -12
  33. package/src/client/ssr/pages/maintenance.js +0 -14
  34. package/src/client/ssr/pages/offline.js +0 -21
@@ -4,15 +4,19 @@
4
4
  * @namespace UnderpostBaremetal
5
5
  */
6
6
 
7
+ import { fileURLToPath } from 'url';
7
8
  import { getNpmRootPath, getUnderpostRootPath } from '../server/conf.js';
8
9
  import { openTerminal, pbcopy, shellExec } from '../server/process.js';
9
10
  import dotenv from 'dotenv';
10
11
  import { loggerFactory } from '../server/logger.js';
11
12
  import { getLocalIPv4Address } from '../server/dns.js';
12
13
  import fs from 'fs-extra';
14
+ import path from 'path';
13
15
  import Downloader from '../server/downloader.js';
14
16
  import UnderpostCloudInit from './cloud-init.js';
17
+ import UnderpostRepository from './repository.js';
15
18
  import { s4, timer } from '../client/components/core/CommonJs.js';
19
+ import { spawnSync } from 'child_process';
16
20
 
17
21
  const logger = loggerFactory(import.meta);
18
22
 
@@ -25,6 +29,19 @@ const logger = loggerFactory(import.meta);
25
29
  */
26
30
  class UnderpostBaremetal {
27
31
  static API = {
32
+ /**
33
+ * @method installPacker
34
+ * @description Installs Packer CLI.
35
+ * @memberof UnderpostBaremetal
36
+ * @returns {Promise<void>}
37
+ */
38
+ async installPacker(underpostRoot) {
39
+ const scriptPath = `${underpostRoot}/scripts/packer-setup.sh`;
40
+ logger.info(`Installing Packer using script: ${scriptPath}`);
41
+ shellExec(`sudo chmod +x ${scriptPath}`);
42
+ shellExec(`sudo ${scriptPath}`);
43
+ },
44
+
28
45
  /**
29
46
  * @method callback
30
47
  * @description Initiates a baremetal provisioning workflow based on the provided options.
@@ -40,6 +57,11 @@ class UnderpostBaremetal {
40
57
  * @param {boolean} [options.controlServerUninstall=false] - Flag to uninstall the control server.
41
58
  * @param {boolean} [options.controlServerDbInstall=false] - Flag to install the control server's database.
42
59
  * @param {boolean} [options.controlServerDbUninstall=false] - Flag to uninstall the control server's database.
60
+ * @param {boolean} [options.installPacker=false] - Flag to install Packer CLI.
61
+ * @param {string} [options.packerMaasImageTemplate] - Template path from canonical/packer-maas to extract (requires workflow-id).
62
+ * @param {string} [options.packerWorkflowId] - Workflow ID for Packer MAAS image operations (used with --packer-maas-image-build or --packer-maas-image-upload).
63
+ * @param {boolean} [options.packerMaasImageBuild=false] - Flag to build a Packer MAAS image for the workflow specified by packerWorkflowId.
64
+ * @param {boolean} [options.packerMaasImageUpload=false] - Flag to upload a Packer MAAS image artifact without rebuilding for the workflow specified by packerWorkflowId.
43
65
  * @param {boolean} [options.cloudInitUpdate=false] - Flag to update cloud-init configuration on the baremetal machine.
44
66
  * @param {boolean} [options.commission=false] - Flag to commission the baremetal machine.
45
67
  * @param {boolean} [options.nfsBuild=false] - Flag to build the NFS root filesystem.
@@ -60,6 +82,12 @@ class UnderpostBaremetal {
60
82
  controlServerUninstall: false,
61
83
  controlServerDbInstall: false,
62
84
  controlServerDbUninstall: false,
85
+ installPacker: false,
86
+ packerMaasImageTemplate: false,
87
+ packerWorkflowId: '',
88
+ packerMaasImageBuild: false,
89
+ packerMaasImageUpload: false,
90
+ packerMaasImageCached: false,
63
91
  cloudInitUpdate: false,
64
92
  commission: false,
65
93
  nfsBuild: false,
@@ -108,6 +136,188 @@ class UnderpostBaremetal {
108
136
  // Log the initiation of the baremetal callback with relevant metadata.
109
137
  logger.info('Baremetal callback', callbackMetaData);
110
138
 
139
+ if (options.installPacker) {
140
+ await UnderpostBaremetal.API.installPacker(underpostRoot);
141
+ return;
142
+ }
143
+
144
+ if (options.packerMaasImageTemplate) {
145
+ workflowId = options.packerWorkflowId;
146
+ if (!workflowId) {
147
+ throw new Error('--packer-workflow-id is required when using --packer-maas-image-template');
148
+ }
149
+
150
+ const templatePath = options.packerMaasImageTemplate;
151
+ const targetDir = `${underpostRoot}/packer/images/${workflowId}`;
152
+
153
+ logger.info(`Creating new Packer MAAS image template for workflow: ${workflowId}`);
154
+ logger.info(`Template path: ${templatePath}`);
155
+ logger.info(`Target directory: ${targetDir}`);
156
+
157
+ try {
158
+ // Use UnderpostRepository to copy files from GitHub
159
+ const result = await UnderpostRepository.API.copyGitUrlDirectoryRecursive({
160
+ gitUrl: 'https://github.com/canonical/packer-maas',
161
+ directoryPath: templatePath,
162
+ targetPath: targetDir,
163
+ branch: 'main',
164
+ overwrite: false,
165
+ });
166
+
167
+ logger.info(`\nSuccessfully copied ${result.filesCount} files`);
168
+
169
+ // Create empty workflow configuration entry
170
+ const workflowConfig = {
171
+ dir: `packer/images/${workflowId}`,
172
+ maas: {
173
+ name: `custom/${workflowId.toLowerCase()}`,
174
+ title: `${workflowId} Custom`,
175
+ architecture: 'amd64/generic',
176
+ base_image: 'ubuntu/22.04',
177
+ filetype: 'tgz',
178
+ content: `${workflowId.toLowerCase()}.tar.gz`,
179
+ },
180
+ };
181
+
182
+ const workflows = UnderpostBaremetal.API.loadPackerMaasImageBuildWorkflows();
183
+ workflows[workflowId] = workflowConfig;
184
+ UnderpostBaremetal.API.writePackerMaasImageBuildWorkflows(workflows);
185
+
186
+ logger.info('\nTemplate extracted successfully!');
187
+ logger.info(`\nAdded configuration for ${workflowId} to engine/baremetal/packer-workflows.json`);
188
+ logger.info('\nNext steps:');
189
+ logger.info(`1. Review and customize the Packer template files in: ${targetDir}`);
190
+ logger.info(`2. Review the workflow configuration in engine/baremetal/packer-workflows.json`);
191
+ logger.info(
192
+ `3. Build the image with: underpost baremetal --packer-workflow-id ${workflowId} --packer-maas-image-build`,
193
+ );
194
+ } catch (error) {
195
+ throw new Error(`Failed to extract template: ${error.message}`);
196
+ }
197
+
198
+ return;
199
+ }
200
+
201
+ if (options.packerMaasImageBuild || options.packerMaasImageUpload) {
202
+ // Use the workflow ID from --packer-workflow-id option
203
+ if (!options.packerWorkflowId) {
204
+ throw new Error('Workflow ID is required. Please specify using --packer-workflow-id <workflow-id>');
205
+ }
206
+
207
+ workflowId = options.packerWorkflowId;
208
+
209
+ const workflow = UnderpostBaremetal.API.loadPackerMaasImageBuildWorkflows()[workflowId];
210
+ if (!workflow) {
211
+ throw new Error(`Packer MAAS image build workflow not found: ${workflowId}`);
212
+ }
213
+ const packerDir = `${underpostRoot}/${workflow.dir}`;
214
+ const tarballPath = `${packerDir}/${workflow.maas.content}`;
215
+
216
+ // Build phase (skip if upload-only mode)
217
+ if (options.packerMaasImageBuild) {
218
+ if (shellExec('packer version', { silent: true }).code !== 0) {
219
+ throw new Error('Packer is not installed. Please install Packer to proceed.');
220
+ }
221
+
222
+ // Check for QEMU support if building for a different architecture (validator bots case)
223
+ UnderpostBaremetal.API.checkQemuCrossArchSupport(workflow);
224
+
225
+ logger.info(`Building Packer image for ${workflowId} in ${packerDir}...`);
226
+
227
+ // Only remove artifacts if not using cached mode
228
+ if (!options.packerMaasImageCached) {
229
+ const artifacts = [
230
+ 'output-rocky9',
231
+ 'packer_cache',
232
+ 'x86_64_VARS.fd',
233
+ 'aarch64_VARS.fd',
234
+ workflow.maas.content,
235
+ ];
236
+ shellExec(`cd packer/images/${workflowId}
237
+ rm -rf ${artifacts.join(' ')}`);
238
+ logger.info('Removed previous build artifacts');
239
+ } else {
240
+ logger.info('Cached mode: Keeping existing artifacts for incremental build');
241
+ }
242
+ shellExec(`chmod +x ${underpostRoot}/scripts/packer-init-vars-file.sh`);
243
+ shellExec(`${underpostRoot}/scripts/packer-init-vars-file.sh`);
244
+
245
+ const init = spawnSync('packer', ['init', '.'], { stdio: 'inherit', cwd: packerDir });
246
+ if (init.status !== 0) {
247
+ throw new Error('Packer init failed');
248
+ }
249
+
250
+ const isArm = process.arch === 'arm64';
251
+ // Add /usr/local/bin to PATH so Packer can find compiled QEMU binaries
252
+ const packerEnv = {
253
+ ...process.env,
254
+ PACKER_LOG: '1',
255
+ PATH: `/usr/local/bin:${process.env.PATH || '/usr/bin:/bin'}`,
256
+ };
257
+ const build = spawnSync('packer', ['build', '-var', `host_is_arm=${isArm}`, '.'], {
258
+ stdio: 'inherit',
259
+ cwd: packerDir,
260
+ env: packerEnv,
261
+ });
262
+
263
+ if (build.status !== 0) {
264
+ throw new Error('Packer build failed');
265
+ }
266
+ } else {
267
+ // Upload-only mode: verify tarball exists
268
+ logger.info(`Upload-only mode: checking for existing build artifact...`);
269
+ if (!fs.existsSync(tarballPath)) {
270
+ throw new Error(
271
+ `Build artifact not found: ${tarballPath}\n` +
272
+ `Please build first with: --packer-workflow-id ${workflowId} --packer-maas-image-build`,
273
+ );
274
+ }
275
+ const stats = fs.statSync(tarballPath);
276
+ logger.info(`Found existing artifact: ${tarballPath} (${(stats.size / 1024 / 1024 / 1024).toFixed(2)} GB)`);
277
+ }
278
+
279
+ logger.info(`Uploading image to MAAS...`);
280
+
281
+ // Detect MAAS profile from 'maas list' output
282
+ let maasProfile = process.env.MAAS_ADMIN_USERNAME;
283
+ if (!maasProfile) {
284
+ const profileList = shellExec('maas list', { silent: true, stdout: true });
285
+ if (profileList) {
286
+ const firstLine = profileList.trim().split('\n')[0];
287
+ const match = firstLine.match(/^(\S+)\s+http/);
288
+ if (match) {
289
+ maasProfile = match[1];
290
+ logger.info(`Detected MAAS profile: ${maasProfile}`);
291
+ }
292
+ }
293
+ }
294
+
295
+ if (!maasProfile) {
296
+ throw new Error(
297
+ 'MAAS profile not found. Please run "maas login" first or set MAAS_ADMIN_USERNAME environment variable.',
298
+ );
299
+ }
300
+
301
+ // Use the upload script to avoid MAAS CLI bugs
302
+ const uploadScript = `${underpostRoot}/scripts/maas-upload-boot-resource.sh`;
303
+ const uploadCmd = `${uploadScript} ${maasProfile} "${workflow.maas.name}" "${workflow.maas.title}" "${workflow.maas.architecture}" "${workflow.maas.base_image}" "${workflow.maas.filetype}" "${tarballPath}"`;
304
+
305
+ logger.info(`Uploading to MAAS using: ${uploadScript}`);
306
+ const uploadResult = shellExec(uploadCmd);
307
+ if (uploadResult.code !== 0) {
308
+ logger.error(`Upload failed with exit code: ${uploadResult.code}`);
309
+ if (uploadResult.stdout) {
310
+ logger.error(`Upload output:\n${uploadResult.stdout}`);
311
+ }
312
+ if (uploadResult.stderr) {
313
+ logger.error(`Upload error output:\n${uploadResult.stderr}`);
314
+ }
315
+ throw new Error('MAAS upload failed - see output above for details');
316
+ }
317
+ logger.info(`Successfully uploaded ${workflow.maas.name} to MAAS!`);
318
+ return;
319
+ }
320
+
111
321
  // Handle various log display options.
112
322
  if (options.logs === 'dhcp') {
113
323
  shellExec(`journalctl -f -t dhcpd -u snap.maas.pebble.service`);
@@ -131,7 +341,11 @@ class UnderpostBaremetal {
131
341
 
132
342
  // Handle NFS shell access option.
133
343
  if (options.nfsSh === true) {
134
- const { debootstrap } = UnderpostBaremetal.API.workflowsConfig[workflowId];
344
+ const workflowsConfig = UnderpostBaremetal.API.loadWorkflowsConfig();
345
+ if (!workflowsConfig[workflowId]) {
346
+ throw new Error(`Workflow configuration not found for ID: ${workflowId}`);
347
+ }
348
+ const { debootstrap } = workflowsConfig[workflowId];
135
349
  // Copy the chroot command to the clipboard for easy execution.
136
350
  if (debootstrap.image.architecture !== callbackMetaData.runnerHost.architecture)
137
351
  switch (debootstrap.image.architecture) {
@@ -196,9 +410,14 @@ class UnderpostBaremetal {
196
410
  return;
197
411
  }
198
412
 
413
+ const workflowsConfig = UnderpostBaremetal.API.loadWorkflowsConfig();
414
+ if (!workflowsConfig[workflowId]) {
415
+ throw new Error(`Workflow configuration not found for ID: ${workflowId}`);
416
+ }
417
+
199
418
  // Set debootstrap architecture.
200
419
  {
201
- const { architecture } = UnderpostBaremetal.API.workflowsConfig[workflowId].debootstrap.image;
420
+ const { architecture } = workflowsConfig[workflowId].debootstrap.image;
202
421
  debootstrapArch = architecture;
203
422
  }
204
423
 
@@ -228,7 +447,7 @@ class UnderpostBaremetal {
228
447
 
229
448
  // Perform the first stage of debootstrap.
230
449
  {
231
- const { architecture, name } = UnderpostBaremetal.API.workflowsConfig[workflowId].debootstrap.image;
450
+ const { architecture, name } = workflowsConfig[workflowId].debootstrap.image;
232
451
  shellExec(
233
452
  [
234
453
  `sudo debootstrap`,
@@ -273,7 +492,7 @@ class UnderpostBaremetal {
273
492
 
274
493
  // Apply system provisioning steps (base, user, timezone, keyboard).
275
494
  {
276
- const { systemProvisioning, kernelLibVersion, chronyc } = UnderpostBaremetal.API.workflowsConfig[workflowId];
495
+ const { systemProvisioning, kernelLibVersion, chronyc } = workflowsConfig[workflowId];
277
496
  const { timezone, chronyConfPath } = chronyc;
278
497
 
279
498
  UnderpostBaremetal.API.crossArchRunner({
@@ -327,8 +546,7 @@ class UnderpostBaremetal {
327
546
 
328
547
  // Handle commissioning tasks (placeholder for future implementation).
329
548
  if (options.commission === true) {
330
- const { firmwares, networkInterfaceName, maas, netmask, menuentryStr } =
331
- UnderpostBaremetal.API.workflowsConfig[workflowId];
549
+ const { firmwares, networkInterfaceName, maas, netmask, menuentryStr } = workflowsConfig[workflowId];
332
550
  const resource = resources.find(
333
551
  (o) => o.architecture === maas.image.architecture && o.name === maas.image.name,
334
552
  );
@@ -490,7 +708,7 @@ menuentry '${menuentryStr}' {
490
708
 
491
709
  // Final commissioning steps.
492
710
  if (options.commission === true || options.cloudInitUpdate === true) {
493
- const { debootstrap, networkInterfaceName, chronyc, maas } = UnderpostBaremetal.API.workflowsConfig[workflowId];
711
+ const { debootstrap, networkInterfaceName, chronyc, maas } = workflowsConfig[workflowId];
494
712
  const { timezone, chronyConfPath } = chronyc;
495
713
 
496
714
  // Build cloud-init tools.
@@ -742,7 +960,7 @@ menuentry '${menuentryStr}' {
742
960
  // Install necessary packages for debootstrap and QEMU.
743
961
  shellExec(`sudo dnf install -y iptables-legacy`);
744
962
  shellExec(`sudo dnf install -y debootstrap`);
745
- shellExec(`sudo dnf install kernel-modules-extra-$(uname -r)`);
963
+ shellExec(`sudo dnf install -y kernel-modules-extra-$(uname -r)`);
746
964
  // Reset QEMU user-static binfmt for proper cross-architecture execution.
747
965
  shellExec(`sudo podman run --rm --privileged docker.io/multiarch/qemu-user-static:latest --reset -p yes`);
748
966
  // Mount binfmt_misc filesystem.
@@ -914,9 +1132,13 @@ EOF`);
914
1132
  */
915
1133
  nfsMountCallback({ hostname, workflowId, mount, unmount }) {
916
1134
  let isMounted = false;
1135
+ const workflowsConfig = UnderpostBaremetal.API.loadWorkflowsConfig();
1136
+ if (!workflowsConfig[workflowId]) {
1137
+ throw new Error(`Workflow configuration not found for ID: ${workflowId}`);
1138
+ }
917
1139
  // Iterate through defined NFS mounts in the workflow configuration.
918
- for (const mountCmd of Object.keys(UnderpostBaremetal.API.workflowsConfig[workflowId].nfs.mounts)) {
919
- for (const mountPath of UnderpostBaremetal.API.workflowsConfig[workflowId].nfs.mounts[mountCmd]) {
1140
+ for (const mountCmd of Object.keys(workflowsConfig[workflowId].nfs.mounts)) {
1141
+ for (const mountPath of workflowsConfig[workflowId].nfs.mounts[mountCmd]) {
920
1142
  const hostMountPath = `${process.env.NFS_EXPORT_PATH}/${hostname}${mountPath}`;
921
1143
  // Check if the path is already mounted using `mountpoint` command.
922
1144
  const isPathMounted = !shellExec(`mountpoint ${hostMountPath}`, { silent: true, stdout: true }).match(
@@ -1191,6 +1413,80 @@ udp-port = 32766
1191
1413
  logger.info('NFS server restarted.');
1192
1414
  },
1193
1415
 
1416
+ /**
1417
+ * @method checkQemuCrossArchSupport
1418
+ * @description Checks for QEMU support when building for a different architecture.
1419
+ * This is essential for validator bots that need to build images for architectures
1420
+ * different from the host system (e.g., building arm64 on x86_64 or vice versa).
1421
+ * @param {object} workflow - The workflow configuration object.
1422
+ * @param {object} workflow.maas - The MAAS configuration.
1423
+ * @param {string} workflow.maas.architecture - Target architecture (e.g., 'arm64/generic', 'amd64/generic').
1424
+ * @memberof UnderpostBaremetal
1425
+ * @throws {Error} If QEMU is not installed or doesn't support required machine types.
1426
+ * @returns {void}
1427
+ */
1428
+ checkQemuCrossArchSupport(workflow) {
1429
+ // Check for QEMU support if building for a different architecture (validator bots case)
1430
+ if (workflow.maas.architecture.startsWith('arm64') && process.arch !== 'arm64') {
1431
+ // Building arm64/aarch64 on x86_64 host
1432
+ // Check both /usr/local/bin (compiled) and system paths
1433
+ let qemuAarch64Path = null;
1434
+
1435
+ if (shellExec('test -x /usr/local/bin/qemu-system-aarch64', { silent: true }).code === 0) {
1436
+ qemuAarch64Path = '/usr/local/bin/qemu-system-aarch64';
1437
+ } else if (shellExec('which qemu-system-aarch64', { silent: true }).code === 0) {
1438
+ qemuAarch64Path = shellExec('which qemu-system-aarch64', { silent: true }).stdout.trim();
1439
+ }
1440
+
1441
+ if (!qemuAarch64Path) {
1442
+ throw new Error(
1443
+ 'qemu-system-aarch64 is not installed. Please install it to build ARM64 images on x86_64 hosts.\n' +
1444
+ 'Run: node bin baremetal --dev --install-packer',
1445
+ );
1446
+ }
1447
+
1448
+ logger.info(`Found qemu-system-aarch64 at: ${qemuAarch64Path}`);
1449
+
1450
+ // Verify that the installed qemu supports the 'virt' machine type (required for arm64)
1451
+ const machineHelp = shellExec(`${qemuAarch64Path} -machine help`, { silent: true }).stdout;
1452
+ if (!machineHelp.includes('virt')) {
1453
+ throw new Error(
1454
+ 'The installed qemu-system-aarch64 does not support the "virt" machine type.\n' +
1455
+ 'This usually happens if qemu-system-aarch64 is a symlink to qemu-kvm on x86_64.\n' +
1456
+ 'Run: node bin baremetal --dev --install-packer',
1457
+ );
1458
+ }
1459
+ } else if (workflow.maas.architecture.startsWith('amd64') && process.arch !== 'x64') {
1460
+ // Building amd64/x86_64 on aarch64 host
1461
+ // Check both /usr/local/bin (compiled) and system paths
1462
+ let qemuX86Path = null;
1463
+
1464
+ if (shellExec('test -x /usr/local/bin/qemu-system-x86_64', { silent: true }).code === 0) {
1465
+ qemuX86Path = '/usr/local/bin/qemu-system-x86_64';
1466
+ } else if (shellExec('which qemu-system-x86_64', { silent: true }).code === 0) {
1467
+ qemuX86Path = shellExec('which qemu-system-x86_64', { silent: true }).stdout.trim();
1468
+ }
1469
+
1470
+ if (!qemuX86Path) {
1471
+ throw new Error(
1472
+ 'qemu-system-x86_64 is not installed. Please install it to build x86_64 images on aarch64 hosts.\n' +
1473
+ 'Run: node bin baremetal --dev --install-packer',
1474
+ );
1475
+ }
1476
+
1477
+ logger.info(`Found qemu-system-x86_64 at: ${qemuX86Path}`);
1478
+
1479
+ // Verify that the installed qemu supports the 'pc' or 'q35' machine type (required for x86_64)
1480
+ const machineHelp = shellExec(`${qemuX86Path} -machine help`, { silent: true }).stdout;
1481
+ if (!machineHelp.includes('pc') && !machineHelp.includes('q35')) {
1482
+ throw new Error(
1483
+ 'The installed qemu-system-x86_64 does not support the "pc" or "q35" machine type.\n' +
1484
+ 'Run: node bin baremetal --dev --install-packer',
1485
+ );
1486
+ }
1487
+ }
1488
+ },
1489
+
1194
1490
  /**
1195
1491
  * @method bootConfFactory
1196
1492
  * @description Generates the boot configuration file for specific workflows,
@@ -1262,55 +1558,46 @@ GATEWAY=${gateway}`;
1262
1558
  },
1263
1559
 
1264
1560
  /**
1265
- * @property {object} workflowsConfig
1266
- * @description Configuration for different baremetal provisioning workflows.
1561
+ * @method loadWorkflowsConfig
1562
+ * @namespace UnderpostBaremetal.API
1563
+ * @description Loads the commission workflows configuration from commission-workflows.json.
1267
1564
  * Each workflow defines specific parameters like system provisioning type,
1268
1565
  * kernel version, Chrony settings, debootstrap image details, and NFS mounts. *
1269
1566
  * @memberof UnderpostBaremetal
1270
1567
  */
1271
- workflowsConfig: {
1272
- /**
1273
- * @property {object} rpi4mb
1274
- * @description Configuration for the Raspberry Pi 4 Model B workflow.
1275
- * @memberof UnderpostBaremetal.workflowsConfig
1276
- */
1277
- rpi4mb: {
1278
- menuentryStr: 'UNDERPOST.NET UEFI/GRUB/MAAS RPi4 commissioning (ARM64)',
1279
- systemProvisioning: 'ubuntu', // Specifies the system provisioning factory to use.
1280
- kernelLibVersion: `6.8.0-41-generic`, // The kernel library version for this workflow.
1281
- networkInterfaceName: 'enabcm6e4ei0', // The name of the primary network interface on the RPi4.
1282
- netmask: '255.255.255.0', // Subnet mask for the network.
1283
- firmwares: [
1284
- {
1285
- url: 'https://github.com/pftf/RPi4/releases/download/v1.41/RPi4_UEFI_Firmware_v1.41.zip',
1286
- gateway: '192.168.1.1',
1287
- subnet: '255.255.255.0',
1288
- },
1289
- ],
1290
- chronyc: {
1291
- timezone: 'America/New_York', // Timezone for Chrony configuration.
1292
- chronyConfPath: `/etc/chrony/chrony.conf`, // Path to Chrony configuration file.
1293
- },
1294
- debootstrap: {
1295
- image: {
1296
- architecture: 'arm64', // Architecture for the debootstrap image.
1297
- name: 'noble', // Codename of the Ubuntu release (e.g., 'noble' for 24.04 LTS).
1298
- },
1299
- },
1300
- maas: {
1301
- image: {
1302
- architecture: 'arm64/ga-24.04', // Architecture for MAAS image.
1303
- name: 'ubuntu/noble', // Name of the MAAS Ubuntu image.
1304
- },
1305
- },
1306
- nfs: {
1307
- mounts: {
1308
- // Define NFS mount points and their types (bind, rbind).
1309
- bind: ['/proc', '/sys', '/run'], // Standard bind mounts.
1310
- rbind: ['/dev'], // Recursive bind mount for /dev.
1311
- },
1312
- },
1313
- },
1568
+ loadWorkflowsConfig() {
1569
+ if (this._workflowsConfig) return this._workflowsConfig;
1570
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
1571
+ const configPath = path.resolve(__dirname, '../../baremetal/commission-workflows.json');
1572
+ this._workflowsConfig = fs.readJsonSync(configPath);
1573
+ return this._workflowsConfig;
1574
+ },
1575
+
1576
+ /**
1577
+ * @property {object} packerMaasImageBuildWorkflows
1578
+ * @description Configuration for PACKe mass image workflows.
1579
+ * @memberof UnderpostBaremetal
1580
+ */
1581
+ loadPackerMaasImageBuildWorkflows() {
1582
+ if (this._packerMaasImageBuildWorkflows) return this._packerMaasImageBuildWorkflows;
1583
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
1584
+ const configPath = path.resolve(__dirname, '../../baremetal/packer-workflows.json');
1585
+ this._packerMaasImageBuildWorkflows = fs.readJsonSync(configPath);
1586
+ return this._packerMaasImageBuildWorkflows;
1587
+ },
1588
+
1589
+ /**
1590
+ * Write Packer MAAS image build workflows configuration to file
1591
+ * @param {object} workflows - The workflows configuration object
1592
+ * @description Writes the Packer MAAS image build workflows to packer-workflows.json
1593
+ * @memberof UnderpostBaremetal
1594
+ */
1595
+ writePackerMaasImageBuildWorkflows(workflows) {
1596
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
1597
+ const configPath = path.resolve(__dirname, '../../baremetal/packer-workflows.json');
1598
+ fs.writeJsonSync(configPath, workflows, { spaces: 2 });
1599
+ this._packerMaasImageBuildWorkflows = workflows;
1600
+ return configPath;
1314
1601
  },
1315
1602
  };
1316
1603
  }
@@ -43,7 +43,7 @@ class UnderpostCloudInit {
43
43
  buildTools({ workflowId, nfsHostPath, hostname, callbackMetaData, dev }) {
44
44
  // Destructure workflow configuration for easier access.
45
45
  const { systemProvisioning, chronyc, networkInterfaceName, debootstrap } =
46
- UnderpostBaremetal.API.workflowsConfig[workflowId];
46
+ UnderpostBaremetal.API.loadWorkflowsConfig()[workflowId];
47
47
  const { timezone, chronyConfPath } = chronyc;
48
48
  // Define the specific directory for underpost tools within the NFS host path.
49
49
  const nfsHostToolsPath = `${nfsHostPath}/underpost`;
package/src/cli/env.js CHANGED
@@ -74,17 +74,38 @@ class UnderpostRootEnv {
74
74
  /**
75
75
  * @method list
76
76
  * @description Lists all environment variables in the underpost root environment.
77
+ * @param {string} key - Not used for list operation.
78
+ * @param {string} value - Not used for list operation.
79
+ * @param {object} options - Options for listing environment variables.
80
+ * @param {string} [options.filter] - Filter keyword to match against keys or values.
77
81
  * @memberof UnderpostEnv
78
82
  */
79
- list() {
83
+ list(key, value, options = {}) {
80
84
  const exeRootPath = `${getNpmRootPath()}/underpost`;
81
85
  const envPath = `${exeRootPath}/.env`;
82
86
  if (!fs.existsSync(envPath)) {
83
87
  logger.warn(`Empty environment variables`);
84
88
  return {};
85
89
  }
86
- const env = dotenv.parse(fs.readFileSync(envPath, 'utf8'));
87
- logger.info('underpost root', env);
90
+ let env = dotenv.parse(fs.readFileSync(envPath, 'utf8'));
91
+
92
+ // Apply filter if provided
93
+ if (options.filter) {
94
+ const filterKeyword = options.filter.toLowerCase();
95
+ const filtered = {};
96
+ for (const [envKey, envValue] of Object.entries(env)) {
97
+ const keyMatch = envKey.toLowerCase().includes(filterKeyword);
98
+ const valueMatch = String(envValue).toLowerCase().includes(filterKeyword);
99
+ if (keyMatch || valueMatch) {
100
+ filtered[envKey] = envValue;
101
+ }
102
+ }
103
+ env = filtered;
104
+ logger.info(`underpost root (filtered by: ${options.filter})`, env);
105
+ } else {
106
+ logger.info('underpost root', env);
107
+ }
108
+
88
109
  return env;
89
110
  },
90
111
  /**
package/src/cli/index.js CHANGED
@@ -181,6 +181,7 @@ program
181
181
  .argument('[key]', 'Optional: The specific configuration key to manage.')
182
182
  .argument('[value]', 'Optional: The value to set for the configuration key.')
183
183
  .option('--plain', 'Prints the configuration value in plain text.')
184
+ .option('--filter <keyword>', 'Filters the list by matching key or value (only for list operation).')
184
185
  .description(`Manages Underpost configurations using various operators.`)
185
186
  .action((...args) => Underpost.env[args[0]](args[1], args[2], args[3]));
186
187
 
@@ -610,6 +611,24 @@ program
610
611
  .option('--control-server-uninstall', 'Uninstalls the baremetal control server.')
611
612
  .option('--control-server-db-install', 'Installs up the database for the baremetal control server.')
612
613
  .option('--control-server-db-uninstall', 'Uninstalls the database for the baremetal control server.')
614
+ .option('--install-packer', 'Installs Packer CLI.')
615
+ .option(
616
+ '--packer-maas-image-template <template-path>',
617
+ 'Creates a new image folder from canonical/packer-maas template path (requires workflow-id).',
618
+ )
619
+ .option('--packer-workflow-id <workflow-id>', 'Specifies the workflow ID for Packer MAAS image operations.')
620
+ .option(
621
+ '--packer-maas-image-build',
622
+ 'Builds a MAAS image using Packer for the workflow specified by --packer-workflow-id.',
623
+ )
624
+ .option(
625
+ '--packer-maas-image-upload',
626
+ 'Uploads an existing MAAS image artifact without rebuilding for the workflow specified by --packer-workflow-id.',
627
+ )
628
+ .option(
629
+ '--packer-maas-image-cached',
630
+ 'Continue last build without removing artifacts (used with --packer-maas-image-build).',
631
+ )
613
632
  .option('--commission', 'Init workflow for commissioning a physical machine.')
614
633
  .option('--nfs-build', 'Builds an NFS root filesystem for a workflow id config architecture using QEMU emulation.')
615
634
  .option('--nfs-mount', 'Mounts the NFS root filesystem for a workflow id config architecture.')