@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.
- package/README.md +2 -2
- package/baremetal/commission-workflows.json +33 -3
- package/bin/deploy.js +1 -1
- package/cli.md +5 -2
- package/conf.js +1 -0
- package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
- package/manifests/deployment/dd-test-development/deployment.yaml +2 -2
- package/package.json +1 -1
- package/packer/scripts/fuse-tar-root +3 -3
- package/scripts/disk-clean.sh +23 -23
- package/scripts/gpu-diag.sh +2 -2
- package/scripts/ip-info.sh +11 -11
- package/scripts/maas-upload-boot-resource.sh +1 -1
- package/scripts/nvim.sh +1 -1
- package/scripts/packer-setup.sh +13 -13
- package/scripts/rocky-setup.sh +2 -2
- package/scripts/rpmfusion-ffmpeg-setup.sh +4 -4
- package/scripts/ssl.sh +7 -7
- package/src/api/document/document.model.js +30 -1
- package/src/api/document/document.router.js +1 -0
- package/src/api/document/document.service.js +339 -25
- package/src/cli/baremetal.js +689 -329
- package/src/cli/cluster.js +50 -52
- package/src/cli/deploy.js +1 -1
- package/src/cli/index.js +4 -1
- package/src/cli/lxd.js +3 -3
- package/src/cli/run.js +1 -1
- package/src/client/components/core/Css.js +16 -2
- package/src/client/components/core/Modal.js +125 -159
- package/src/client/components/core/Panel.js +276 -17
- package/src/client/components/core/PanelForm.js +24 -2
- package/src/client/components/core/SearchBox.js +801 -0
- package/src/client/services/document/document.service.js +23 -0
- package/src/index.js +1 -1
- package/src/server/dns.js +4 -4
package/src/cli/baremetal.js
CHANGED
|
@@ -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
|
|
172
|
-
let
|
|
177
|
+
// Define the bootstrap architecture.
|
|
178
|
+
let bootstrapArch;
|
|
173
179
|
|
|
174
|
-
// Set
|
|
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
|
-
|
|
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 (
|
|
489
|
-
switch (
|
|
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
|
|
525
|
+
shellExec(`sudo snap stop maas.pebble`);
|
|
519
526
|
shellExec(`sudo snap stop maas`);
|
|
520
|
-
shellExec(`sudo snap remove maas --purge
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
625
|
+
if (bootstrapArch !== callbackMetaData.runnerHost.architecture)
|
|
615
626
|
UnderpostBaremetal.API.crossArchBinFactory({
|
|
616
627
|
nfsHostPath,
|
|
617
|
-
|
|
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
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
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
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
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 =
|
|
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
|
-
|
|
973
|
-
|
|
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
|
-
|
|
984
|
-
}
|
|
1101
|
+
|
|
985
1102
|
const commissionMonitorPayload = {
|
|
986
1103
|
macAddress,
|
|
987
1104
|
ipAddress,
|
|
988
1105
|
hostname,
|
|
989
|
-
|
|
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
|
|
1040
|
-
* @description Downloads
|
|
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
|
-
* @
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1072
|
-
const
|
|
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
|
-
//
|
|
1075
|
-
|
|
1076
|
-
|
|
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/
|
|
1086
|
-
const extractDir = `${nfsHostPath}
|
|
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
|
|
1194
|
+
logger.info(`Downloading ISO for ${arch}...`);
|
|
1090
1195
|
logger.info(`URL: ${isoUrl}`);
|
|
1091
|
-
|
|
1092
|
-
shellExec(`wget --progress=bar:force -O ${isoPath} "${isoUrl}"
|
|
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
|
|
1106
|
-
const mountPoint = `${nfsHostPath}/mnt-${
|
|
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
|
|
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
|
-
//
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
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
|
-
|
|
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: (
|
|
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 {
|
|
1218
|
-
* @param {string} params.nfsHostPath - The NFS host path for
|
|
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
|
|
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
|
|
1315
|
+
logger.info('Using live ISO for boot (disk-based commissioning)');
|
|
1226
1316
|
const arch = resource.architecture.split('/')[0];
|
|
1227
|
-
const
|
|
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}'
|
|
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
|
-
|
|
1965
|
-
|
|
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,
|
|
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 {
|
|
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
|
|
2202
|
+
* @returns {Promise<void>} A promise object with machine and discovery details.
|
|
2004
2203
|
* @memberof UnderpostBaremetal
|
|
2005
2204
|
*/
|
|
2006
|
-
async commissionMonitor({ macAddress, ipAddress, hostname,
|
|
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
|
-
|
|
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({
|
|
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,
|
|
2193
|
-
switch (
|
|
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
|
|
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,
|
|
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 (
|
|
2231
|
-
switch (
|
|
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 {
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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 (
|
|
2337
|
-
|
|
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
|
|
2405
|
-
`apt install -y systemd-sysv openssh-server sudo locales udev util-linux
|
|
2406
|
-
|
|
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
|
-
|
|
2409
|
-
`
|
|
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
|
/**
|