@underpostnet/underpost 2.99.5 → 2.99.7

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 (37) hide show
  1. package/.github/workflows/ghpkg.ci.yml +10 -25
  2. package/.github/workflows/npmpkg.ci.yml +13 -2
  3. package/CHANGELOG.md +496 -0
  4. package/README.md +4 -4
  5. package/baremetal/commission-workflows.json +43 -6
  6. package/bin/deploy.js +13 -0
  7. package/cli.md +84 -42
  8. package/examples/static-page/README.md +80 -13
  9. package/jsdoc.json +26 -5
  10. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +47 -0
  11. package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +47 -0
  12. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  13. package/manifests/deployment/dd-test-development/deployment.yaml +2 -2
  14. package/package.json +2 -4
  15. package/scripts/maas-setup.sh +13 -9
  16. package/scripts/rocky-kickstart.sh +294 -0
  17. package/src/cli/baremetal.js +237 -555
  18. package/src/cli/cloud-init.js +27 -45
  19. package/src/cli/index.js +52 -6
  20. package/src/cli/kickstart.js +149 -0
  21. package/src/cli/repository.js +166 -13
  22. package/src/cli/run.js +26 -19
  23. package/src/cli/ssh.js +1 -1
  24. package/src/cli/static.js +27 -1
  25. package/src/cli/system.js +332 -0
  26. package/src/client/components/core/Docs.js +22 -3
  27. package/src/db/DataBaseProvider.js +3 -3
  28. package/src/db/mariadb/MariaDB.js +3 -3
  29. package/src/db/mongo/MongooseDB.js +3 -3
  30. package/src/index.js +28 -5
  31. package/src/mailer/EmailRender.js +3 -3
  32. package/src/mailer/MailerProvider.js +4 -4
  33. package/src/server/backup.js +23 -5
  34. package/src/server/client-build-docs.js +29 -3
  35. package/src/server/conf.js +6 -27
  36. package/src/server/cron.js +354 -135
  37. package/src/server/dns.js +2 -0
@@ -6,7 +6,7 @@
6
6
 
7
7
  import { fileURLToPath } from 'url';
8
8
  import { getNpmRootPath, getUnderpostRootPath } from '../server/conf.js';
9
- import { openTerminal, pbcopy, shellExec } from '../server/process.js';
9
+ import { pbcopy, shellExec } from '../server/process.js';
10
10
  import dotenv from 'dotenv';
11
11
  import { loggerFactory, loggerMiddleware } from '../server/logger.js';
12
12
  import fs from 'fs-extra';
@@ -65,6 +65,7 @@ class UnderpostBaremetal {
65
65
  * @param {boolean} [options.commission=false] - Flag to commission the baremetal machine.
66
66
  * @param {number} [options.bootstrapHttpServerPort=8888] - Port for the bootstrap HTTP server.
67
67
  * @param {string} [options.bootstrapHttpServerPath='./public/localhost'] - Path for the bootstrap HTTP server files.
68
+ * @param {boolean} [options.bootstrapHttpServerRun=false] - Flag to start the bootstrap HTTP server.
68
69
  * @param {string} [options.isoUrl=''] - Uses a custom ISO URL for baremetal machine commissioning.
69
70
  * @param {boolean} [options.ubuntuToolsBuild=false] - Builds ubuntu tools for chroot environment.
70
71
  * @param {boolean} [options.ubuntuToolsTest=false] - Tests ubuntu tools in chroot environment.
@@ -75,6 +76,7 @@ class UnderpostBaremetal {
75
76
  * @param {boolean} [options.nfsBuild=false] - Flag to build the NFS root filesystem.
76
77
  * @param {boolean} [options.nfsBuildServer=false] - Flag to build the NFS server components.
77
78
  * @param {boolean} [options.nfsMount=false] - Flag to mount the NFS root filesystem.
79
+ * @param {boolean} [options.nfsReset=false] - Flag to reset the NFS environment by unmounting and cleaning the host path.
78
80
  * @param {boolean} [options.nfsUnmount=false] - Flag to unmount the NFS root filesystem.
79
81
  * @param {boolean} [options.nfsSh=false] - Flag to chroot into the NFS environment for shell access.
80
82
  * @param {string} [options.logs=''] - Specifies which logs to display ('dhcp', 'cloud', 'machine', 'cloud-config').
@@ -114,6 +116,7 @@ class UnderpostBaremetal {
114
116
  commission: false,
115
117
  bootstrapHttpServerPort: 8888,
116
118
  bootstrapHttpServerPath: './public/localhost',
119
+ bootstrapHttpServerRun: false,
117
120
  isoUrl: '',
118
121
  ubuntuToolsBuild: false,
119
122
  ubuntuToolsTest: false,
@@ -124,6 +127,7 @@ class UnderpostBaremetal {
124
127
  nfsBuild: false,
125
128
  nfsBuildServer: false,
126
129
  nfsMount: false,
130
+ nfsReset: false,
127
131
  nfsUnmount: false,
128
132
  nfsSh: false,
129
133
  logs: '',
@@ -184,47 +188,6 @@ class UnderpostBaremetal {
184
188
  // Define the TFTP root prefix path based
185
189
  const tftpRootPath = `${process.env.TFTP_ROOT}/${tftpPrefix}`;
186
190
 
187
- if (options.ipxeBuildIso) {
188
- let machine = null;
189
-
190
- if (options.cloudInit) {
191
- // Search for an existing machine by hostname to extract system_id for cloud-init
192
- const [searchMachine] = Underpost.baremetal.maasCliExec(`machines read hostname=${hostname}`);
193
-
194
- if (searchMachine) {
195
- logger.info(`Found existing machine ${hostname} with system_id ${searchMachine.system_id}`);
196
- machine = searchMachine;
197
- } else {
198
- // Machine does not exist, create it to obtain a system_id
199
- logger.info(`Machine ${hostname} not found, creating new machine for cloud-init system_id...`);
200
- machine = Underpost.baremetal.machineFactory({
201
- hostname,
202
- ipAddress,
203
- macAddress,
204
- architecture: workflowsConfig[workflowId].architecture,
205
- }).machine;
206
- logger.info(`✓ Machine created with system_id ${machine.system_id}`);
207
- }
208
- }
209
-
210
- await Underpost.baremetal.ipxeBuildIso({
211
- workflowId,
212
- isoOutputPath: options.ipxeBuildIso,
213
- tftpPrefix,
214
- ipFileServer,
215
- ipAddress,
216
- ipConfig,
217
- netmask,
218
- dnsServer,
219
- macAddress,
220
- cloudInit: options.cloudInit,
221
- machine,
222
- dev: options.dev,
223
- bootstrapHttpServerPort: options.bootstrapHttpServerPort,
224
- });
225
- return;
226
- }
227
-
228
191
  // Define the iPXE cache directory to preserve builds across tftproot cleanups
229
192
  const ipxeCacheDir = `/tmp/ipxe-cache/${tftpPrefix}`;
230
193
 
@@ -296,6 +259,27 @@ class UnderpostBaremetal {
296
259
  }
297
260
  }
298
261
 
262
+ if (options.ipxeBuildIso)
263
+ return await Underpost.baremetal.ipxeBuildIso({
264
+ workflowId,
265
+ isoOutputPath: options.ipxeBuildIso,
266
+ tftpPrefix,
267
+ ipFileServer,
268
+ ipAddress,
269
+ ipConfig,
270
+ netmask,
271
+ dnsServer,
272
+ macAddress,
273
+ cloudInit: options.cloudInit,
274
+ dev: options.dev,
275
+ forceRebuild: options.ipxeRebuild,
276
+ bootstrapHttpServerPort: Underpost.baremetal.bootstrapHttpServerPortFactory({
277
+ port: options.bootstrapHttpServerPort,
278
+ workflowId,
279
+ workflowsConfig,
280
+ }),
281
+ });
282
+
299
283
  if (options.installPacker) {
300
284
  await Underpost.baremetal.installPacker(underpostRoot);
301
285
  return;
@@ -438,29 +422,9 @@ rm -rf ${artifacts.join(' ')}`);
438
422
 
439
423
  logger.info(`Uploading image to MAAS...`);
440
424
 
441
- // Detect MAAS profile from 'maas list' output
442
- let maasProfile = process.env.MAAS_ADMIN_USERNAME;
443
- if (!maasProfile) {
444
- const profileList = shellExec('maas list', { silent: true, stdout: true });
445
- if (profileList) {
446
- const firstLine = profileList.trim().split('\n')[0];
447
- const match = firstLine.match(/^(\S+)\s+http/);
448
- if (match) {
449
- maasProfile = match[1];
450
- logger.info(`Detected MAAS profile: ${maasProfile}`);
451
- }
452
- }
453
- }
454
-
455
- if (!maasProfile) {
456
- throw new Error(
457
- 'MAAS profile not found. Please run "maas login" first or set MAAS_ADMIN_USERNAME environment variable.',
458
- );
459
- }
460
-
461
425
  // Use the upload script to avoid MAAS CLI bugs
462
426
  const uploadScript = `${underpostRoot}/scripts/maas-upload-boot-resource.sh`;
463
- const uploadCmd = `${uploadScript} ${maasProfile} "${workflow.maas.name}" "${workflow.maas.title}" "${workflow.maas.architecture}" "${workflow.maas.base_image}" "${workflow.maas.filetype}" "${tarballPath}"`;
427
+ const uploadCmd = `${uploadScript} ${process.env.MAAS_ADMIN_USERNAME} "${workflow.maas.name}" "${workflow.maas.title}" "${workflow.maas.architecture}" "${workflow.maas.base_image}" "${workflow.maas.filetype}" "${tarballPath}"`;
464
428
 
465
429
  logger.info(`Uploading to MAAS using: ${uploadScript}`);
466
430
  const uploadResult = shellExec(uploadCmd);
@@ -703,7 +667,11 @@ rm -rf ${artifacts.join(' ')}`);
703
667
  bootstrapArch,
704
668
  callbackMetaData,
705
669
  steps: [
706
- `dnf install -y --allowerasing ${allPackages.join(' ')} 2>/dev/null || yum install -y --allowerasing ${allPackages.join(' ')} 2>/dev/null || echo "Package install completed"`,
670
+ `dnf install -y --allowerasing ${allPackages.join(
671
+ ' ',
672
+ )} 2>/dev/null || yum install -y --allowerasing ${allPackages.join(
673
+ ' ',
674
+ )} 2>/dev/null || echo "Package install completed"`,
707
675
  `dnf clean all`,
708
676
  `echo "=== Installed packages verification ==="`,
709
677
  `rpm -qa | grep -E "dracut|kernel|nfs" | sort`,
@@ -805,13 +773,13 @@ rm -rf ${artifacts.join(' ')}`);
805
773
  bootstrapArch,
806
774
  callbackMetaData,
807
775
  steps: [
808
- ...Underpost.baremetal.systemProvisioningFactory[systemProvisioning].base(),
809
- ...Underpost.baremetal.systemProvisioningFactory[systemProvisioning].user(),
810
- ...Underpost.baremetal.systemProvisioningFactory[systemProvisioning].timezone({
776
+ ...Underpost.system.factory[systemProvisioning].base(),
777
+ ...Underpost.system.factory[systemProvisioning].user(),
778
+ ...Underpost.system.factory[systemProvisioning].timezone({
811
779
  timezone,
812
780
  chronyConfPath,
813
781
  }),
814
- ...Underpost.baremetal.systemProvisioningFactory[systemProvisioning].keyboard(keyboard.layout),
782
+ ...Underpost.system.factory[systemProvisioning].keyboard(keyboard.layout),
815
783
  ],
816
784
  });
817
785
  }
@@ -833,7 +801,6 @@ rm -rf ${artifacts.join(' ')}`);
833
801
  `chmod +x /underpost/shutdown.sh`,
834
802
  `chmod +x /underpost/device_scan.sh`,
835
803
  `chmod +x /underpost/mac.sh`,
836
- `chmod +x /underpost/enlistment.sh`,
837
804
  `sudo chmod 700 ~/.ssh/`, // Set secure permissions for .ssh directory.
838
805
  `sudo chmod 600 ~/.ssh/authorized_keys`, // Set secure permissions for authorized_keys.
839
806
  `sudo chmod 644 ~/.ssh/known_hosts`, // Set permissions for known_hosts.
@@ -859,13 +826,13 @@ rm -rf ${artifacts.join(' ')}`);
859
826
  bootstrapArch,
860
827
  callbackMetaData,
861
828
  steps: [
862
- ...Underpost.baremetal.systemProvisioningFactory[systemProvisioning].base(),
863
- ...Underpost.baremetal.systemProvisioningFactory[systemProvisioning].user(),
864
- ...Underpost.baremetal.systemProvisioningFactory[systemProvisioning].timezone({
829
+ ...Underpost.system.factory[systemProvisioning].base(),
830
+ ...Underpost.system.factory[systemProvisioning].user(),
831
+ ...Underpost.system.factory[systemProvisioning].timezone({
865
832
  timezone,
866
833
  chronyConfPath: chronyc.chronyConfPath,
867
834
  }),
868
- ...Underpost.baremetal.systemProvisioningFactory[systemProvisioning].keyboard(keyboard.layout),
835
+ ...Underpost.system.factory[systemProvisioning].keyboard(keyboard.layout),
869
836
  ],
870
837
  });
871
838
  }
@@ -889,11 +856,14 @@ rm -rf ${artifacts.join(' ')}`);
889
856
  });
890
857
  }
