@underpostnet/underpost 2.97.0 → 2.97.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -82,9 +82,12 @@ class UnderpostBaremetal {
82
82
  * @param {string} [options.isoUrl=''] - Uses a custom ISO URL for baremetal machine commissioning.
83
83
  * @param {boolean} [options.ubuntuToolsBuild=false] - Builds ubuntu tools for chroot environment.
84
84
  * @param {boolean} [options.ubuntuToolsTest=false] - Tests ubuntu tools in chroot environment.
85
+ * @param {boolean} [options.rockyToolsBuild=false] - Builds rocky linux tools for chroot environment.
86
+ * @param {boolean} [options.rockyToolsTest=false] - Tests rocky linux tools in chroot environment.
85
87
  * @param {string} [options.bootcmd=''] - Comma-separated list of boot commands to execute.
86
88
  * @param {string} [options.runcmd=''] - Comma-separated list of run commands to execute.
87
89
  * @param {boolean} [options.nfsBuild=false] - Flag to build the NFS root filesystem.
90
+ * @param {boolean} [options.nfsBuildServer=false] - Flag to build the NFS server components.
88
91
  * @param {boolean} [options.nfsMount=false] - Flag to mount the NFS root filesystem.
89
92
  * @param {boolean} [options.nfsUnmount=false] - Flag to unmount the NFS root filesystem.
90
93
  * @param {boolean} [options.nfsSh=false] - Flag to chroot into the NFS environment for shell access.
@@ -127,9 +130,12 @@ class UnderpostBaremetal {
127
130
  isoUrl: '',
128
131
  ubuntuToolsBuild: false,
129
132
  ubuntuToolsTest: false,
133
+ rockyToolsBuild: false,
134
+ rockyToolsTest: false,
130
135
  bootcmd: '',
131
136
  runcmd: '',
132
137
  nfsBuild: false,
138
+ nfsBuildServer: false,
133
139
  nfsMount: false,
134
140
  nfsUnmount: false,
135
141
  nfsSh: false,
@@ -168,13 +174,16 @@ class UnderpostBaremetal {
168
174
  }
169
175
 
170
176
  const tftpPrefix = workflowsConfig[workflowId].tftpPrefix || 'rpi4mb';
171
- // Define the debootstrap architecture.
172
- let debootstrapArch;
177
+ // Define the bootstrap architecture.
178
+ let bootstrapArch;
173
179
 
174
- // Set debootstrap architecture.
175
- if (workflowsConfig[workflowId].type === 'chroot') {
180
+ // Set bootstrap architecture.
181
+ if (workflowsConfig[workflowId].type === 'chroot-debootstrap') {
176
182
  const { architecture } = workflowsConfig[workflowId].debootstrap.image;
177
- debootstrapArch = architecture;
183
+ bootstrapArch = architecture;
184
+ } else if (workflowsConfig[workflowId].type === 'chroot-container') {
185
+ const { architecture } = workflowsConfig[workflowId].container;
186
+ bootstrapArch = architecture;
178
187
  }
179
188
 
180
189
  // Define the database provider ID.
@@ -186,6 +195,9 @@ class UnderpostBaremetal {
186
195
  // Define the TFTP root prefix path based
187
196
  const tftpRootPath = `${process.env.TFTP_ROOT}/${tftpPrefix}`;
188
197
 
198
+ // Define the iPXE cache directory to preserve builds across tftproot cleanups
199
+ const ipxeCacheDir = `/tmp/ipxe-cache/${tftpPrefix}`;
200
+
189
201
  // Define the bootstrap HTTP server path.
190
202
  const bootstrapHttpServerPath = options.bootstrapHttpServerPath
191
203
  ? options.bootstrapHttpServerPath
@@ -479,14 +491,9 @@ rm -rf ${artifacts.join(' ')}`);
479
491
 
480
492
  // Handle NFS shell access option.
481
493
  if (options.nfsSh === true) {
482
- const workflowsConfig = UnderpostBaremetal.API.loadWorkflowsConfig();
483
- if (!workflowsConfig[workflowId]) {
484
- throw new Error(`Workflow configuration not found for ID: ${workflowId}`);
485
- }
486
- const { debootstrap } = workflowsConfig[workflowId];
487
494
  // Copy the chroot command to the clipboard for easy execution.
488
- if (debootstrap.image.architecture !== callbackMetaData.runnerHost.architecture)
489
- switch (debootstrap.image.architecture) {
495
+ if (bootstrapArch && bootstrapArch !== callbackMetaData.runnerHost.architecture)
496
+ switch (bootstrapArch) {
490
497
  case 'arm64':
491
498
  pbcopy(`sudo chroot ${nfsHostPath} /usr/bin/qemu-aarch64-static /bin/bash`);
492
499
  break;
@@ -515,9 +522,9 @@ rm -rf ${artifacts.join(' ')}`);
515
522
  // Handle control server uninstallation.
516
523
  if (options.controlServerUninstall === true) {
517
524
  // Stop and remove MAAS services, handling potential errors gracefully.
518
- shellExec(`sudo snap stop maas.pebble || true`);
525
+ shellExec(`sudo snap stop maas.pebble`);
519
526
  shellExec(`sudo snap stop maas`);
520
- shellExec(`sudo snap remove maas --purge || true`);
527
+ shellExec(`sudo snap remove maas --purge`);
521
528
 
522
529
  // Remove residual snap data to ensure a clean uninstall.
523
530
  shellExec(`sudo rm -rf /var/snap/maas`);
@@ -555,7 +562,7 @@ rm -rf ${artifacts.join(' ')}`);
555
562
 
556
563
  // Handle NFS mount operation.
557
564
  if (options.nfsMount === true) {
558
- const { isMounted } = UnderpostBaremetal.API.nfsMountCallback({
565
+ await UnderpostBaremetal.API.nfsMountCallback({
559
566
  hostname,
560
567
  nfsHostPath,
561
568
  workflowId,
@@ -566,7 +573,7 @@ rm -rf ${artifacts.join(' ')}`);
566
573
 
567
574
  // Handle NFS unmount operation.
568
575
  if (options.nfsUnmount === true) {
569
- const { isMounted } = UnderpostBaremetal.API.nfsMountCallback({
576
+ await UnderpostBaremetal.API.nfsMountCallback({
570
577
  hostname,
571
578
  nfsHostPath,
572
579
  workflowId,
@@ -577,21 +584,19 @@ rm -rf ${artifacts.join(' ')}`);
577
584
 
578
585
  // Handle NFS root filesystem build operation.
579
586
  if (options.nfsBuild === true) {
580
- const { isMounted } = UnderpostBaremetal.API.nfsMountCallback({
587
+ await UnderpostBaremetal.API.nfsMountCallback({
581
588
  hostname,
582
589
  nfsHostPath,
583
590
  workflowId,
584
591
  unmount: true,
585
592
  });
586
593
 
587
- if (isMounted) throw new Error(`NFS path ${nfsHostPath} is currently mounted. Please unmount before building.`);
588
-
589
594
  // Clean and create the NFS host path.
590
595
  shellExec(`sudo rm -rf ${nfsHostPath}/*`);
591
596
  shellExec(`mkdir -p ${nfsHostPath}`);
592
597
 
593
598
  // Perform the first stage of debootstrap.
594
- {
599
+ if (workflowsConfig[workflowId].type === 'chroot-debootstrap') {
595
600
  const { architecture, name } = workflowsConfig[workflowId].debootstrap.image;
596
601
  shellExec(
597
602
  [
@@ -604,6 +609,12 @@ rm -rf ${artifacts.join(' ')}`);
604
609
  `http://ports.ubuntu.com/ubuntu-ports/`,
605
610
  ].join(' '),
606
611
  );
612
+ } else if (workflowsConfig[workflowId].type === 'chroot-container') {
613
+ const { image } = workflowsConfig[workflowId].container;
614
+ shellExec(`sudo podman pull --arch=${bootstrapArch} ${image}`);
615
+ shellExec(`sudo podman create --arch=${bootstrapArch} --name chroot-source ${image}`);
616
+ shellExec(`sudo podman export chroot-source | sudo tar -x -C ${nfsHostPath}`);
617
+ shellExec(`sudo podman rm chroot-source`);
607
618
  }
608
619
 
609
620
  // Create a podman container to extract QEMU static binaries.
@@ -611,10 +622,10 @@ rm -rf ${artifacts.join(' ')}`);
611
622
  shellExec(`podman ps -a`); // List all podman containers for verification.
612
623
 
613
624
  // If cross-architecture, copy the QEMU static binary into the chroot.
614
- if (debootstrapArch !== callbackMetaData.runnerHost.architecture)
625
+ if (bootstrapArch !== callbackMetaData.runnerHost.architecture)
615
626
  UnderpostBaremetal.API.crossArchBinFactory({
616
627
  nfsHostPath,
617
- debootstrapArch,
628
+ bootstrapArch,
618
629
  });
619
630
 
620
631
  // Clean up the temporary podman container.
@@ -623,7 +634,7 @@ rm -rf ${artifacts.join(' ')}`);
623
634
  shellExec(`file ${nfsHostPath}/bin/bash`); // Verify the bash executable in the chroot.
624
635
 
625
636
  // Mount necessary filesystems and register binfmt for the second stage.
626
- UnderpostBaremetal.API.nfsMountCallback({
637
+ await UnderpostBaremetal.API.nfsMountCallback({
627
638
  hostname,
628
639
  nfsHostPath,
629
640
  workflowId,
@@ -631,13 +642,96 @@ rm -rf ${artifacts.join(' ')}`);
631
642
  });
632
643
 
633
644
  // Perform the second stage of debootstrap within the chroot environment.
634
- UnderpostBaremetal.API.crossArchRunner({
635
- nfsHostPath,
636
- debootstrapArch,
637
- callbackMetaData,
638
- steps: [`/debootstrap/debootstrap --second-stage`],
639
- });
640
- return;
645
+ if (workflowsConfig[workflowId].type === 'chroot-debootstrap') {
646
+ UnderpostBaremetal.API.crossArchRunner({
647
+ nfsHostPath,
648
+ bootstrapArch,
649
+ callbackMetaData,
650
+ steps: [`/debootstrap/debootstrap --second-stage`],
651
+ });
652
+ } else if (
653
+ workflowsConfig[workflowId].type === 'chroot-container' &&
654
+ workflowsConfig[workflowId].osIdLike.match('rhel')
655
+ ) {
656
+ // Copy resolv.conf to allow network access inside chroot
657
+ shellExec(`sudo cp /etc/resolv.conf ${nfsHostPath}/etc/resolv.conf`);
658
+
659
+ // Consolidate all package installations into one step to avoid redundancy
660
+ const { packages } = workflowsConfig[workflowId].container;
661
+ const basePackages = [
662
+ 'findutils',
663
+ 'systemd',
664
+ 'sudo',
665
+ 'dracut',
666
+ 'dracut-network',
667
+ 'dracut-config-generic',
668
+ 'nfs-utils',
669
+ 'file',
670
+ 'binutils',
671
+ 'kernel-modules-core',
672
+ 'NetworkManager',
673
+ 'dhclient',
674
+ 'iputils',
675
+ ];
676
+ const allPackages = packages && packages.length > 0 ? [...basePackages, ...packages] : basePackages;
677
+
678
+ UnderpostBaremetal.API.crossArchRunner({
679
+ nfsHostPath,
680
+ bootstrapArch,
681
+ callbackMetaData,
682
+ steps: [
683
+ `dnf install -y --allowerasing ${allPackages.join(' ')} 2>/dev/null || yum install -y --allowerasing ${allPackages.join(' ')} 2>/dev/null || echo "Package install completed"`,
684
+ `dnf clean all`,
685
+ `echo "=== Installed packages verification ==="`,
686
+ `rpm -qa | grep -E "dracut|kernel|nfs" | sort`,
687
+ `echo "=== Boot directory contents ==="`,
688
+ `ls -la /boot /lib/modules/*/`,
689
+ // Search for bootable kernel in order of preference:
690
+ // 1. Raw ARM64 Image file (preferred for GRUB)
691
+ // 2. vmlinuz or vmlinux (may be PE32+ on Rocky Linux)
692
+ `echo "Searching for bootable kernel..."`,
693
+ `KERNEL_FILE=""`,
694
+ // First try to find raw Image file
695
+ `if [ -f /boot/Image ]; then KERNEL_FILE=/boot/Image; echo "Found raw ARM64 Image: $KERNEL_FILE"; fi`,
696
+ `if [ -z "$KERNEL_FILE" ]; then KERNEL_FILE=$(find /lib/modules -name "Image" -o -name "Image.gz" 2>/dev/null | head -n 1); test -n "$KERNEL_FILE" && echo "Found kernel Image in modules: $KERNEL_FILE"; fi`,
697
+ // Fallback to vmlinuz
698
+ `if [ -z "$KERNEL_FILE" ]; then KERNEL_FILE=$(find /boot -name "vmlinuz-*" 2>/dev/null | head -n 1); test -n "$KERNEL_FILE" && echo "Found vmlinuz: $KERNEL_FILE"; fi`,
699
+ `if [ -z "$KERNEL_FILE" ]; then KERNEL_FILE=$(find /lib/modules -name "vmlinuz" 2>/dev/null | head -n 1); test -n "$KERNEL_FILE" && echo "Found vmlinuz in modules: $KERNEL_FILE"; fi`,
700
+ // Last resort: any vmlinux
701
+ `if [ -z "$KERNEL_FILE" ]; then KERNEL_FILE=$(find /lib/modules -name "vmlinux" 2>/dev/null | head -n 1); test -n "$KERNEL_FILE" && echo "Found vmlinux: $KERNEL_FILE"; fi`,
702
+ `if [ -z "$KERNEL_FILE" ]; then echo "ERROR: No kernel found!"; exit 1; fi`,
703
+ // Copy and check kernel type
704
+ `cp "$KERNEL_FILE" /boot/vmlinuz-efi.tmp`,
705
+ // Decompress if gzipped
706
+ `if file /boot/vmlinuz-efi.tmp | grep -q gzip; then echo "Decompressing gzipped kernel..."; gunzip -c /boot/vmlinuz-efi.tmp > /boot/vmlinuz-efi && rm /boot/vmlinuz-efi.tmp; else mv /boot/vmlinuz-efi.tmp /boot/vmlinuz-efi; fi`,
707
+ `KERNEL_TYPE=$(file /boot/vmlinuz-efi 2>/dev/null)`,
708
+ `echo "Final kernel file type: $KERNEL_TYPE"`,
709
+ // Handle PE32+ if still present - use kernel directly without extraction since iPXE can boot it
710
+ `case "$KERNEL_TYPE" in *PE32+*|*EFI*application*) echo "WARNING: Kernel is PE32+ EFI executable"; echo "GRUB may fail to boot this - recommend using iPXE chainload or installing kernel-core package"; echo "Keeping PE32+ kernel as-is for now..."; ;; *ARM64*|*aarch64*|*Image*|*data*) echo "Kernel appears to be raw ARM64 format - suitable for GRUB"; ;; *) echo "Unknown kernel format - attempting to use anyway"; ;; esac`,
711
+ // Get kernel version for initramfs rebuild
712
+ `KVER=$(basename $(dirname "$KERNEL_FILE"))`,
713
+ `echo "Kernel version: $KVER"`,
714
+ // Rebuild initramfs with NFS and network support
715
+ `echo "Rebuilding initramfs with NFS and network support..."`,
716
+ `echo "Available dracut modules:"`,
717
+ `dracut --list-modules 2>/dev/null | grep -E "network|nfs" || echo "No network modules listed"`,
718
+ // Use network-manager module (it's available in Rocky 9) for better compatibility
719
+ `dracut --force --add "nfs network base" --add-drivers "nfs sunrpc" --kver "$KVER" /boot/initrd.img "$KVER" 2>&1 || echo "Initramfs rebuild failed"`,
720
+ // Fallback: if rebuild fails, use existing initramfs
721
+ `if [ ! -f /boot/initrd.img ]; then echo "Initramfs rebuild failed, using existing..."; INITRD=$(find /boot -name "initramfs-$KVER.img" 2>/dev/null | head -n 1); if [ -z "$INITRD" ]; then INITRD=$(find /boot -name "initramfs*.img" 2>/dev/null | grep -v kdump | head -n 1); fi; if [ -n "$INITRD" ]; then cp "$INITRD" /boot/initrd.img; echo "Copied existing initramfs: $INITRD"; else echo "ERROR: No initramfs found!"; fi; fi`,
722
+ `echo "=== Final boot files ==="`,
723
+ `ls -lh /boot/vmlinuz-efi /boot/initrd.img`,
724
+ `file /boot/vmlinuz-efi`,
725
+ `file /boot/initrd.img`,
726
+ `echo "=== Setting root password ==="`,
727
+ `echo "root:root" | chpasswd`,
728
+ ],
729
+ });
730
+ } else {
731
+ throw new Error(
732
+ `Unsupported workflow type for NFS build: ${workflowsConfig[workflowId].type} and like os ID ${workflowsConfig[workflowId].osIdLike}`,
733
+ );
734
+ }
641
735
  }
642
736
 
643
737
  // Fetch boot resources and machines if commissioning or listing.
@@ -679,13 +773,158 @@ rm -rf ${artifacts.join(' ')}`);
679
773
  ignore: machine ? [machine.system_id] : [],
680
774
  });
681
775
 
682
- // Handle commissioning tasks (placeholder for future implementation).
776
+ if (workflowsConfig[workflowId].type === 'chroot-debootstrap') {
777
+ if (options.ubuntuToolsBuild) {
778
+ UnderpostCloudInit.API.buildTools({
779
+ workflowId,
780
+ nfsHostPath,
781
+ hostname,
782
+ callbackMetaData,
783
+ dev: options.dev,
784
+ });
785
+
786
+ const { chronyc, keyboard } = workflowsConfig[workflowId];
787
+ const { timezone, chronyConfPath } = chronyc;
788
+ const systemProvisioning = 'ubuntu';
683
789
 
790
+ UnderpostBaremetal.API.crossArchRunner({
791
+ nfsHostPath,
792
+ bootstrapArch,
793
+ callbackMetaData,
794
+ steps: [
795
+ ...UnderpostBaremetal.API.systemProvisioningFactory[systemProvisioning].base(),
796
+ ...UnderpostBaremetal.API.systemProvisioningFactory[systemProvisioning].user(),
797
+ ...UnderpostBaremetal.API.systemProvisioningFactory[systemProvisioning].timezone({
798
+ timezone,
799
+ chronyConfPath,
800
+ }),
801
+ ...UnderpostBaremetal.API.systemProvisioningFactory[systemProvisioning].keyboard(keyboard.layout),
802
+ ],
803
+ });
804
+ }
805
+
806
+ if (options.ubuntuToolsTest)
807
+ UnderpostBaremetal.API.crossArchRunner({
808
+ nfsHostPath,
809
+ bootstrapArch,
810
+ callbackMetaData,
811
+ steps: [
812
+ `chmod +x /underpost/date.sh`,
813
+ `chmod +x /underpost/keyboard.sh`,
814
+ `chmod +x /underpost/dns.sh`,
815
+ `chmod +x /underpost/help.sh`,
816
+ `chmod +x /underpost/host.sh`,
817
+ `chmod +x /underpost/test.sh`,
818
+ `chmod +x /underpost/start.sh`,
819
+ `chmod +x /underpost/reset.sh`,
820
+ `chmod +x /underpost/shutdown.sh`,
821
+ `chmod +x /underpost/device_scan.sh`,
822
+ `chmod +x /underpost/mac.sh`,
823
+ `chmod +x /underpost/enlistment.sh`,
824
+ `sudo chmod 700 ~/.ssh/`, // Set secure permissions for .ssh directory.
825
+ `sudo chmod 600 ~/.ssh/authorized_keys`, // Set secure permissions for authorized_keys.
826
+ `sudo chmod 644 ~/.ssh/known_hosts`, // Set permissions for known_hosts.
827
+ `sudo chmod 600 ~/.ssh/id_rsa`, // Set secure permissions for private key.
828
+ `sudo chmod 600 /etc/ssh/ssh_host_ed25519_key`, // Set secure permissions for host key.
829
+ `chown -R root:root ~/.ssh`, // Ensure root owns the .ssh directory.
830
+ `/underpost/test.sh`,
831
+ ],
832
+ });
833
+ }
834
+
835
+ if (
836
+ workflowsConfig[workflowId].type === 'chroot-container' &&
837
+ workflowsConfig[workflowId].osIdLike.match('rhel')
838
+ ) {
839
+ if (options.rockyToolsBuild) {
840
+ const { chronyc, keyboard } = workflowsConfig[workflowId];
841
+ const { timezone } = chronyc;
842
+ const systemProvisioning = 'rocky';
843
+
844
+ UnderpostBaremetal.API.crossArchRunner({
845
+ nfsHostPath,
846
+ bootstrapArch,
847
+ callbackMetaData,
848
+ steps: [
849
+ ...UnderpostBaremetal.API.systemProvisioningFactory[systemProvisioning].base(),
850
+ ...UnderpostBaremetal.API.systemProvisioningFactory[systemProvisioning].user(),
851
+ ...UnderpostBaremetal.API.systemProvisioningFactory[systemProvisioning].timezone({
852
+ timezone,
853
+ chronyConfPath: chronyc.chronyConfPath,
854
+ }),
855
+ ...UnderpostBaremetal.API.systemProvisioningFactory[systemProvisioning].keyboard(keyboard.layout),
856
+ ],
857
+ });
858
+ }
859
+
860
+ if (options.rockyToolsTest)
861
+ UnderpostBaremetal.API.crossArchRunner({
862
+ nfsHostPath,
863
+ bootstrapArch,
864
+ callbackMetaData,
865
+ steps: [
866
+ `node --version`,
867
+ `npm --version`,
868
+ `underpost --version`,
869
+ `timedatectl status`,
870
+ `localectl status`,
871
+ `id root`,
872
+ `ls -la /home/root/.ssh/`,
873
+ `cat /home/root/.ssh/authorized_keys`,
874
+ 'underpost test',
875
+ ],
876
+ });
877
+ }
878
+
879
+ if (options.cloudInit || options.cloudInitUpdate) {
880
+ const { chronyc, networkInterfaceName } = workflowsConfig[workflowId];
881
+ const { timezone, chronyConfPath } = chronyc;
882
+ const authCredentials = UnderpostCloudInit.API.authCredentialsFactory();
883
+ const { cloudConfigSrc } = UnderpostCloudInit.API.configFactory(
884
+ {
885
+ controlServerIp: callbackMetaData.runnerHost.ip,
886
+ hostname,
887
+ commissioningDeviceIp: ipAddress,
888
+ gatewayip: callbackMetaData.runnerHost.ip,
889
+ mac: macAddress,
890
+ timezone,
891
+ chronyConfPath,
892
+ networkInterfaceName,
893
+ ubuntuToolsBuild: options.ubuntuToolsBuild,
894
+ bootcmd: options.bootcmd,
895
+ runcmd: options.runcmd,
896
+ },
897
+ authCredentials,
898
+ );
899
+
900
+ UnderpostBaremetal.API.httpBootstrapServerStaticFactory({
901
+ bootstrapHttpServerPath,
902
+ hostname,
903
+ cloudConfigSrc,
904
+ });
905
+ }
906
+
907
+ // Rebuild NFS server configuration.
908
+ if (
909
+ (options.nfsBuildServer === true || options.commission === true) &&
910
+ (workflowsConfig[workflowId].type === 'iso-nfs' ||
911
+ workflowsConfig[workflowId].type === 'chroot-debootstrap' ||
912
+ workflowsConfig[workflowId].type === 'chroot-container')
913
+ ) {
914
+ shellExec(`${underpostRoot}/scripts/nat-iptables.sh`, { silent: true });
915
+ UnderpostBaremetal.API.rebuildNfsServer({
916
+ nfsHostPath,
917
+ });
918
+ }
919
+ // Handle commissioning tasks
684
920
  if (options.commission === true) {
685
921
  let { firmwares, networkInterfaceName, maas, menuentryStr, type } = workflowsConfig[workflowId];
686
922
 
687
923
  // Use commissioning config (Ubuntu ephemeral) for PXE boot resources
688
- const commissioningImage = maas.commissioning;
924
+ const commissioningImage = maas?.commissioning || {
925
+ architecture: 'arm64/generic',
926
+ name: 'ubuntu/noble',
927
+ };
689
928
  const resource = resources.find(
690
929
  (o) => o.architecture === commissioningImage.architecture && o.name === commissioningImage.name,
691
930
  );
@@ -701,6 +940,11 @@ rm -rf ${artifacts.join(' ')}`);
701
940
  shellExec(`sudo rm -rf ${tftpRootPath}`);
702
941
  shellExec(`mkdir -p ${tftpRootPath}/pxe`);
703
942
 
943
+ // Restore iPXE build from cache if available and not forcing rebuild
944
+ if (fs.existsSync(`${ipxeCacheDir}/ipxe.efi`) && !options.ipxeRebuild) {
945
+ shellExec(`cp ${ipxeCacheDir}/ipxe.efi ${tftpRootPath}/ipxe.efi`);
946
+ }
947
+
704
948
  // Process firmwares for TFTP.
705
949
  for (const firmware of firmwares) {
706
950
  const { url, gateway, subnet } = firmware;
@@ -717,7 +961,7 @@ rm -rf ${artifacts.join(' ')}`);
717
961
  const bootConfSrc = UnderpostBaremetal.API.bootConfFactory({
718
962
  workflowId,
719
963
  tftpIp: callbackMetaData.runnerHost.ip,
720
- tftpPrefixStr: hostname,
964
+ tftpPrefixStr: tftpPrefix,
721
965
  macAddress,
722
966
  clientIp: ipAddress,
723
967
  subnet,
@@ -732,12 +976,25 @@ rm -rf ${artifacts.join(' ')}`);
732
976
  {
733
977
  // Fetch kernel and initrd paths from MAAS boot resource.
734
978
  // 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
- });
979
+ let kernelFilesPaths, resourcesPath;
980
+ if (workflowsConfig[workflowId].type === 'chroot-container') {
981
+ const arch = commissioningImage.architecture.split('/')[0];
982
+ resourcesPath = `/var/snap/maas/common/maas/image-storage/bootloaders/uefi/${arch}`;
983
+ kernelFilesPaths = {
984
+ 'vmlinuz-efi': `${nfsHostPath}/boot/vmlinuz-efi`,
985
+ 'initrd.img': `${nfsHostPath}/boot/initrd.img`,
986
+ };
987
+ } else {
988
+ const kf = UnderpostBaremetal.API.kernelFactory({
989
+ resource,
990
+ type,
991
+ nfsHostPath,
992
+ isoUrl: options.isoUrl || workflowsConfig[workflowId].isoUrl,
993
+ workflowId,
994
+ });
995
+ kernelFilesPaths = kf.kernelFilesPaths;
996
+ resourcesPath = kf.resourcesPath;
997
+ }
741
998
 
742
999
  const { cmd } = UnderpostBaremetal.API.kernelCmdBootParamsFactory({
743
1000
  ipClient: ipAddress,
@@ -755,6 +1012,8 @@ rm -rf ${artifacts.join(' ')}`);
755
1012
  macAddress,
756
1013
  cloudInit: options.cloudInit,
757
1014
  machine,
1015
+ dev: options.dev,
1016
+ osIdLike: workflowsConfig[workflowId].osIdLike || '',
758
1017
  });
759
1018
 
760
1019
  // Check if iPXE mode is enabled AND the iPXE EFI binary exists
@@ -783,47 +1042,15 @@ rm -rf ${artifacts.join(' ')}`);
783
1042
  path: `${tftpRootPath}/stable-id.ipxe`,
784
1043
  embeddedPath: `${tftpRootPath}/boot.ipxe`,
785
1044
  });
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`,
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
1045
 
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.');
826
- }
1046
+ UnderpostBaremetal.API.ipxeEfiFactory({
1047
+ tftpRootPath,
1048
+ ipxeCacheDir,
1049
+ arch,
1050
+ underpostRoot,
1051
+ embeddedScriptPath: `${tftpRootPath}/boot.ipxe`,
1052
+ forceRebuild: options.ipxeRebuild,
1053
+ });
827
1054
  }