891
858
 
859
+ // Generate MAAS authentication credentials
892
860
  const authCredentials =
893
861
  options.commission || options.cloudInit || options.cloudInitUpdate
894
862
  ? Underpost.baremetal.maasAuthCredentialsFactory()
895
863
  : { consumer_key: '', consumer_secret: '', token_key: '', token_secret: '' };
896
864
 
865
+ // Generate cloud-init configuration if needed for commissioning or cloud-init update workflows.
866
+ let cloudConfigSrc = '';
897
867
  if (options.cloudInit || options.cloudInitUpdate) {
898
868
  const { chronyc, networkInterfaceName } = workflowsConfig[workflowId];
899
869
  const { timezone, chronyConfPath } = chronyc;
@@ -910,7 +880,7 @@ rm -rf ${artifacts.join(' ')}`);
910
880
  runcmd = '/usr/local/bin/underpost-enlist.sh';
911
881
  }
912
882
 
913
- const { cloudConfigSrc } = Underpost.cloudInit.configFactory(
883
+ cloudConfigSrc = Underpost.cloudInit.configFactory(
914
884
  {
915
885
  controlServerIp: callbackMetaData.runnerHost.ip,
916
886
  hostname,
@@ -926,13 +896,45 @@ rm -rf ${artifacts.join(' ')}`);
926
896
  write_files,
927
897
  },
928
898
  authCredentials,
929
- );
899
+ ).cloudConfigSrc;
900
+ }
901
+
902
+ // Rocky/RHEL Kickstart generation
903
+ let kickstartSrc = '';
904
+ if (Underpost.baremetal.getFamilyBaseOs(workflowsConfig[workflowId].osIdLike).isRhelBased) {
905
+ kickstartSrc = Underpost.kickstart.kickstartFactory({
906
+ lang: 'en_US.UTF-8',
907
+ keyboard: workflowsConfig[workflowId].keyboard?.layout,
908
+ timezone: workflowsConfig[workflowId].chronyc?.timezone,
909
+ rootPassword: process.env.MAAS_ADMIN_PASS,
910
+ authorizedKeys: fs.readFileSync('/home/dd/engine/engine-private/deploy/id_rsa.pub', 'utf8').trim(),
911
+ });
912
+ }
913
+
914
+ // Build and optionally run the HTTP bootstrap server to serve cloud-init, kickstart, and ISO resources for commissioning and provisioning.
915
+ if (cloudConfigSrc || kickstartSrc || workflowsConfig[workflowId].isoUrl)
930
916
  Underpost.baremetal.httpBootstrapServerStaticFactory({
931
917
  bootstrapHttpServerPath,
932
918
  hostname,
933
919
  cloudConfigSrc,
920
+ kickstartSrc,
934
921
  isoUrl: workflowsConfig[workflowId].isoUrl,
935
922
  });
923
+
924
+ // Set up iptables rules for NAT and port forwarding to enable network connectivity for the baremetal machines.
925
+ shellExec(`${underpostRoot}/scripts/nat-iptables.sh`, { silent: true });
926
+
927
+ // Start HTTP bootstrap server if commissioning or if ISO URL is used (for ISO-based workflows).
928
+ if (options.bootstrapHttpServerRun || options.commission) {
929
+ Underpost.baremetal.httpBootstrapServerRunnerFactory({
930
+ hostname,
931
+ bootstrapHttpServerPath,
932
+ bootstrapHttpServerPort: Underpost.baremetal.bootstrapHttpServerPortFactory({
933
+ port: options.bootstrapHttpServerPort,
934
+ workflowId,
935
+ workflowsConfig,
936
+ }),
937
+ });
936
938
  }
937
939
 
938
940
  // Rebuild NFS server configuration.
@@ -941,12 +943,12 @@ rm -rf ${artifacts.join(' ')}`);
941
943
  (workflowsConfig[workflowId].type === 'iso-nfs' ||
942
944
  workflowsConfig[workflowId].type === 'chroot-debootstrap' ||
943
945
  workflowsConfig[workflowId].type === 'chroot-container')
944
- ) {
945
- shellExec(`${underpostRoot}/scripts/nat-iptables.sh`);
946
+ )
946
947
  Underpost.baremetal.rebuildNfsServer({
947
948
  nfsHostPath,
949
+ nfsReset: options.nfsReset,
948
950
  });
949
- }
951
+
950
952
  // Handle commissioning tasks
951
953
  if (options.commission === true) {
952
954
  let { firmwares, networkInterfaceName, maas, menuentryStr, type } = workflowsConfig[workflowId];
@@ -961,9 +963,11 @@ rm -rf ${artifacts.join(' ')}`);
961
963
  );
962
964
  logger.info('Commissioning resource', resource);
963
965
 
964
- if (type === 'iso-nfs') {
966
+ if (
967
+ Underpost.baremetal.getFamilyBaseOs(workflowsConfig[workflowId].osIdLike).isDebianBased &&
968
+ (type === 'iso-nfs' || type === 'chroot-debootstrap' || type === 'chroot-container')
969
+ ) {
965
970
  // Prepare NFS casper path if using NFS boot.
966
- shellExec(`sudo rm -rf ${nfsHostPath}`);
967
971
  shellExec(`mkdir -p ${nfsHostPath}/casper`);
968
972
  }
969
973
 
@@ -1038,7 +1042,11 @@ rm -rf ${artifacts.join(' ')}`);
1038
1042
  networkInterfaceName,
1039
1043
  fileSystemUrl:
1040
1044
  type === 'iso-ram'
1041
- ? `http://${callbackMetaData.runnerHost.ip}:${Underpost.baremetal.bootstrapHttpServerPortFactory({ port: options.bootstrapHttpServerPort, workflowId, workflowsConfig })}/${hostname}/${kernelFilesPaths.isoUrl.split('/').pop()}`
1045
+ ? `http://${callbackMetaData.runnerHost.ip}:${Underpost.baremetal.bootstrapHttpServerPortFactory({
1046
+ port: options.bootstrapHttpServerPort,
1047
+ workflowId,
1048
+ workflowsConfig,
1049
+ })}/${hostname}/${kernelFilesPaths.isoUrl.split('/').pop()}`
1042
1050
  : kernelFilesPaths.isoUrl,
1043
1051
  bootstrapHttpServerPort: Underpost.baremetal.bootstrapHttpServerPortFactory({
1044
1052
  port: options.bootstrapHttpServerPort,
@@ -1048,10 +1056,10 @@ rm -rf ${artifacts.join(' ')}`);
1048
1056
  type,
1049
1057
  macAddress,
1050
1058
  cloudInit: options.cloudInit,
1051
- machine,
1052
1059
  dev: options.dev,
1053
1060
  osIdLike: workflowsConfig[workflowId].osIdLike || '',
1054
1061
  authCredentials,
1062
+ architecture: workflowsConfig[workflowId].architecture,
1055
1063
  });
1056
1064
 
1057
1065
  // Check if iPXE mode is enabled AND the iPXE EFI binary exists
@@ -1122,16 +1130,6 @@ rm -rf ${artifacts.join(' ')}`);
1122
1130
  shellExec(`sudo chown -R $(whoami):$(whoami) ${process.env.TFTP_ROOT}`);
1123
1131
  shellExec(`sudo sudo chmod 755 ${process.env.TFTP_ROOT}`);
1124
1132
 
1125
- Underpost.baremetal.httpBootstrapServerRunnerFactory({
1126
- hostname,
1127
- bootstrapHttpServerPath,
1128
- bootstrapHttpServerPort: Underpost.baremetal.bootstrapHttpServerPortFactory({
1129
- port: options.bootstrapHttpServerPort,
1130
- workflowId,
1131
- workflowsConfig,
1132
- }),
1133
- });
1134
-
1135
1133
  if (type === 'chroot-debootstrap' || type === 'chroot-container')
1136
1134
  await Underpost.baremetal.nfsMountCallback({
1137
1135
  hostname,
@@ -1155,49 +1153,6 @@ rm -rf ${artifacts.join(' ')}`);
1155
1153
  const { discovery, machine: discoveredMachine } =
1156
1154
  await Underpost.baremetal.commissionMonitor(commissionMonitorPayload);
1157
1155
  if (discoveredMachine) machine = discoveredMachine;
1158
-
1159
- if (machine) {
1160
- const write_files = Underpost.baremetal.commissioningWriteFilesFactory({
1161
- machine,
1162
- authCredentials,
1163
- runnerHostIp: callbackMetaData.runnerHost.ip,
1164
- });
1165
-
1166
- const { cloudConfigSrc } = Underpost.cloudInit.configFactory(
1167
- {
1168
- controlServerIp: callbackMetaData.runnerHost.ip,
1169
- hostname,
1170
- commissioningDeviceIp: ipAddress,
1171
- gatewayip: callbackMetaData.runnerHost.ip,
1172
- mac: macAddress,
1173
- timezone: workflowsConfig[workflowId].chronyc.timezone,
1174
- chronyConfPath: workflowsConfig[workflowId].chronyc.chronyConfPath,
1175
- networkInterfaceName: workflowsConfig[workflowId].networkInterfaceName,
1176
- ubuntuToolsBuild: options.ubuntuToolsBuild,
1177
- bootcmd: options.bootcmd,
1178
- runcmd: '/usr/local/bin/underpost-enlist.sh',
1179
- write_files,
1180
- },
1181
- authCredentials,
1182
- );
1183
-
1184
- Underpost.baremetal.httpBootstrapServerStaticFactory({
1185
- bootstrapHttpServerPath,
1186
- hostname,
1187
- cloudConfigSrc,
1188
- isoUrl: workflowsConfig[workflowId].isoUrl,
1189
- });
1190
- }
1191
-
1192
- if ((type === 'chroot-debootstrap' || type === 'chroot-container') && options.cloudInit === true) {
1193
- openTerminal(`node ${underpostRoot}/bin baremetal ${workflowId} ${ipAddress} ${hostname} --logs cloud-init`);
1194
- openTerminal(
1195
- `node ${underpostRoot}/bin baremetal ${workflowId} ${ipAddress} ${hostname} --logs cloud-init-machine`,
1196
- );
1197
- shellExec(
1198
- `node ${underpostRoot}/bin baremetal ${workflowId} ${ipAddress} ${hostname} --logs cloud-init-config`,
1199
- );
1200
- }
1201
1156
  }
1202
1157
  },
1203
1158
 
@@ -1228,8 +1183,8 @@ rm -rf ${artifacts.join(' ')}`);
1228
1183
  * @param {string} [params.dnsServer='8.8.8.8'] - The DNS server address.
1229
1184
  * @param {string} [params.macAddress=''] - The MAC address of the client machine.
1230
1185
  * @param {boolean} [params.cloudInit=false] - Flag to enable cloud-init.
1231
- * @param {object} [params.machine=null] - The machine object containing system_id for cloud-init.
1232
1186
  * @param {boolean} [params.dev=false] - Development mode flag to determine paths.
1187
+ * @param {boolean} [params.forceRebuild=false] - Force a complete iPXE rebuild. Without this, reuses existing ISO.
1233
1188
  * @param {number} [params.bootstrapHttpServerPort=8888] - Port for the bootstrap HTTP server used in ISO RAM workflows.
1234
1189
  * @memberof UnderpostBaremetal
1235
1190
  * @returns {Promise<void>}
@@ -1245,12 +1200,11 @@ rm -rf ${artifacts.join(' ')}`);
1245
1200
  dnsServer,
1246
1201
  macAddress,
1247
1202
  cloudInit,
1248
- machine,
1249
1203
  dev,
1204
+ forceRebuild = false,
1250
1205
  bootstrapHttpServerPort,
1251
1206
  }) {
1252
1207
  const outputPath = !isoOutputPath || isoOutputPath === '.' ? `./ipxe-${workflowId}.iso` : isoOutputPath;
1253
- if (fs.existsSync(outputPath)) fs.removeSync(outputPath);
1254
1208
  shellExec(`mkdir -p $(dirname ${outputPath})`);
1255
1209
 
1256
1210
  const workflowsConfig = Underpost.baremetal.loadWorkflowsConfig();
@@ -1272,12 +1226,16 @@ rm -rf ${artifacts.join(' ')}`);
1272
1226
  dnsServer,
1273
1227
  fileSystemUrl:
1274
1228
  dev && workflowsConfig[workflowId].type === 'iso-ram'
1275
- ? `http://${ipFileServer}:${Underpost.baremetal.bootstrapHttpServerPortFactory({ port: bootstrapHttpServerPort, workflowId, workflowsConfig })}/${workflowId}/${workflowsConfig[workflowId].isoUrl.split('/').pop()}`
1229
+ ? `http://${ipFileServer}:${Underpost.baremetal.bootstrapHttpServerPortFactory({
1230
+ port: bootstrapHttpServerPort,
1231
+ workflowId,
1232
+ workflowsConfig,
1233
+ })}/${workflowId}/${workflowsConfig[workflowId].isoUrl.split('/').pop()}`
1276
1234
  : workflowsConfig[workflowId].isoUrl,
1277
1235
  type: workflowsConfig[workflowId].type,
1236
+ architecture: workflowsConfig[workflowId].architecture,
1278
1237
  macAddress,
1279
1238
  cloudInit,
1280
- machine,
1281
1239
  osIdLike: workflowsConfig[workflowId].osIdLike,
1282
1240
  networkInterfaceName: workflowsConfig[workflowId].networkInterfaceName,
1283
1241
  authCredentials,
@@ -1293,20 +1251,18 @@ rm -rf ${artifacts.join(' ')}`);
1293
1251
  const embedScriptName = `embed_${workflowId}.ipxe`;
1294
1252
  const embedScriptPath = path.join(ipxeSrcDir, embedScriptName);
1295
1253
 
1296
- const embedScriptContent = `#!ipxe
1297
- dhcp
1298
- set server_ip ${ipFileServer}
1299
- set tftp_prefix ${tftpPrefix}
1300
- kernel tftp://\${server_ip}/\${tftp_prefix}/pxe/vmlinuz-efi ${cmd}
1301
- initrd tftp://\${server_ip}/\${tftp_prefix}/pxe/initrd.img
1302
- boot || shell
1303
- `;
1254
+ const embedScriptContent = Underpost.baremetal.ipxeScriptFactory({
1255
+ maasIp: ipFileServer,
1256
+ tftpPrefix,
1257
+ kernelCmd: cmd,
1258
+ minimal: true,
1259
+ });
1304
1260
 
1305
1261
  fs.writeFileSync(embedScriptPath, embedScriptContent);
1306
1262
  logger.info(`Created embedded script at ${embedScriptPath}`);
1307
1263
 
1308
1264
  // Determine target architecture
1309
- let targetArch = 'x86_64'; // Default to x86_64
1265
+ let targetArch = 'x86_64';
1310
1266
  if (
1311
1267
  workflowsConfig[workflowId].architecture === 'arm64' ||
1312
1268
  workflowsConfig[workflowId].architecture === 'aarch64'
@@ -1314,32 +1270,38 @@ boot || shell
1314
1270
  targetArch = 'arm64';
1315
1271
  }
1316
1272
 
1317
- // Determine host architecture
1318
- const hostArch = process.arch === 'arm64' ? 'arm64' : 'x86_64';
1319
-
1320
- let crossCompile = '';
1321
- if (hostArch === 'x86_64' && targetArch === 'arm64') {
1322
- crossCompile = 'CROSS_COMPILE=aarch64-linux-gnu-';
1323
- } else if (hostArch === 'arm64' && targetArch === 'x86_64') {
1324
- crossCompile = 'CROSS_COMPILE=x86_64-linux-gnu-';
1325
- }
1326
-
1327
1273
  const platformDir = targetArch === 'arm64' ? 'bin-arm64-efi' : 'bin-x86_64-efi';
1328
1274
  const makeTarget = `${platformDir}/ipxe.iso`;
1329
-
1330
- logger.info(
1331
- `Building iPXE ISO for ${targetArch} on ${hostArch}: make ${makeTarget} ${crossCompile} EMBED=${embedScriptName}`,
1332
- );
1333
-
1334
- const buildCmd = `cd ${ipxeSrcDir} && make ${makeTarget} ${crossCompile} EMBED=${embedScriptName}`;
1335
- shellExec(buildCmd);
1336
-
1337
1275
  const builtIsoPath = path.join(ipxeSrcDir, makeTarget);
1338
- if (fs.existsSync(builtIsoPath)) {
1276
+
1277
+ if (!forceRebuild && fs.existsSync(builtIsoPath)) {
1339
1278
  fs.copySync(builtIsoPath, outputPath);
1340
- logger.info(`ISO successfully built and copied to ${outputPath}`);
1279
+ logger.info(`Reusing existing iPXE ISO: ${builtIsoPath} -> ${outputPath}`);
1341
1280
  } else {
1342
- logger.error(`Failed to build ISO at ${builtIsoPath}`);
1281
+ logger.info(`Rebuild: cleaning iPXE build artifacts...`);
1282
+ shellExec(`cd ${ipxeSrcDir} && make clean`);
1283
+
1284
+ const hostArch = process.arch === 'arm64' ? 'arm64' : 'x86_64';
1285
+ let crossCompile = '';
1286
+ if (hostArch === 'x86_64' && targetArch === 'arm64') {
1287
+ crossCompile = 'CROSS_COMPILE=aarch64-linux-gnu-';
1288
+ } else if (hostArch === 'arm64' && targetArch === 'x86_64') {
1289
+ crossCompile = 'CROSS_COMPILE=x86_64-linux-gnu-';
1290
+ }
1291
+
1292
+ logger.info(
1293
+ `Building iPXE ISO for ${targetArch} on ${hostArch}: make ${makeTarget} ${crossCompile} EMBED=${embedScriptName}`,
1294
+ );
1295
+
1296
+ const buildCmd = `cd ${ipxeSrcDir} && make ${makeTarget} ${crossCompile} EMBED=${embedScriptName}`;
1297
+ shellExec(buildCmd);
1298
+
1299
+ if (fs.existsSync(builtIsoPath)) {
1300
+ fs.copySync(builtIsoPath, outputPath);
1301
+ logger.info(`ISO successfully built and copied to ${outputPath}`);
1302
+ } else {
1303
+ logger.error(`Failed to build ISO at ${builtIsoPath}`);
1304
+ }
1343
1305
  }
1344
1306
  },
1345
1307
 
@@ -1410,8 +1372,7 @@ boot || shell
1410
1372
  const isoFilename = isoUrl.split('/').pop();
1411
1373
 
1412
1374
  // Determine OS family from osIdLike
1413
- const isDebianBased = osIdLike && osIdLike.match(/debian|ubuntu/i);
1414
- const isRhelBased = osIdLike && osIdLike.match(/rhel|centos|fedora|alma|rocky/i);
1375
+ const { isDebianBased, isRhelBased } = Underpost.baremetal.getFamilyBaseOs(osIdLike);
1415
1376
 
1416
1377
  // Set extraction directory based on OS family
1417
1378
  const extractDirName = isDebianBased ? 'casper' : 'iso-extract';
@@ -1437,7 +1398,7 @@ boot || shell
1437
1398
  logger.info(`Downloaded ISO to ${isoPath} (${(stats.size / 1024 / 1024 / 1024).toFixed(2)} GB)`);
1438
1399
  }
1439
1400
 
1440
- // Mount ISO and extract boot files
1401
+ // ISO Logic
1441
1402
  const mountPoint = `${nfsHostPath}/mnt-iso-${arch}`;
1442
1403
  shellExec(`mkdir -p ${mountPoint}`);
1443
1404
 
@@ -1446,7 +1407,7 @@ boot || shell
1446
1407
 
1447
1408
  try {
1448
1409
  // Mount the ISO
1449
- shellExec(`sudo mount -o loop,ro ${isoPath} ${mountPoint}`, { silent: false });
1410
+ shellExec(`sudo mount -o loop,ro ${isoPath} ${mountPoint}`);
1450
1411
  logger.info(`Mounted ISO at ${mountPoint}`);
1451
1412
 
1452
1413
  // Distribution-specific extraction logic
@@ -1458,6 +1419,15 @@ boot || shell
1458
1419
  logger.info(`Checking casper directory contents...`);
1459
1420
  shellExec(`ls -la ${mountPoint}/casper/ 2>/dev/null || echo "casper directory not found"`);
1460
1421
  shellExec(`sudo cp -a ${mountPoint}/casper/* ${extractDir}/`);
1422
+ } else if (isRhelBased) {
1423
+ // RHEL/Rocky: Extract from images/pxeboot directory
1424
+ const pxebootDir = `${mountPoint}/images/pxeboot`;
1425
+ if (!fs.existsSync(pxebootDir)) {
1426
+ throw new Error(`Failed to mount ISO or images/pxeboot directory not found: ${isoPath}`);
1427
+ }
1428
+ logger.info(`Extracting kernel and initrd from ${pxebootDir}...`);
1429
+ shellExec(`sudo cp -a ${pxebootDir}/vmlinuz ${extractDir}/vmlinuz`);
1430
+ shellExec(`sudo cp -a ${pxebootDir}/initrd.img ${extractDir}/initrd`);
1461
1431
  }
1462
1432
  } finally {
1463
1433
  shellExec(`ls -la ${mountPoint}/`);
@@ -1522,6 +1492,20 @@ boot || shell
1522
1492
  }
1523
1493
  },
1524
1494
 
1495
+ /**
1496
+ * @method getFamilyBaseOs
1497
+ * @description Determines if the OS belongs to Debian-based or RHEL-based family based on osIdLike string.
1498
+ * @param {string} osIdLike - The os_id_like string from MAAS boot resource or workflow configuration.
1499
+ * @returns {object} An object with boolean properties isDebianBased and isRhelBased indicating the OS family.
1500
+ * @memberof UnderpostBaremetal
1501
+ */
1502
+ getFamilyBaseOs(osIdLike = '') {
1503
+ return {
1504
+ isDebianBased: osIdLike.match(/debian|ubuntu/i),
1505
+ isRhelBased: osIdLike.match(/rhel|centos|fedora|alma|rocky/i),
1506
+ };
1507
+ },
1508
+
1525
1509
  /**
1526
1510
  * @method kernelFactory
1527
1511
  * @description Retrieves kernel, initrd, and root filesystem paths from a MAAS boot resource.
@@ -1538,8 +1522,10 @@ boot || shell
1538
1522
  // For disk-based commissioning (casper/iso), use live ISO files
1539
1523
  if (type === 'iso-ram' || type === 'iso-nfs') {
1540
1524
  logger.info('Using live ISO for boot (disk-based commissioning)');
1541
- const arch = resource.architecture.split('/')[0];
1542
1525
  const workflowsConfig = Underpost.baremetal.loadWorkflowsConfig();
1526
+ const arch = resource?.architecture
1527
+ ? resource.architecture.split('/')[0]
1528
+ : workflowsConfig[workflowId].architecture;
1543
1529
  const kernelFilesPaths = Underpost.baremetal.downloadISO({
1544
1530
  resource,
1545
1531
  architecture: arch,
@@ -1877,16 +1863,28 @@ shell
1877
1863
  * @method ipxeScriptFactory
1878
1864
  * @description Generates the iPXE script content for stable identity.
1879
1865
  * This iPXE script uses directly boots kernel/initrd via TFTP.
1866
+ * When minimal mode is enabled, generates a simple dhcp+kernel+initrd+boot script.
1880
1867
  * @param {object} params - The parameters for generating the script.
1881
- * @param {string} params.maasIp - The IP address of the MAAS server.
1868
+ * @param {string} params.maasIp - The IP address of the MAAS/file server.
1882
1869
  * @param {string} [params.macAddress] - The MAC address registered in MAAS (for display only).
1883
- * @param {string} params.architecture - The architecture (arm64/amd64).
1870
+ * @param {string} [params.architecture] - The architecture (arm64/amd64).
1884
1871
  * @param {string} params.tftpPrefix - The TFTP prefix path (e.g., 'rpi4mb').
1885
1872
  * @param {string} params.kernelCmd - The kernel command line parameters.
1873
+ * @param {boolean} [params.minimal=false] - Generate a minimal embedded script for ISO builds.
1886
1874
  * @returns {string} The iPXE script content.
1887
1875
  * @memberof UnderpostBaremetal
1888
1876
  */
1889
- ipxeScriptFactory({ maasIp, macAddress, architecture, tftpPrefix, kernelCmd }) {
1877
+ ipxeScriptFactory({ maasIp, macAddress, architecture, tftpPrefix, kernelCmd, minimal = false }) {
1878
+ if (minimal) {
1879
+ return `#!ipxe
1880
+ dhcp
1881
+ set server_ip ${maasIp}
1882
+ set tftp_prefix ${tftpPrefix}
1883
+ kernel tftp://\${server_ip}/\${tftp_prefix}/pxe/vmlinuz-efi ${kernelCmd}
1884
+ initrd tftp://\${server_ip}/\${tftp_prefix}/pxe/initrd.img
1885
+ boot || shell
1886
+ `;
1887
+ }
1890
1888
  const macInfo =
1891
1889
  macAddress && macAddress !== '00:00:00:00:00:00'
1892
1890
  ? `echo Registered MAC: ${macAddress}`
@@ -1924,7 +1922,11 @@ echo ========================================
1924
1922
  echo Loading kernel and initrd via TFTP...
1925
1923
  echo Kernel: tftp://${maasIp}/${kernelPath}
1926
1924
  echo Initrd: tftp://${maasIp}/${initrdPath}
1927
- ${macAddress && macAddress !== '00:00:00:00:00:00' ? `echo Kernel will use MAC: ${macAddress} (via ifname parameter)` : 'echo Kernel will use hardware MAC'}
1925
+ ${
1926
+ macAddress && macAddress !== '00:00:00:00:00:00'
1927
+ ? `echo Kernel will use MAC: ${macAddress} (via ifname parameter)`
1928
+ : 'echo Kernel will use hardware MAC'
1929
+ }
1928
1930
  echo ========================================
1929
1931
 
1930
1932
  # Load kernel via TFTP
@@ -1955,7 +1957,9 @@ echo ========================================
1955
1957
  # Fallback: Try MAAS HTTP bootloader (may have certificate issues)
1956
1958
  set boot-url http://${maasIp}:5248/images/bootloader
1957
1959
  echo Boot URL: \${boot-url}
1958
- chain \${boot-url}/uefi/${architecture}/${grubBootloader === 'grubaa64.efi' ? 'bootaa64.efi' : 'bootx64.efi'} || goto error
1960
+ chain \${boot-url}/uefi/${architecture}/${
1961
+ grubBootloader === 'grubaa64.efi' ? 'bootaa64.efi' : 'bootx64.efi'
1962
+ } || goto error
1959
1963
 
1960
1964
  :error
1961
1965
  echo ========================================
@@ -1991,22 +1995,12 @@ shell
1991
1995
  * @memberof UnderpostBaremetal
1992
1996
  */
1993
1997
  ipxeEfiFactory({ tftpRootPath, ipxeCacheDir, arch, underpostRoot, embeddedScriptPath, forceRebuild = false }) {
1994
- const shouldRebuild =
1995
- forceRebuild || (!fs.existsSync(`${tftpRootPath}/ipxe.efi`) && !fs.existsSync(`${ipxeCacheDir}/ipxe.efi`));
1998
+ const embedArg =
1999
+ embeddedScriptPath && fs.existsSync(embeddedScriptPath) ? ` --embed-script ${embeddedScriptPath}` : '';
2000
+ const rebuildArg = forceRebuild ? ' --rebuild' : '';
1996
2001
 
1997
- if (!shouldRebuild) return;
1998
-
1999
- if (embeddedScriptPath && fs.existsSync(embeddedScriptPath)) {
2000
- logger.info('Rebuilding iPXE with embedded boot script...', {
2001
- embeddedScriptPath,
2002
- forced: forceRebuild,
2003
- });
2004
- shellExec(
2005
- `${underpostRoot}/scripts/ipxe-setup.sh ${tftpRootPath} --target-arch ${arch} --embed-script ${embeddedScriptPath} --rebuild`,
2006
- );
2007
- } else if (shouldRebuild) {
2008
- shellExec(`${underpostRoot}/scripts/ipxe-setup.sh ${tftpRootPath} --target-arch ${arch}`);
2009
- }
2002
+ logger.info('Building iPXE EFI binary...', { embeddedScriptPath, forced: forceRebuild });
2003
+ shellExec(`${underpostRoot}/scripts/ipxe-setup.sh ${tftpRootPath} --target-arch ${arch}${embedArg}${rebuildArg}`);
2010
2004
 
2011
2005
  shellExec(`mkdir -p ${ipxeCacheDir}`);
2012
2006
  shellExec(`cp ${tftpRootPath}/ipxe.efi ${ipxeCacheDir}/ipxe.efi`);
@@ -2060,7 +2054,11 @@ shell
2060
2054
  echo "${menuentryStr}"
2061
2055
  echo " ${Underpost.version}"
2062
2056
  echo "Date: ${new Date().toISOString()}"
2063
- ${cmd.match('/MAAS/metadata/by-id/') ? `echo "System ID: ${cmd.split('/MAAS/metadata/by-id/')[1].split('/')[0]}"` : ''}
2057
+ ${
2058
+ cmd.match('/MAAS/metadata/by-id/')
2059
+ ? `echo "System ID: ${cmd.split('/MAAS/metadata/by-id/')[1].split('/')[0]}"`
2060
+ : ''
2061
+ }
2064
2062
  echo "TFTP server: ${tftpIp}"
2065
2063
  echo "Kernel path: ${kernelPath}"
2066
2064
  echo "Initrd path: ${initrdPath}"
@@ -2132,8 +2130,7 @@ echo "Response Body:" | tee -a "$LOG_FILE"
2132
2130
  cat "$RESPONSE_FILE" | tee -a "$LOG_FILE"
2133
2131
 
2134
2132
  if [ "$HTTP_STATUS" -eq 200 ]; then
2135
- echo "Commissioning requested successfully. Rebooting to start commissioning..." | tee -a "$LOG_FILE"
2136
- reboot
2133
+ echo "Commissioning requested successfully." | tee -a "$LOG_FILE"
2137
2134
  else
2138
2135
  echo "ERROR: MAAS commissioning failed with status $HTTP_STATUS" | tee -a "$LOG_FILE"
2139
2136
  exit 0
@@ -2150,34 +2147,24 @@ fi
2150
2147
  * @param {string} params.bootstrapHttpServerPath - The path where static files will be created.
2151
2148
  * @param {string} params.hostname - The hostname of the client machine.
2152
2149
  * @param {string} params.cloudConfigSrc - The cloud-init configuration YAML source.
2153
- * @param {object} [params.metadata] - Optional metadata to include in meta-data file.
2154
- * @param {string} [params.vendorData] - Optional vendor-data content (default: empty string).
2155
- * @param {string} [params.isoUrl] - Optional ISO URL to cache and serve.
2150
+ * @param {string} params.kickstartSrc - The kickstart configuration content.
2151
+ * @param {string} params.vendorData - The cloud-init vendor-data content.
2152
+ * @param {string} params.isoUrl - Optional ISO URL to cache and serve.
2156
2153
  * @memberof UnderpostBaremetal
2157
2154
  * @returns {void}
2158
2155
  */
2159
2156
  httpBootstrapServerStaticFactory({
2160
- bootstrapHttpServerPath,
2161
- hostname,
2162
- cloudConfigSrc,
2163
- metadata = {},
2157
+ bootstrapHttpServerPath = '',
2158
+ hostname = '',
2159
+ cloudConfigSrc = '',
2160
+ kickstartSrc = '',
2164
2161
  vendorData = '',
2165
2162
  isoUrl = '',
2166
2163
  }) {
2167
- // Create directory structure
2168
- shellExec(`mkdir -p ${bootstrapHttpServerPath}/${hostname}/cloud-init`);
2169
-
2170
- // Write user-data file
2171
- fs.writeFileSync(`${bootstrapHttpServerPath}/${hostname}/cloud-init/user-data`, cloudConfigSrc, 'utf8');
2172
-
2173
- // Write meta-data file
2174
- const metaDataContent = `instance-id: ${metadata.instanceId || hostname}\nlocal-hostname: ${metadata.localHostname || hostname}`;
2175
- fs.writeFileSync(`${bootstrapHttpServerPath}/${hostname}/cloud-init/meta-data`, metaDataContent, 'utf8');
2176
-
2177
- // Write vendor-data file
2178
- fs.writeFileSync(`${bootstrapHttpServerPath}/${hostname}/cloud-init/vendor-data`, vendorData, 'utf8');
2164
+ shellExec(`mkdir -p ${bootstrapHttpServerPath}/${hostname}`);
2179
2165
 
2180
- logger.info(`Cloud-init files written to ${bootstrapHttpServerPath}/${hostname}/cloud-init`);
2166
+ Underpost.cloudInit.httpServerStaticFactory({ bootstrapHttpServerPath, hostname, cloudConfigSrc, vendorData });
2167
+ Underpost.kickstart.httpServerStaticFactory({ bootstrapHttpServerPath, hostname, kickstartSrc });
2181
2168
 
2182
2169
  if (isoUrl) {
2183
2170
  const isoFilename = isoUrl.split('/').pop();
@@ -2213,8 +2200,7 @@ fi
2213
2200
  const bootstrapHttpServerPath = options.bootstrapHttpServerPath || './public/localhost';
2214
2201
  const hostname = options.hostname || 'localhost';
2215
2202
 
2216
- shellExec(`mkdir -p ${bootstrapHttpServerPath}/${hostname}/cloud-init`);
2217
- shellExec(`node bin run kill ${port}`);
2203
+ shellExec(`node bin run kill ${port}`, { silent: true });
2218
2204
 
2219
2205
  const app = express();
2220
2206
 
@@ -2253,9 +2239,10 @@ fi
2253
2239
  */
2254
2240
  updateKernelFiles({ commissioningImage, resourcesPath, tftpRootPath, kernelFilesPaths }) {
2255
2241
  // Copy EFI bootloaders to TFTP path.
2256
- const efiFiles = commissioningImage.architecture.match('arm64')
2257
- ? ['bootaa64.efi', 'grubaa64.efi']
2258
- : ['bootx64.efi', 'grubx64.efi'];
2242
+ const arch = resourcesPath.split('/').pop();
2243
+ const efiFiles =
2244
+ arch === 'arm64' || arch === 'aarch64' ? ['bootaa64.efi', 'grubaa64.efi'] : ['bootx64.efi', 'grubx64.efi'];
2245
+
2259
2246
  for (const file of efiFiles) {
2260
2247
  shellExec(`sudo cp -a ${resourcesPath}/${file} ${tftpRootPath}/pxe/${file}`);
2261
2248
  }
@@ -2305,7 +2292,6 @@ fi
2305
2292
  * @param {string} options.macAddress - The MAC address of the client.
2306
2293
  * @param {boolean} options.cloudInit - Whether to include cloud-init parameters.
2307
2294
  * @param {object} options.machine - The machine object containing system_id.
2308
- * @param {string} options.machine.system_id - The system ID of the machine (for MAAS metadata).
2309
2295
  * @param {boolean} [options.dev=false] - Whether to enable dev mode with dracut debugging parameters.
2310
2296
  * @param {string} [options.osIdLike=''] - OS family identifier (e.g., 'rhel centos fedora' or 'debian ubuntu').
2311
2297
  * @param {object} options.authCredentials - Authentication credentials for fetching files (if needed).
@@ -2313,6 +2299,7 @@ fi
2313
2299
  * @param {string} options.authCredentials.consumer_secret - Consumer secret for authentication.
2314
2300
  * @param {string} options.authCredentials.token_key - Token key for authentication.
2315
2301
  * @param {string} options.authCredentials.token_secret - Token secret for authentication.
2302
+ * @param {string} options.architecture - The architecture of the machine (e.g., 'amd64', 'arm64').
2316
2303
  * @returns {object} An object containing the constructed command line string.
2317
2304
  * @memberof UnderpostBaremetal
2318
2305
  */
@@ -2331,10 +2318,10 @@ fi
2331
2318
  type: '',
2332
2319
  macAddress: '',
2333
2320
  cloudInit: false,
2334
- machine: { system_id: '' },
2335
2321
  dev: false,
2336
2322
  osIdLike: '',
2337
2323
  authCredentials: { consumer_key: '', consumer_secret: '', token_key: '', token_secret: '' },
2324
+ architecture,
2338
2325
  },
2339
2326
  ) {
2340
2327
  // Construct kernel command line arguments for NFS boot.
@@ -2353,8 +2340,12 @@ fi
2353
2340
  macAddress,
2354
2341
  cloudInit,
2355
2342
  osIdLike,
2343
+ architecture,
2356
2344
  } = options;
2357
2345
 
2346
+ // Determine OS family from osIdLike
2347
+ const { isDebianBased, isRhelBased } = Underpost.baremetal.getFamilyBaseOs(options.osIdLike);
2348
+
2358
2349
  const ipParam =
2359
2350
  `ip=${ipClient}:${ipFileServer}:${ipDhcpServer}:${netmask}:${hostname}` +
2360
2351
  `:${networkInterfaceName ? networkInterfaceName : 'eth0'}:${ipConfig}:${dnsServer}`;
@@ -2384,7 +2375,9 @@ fi
2384
2375
  : []
2385
2376
  }`;
2386
2377
 
2387
- const nfsRootParam = `nfsroot=${ipFileServer}:${process.env.NFS_EXPORT_PATH}/${hostname}${nfsOptions ? `,${nfsOptions}` : ''}`;
2378
+ const nfsRootParam = `nfsroot=${ipFileServer}:${process.env.NFS_EXPORT_PATH}/${hostname}${
2379
+ nfsOptions ? `,${nfsOptions}` : ''
2380
+ }`;
2388
2381
 
2389
2382
  const permissionsParams = [
2390
2383
  `rw`,
@@ -2402,8 +2395,8 @@ fi
2402
2395
  // `layerfs-path=filesystem.squashfs`,
2403
2396
  // `root=/dev/ram0`,
2404
2397
  // `toram`,
2405
- 'nomodeset',
2406
- `editable_rootfs=tmpfs`,
2398
+ // 'nomodeset',
2399
+ // `editable_rootfs=tmpfs`, // all writes to rootfs go to RAM, keeping underlying storage pristine
2407
2400
  // `ramdisk_size=3550000`,
2408
2401
  // `root=/dev/sda1`, // rpi4 usb port unit
2409
2402
  'apparmor=0', // Disable AppArmor security
@@ -2446,22 +2439,22 @@ fi
2446
2439
 
2447
2440
  let cmd = [];
2448
2441
  if (type === 'iso-ram') {
2449
- const netBootParams = [`netboot=url`];
2450
- if (fileSystemUrl) netBootParams.push(`url=${fileSystemUrl.replace('https', 'http')}`);
2451
- cmd = [ipParam, `boot=casper`, 'toram', ...netBootParams, ...kernelParams, ...performanceParams];
2442
+ if (isRhelBased) {
2443
+ cmd = Underpost.kickstart.kernelParamsFactory(macAddress, [ipParam, ...kernelParams], options);
2444
+ } else {
2445
+ // ISO-RAM (Debian/Ubuntu): full live ISO downloaded into RAM via casper toram.
2446
+ const netBootParams = [`netboot=url`];
2447
+ if (fileSystemUrl) netBootParams.push(`url=${fileSystemUrl.replace('https', 'http')}`);
2448
+ cmd = [ipParam, `boot=casper`, 'toram', ...netBootParams, ...kernelParams, ...performanceParams];
2449
+ }
2452
2450
  } else if (type === 'chroot-debootstrap' || type === 'chroot-container') {
2453
2451
  let qemuNfsRootParams = [`root=/dev/nfs`, `rootfstype=nfs`];
2454
2452
  cmd = [ipParam, ...qemuNfsRootParams, nfsRootParam, ...kernelParams];
2455
2453
  } else {
2456
- // 'iso-nfs'
2454
+ // 'iso-nfs' — Debian/Ubuntu NFS root boot: kernel/initrd from ISO, root filesystem served via NFS.
2457
2455
  cmd = [ipParam, `netboot=nfs`, nfsRootParam, ...kernelParams, ...performanceParams];
2458
- // cmd.push(`ifname=${networkInterfaceName}:${macAddress}`);
2459
2456
  }
2460
2457
 
2461
- // Determine OS family from osIdLike configuration
2462
- const isRhelBased = osIdLike && osIdLike.match(/rhel|centos|fedora|alma|rocky/i);
2463
- const isDebianBased = osIdLike && osIdLike.match(/debian|ubuntu/i);
2464
-
2465
2458
  // Add RHEL/Rocky/Fedora based images specific parameters
2466
2459
  if (isRhelBased) {
2467
2460
  cmd = cmd.concat([`rd.neednet=1`, `rd.timeout=180`, `selinux=0`, `enforcing=0`]);
@@ -2968,320 +2961,6 @@ EOF`);
2968
2961
  throw new Error(`Unsupported host architecture: ${machine}`);
2969
2962
  },
2970
2963
 
2971
- /**
2972
- * @property {object} systemProvisioningFactory
2973
- * @description A factory object containing functions for system provisioning based on OS type.
2974
- * Each OS type (e.g., 'ubuntu') provides methods for base system setup, user creation,
2975
- * timezone configuration, and keyboard layout settings. *
2976
- * @memberof UnderpostBaremetal
2977
- * @namespace UnderpostBaremetal.systemProvisioningFactory
2978
- */
2979
- systemProvisioningFactory: {
2980
- /**
2981
- * @property {object} ubuntu
2982
- * @description Provisioning steps for Ubuntu-based systems.
2983
- * @memberof UnderpostBaremetal.systemProvisioningFactory
2984
- * @namespace UnderpostBaremetal.systemProvisioningFactory.ubuntu
2985
- */
2986
- ubuntu: {
2987
- /**
2988
- * @method base
2989
- * @description Generates shell commands for basic Ubuntu system provisioning.
2990
- * This includes updating package lists, installing essential build tools,
2991
- * kernel modules, cloud-init, SSH server, and other core utilities.
2992
- * @param {object} params - The parameters for the function.
2993
- * @memberof UnderpostBaremetal.systemProvisioningFactory.ubuntu
2994
- * @returns {string[]} An array of shell commands.
2995
- */
2996
- base: () => [
2997
- // Configure APT sources for Ubuntu ports
2998
- `cat <<SOURCES | tee /etc/apt/sources.list
2999
- deb http://ports.ubuntu.com/ubuntu-ports noble main restricted universe multiverse
3000
- deb http://ports.ubuntu.com/ubuntu-ports noble-updates main restricted universe multiverse
3001
- deb http://ports.ubuntu.com/ubuntu-ports noble-security main restricted universe multiverse
3002
- SOURCES`,
3003
-
3004
- // Update package lists and perform a full system upgrade
3005
- `apt update -qq`,
3006
- `apt -y full-upgrade`,
3007
-
3008
- // Install all essential packages in one consolidated step
3009
- `DEBIAN_FRONTEND=noninteractive apt install -y build-essential xinput x11-xkb-utils usbutils uuid-runtime linux-image-generic systemd-sysv openssh-server sudo locales udev util-linux iproute2 netplan.io ca-certificates curl wget chrony apt-utils tzdata kmod keyboard-configuration console-setup iputils-ping`,
3010
-
3011
- // Ensure systemd is the init system
3012
- `ln -sf /lib/systemd/systemd /sbin/init`,
3013
-
3014
- // Clean up
3015
- `apt-get clean`,
3016
- ],
3017
- /**
3018
- * @method user
3019
- * @description Generates shell commands for creating a root user and configuring SSH access.
3020
- * This is a critical security step for initial access to the provisioned system.
3021
- * @memberof UnderpostBaremetal.systemProvisioningFactory.ubuntu
3022
- * @returns {string[]} An array of shell commands.
3023
- */
3024
- user: () => [
3025
- `useradd -m -s /bin/bash -G sudo root`, // Create a root user with bash shell and sudo privileges.
3026
- `echo 'root:root' | chpasswd`, // Set a default password for the root user (consider more secure methods for production).
3027
- `mkdir -p /home/root/.ssh`, // Create .ssh directory for authorized keys.
3028
- // Add the public SSH key to authorized_keys for passwordless login.
3029
- `echo '${fs.readFileSync(
3030
- `/home/dd/engine/engine-private/deploy/id_rsa.pub`,
3031
- 'utf8',
3032
- )}' > /home/root/.ssh/authorized_keys`,
3033
- `chown -R root /home/root/.ssh`, // Set ownership for security.
3034
- `chmod 700 /home/root/.ssh`, // Set permissions for the .ssh directory.
3035
- `chmod 600 /home/root/.ssh/authorized_keys`, // Set permissions for authorized_keys.
3036
- ],
3037
- /**
3038
- * @method timezone
3039
- * @description Generates shell commands for configuring the system timezone and Chrony (NTP client).
3040
- * Accurate time synchronization is essential for logging, security, and distributed systems.
3041
- * @param {object} params - The parameters for the function.
3042
- * @param {string} params.timezone - The timezone string (e.g., 'America/New_York').
3043
- * @param {string} params.chronyConfPath - The path to the Chrony configuration file.
3044
- * @param {string} [alias='chrony'] - The alias for the chrony service.
3045
- * @memberof UnderpostBaremetal.systemProvisioningFactory.ubuntu
3046
- * @returns {string[]} An array of shell commands.
3047
- */
3048
- timezone: ({ timezone, chronyConfPath }, alias = 'chrony') => [
3049
- `export DEBIAN_FRONTEND=noninteractive`, // Set non-interactive mode for Debian packages.
3050
- `ln -fs /usr/share/zoneinfo/${timezone} /etc/localtime`, // Symlink timezone.
3051
- `sudo dpkg-reconfigure --frontend noninteractive tzdata`, // Reconfigure timezone data.
3052
- `sudo timedatectl set-timezone ${timezone}`, // Set timezone using timedatectl.
3053
- `sudo timedatectl set-ntp true`, // Enable NTP synchronization.
3054
-
3055
- // Write the Chrony configuration file.
3056
- `echo '
3057
- # Use public servers from the pool.ntp.org project.
3058
- # Please consider joining the pool (http://www.pool.ntp.org/join.html).
3059
- # pool 2.pool.ntp.org iburst
3060
- server ${process.env.MAAS_NTP_SERVER} iburst
3061
-
3062
- # Record the rate at which the system clock gains/losses time.
3063
- driftfile /var/lib/chrony/drift
3064
-
3065
- # Allow the system clock to be stepped in the first three updates
3066
- # if its offset is larger than 1 second.
3067
- makestep 1.0 3
3068
-
3069
- # Enable kernel synchronization of the real-time clock (RTC).
3070
- rtcsync
3071
-
3072
- # Enable hardware timestamping on all interfaces that support it.
3073
- #hwtimestamp *
3074
-
3075
- # Increase the minimum number of selectable sources required to adjust
3076
- # the system clock.
3077
- #minsources 2
3078
-
3079
- # Allow NTP client access from local network.
3080
- #allow 192.168.0.0/16
3081
-
3082
- # Serve time even if not synchronized to a time source.
3083
- #local stratum 10
3084
-
3085
- # Specify file containing keys for NTP authentication.
3086
- keyfile /etc/chrony.keys
3087
-
3088
- # Get TAI-UTC offset and leap seconds from the system tz database.
3089
- leapsectz right/UTC
3090
-
3091
- # Specify directory for log files.
3092
- logdir /var/log/chrony
3093
-
3094
- # Select which information is logged.
3095
- #log measurements statistics tracking
3096
- ' > ${chronyConfPath}`,
3097
- `systemctl stop ${alias}`, // Stop Chrony service before reconfiguring.
3098
-
3099
- // Enable, restart, and check status of Chrony service.
3100
- `sudo systemctl enable --now ${alias}`,
3101
- `sudo systemctl restart ${alias}`,
3102
-
3103
- // Wait for chrony to synchronize
3104
- `echo "Waiting for chrony to synchronize..."`,
3105
- `for i in {1..30}; do chronyc tracking | grep -q "Leap status : Normal" && break || sleep 2; done`,
3106
-
3107
- `sudo systemctl status ${alias}`,
3108
-
3109
- // Verify Chrony synchronization.
3110
- `chronyc sources`,
3111
- `chronyc tracking`,
3112
-
3113
- `chronyc sourcestats -v`, // Display source statistics.
3114
- `timedatectl status`, // Display current time and date settings.
3115
- ],
3116
- /**
3117
- * @method keyboard
3118
- * @description Generates shell commands for configuring the keyboard layout.
3119
- * This ensures correct input behavior on the provisioned system.
3120
- * @param {string} [keyCode='en'] - The keyboard layout code (e.g., 'en', 'es').
3121
- * @memberof UnderpostBaremetal.systemProvisioningFactory.ubuntu
3122
- * @returns {string[]} An array of shell commands.
3123
- */
3124
- keyboard: (keyCode = 'en') => [
3125
- `sudo locale-gen en_US.UTF-8`,
3126
- `sudo update-locale LANG=en_US.UTF-8`,
3127
- `sudo sed -i 's/XKBLAYOUT="us"/XKBLAYOUT="${keyCode}"/' /etc/default/keyboard`,
3128
- `sudo dpkg-reconfigure --frontend noninteractive keyboard-configuration`,
3129
- `sudo systemctl restart keyboard-setup.service`,
3130
- ],
3131
- },
3132
- /**
3133
- * @property {object} rocky
3134
- * @description Provisioning steps for Rocky Linux-based systems.
3135
- * @memberof UnderpostBaremetal.systemProvisioningFactory
3136
- * @namespace UnderpostBaremetal.systemProvisioningFactory.rocky
3137
- */
3138
- rocky: {
3139
- /**
3140
- * @method base
3141
- * @description Generates shell commands for basic Rocky Linux system provisioning.
3142
- * This includes installing Node.js, npm, and underpost CLI tools.
3143
- * @param {object} params - The parameters for the function.
3144
- * @memberof UnderpostBaremetal.systemProvisioningFactory.rocky
3145
- * @returns {string[]} An array of shell commands.
3146
- */
3147
- base: () => [
3148
- // Update system and install EPEL repository
3149
- `dnf -y update`,
3150
- `dnf -y install epel-release`,
3151
-
3152
- // Install essential system tools (avoiding duplicates from container packages)
3153
- `dnf -y install --allowerasing bzip2 openssh-server nano vim-enhanced less openssl-devel git gnupg2 libnsl perl`,
3154
- `dnf clean all`,
3155
-
3156
- // Install Node.js
3157
- `curl -fsSL https://rpm.nodesource.com/setup_24.x | bash -`,
3158
- `dnf install -y nodejs`,
3159
- `dnf clean all`,
3160
-
3161
- // Verify Node.js and npm versions
3162
- `node --version`,
3163
- `npm --version`,
3164
-
3165
- // Install underpost ci/cd cli
3166
- `npm install -g underpost`,
3167
- `underpost --version`,
3168
- ],
3169
- /**
3170
- * @method user
3171
- * @description Generates shell commands for creating a root user and configuring SSH access on Rocky Linux.
3172
- * This is a critical security step for initial access to the provisioned system.
3173
- * @memberof UnderpostBaremetal.systemProvisioningFactory.rocky
3174
- * @returns {string[]} An array of shell commands.
3175
- */
3176
- user: () => [
3177
- `useradd -m -s /bin/bash -G wheel root`, // Create a root user with bash shell and wheel group (sudo on RHEL)
3178
- `echo 'root:root' | chpasswd`, // Set a default password for the root user
3179
- `mkdir -p /home/root/.ssh`, // Create .ssh directory for authorized keys
3180
- // Add the public SSH key to authorized_keys for passwordless login
3181
- `echo '${fs.readFileSync(
3182
- `/home/dd/engine/engine-private/deploy/id_rsa.pub`,
3183
- 'utf8',
3184
- )}' > /home/root/.ssh/authorized_keys`,
3185
- `chown -R root:root /home/root/.ssh`, // Set ownership for security
3186
- `chmod 700 /home/root/.ssh`, // Set permissions for the .ssh directory
3187
- `chmod 600 /home/root/.ssh/authorized_keys`, // Set permissions for authorized_keys
3188
- ],
3189
- /**
3190
- * @method timezone
3191
- * @description Generates shell commands for configuring the system timezone on Rocky Linux.
3192
- * @param {object} params - The parameters for the function.
3193
- * @param {string} params.timezone - The timezone string (e.g., 'America/Santiago').
3194
- * @param {string} params.chronyConfPath - The path to the Chrony configuration file (optional).
3195
- * @memberof UnderpostBaremetal.systemProvisioningFactory.rocky
3196
- * @returns {string[]} An array of shell commands.
3197
- */
3198
- timezone: ({ timezone, chronyConfPath = '/etc/chrony.conf' }) => [
3199
- // Set system timezone using both methods (for chroot and running system)
3200
- `ln -sf /usr/share/zoneinfo/${timezone} /etc/localtime`,
3201
- `echo '${timezone}' > /etc/timezone`,
3202
- `timedatectl set-timezone ${timezone} 2>/dev/null`,
3203
-
3204
- // Configure chrony with local NTP server and common NTP pools
3205
- `echo '# Local NTP server' > ${chronyConfPath}`,
3206
- `echo 'server 192.168.1.1 iburst prefer' >> ${chronyConfPath}`,
3207
- `echo '' >> ${chronyConfPath}`,
3208
- `echo '# Fallback public NTP servers' >> ${chronyConfPath}`,
3209
- `echo 'server 0.pool.ntp.org iburst' >> ${chronyConfPath}`,
3210
- `echo 'server 1.pool.ntp.org iburst' >> ${chronyConfPath}`,
3211
- `echo 'server 2.pool.ntp.org iburst' >> ${chronyConfPath}`,
3212
- `echo 'server 3.pool.ntp.org iburst' >> ${chronyConfPath}`,
3213
- `echo '' >> ${chronyConfPath}`,
3214
- `echo '# Configuration' >> ${chronyConfPath}`,
3215
- `echo 'driftfile /var/lib/chrony/drift' >> ${chronyConfPath}`,
3216
- `echo 'makestep 1.0 3' >> ${chronyConfPath}`,
3217
- `echo 'rtcsync' >> ${chronyConfPath}`,
3218
- `echo 'logdir /var/log/chrony' >> ${chronyConfPath}`,
3219
-
3220
- // Enable chronyd to start on boot
3221
- `systemctl enable chronyd 2>/dev/null`,
3222
-
3223
- // Create systemd link for boot (works in chroot)
3224
- `mkdir -p /etc/systemd/system/multi-user.target.wants`,
3225
- `ln -sf /usr/lib/systemd/system/chronyd.service /etc/systemd/system/multi-user.target.wants/chronyd.service 2>/dev/null`,
3226
-
3227
- // Start chronyd if systemd is running
3228
- `systemctl start chronyd 2>/dev/null`,
3229
-
3230
- // Restart chronyd to apply configuration
3231
- `systemctl restart chronyd 2>/dev/null`,
3232
-
3233
- // Force immediate time synchronization (only if chronyd is running)
3234
- `chronyc makestep 2>/dev/null`,
3235
-
3236
- // Verify timezone configuration
3237
- `ls -l /etc/localtime`,
3238
- `cat /etc/timezone || echo 'No /etc/timezone file'`,
3239
- `timedatectl status 2>/dev/null || echo 'Timezone set to ${timezone} (timedatectl not available in chroot)'`,
3240
- `chronyc tracking 2>/dev/null || echo 'Chrony configured but not running (will start on boot)'`,
3241
- ],
3242
- /**
3243
- * @method keyboard
3244
- * @description Generates shell commands for configuring the keyboard layout on Rocky Linux.
3245
- * This uses localectl to set the keyboard layout for both console and X11.
3246
- * @param {string} [keyCode='us'] - The keyboard layout code (e.g., 'us', 'es').
3247
- * @memberof UnderpostBaremetal.systemProvisioningFactory.rocky
3248
- * @returns {string[]} An array of shell commands.
3249
- */
3250
- keyboard: (keyCode = 'us') => [
3251
- // Configure vconsole.conf for console keyboard layout (persistent)
3252
- `echo 'KEYMAP=${keyCode}' > /etc/vconsole.conf`,
3253
- `echo 'FONT=latarcyrheb-sun16' >> /etc/vconsole.conf`,
3254
-
3255
- // Configure locale.conf for system locale
3256
- `echo 'LANG=en_US.UTF-8' > /etc/locale.conf`,
3257
- `echo 'LC_ALL=en_US.UTF-8' >> /etc/locale.conf`,
3258
-
3259
- // Set keyboard layout using localectl (works if systemd is running)
3260
- `localectl set-locale LANG=en_US.UTF-8 2>/dev/null`,
3261
- `localectl set-keymap ${keyCode} 2>/dev/null`,
3262
- `localectl set-x11-keymap ${keyCode} 2>/dev/null`,
3263
-
3264
- // Configure X11 keyboard layout file directly
3265
- `mkdir -p /etc/X11/xorg.conf.d`,
3266
- `echo 'Section "InputClass"' > /etc/X11/xorg.conf.d/00-keyboard.conf`,
3267
- `echo ' Identifier "system-keyboard"' >> /etc/X11/xorg.conf.d/00-keyboard.conf`,
3268
- `echo ' MatchIsKeyboard "on"' >> /etc/X11/xorg.conf.d/00-keyboard.conf`,
3269
- `echo ' Option "XkbLayout" "${keyCode}"' >> /etc/X11/xorg.conf.d/00-keyboard.conf`,
3270
- `echo 'EndSection' >> /etc/X11/xorg.conf.d/00-keyboard.conf`,
3271
-
3272
- // Load the keymap immediately (if not in chroot)
3273
- `loadkeys ${keyCode} 2>/dev/null || echo 'Keymap ${keyCode} configured (loadkeys not available in chroot)'`,
3274
-
3275
- // Verify configuration
3276
- `echo 'Keyboard configuration files:'`,
3277
- `cat /etc/vconsole.conf`,
3278
- `cat /etc/locale.conf`,
3279
- `cat /etc/X11/xorg.conf.d/00-keyboard.conf 2>/dev/null || echo 'X11 config created'`,
3280
- `localectl status 2>/dev/null || echo 'Keyboard layout set to ${keyCode} (localectl not available in chroot)'`,
3281
- ],
3282
- },
3283
- },
3284
-
3285
2964
  /**
3286
2965
  * @method rebuildNfsServer
3287
2966
  * @description Configures and restarts the NFS server to export the specified path.
@@ -3290,9 +2969,10 @@ logdir /var/log/chrony
3290
2969
  * @param {string} params.nfsHostPath - The path to the NFS server export.
3291
2970
  * @memberof UnderpostBaremetal
3292
2971
  * @param {string} [params.subnet='192.168.1.0/24'] - The subnet allowed to access the NFS export.
2972
+ * @param {boolean} [params.nfsReset=false] - Flag to completely reset the NFS server (restart service).
3293
2973
  * @returns {void}
3294
2974
  */
3295
- rebuildNfsServer({ nfsHostPath, subnet }) {
2975
+ rebuildNfsServer({ nfsHostPath, subnet, nfsReset }) {
3296
2976
  if (!subnet) subnet = '192.168.1.0/24'; // Default subnet if not provided.
3297
2977
  // Write the NFS exports configuration to /etc/exports.
3298
2978
  fs.writeFileSync(
@@ -3341,9 +3021,11 @@ udp-port = 32766
3341
3021
 
3342
3022
  // Restart the nfs-server service to apply all configuration changes,
3343
3023
  // including port settings from /etc/nfs.conf and export changes.
3344
- logger.info('Restarting nfs-server service...');
3345
- shellExec(`sudo systemctl restart nfs-server`);
3346
- logger.info('NFS server restarted.');
3024
+ if (nfsReset) {
3025
+ logger.info('Restarting nfs-server service...');
3026
+ shellExec(`sudo systemctl restart nfs-server`);
3027
+ logger.info('NFS server restarted.');
3028
+ }
3347
3029
  },
3348
3030
 
3349
3031
  /**