828
1055
 
829
1056
  const { grubCfgSrc } = UnderpostBaremetal.API.grubFactory({
@@ -850,7 +1077,7 @@ rm -rf ${artifacts.join(' ')}`);
850
1077
  }
851
1078
 
852
1079
  // Pass architecture from commissioning or deployment config
853
- const grubArch = maas.commissioning.architecture;
1080
+ const grubArch = commissioningImage.architecture;
854
1081
  UnderpostBaremetal.API.efiGrubModulesFactory({ image: { architecture: grubArch } });
855
1082
 
856
1083
  // Set ownership and permissions for TFTP root.
@@ -863,130 +1090,24 @@ rm -rf ${artifacts.join(' ')}`);
863
1090
  bootstrapHttpServerPort:
864
1091
  options.bootstrapHttpServerPort || workflowsConfig[workflowId].bootstrapHttpServerPort,
865
1092
  });
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')
968
- UnderpostBaremetal.API.rebuildNfsServer({
969
- nfsHostPath,
970
- });
971
1093
 
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({
1094
+ if (type === 'chroot-debootstrap' || type === 'chroot-container')
1095
+ await UnderpostBaremetal.API.nfsMountCallback({
978
1096
  hostname,
979
1097
  nfsHostPath,
980
1098
  workflowId,
981
1099
  mount: true,
982
1100
  });
983
- if (!isMounted) throw new Error('NFS root filesystem is not mounted');
984
- }
1101
+
985
1102
  const commissionMonitorPayload = {
986
1103
  macAddress,
987
1104
  ipAddress,
988
1105
  hostname,
989
- maas: workflowsConfig[workflowId].maas,
1106
+ architecture:
1107
+ workflowsConfig[workflowId].maas?.commissioning?.architecture ||
1108
+ workflowsConfig[workflowId].container?.architecture ||
1109
+ workflowsConfig[workflowId].debootstrap?.image?.architecture ||
1110
+ 'arm64/generic',
990
1111
  machine,
991
1112
  };
992
1113
  logger.info('Waiting for commissioning...', {
@@ -996,7 +1117,7 @@ rm -rf ${artifacts.join(' ')}`);
996
1117
 
997
1118
  const { discovery } = await UnderpostBaremetal.API.commissionMonitor(commissionMonitorPayload);
998
1119
 
999
- if (type === 'chroot' && options.cloudInit === true) {
1120
+ if ((type === 'chroot-debootstrap' || type === 'chroot-container') && options.cloudInit === true) {
1000
1121
  openTerminal(`node ${underpostRoot}/bin baremetal ${workflowId} ${ipAddress} ${hostname} --logs cloud-init`);
1001
1122
  openTerminal(
1002
1123
  `node ${underpostRoot}/bin baremetal ${workflowId} ${ipAddress} ${hostname} --logs cloud-init-machine`,
@@ -1036,60 +1157,44 @@ rm -rf ${artifacts.join(' ')}`);
1036
1157
  },
1037
1158
 
1038
1159
  /**
1039
- * @method downloadUbuntuLiveISO
1040
- * @description Downloads Ubuntu live ISO and extracts casper boot files for live boot.
1160
+ * @method downloadISO
1161
+ * @description Downloads a generic ISO and extracts kernel boot files.
1041
1162
  * @param {object} params - Parameters for the method.
1042
1163
  * @param {object} params.resource - The MAAS boot resource object.
1043
1164
  * @param {string} params.architecture - The architecture (arm64 or amd64).
1044
1165
  * @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.
1166
+ * @param {string} params.isoUrl - The full URL to the ISO file to download.
1167
+ * @param {string} params.osIdLike - OS family identifier (e.g., 'debian ubuntu' or 'rhel centos fedora').
1168
+ * @returns {object} An object containing paths to the extracted kernel, initrd, and optionally squashfs.
1046
1169
  * @memberof UnderpostBaremetal
1047
1170
  */
1048
- downloadUbuntuLiveISO({ resource, architecture, nfsHostPath, isoUrl }) {
1171
+ downloadISO({ resource, architecture, nfsHostPath, isoUrl, osIdLike }) {
1049
1172
  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
1173
 
1069
- shellExec(`mkdir -p ${nfsHostPath}/casper`);
1174
+ // Validate that isoUrl is provided
1175
+ if (!isoUrl) {
1176
+ throw new Error('isoUrl parameter is required. Please specify the full ISO URL in the workflow configuration.');
1177
+ }
1178
+
1179
+ // Extract ISO filename from URL
1180
+ const isoFilename = isoUrl.split('/').pop();
1070
1181
 
1071
- const version = (versionMap[arch] && versionMap[arch][osName]) || '20.04.5';
1072
- const majorVersion = version.split('.').slice(0, 2).join('.');
1182
+ // Determine OS family from osIdLike
1183
+ const isDebianBased = osIdLike && osIdLike.match(/debian|ubuntu/i);
1184
+ const isRhelBased = osIdLike && osIdLike.match(/rhel|centos|fedora|alma|rocky/i);
1073
1185
 
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();
1186
+ // Set extraction directory based on OS family
1187
+ const extractDirName = isDebianBased ? 'casper' : 'iso-extract';
1188
+ shellExec(`mkdir -p ${nfsHostPath}/${extractDirName}`);
1084
1189
 
1085
- const isoPath = `/var/tmp/ubuntu-live-iso/${isoFilename}`;
1086
- const extractDir = `${nfsHostPath}/casper`;
1190
+ const isoPath = `/var/tmp/live-iso/${isoFilename}`;
1191
+ const extractDir = `${nfsHostPath}/${extractDirName}`;
1087
1192
 
1088
1193
  if (!fs.existsSync(isoPath)) {
1089
- logger.info(`Downloading Ubuntu ${version} live ISO for ${arch}...`);
1194
+ logger.info(`Downloading ISO for ${arch}...`);
1090
1195
  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 });
1196
+ shellExec(`mkdir -p /var/tmp/live-iso`);
1197
+ shellExec(`wget --progress=bar:force -O ${isoPath} "${isoUrl}"`);
1093
1198
  // Verify download by checking file existence and size (not exit code, which can be unreliable)
1094
1199
  if (!fs.existsSync(isoPath)) {
1095
1200
  throw new Error(`Failed to download ISO from ${isoUrl} - file not created`);
@@ -1102,46 +1207,32 @@ rm -rf ${artifacts.join(' ')}`);
1102
1207
  logger.info(`Downloaded ISO to ${isoPath} (${(stats.size / 1024 / 1024 / 1024).toFixed(2)} GB)`);
1103
1208
  }
1104
1209
 
1105
- // Mount ISO and extract casper files
1106
- const mountPoint = `${nfsHostPath}/mnt-${osName}-${arch}`;
1210
+ // Mount ISO and extract boot files
1211
+ const mountPoint = `${nfsHostPath}/mnt-iso-${arch}`;
1107
1212
  shellExec(`mkdir -p ${mountPoint}`);
1108
1213
 
1109
1214
  // Ensure mount point is not already mounted
1110
- shellExec(`sudo umount ${mountPoint} 2>/dev/null || true`, { silent: true });
1215
+ shellExec(`sudo umount ${mountPoint} 2>/dev/null`, { silent: true });
1111
1216
 
1112
1217
  try {
1113
1218
  // Mount the ISO
1114
1219
  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
1220
  logger.info(`Mounted ISO at ${mountPoint}`);
1120
1221
 
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`);
1222
+ // Distribution-specific extraction logic
1223
+ if (isDebianBased) {
1224
+ // Ubuntu/Debian: Extract from casper directory
1225
+ if (!fs.existsSync(`${mountPoint}/casper`)) {
1226
+ throw new Error(`Failed to mount ISO or casper directory not found: ${isoPath}`);
1227
+ }
1228
+ logger.info(`Checking casper directory contents...`);
1229
+ shellExec(`ls -la ${mountPoint}/casper/ 2>/dev/null || echo "casper directory not found"`);
1230
+ shellExec(`sudo cp -a ${mountPoint}/casper/* ${extractDir}/`);
1141
1231
  }
1142
1232
  } finally {
1143
1233
  shellExec(`ls -la ${mountPoint}/`);
1144
1234
 
1235
+ shellExec(`sudo chown -R $(whoami):$(whoami) ${extractDir}`);
1145
1236
  // Unmount ISO
1146
1237
  shellExec(`sudo umount ${mountPoint}`, { silent: true });
1147
1238
  logger.info(`Unmounted ISO`);
@@ -1174,15 +1265,12 @@ rm -rf ${artifacts.join(' ')}`);
1174
1265
  hostname: '',
1175
1266
  ipAddress: '',
1176
1267
  powerType: 'manual',
1177
- maas: {},
1268
+ architecture: 'arm64/generic',
1178
1269
  },
1179
1270
  ) {
1180
1271
  if (!options.powerType) options.powerType = 'manual';
1181
- const maas = options.maas || {};
1182
1272
  const payload = {
1183
- architecture: (maas.commissioning?.architecture || 'arm64/generic').match('arm')
1184
- ? 'arm64/generic'
1185
- : 'amd64/generic',
1273
+ architecture: (options.architecture || 'arm64/generic').match('arm') ? 'arm64/generic' : 'amd64/generic',
1186
1274
  mac_address: options.macAddress,
1187
1275
  mac_addresses: options.macAddress,
1188
1276
  hostname: options.hostname,
@@ -1214,21 +1302,25 @@ rm -rf ${artifacts.join(' ')}`);
1214
1302
  * @description Retrieves kernel, initrd, and root filesystem paths from a MAAS boot resource.
1215
1303
  * @param {object} params - Parameters for the method.
1216
1304
  * @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.
1305
+ * @param {string} params.type - The type of boot (e.g., 'iso-ram', 'iso-nfs', etc.).
1306
+ * @param {string} params.nfsHostPath - The NFS host path (used for ISO types).
1307
+ * @param {string} params.isoUrl - The ISO URL (used for ISO types).
1308
+ * @param {string} params.workflowId - The workflow identifier.
1219
1309
  * @returns {object} An object containing paths to the kernel, initrd, and root filesystem.
1220
1310
  * @memberof UnderpostBaremetal
1221
1311
  */
1222
- kernelFactory({ resource, type, nfsHostPath, isoUrl }) {
1223
- // For disk-based commissioning (casper), use Ubuntu live ISO files
1312
+ kernelFactory({ resource, type, nfsHostPath, isoUrl, workflowId }) {
1313
+ // For disk-based commissioning (casper/iso), use live ISO files
1224
1314
  if (type === 'iso-ram' || type === 'iso-nfs') {
1225
- logger.info('Using Ubuntu live ISO for casper boot (disk-based commissioning)');
1315
+ logger.info('Using live ISO for boot (disk-based commissioning)');
1226
1316
  const arch = resource.architecture.split('/')[0];
1227
- const kernelFilesPaths = UnderpostBaremetal.API.downloadUbuntuLiveISO({
1317
+ const workflowsConfig = UnderpostBaremetal.API.loadWorkflowsConfig();
1318
+ const kernelFilesPaths = UnderpostBaremetal.API.downloadISO({
1228
1319
  resource,
1229
1320
  architecture: arch,
1230
1321
  nfsHostPath,
1231
1322
  isoUrl,
1323
+ osIdLike: workflowsConfig[workflowId].osIdLike || '',
1232
1324
  });
1233
1325
  const resourcesPath = `/var/snap/maas/common/maas/image-storage/bootloaders/uefi/${arch}`;
1234
1326
  return { kernelFilesPaths, resourcesPath };
@@ -1665,6 +1757,42 @@ shell
1665
1757
  `;
1666
1758
  },
1667
1759
 
1760
+ /**
1761
+ * @method ipxeEfiFactory
1762
+ * @description Manages iPXE EFI binary build with cache support.
1763
+ * Checks cache, builds only if needed, saves to cache after build.
1764
+ * @param {object} params - The parameters for iPXE build.
1765
+ * @param {string} params.tftpRootPath - TFTP root directory path.
1766
+ * @param {string} params.ipxeCacheDir - iPXE cache directory path.
1767
+ * @param {string} params.arch - Target architecture (arm64/amd64).
1768
+ * @param {string} params.underpostRoot - Underpost root directory.
1769
+ * @param {string} [params.embeddedScriptPath] - Path to embedded boot script.
1770
+ * @param {boolean} [params.forceRebuild=false] - Force rebuild regardless of cache.
1771
+ * @returns {void}
1772
+ * @memberof UnderpostBaremetal
1773
+ */
1774
+ ipxeEfiFactory({ tftpRootPath, ipxeCacheDir, arch, underpostRoot, embeddedScriptPath, forceRebuild = false }) {
1775
+ const shouldRebuild =
1776
+ forceRebuild || (!fs.existsSync(`${tftpRootPath}/ipxe.efi`) && !fs.existsSync(`${ipxeCacheDir}/ipxe.efi`));
1777
+
1778
+ if (!shouldRebuild) return;
1779
+
1780
+ if (embeddedScriptPath && fs.existsSync(embeddedScriptPath)) {
1781
+ logger.info('Rebuilding iPXE with embedded boot script...', {
1782
+ embeddedScriptPath,
1783
+ forced: forceRebuild,
1784
+ });
1785
+ shellExec(
1786
+ `${underpostRoot}/scripts/ipxe-setup.sh ${tftpRootPath} --target-arch ${arch} --embed-script ${embeddedScriptPath} --rebuild`,
1787
+ );
1788
+ } else if (shouldRebuild) {
1789
+ shellExec(`${underpostRoot}/scripts/ipxe-setup.sh ${tftpRootPath} --target-arch ${arch}`);
1790
+ }
1791
+
1792
+ shellExec(`mkdir -p ${ipxeCacheDir}`);
1793
+ shellExec(`cp ${tftpRootPath}/ipxe.efi ${ipxeCacheDir}/ipxe.efi`);
1794
+ },
1795
+
1668
1796
  /**
1669
1797
  * @method grubFactory
1670
1798
  * @description Generates the GRUB configuration file content.
@@ -1729,6 +1857,45 @@ shell
1729
1857
  };
1730
1858
  },
1731
1859
 
1860
+ /**
1861
+ * @method httpBootstrapServerStaticFactory
1862
+ * @description Creates static files for the bootstrap HTTP server including cloud-init configuration.
1863
+ * @param {object} params - Parameters for creating static files.
1864
+ * @param {string} params.bootstrapHttpServerPath - The path where static files will be created.
1865
+ * @param {string} params.hostname - The hostname of the client machine.
1866
+ * @param {string} params.cloudConfigSrc - The cloud-init configuration YAML source.
1867
+ * @param {object} [params.metadata] - Optional metadata to include in meta-data file.
1868
+ * @param {string} [params.vendorData] - Optional vendor-data content (default: empty string).
1869
+ * @memberof UnderpostBaremetal
1870
+ * @returns {void}
1871
+ */
1872
+ httpBootstrapServerStaticFactory({
1873
+ bootstrapHttpServerPath,
1874
+ hostname,
1875
+ cloudConfigSrc,
1876
+ metadata = {},
1877
+ vendorData = '',
1878
+ }) {
1879
+ // Create directory structure
1880
+ shellExec(`mkdir -p ${bootstrapHttpServerPath}/${hostname}/cloud-init`);
1881
+
1882
+ // Write user-data file
1883
+ fs.writeFileSync(
1884
+ `${bootstrapHttpServerPath}/${hostname}/cloud-init/user-data`,
1885
+ `#cloud-config\n${cloudConfigSrc}`,
1886
+ 'utf8',
1887
+ );
1888
+
1889
+ // Write meta-data file
1890
+ const metaDataContent = `instance-id: ${metadata.instanceId || hostname}\nlocal-hostname: ${metadata.localHostname || hostname}`;
1891
+ fs.writeFileSync(`${bootstrapHttpServerPath}/${hostname}/cloud-init/meta-data`, metaDataContent, 'utf8');
1892
+
1893
+ // Write vendor-data file
1894
+ fs.writeFileSync(`${bootstrapHttpServerPath}/${hostname}/cloud-init/vendor-data`, vendorData, 'utf8');
1895
+
1896
+ logger.info(`Cloud-init files written to ${bootstrapHttpServerPath}/${hostname}/cloud-init`);
1897
+ },
1898
+
1732
1899
  /**
1733
1900
  * @method httpBootstrapServerRunnerFactory
1734
1901
  * @description Starts a simple HTTP server to serve boot files for network booting.
@@ -1749,7 +1916,7 @@ shell
1749
1916
  shellExec(`mkdir -p ${bootstrapHttpServerPath}/${hostname}/cloud-init`);
1750
1917
 
1751
1918
  // Kill any existing HTTP server
1752
- shellExec(`sudo pkill -f 'python3 -m http.server ${port}' || true`, { silent: true });
1919
+ shellExec(`sudo pkill -f 'python3 -m http.server ${port}'`, { silent: true });
1753
1920
 
1754
1921
  shellExec(
1755
1922
  `cd ${bootstrapHttpServerPath} && nohup python3 -m http.server ${port} --bind 0.0.0.0 > /tmp/http-boot-server.log 2>&1 &`,
@@ -1771,7 +1938,8 @@ shell
1771
1938
  /**
1772
1939
  * @method updateKernelFiles
1773
1940
  * @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.
1941
+ * It also handles decompression of the kernel if necessary for ARM64 compatibility,
1942
+ * and extracts raw kernel images from PE32+ EFI wrappers (common in Rocky Linux ARM64).
1775
1943
  * @param {object} params - The parameters for the function.
1776
1944
  * @param {object} params.commissioningImage - The commissioning image configuration.
1777
1945
  * @param {string} params.resourcesPath - The path where resources are located.
@@ -1797,11 +1965,21 @@ shell
1797
1965
  if (file === 'vmlinuz-efi') {
1798
1966
  const kernelDest = `${tftpRootPath}/pxe/${file}`;
1799
1967
  const fileType = shellExec(`file ${kernelDest}`, { silent: true }).stdout;
1968
+
1969
+ // Handle gzip compressed kernels
1800
1970
  if (fileType.includes('gzip compressed data')) {
1801
1971
  logger.info(`Decompressing kernel ${file} for ARM64 UEFI compatibility...`);
1802
1972
  shellExec(`sudo mv ${kernelDest} ${kernelDest}.gz`);
1803
1973
  shellExec(`sudo gunzip ${kernelDest}.gz`);
1804
1974
  }
1975
+
1976
+ // Handle PE32+ EFI wrapped kernels (common in Rocky Linux ARM64)
1977
+ // Rocky Linux ARM64 kernels are distributed as PE32+ EFI executables, which
1978
+ // are bootable directly via UEFI firmware. However, GRUB's 'linux' command
1979
+ // expects a raw ARM64 Linux kernel Image format, not a PE32+ wrapper.
1980
+ if (fileType.includes('PE32+') || fileType.includes('EFI application')) {
1981
+ logger.warn('Detected PE32+ EFI wrapped kernel. Need to extract raw kernel image for GRUB.');
1982
+ }
1805
1983
  }
1806
1984
  }
1807
1985
  },
@@ -1820,10 +1998,12 @@ shell
1820
1998
  * @param {string} options.networkInterfaceName - The name of the network interface.
1821
1999
  * @param {string} options.fileSystemUrl - The URL of the root filesystem.
1822
2000
  * @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.).
2001
+ * @param {string} options.type - The type of boot ('iso-ram', 'chroot-debootstrap', 'chroot-container', 'iso-nfs', etc.).
1824
2002
  * @param {string} options.macAddress - The MAC address of the client.
1825
2003
  * @param {boolean} options.cloudInit - Whether to include cloud-init parameters.
1826
2004
  * @param {object} options.machine - The machine object containing system_id.
2005
+ * @param {boolean} [options.dev=false] - Whether to enable dev mode with dracut debugging parameters.
2006
+ * @param {string} [options.osIdLike=''] - OS family identifier (e.g., 'rhel centos fedora' or 'debian ubuntu').
1827
2007
  * @returns {object} An object containing the constructed command line string.
1828
2008
  * @memberof UnderpostBaremetal
1829
2009
  */
@@ -1843,6 +2023,8 @@ shell
1843
2023
  macAddress: '',
1844
2024
  cloudInit: false,
1845
2025
  machine: { system_id: '' },
2026
+ dev: false,
2027
+ osIdLike: '',
1846
2028
  },
1847
2029
  ) {
1848
2030
  // Construct kernel command line arguments for NFS boot.
@@ -1860,6 +2042,7 @@ shell
1860
2042
  type,
1861
2043
  macAddress,
1862
2044
  cloudInit,
2045
+ osIdLike,
1863
2046
  } = options;
1864
2047
 
1865
2048
  const ipParam = true
@@ -1868,12 +2051,11 @@ shell
1868
2051
  : 'ip=dhcp';
1869
2052
 
1870
2053
  const nfsOptions = `${
1871
- type === 'chroot'
2054
+ type === 'chroot-debootstrap' || type === 'chroot-container'
1872
2055
  ? [
1873
2056
  'tcp',
1874
2057
  'nfsvers=3',
1875
2058
  'nolock',
1876
- 'vers=3',
1877
2059
  // 'protocol=tcp',
1878
2060
  // 'hard=true',
1879
2061
  'port=2049',
@@ -1953,19 +2135,36 @@ shell
1953
2135
  'overlayroot_cfgdisk=disabled', // Ignore external overlay configurations
1954
2136
  ];
1955
2137
 
1956
- const baseNfsParams = [`netboot=nfs`];
1957
-
1958
2138
  let cmd = [];
1959
2139
  if (type === 'iso-ram') {
1960
2140
  const netBootParams = [`netboot=url`];
1961
2141
  if (fileSystemUrl) netBootParams.push(`url=${fileSystemUrl.replace('https', 'http')}`);
1962
2142
  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];
2143
+ } else if (type === 'chroot-debootstrap' || type === 'chroot-container') {
2144
+ let qemuNfsRootParams = [`root=/dev/nfs`, `rootfstype=nfs`];
2145
+
2146
+ // Determine OS family from osIdLike configuration
2147
+ const isRhelBased = osIdLike && osIdLike.match(/rhel|centos|fedora|alma|rocky/i);
2148
+ const isDebianBased = osIdLike && osIdLike.match(/debian|ubuntu/i);
2149
+
2150
+ // Add RHEL/Rocky/Fedora based images specific parameters
2151
+ if (isRhelBased) {
2152
+ qemuNfsRootParams = qemuNfsRootParams.concat([`rd.neednet=1`, `rd.timeout=180`, `selinux=0`, `enforcing=0`]);
2153
+ }
2154
+ // Add Debian/Ubuntu based images specific parameters
2155
+ else if (isDebianBased) {
2156
+ qemuNfsRootParams = qemuNfsRootParams.concat([`initrd=initrd.img`, `init=/sbin/init`]);
2157
+ }
2158
+
2159
+ // Add debugging parameters in dev mode for dracut troubleshooting
2160
+ if (options.dev) {
2161
+ // qemuNfsRootParams = qemuNfsRootParams.concat([`rd.shell`, `rd.debug`]);
2162
+ }
2163
+
2164
+ cmd = [ipParam, ...qemuNfsRootParams, nfsRootParam, ...kernelParams];
1966
2165
  } else {
1967
2166
  // 'iso-nfs'
1968
- cmd = [ipParam, ...baseNfsParams, nfsRootParam, ...kernelParams, ...performanceParams];
2167
+ cmd = [ipParam, `netboot=nfs`, nfsRootParam, ...kernelParams, ...performanceParams];
1969
2168
 
1970
2169
  cmd.push(`ifname=${networkInterfaceName}:${macAddress}`);
1971
2170
 
@@ -1998,12 +2197,12 @@ shell
1998
2197
  * @param {string} params.macAddress - The MAC address to monitor for.
1999
2198
  * @param {string} params.ipAddress - The IP address of the machine (used if MAC is all zeros).
2000
2199
  * @param {string} [params.hostname] - The hostname for the machine (optional).
2001
- * @param {object} [params.maas] - Additional MAAS-specific options (optional).
2200
+ * @param {string} [params.architecture] - The architecture of the machine (optional).
2002
2201
  * @param {object} [params.machine] - Existing machine payload to use (optional).
2003
- * @returns {Promise<void>} A promise that resolves when commissioning is initiated or after a delay.
2202
+ * @returns {Promise<void>} A promise object with machine and discovery details.
2004
2203
  * @memberof UnderpostBaremetal
2005
2204
  */
2006
- async commissionMonitor({ macAddress, ipAddress, hostname, maas, machine }) {
2205
+ async commissionMonitor({ macAddress, ipAddress, hostname, architecture, machine }) {
2007
2206
  {
2008
2207
  // Query observed discoveries from MAAS.
2009
2208
  const discoveries = JSON.parse(
@@ -2038,7 +2237,7 @@ shell
2038
2237
  ipAddress,
2039
2238
  macAddress: discovery.mac_address,
2040
2239
  hostname,
2041
- maas,
2240
+ architecture,
2042
2241
  }).machine;
2043
2242
  console.log('New machine system id:', machine.system_id.bgYellow.bold.black);
2044
2243
  UnderpostBaremetal.API.writeGrubConfigToFile({
@@ -2079,14 +2278,6 @@ shell
2079
2278
  logger.info('✓ Machine interface MAC address updated successfully');
2080
2279
 
2081
2280
  // 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
2281
  }
2091
2282
  logger.info('Machine resource uri', machine.resource_uri);
2092
2283
  for (const iface of machine.interface_set)
@@ -2101,7 +2292,13 @@ shell
2101
2292
  }
2102
2293
  }
2103
2294
  await timer(1000);
2104
- return await UnderpostBaremetal.API.commissionMonitor({ macAddress, ipAddress, hostname, maas, machine });
2295
+ return await UnderpostBaremetal.API.commissionMonitor({
2296
+ macAddress,
2297
+ ipAddress,
2298
+ hostname,
2299
+ architecture,
2300
+ machine,
2301
+ });
2105
2302
  }
2106
2303
  },
2107
2304
 
@@ -2189,8 +2386,8 @@ shell
2189
2386
  * @param {'arm64'|'amd64'} params.debootstrapArch - The target architecture of the debootstrap environment.
2190
2387
  * @returns {void}
2191
2388
  */
2192
- crossArchBinFactory({ nfsHostPath, debootstrapArch }) {
2193
- switch (debootstrapArch) {
2389
+ crossArchBinFactory({ nfsHostPath, bootstrapArch }) {
2390
+ switch (bootstrapArch) {
2194
2391
  case 'arm64':
2195
2392
  // Copy QEMU static binary for ARM64.
2196
2393
  shellExec(`sudo podman cp extract:/usr/bin/qemu-aarch64-static ${nfsHostPath}/usr/bin/`);
@@ -2201,7 +2398,7 @@ shell
2201
2398
  break;
2202
2399
  default:
2203
2400
  // Log a warning or throw an error for unsupported architectures.
2204
- logger.warn(`Unsupported debootstrap architecture: ${debootstrapArch}`);
2401
+ logger.warn(`Unsupported bootstrap architecture: ${bootstrapArch}`);
2205
2402
  break;
2206
2403
  }
2207
2404
  // Install GRUB EFI modules for both architectures to ensure compatibility.
@@ -2221,14 +2418,14 @@ shell
2221
2418
  * @param {string[]} params.steps - An array of shell commands to execute.
2222
2419
  * @returns {void}
2223
2420
  */
2224
- crossArchRunner({ nfsHostPath, debootstrapArch, callbackMetaData, steps }) {
2421
+ crossArchRunner({ nfsHostPath, bootstrapArch, callbackMetaData, steps }) {
2225
2422
  // Render the steps with logging for better visibility during execution.
2226
2423
  steps = UnderpostBaremetal.API.stepsRender(steps, false);
2227
2424
 
2228
2425
  let qemuCrossArchBash = '';
2229
2426
  // Determine if QEMU is needed for cross-architecture execution.
2230
- if (debootstrapArch !== callbackMetaData.runnerHost.architecture)
2231
- switch (debootstrapArch) {
2427
+ if (bootstrapArch !== callbackMetaData.runnerHost.architecture)
2428
+ switch (bootstrapArch) {
2232
2429
  case 'arm64':
2233
2430
  qemuCrossArchBash = '/usr/bin/qemu-aarch64-static ';
2234
2431
  break;
@@ -2287,20 +2484,25 @@ EOF`);
2287
2484
  * @param {string} params.workflowId - The identifier for the workflow configuration.
2288
2485
  * @param {boolean} [params.mount] - If true, attempts to mount the NFS paths.
2289
2486
  * @param {boolean} [params.unmount] - If true, attempts to unmount the NFS paths.
2487
+ * @param {number} [currentRecall=0] - The current recall attempt count for retries.
2488
+ * @param {number} [maxRecalls=5] - The maximum number of recall attempts allowed.
2290
2489
  * @memberof UnderpostBaremetal
2291
- * @returns {{isMounted: boolean}} An object indicating whether any NFS path is currently mounted.
2490
+ * @returns {Promise<void>} A promise that resolves when the mount/unmount operations are complete.
2292
2491
  */
2293
- nfsMountCallback({ hostname, nfsHostPath, workflowId, mount, unmount }) {
2492
+ async nfsMountCallback({ hostname, nfsHostPath, workflowId, mount, unmount }, currentRecall = 0, maxRecalls = 5) {
2294
2493
  // Mount binfmt_misc filesystem.
2295
2494
  if (mount) UnderpostBaremetal.API.mountBinfmtMisc();
2296
- let isMounted = false;
2495
+ const unMountCmds = [];
2297
2496
  const mountCmds = [];
2298
- const currentMounts = [];
2299
2497
  const workflowsConfig = UnderpostBaremetal.API.loadWorkflowsConfig();
2498
+ let recall = false;
2300
2499
  if (!workflowsConfig[workflowId]) {
2301
2500
  throw new Error(`Workflow configuration not found for ID: ${workflowId}`);
2302
2501
  }
2303
- if (workflowsConfig[workflowId].type === 'chroot') {
2502
+ if (
2503
+ workflowsConfig[workflowId].type === 'chroot-debootstrap' ||
2504
+ workflowsConfig[workflowId].type === 'chroot-container'
2505
+ ) {
2304
2506
  const mounts = {
2305
2507
  bind: ['/proc', '/sys', '/run'],
2306
2508
  rbind: ['/dev'],
@@ -2315,12 +2517,11 @@ EOF`);
2315
2517
  );
2316
2518
 
2317
2519
  if (isPathMounted) {
2318
- currentMounts.push(mountPath);
2319
- if (!isMounted) isMounted = true; // Set overall mounted status.
2320
2520
  logger.warn('Nfs path already mounted', mountPath);
2321
2521
  if (unmount === true) {
2322
2522
  // Unmount if requested.
2323
- mountCmds.push(`sudo umount ${hostMountPath}`);
2523
+ unMountCmds.push(`sudo umount -Rfl ${hostMountPath}`);
2524
+ if (!recall) recall = true;
2324
2525
  }
2325
2526
  } else {
2326
2527
  if (mount === true) {
@@ -2332,17 +2533,27 @@ EOF`);
2332
2533
  }
2333
2534
  }
2334
2535
  }
2335
-
2336
- if (!isMounted) {
2337
- // if all path unmounted, set ownership and permissions for the NFS host path.
2536
+ for (const unMountCmd of unMountCmds) shellExec(unMountCmd);
2537
+ if (recall) {
2538
+ if (currentRecall >= maxRecalls) {
2539
+ throw new Error(
2540
+ `Maximum recall attempts (${maxRecalls}) reached for nfsMountCallback. Hostname: ${hostname}`,
2541
+ );
2542
+ }
2543
+ logger.info(`nfsMountCallback recall attempt ${currentRecall + 1}/${maxRecalls} for hostname: ${hostname}`);
2544
+ await timer(1000);
2545
+ return await UnderpostBaremetal.API.nfsMountCallback(
2546
+ { hostname, nfsHostPath, workflowId, mount, unmount },
2547
+ currentRecall + 1,
2548
+ maxRecalls,
2549
+ );
2550
+ }
2551
+ if (mountCmds.length > 0) {
2338
2552
  shellExec(`sudo chown -R $(whoami):$(whoami) ${nfsHostPath}`);
2339
2553
  shellExec(`sudo chmod -R 755 ${nfsHostPath}`);
2554
+ for (const mountCmd of mountCmds) shellExec(mountCmd);
2340
2555
  }
2341
- for (const mountCmd of mountCmds) shellExec(mountCmd);
2342
- if (mount) isMounted = true;
2343
- logger.info('Current mounts', currentMounts);
2344
2556
  }
2345
- return { isMounted, currentMounts };
2346
2557
  },
2347
2558
 
2348
2559
  /**
@@ -2387,27 +2598,25 @@ EOF`);
2387
2598
  * @returns {string[]} An array of shell commands.
2388
2599
  */
2389
2600
  base: () => [
2390
- // Configure APT sources for Ubuntu ports.
2601
+ // Configure APT sources for Ubuntu ports
2391
2602
  `cat <<SOURCES | tee /etc/apt/sources.list
2392
2603
  deb http://ports.ubuntu.com/ubuntu-ports noble main restricted universe multiverse
2393
2604
  deb http://ports.ubuntu.com/ubuntu-ports noble-updates main restricted universe multiverse
2394
2605
  deb http://ports.ubuntu.com/ubuntu-ports noble-security main restricted universe multiverse
2395
2606
  SOURCES`,
2396
2607
 
2397
- // Update package lists and perform a full system upgrade.
2608
+ // Update package lists and perform a full system upgrade
2398
2609
  `apt update -qq`,
2399
2610
  `apt -y full-upgrade`,
2400
- // Install essential development and system utilities.
2401
- `apt install -y build-essential xinput x11-xkb-utils usbutils uuid-runtime`,
2402
- 'apt install -y linux-image-generic',
2403
2611
 
2404
- // Install cloud-init, systemd, SSH, sudo, locales, udev, and networking tools.
2405
- `apt install -y systemd-sysv openssh-server sudo locales udev util-linux systemd-sysv iproute2 netplan.io ca-certificates curl wget chrony`,
2406
- `ln -sf /lib/systemd/systemd /sbin/init`, // Ensure systemd is the init system.
2612
+ // Install all essential packages in one consolidated step
2613
+ `DEBIAN_FRONTEND=noninteractive apt install -y build-essential xinput x11-xkb-utils usbutils uuid-runtime linux-image-generic systemd-sysv openssh-server sudo locales udev util-linux iproute2 netplan.io ca-certificates curl wget chrony apt-utils tzdata kmod keyboard-configuration console-setup iputils-ping`,
2614
+
2615
+ // Ensure systemd is the init system
2616
+ `ln -sf /lib/systemd/systemd /sbin/init`,
2407
2617
 
2408
- `apt-get update`,
2409
- `DEBIAN_FRONTEND=noninteractive apt-get install -y apt-utils`, // Install apt-utils non-interactively.
2410
- `DEBIAN_FRONTEND=noninteractive apt-get install -y tzdata kmod keyboard-configuration console-setup iputils-ping`, // Install timezone data, kernel modules, and network tools.
2618
+ // Clean up
2619
+ `apt-get clean`,
2411
2620
  ],
2412
2621
  /**
2413
2622
  * @method user
@@ -2524,6 +2733,157 @@ logdir /var/log/chrony
2524
2733
  `sudo systemctl restart keyboard-setup.service`,
2525
2734
  ],
2526
2735
  },
2736
+ /**
2737
+ * @property {object} rocky
2738
+ * @description Provisioning steps for Rocky Linux-based systems.
2739
+ * @memberof UnderpostBaremetal.systemProvisioningFactory
2740
+ * @namespace UnderpostBaremetal.systemProvisioningFactory.rocky
2741
+ */
2742
+ rocky: {
2743
+ /**
2744
+ * @method base
2745
+ * @description Generates shell commands for basic Rocky Linux system provisioning.
2746
+ * This includes installing Node.js, npm, and underpost CLI tools.
2747
+ * @param {object} params - The parameters for the function.
2748
+ * @memberof UnderpostBaremetal.systemProvisioningFactory.rocky
2749
+ * @returns {string[]} An array of shell commands.
2750
+ */
2751
+ base: () => [
2752
+ // Update system and install EPEL repository
2753
+ `dnf -y update`,
2754
+ `dnf -y install epel-release`,
2755
+
2756
+ // Install essential system tools (avoiding duplicates from container packages)
2757
+ `dnf -y install --allowerasing bzip2 openssh-server nano vim-enhanced less openssl-devel git gnupg2 libnsl perl`,
2758
+ `dnf clean all`,
2759
+
2760
+ // Install Node.js
2761
+ `curl -fsSL https://rpm.nodesource.com/setup_24.x | bash -`,
2762
+ `dnf install -y nodejs`,
2763
+ `dnf clean all`,
2764
+
2765
+ // Verify Node.js and npm versions
2766
+ `node --version`,
2767
+ `npm --version`,
2768
+
2769
+ // Install underpost ci/cd cli
2770
+ `npm install -g underpost`,
2771
+ `underpost --version`,
2772
+ ],
2773
+ /**
2774
+ * @method user
2775
+ * @description Generates shell commands for creating a root user and configuring SSH access on Rocky Linux.
2776
+ * This is a critical security step for initial access to the provisioned system.
2777
+ * @memberof UnderpostBaremetal.systemProvisioningFactory.rocky
2778
+ * @returns {string[]} An array of shell commands.
2779
+ */
2780
+ user: () => [
2781
+ `useradd -m -s /bin/bash -G wheel root`, // Create a root user with bash shell and wheel group (sudo on RHEL)
2782
+ `echo 'root:root' | chpasswd`, // Set a default password for the root user
2783
+ `mkdir -p /home/root/.ssh`, // Create .ssh directory for authorized keys
2784
+ // Add the public SSH key to authorized_keys for passwordless login
2785
+ `echo '${fs.readFileSync(
2786
+ `/home/dd/engine/engine-private/deploy/id_rsa.pub`,
2787
+ 'utf8',
2788
+ )}' > /home/root/.ssh/authorized_keys`,
2789
+ `chown -R root:root /home/root/.ssh`, // Set ownership for security
2790
+ `chmod 700 /home/root/.ssh`, // Set permissions for the .ssh directory
2791
+ `chmod 600 /home/root/.ssh/authorized_keys`, // Set permissions for authorized_keys
2792
+ ],
2793
+ /**
2794
+ * @method timezone
2795
+ * @description Generates shell commands for configuring the system timezone on Rocky Linux.
2796
+ * @param {object} params - The parameters for the function.
2797
+ * @param {string} params.timezone - The timezone string (e.g., 'America/Santiago').
2798
+ * @param {string} params.chronyConfPath - The path to the Chrony configuration file (optional).
2799
+ * @memberof UnderpostBaremetal.systemProvisioningFactory.rocky
2800
+ * @returns {string[]} An array of shell commands.
2801
+ */
2802
+ timezone: ({ timezone, chronyConfPath = '/etc/chrony.conf' }) => [
2803
+ // Set system timezone using both methods (for chroot and running system)
2804
+ `ln -sf /usr/share/zoneinfo/${timezone} /etc/localtime`,
2805
+ `echo '${timezone}' > /etc/timezone`,
2806
+ `timedatectl set-timezone ${timezone} 2>/dev/null`,
2807
+
2808
+ // Configure chrony with local NTP server and common NTP pools
2809
+ `echo '# Local NTP server' > ${chronyConfPath}`,
2810
+ `echo 'server 192.168.1.1 iburst prefer' >> ${chronyConfPath}`,
2811
+ `echo '' >> ${chronyConfPath}`,
2812
+ `echo '# Fallback public NTP servers' >> ${chronyConfPath}`,
2813
+ `echo 'server 0.pool.ntp.org iburst' >> ${chronyConfPath}`,
2814
+ `echo 'server 1.pool.ntp.org iburst' >> ${chronyConfPath}`,
2815
+ `echo 'server 2.pool.ntp.org iburst' >> ${chronyConfPath}`,
2816
+ `echo 'server 3.pool.ntp.org iburst' >> ${chronyConfPath}`,
2817
+ `echo '' >> ${chronyConfPath}`,
2818
+ `echo '# Configuration' >> ${chronyConfPath}`,
2819
+ `echo 'driftfile /var/lib/chrony/drift' >> ${chronyConfPath}`,
2820
+ `echo 'makestep 1.0 3' >> ${chronyConfPath}`,
2821
+ `echo 'rtcsync' >> ${chronyConfPath}`,
2822
+ `echo 'logdir /var/log/chrony' >> ${chronyConfPath}`,
2823
+
2824
+ // Enable chronyd to start on boot
2825
+ `systemctl enable chronyd 2>/dev/null`,
2826
+
2827
+ // Create systemd link for boot (works in chroot)
2828
+ `mkdir -p /etc/systemd/system/multi-user.target.wants`,
2829
+ `ln -sf /usr/lib/systemd/system/chronyd.service /etc/systemd/system/multi-user.target.wants/chronyd.service 2>/dev/null`,
2830
+
2831
+ // Start chronyd if systemd is running
2832
+ `systemctl start chronyd 2>/dev/null`,
2833
+
2834
+ // Restart chronyd to apply configuration
2835
+ `systemctl restart chronyd 2>/dev/null`,
2836
+
2837
+ // Force immediate time synchronization (only if chronyd is running)
2838
+ `chronyc makestep 2>/dev/null`,
2839
+
2840
+ // Verify timezone configuration
2841
+ `ls -l /etc/localtime`,
2842
+ `cat /etc/timezone || echo 'No /etc/timezone file'`,
2843
+ `timedatectl status 2>/dev/null || echo 'Timezone set to ${timezone} (timedatectl not available in chroot)'`,
2844
+ `chronyc tracking 2>/dev/null || echo 'Chrony configured but not running (will start on boot)'`,
2845
+ ],
2846
+ /**
2847
+ * @method keyboard
2848
+ * @description Generates shell commands for configuring the keyboard layout on Rocky Linux.
2849
+ * This uses localectl to set the keyboard layout for both console and X11.
2850
+ * @param {string} [keyCode='us'] - The keyboard layout code (e.g., 'us', 'es').
2851
+ * @memberof UnderpostBaremetal.systemProvisioningFactory.rocky
2852
+ * @returns {string[]} An array of shell commands.
2853
+ */
2854
+ keyboard: (keyCode = 'us') => [
2855
+ // Configure vconsole.conf for console keyboard layout (persistent)
2856
+ `echo 'KEYMAP=${keyCode}' > /etc/vconsole.conf`,
2857
+ `echo 'FONT=latarcyrheb-sun16' >> /etc/vconsole.conf`,
2858
+
2859
+ // Configure locale.conf for system locale
2860
+ `echo 'LANG=en_US.UTF-8' > /etc/locale.conf`,
2861
+ `echo 'LC_ALL=en_US.UTF-8' >> /etc/locale.conf`,
2862
+
2863
+ // Set keyboard layout using localectl (works if systemd is running)
2864
+ `localectl set-locale LANG=en_US.UTF-8 2>/dev/null`,
2865
+ `localectl set-keymap ${keyCode} 2>/dev/null`,
2866
+ `localectl set-x11-keymap ${keyCode} 2>/dev/null`,
2867
+
2868
+ // Configure X11 keyboard layout file directly
2869
+ `mkdir -p /etc/X11/xorg.conf.d`,
2870
+ `echo 'Section "InputClass"' > /etc/X11/xorg.conf.d/00-keyboard.conf`,
2871
+ `echo ' Identifier "system-keyboard"' >> /etc/X11/xorg.conf.d/00-keyboard.conf`,
2872
+ `echo ' MatchIsKeyboard "on"' >> /etc/X11/xorg.conf.d/00-keyboard.conf`,
2873
+ `echo ' Option "XkbLayout" "${keyCode}"' >> /etc/X11/xorg.conf.d/00-keyboard.conf`,
2874
+ `echo 'EndSection' >> /etc/X11/xorg.conf.d/00-keyboard.conf`,
2875
+
2876
+ // Load the keymap immediately (if not in chroot)
2877
+ `loadkeys ${keyCode} 2>/dev/null || echo 'Keymap ${keyCode} configured (loadkeys not available in chroot)'`,
2878
+
2879
+ // Verify configuration
2880
+ `echo 'Keyboard configuration files:'`,
2881
+ `cat /etc/vconsole.conf`,
2882
+ `cat /etc/locale.conf`,
2883
+ `cat /etc/X11/xorg.conf.d/00-keyboard.conf 2>/dev/null || echo 'X11 config created'`,
2884
+ `localectl status 2>/dev/null || echo 'Keyboard layout set to ${keyCode} (localectl not available in chroot)'`,
2885
+ ],
2886
+ },
2527
2887
  },
2528
2888
 
2529
2889
  /**