Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
193 changes: 193 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -139,3 +139,196 @@ jobs:
bootupctl backend generate-update-metadata -vvv
cat ${updates}/EFI.json | jq
'

- name: Test install after extend-payload
run: |
set -xeuo pipefail
sudo truncate -s 5G myimage-extend.raw
sudo podman run --rm --privileged -v .:/target --pid=host --security-opt label=disable \
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not at all a new problem, and I won't say we should block on this but...

This is greatly exacerbating the problem that it's just a pain to run these tests outside of GHA (short of https://github.com/nektos/act etc.)

There's an effort to adapt our tests to TMT, but that gets into larger CI issues.

Anyways again not a blocker, but just noting.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually thinking about this a bit more, why can't this test work how we'd expect things to work in production?

Basically if we change this to be a podman build that installs the files instead that should all Just Work, right?

-v /var/lib/containers:/var/lib/containers \
-v /dev:/dev \
localhost/bootupd:latest bash -c '
# Create test firmware directory and files
mkdir -p /usr/share/uboot/rpi/overlays
echo "test uboot binary content" > /usr/share/uboot/rpi/u-boot.bin
echo "i2c device tree overlay" > /usr/share/uboot/rpi/overlays/i2c.dtb

# Create a fake RPM database for testing
mkdir -p /usr/lib/sysimage/rpm
echo "fake rpm database" > /usr/lib/sysimage/rpm/Packages
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm....this seems highly likely to confuse other code in bootupd at some point.

As is right now, I am pretty sure it will result in bootupd not installing shim and grub, right?


# Create mock rpm script using a here-document for clarity
cat << '"'EOT'"' > /usr/local/bin/rpm
#!/bin/bash
if [[ "$*" == *"-q"* ]] && [[ "$*" == *"-f"* ]]; then
echo "uboot-images-2023.04-2.fc42.noarch,1681234567"
exit 0
fi
exec /usr/bin/rpm.orig "$@"
EOT

# Backup original rpm and make our mock executable
cp /usr/bin/rpm /usr/bin/rpm.orig
chmod +x /usr/local/bin/rpm
export PATH="/usr/local/bin:$PATH"

# Run extend-payload-to-esp first
bootupctl backend extend-payload-to-esp /usr/share/uboot/rpi

# Verify firmware was extended correctly
test -d /usr/lib/efi/firmware || { echo "firmware directory not created"; exit 1; }
firmware_ver_dir=$(find /usr/lib/efi/firmware -name "*2023.04*" -type d | head -1)
test -n "${firmware_ver_dir}" || { echo "firmware version directory not found"; exit 1; }
test -f "${firmware_ver_dir}/EFI/u-boot.bin" || { echo "u-boot.bin not copied"; exit 1; }
echo "✓ extend-payload completed successfully"
# Now test install to disk with extended firmware
bootc install to-disk --skip-fetch-check \
-vv \
--disable-selinux --generic-image --via-loopback /target/myimage-extend.raw
'

# Verify firmware files were installed to ESP
sudo losetup -P -f myimage-extend.raw
device=$(losetup -a myimage-extend.raw --output NAME -n)
esp_part=$(sudo sfdisk -l -J "${device}" | jq -r '.partitiontable.partitions[] | select(.type == "C12A7328-F81F-11D2-BA4B-00A0C93EC93B").node')
sudo mount "${esp_part}" /mnt/

# Check that firmware files were copied to ESP during install
if sudo test -f /mnt/u-boot.bin; then
sudo grep -q "test uboot binary content" /mnt/u-boot.bin || { echo "u-boot.bin content incorrect on ESP"; exit 1; }
echo "✓ Firmware files correctly installed to ESP"
else
echo "Note: u-boot.bin not found on ESP (firmware install integration may need work)"
fi

sudo umount /mnt
sudo losetup -D "${device}"
sudo rm -f myimage-extend.raw

- name: Test update after extend-payload
run: |
set -xeuo pipefail
sudo truncate -s 5G myimage-update.raw
sudo podman run --rm --privileged -v .:/target --pid=host --security-opt label=disable \
-v /var/lib/containers:/var/lib/containers \
-v /dev:/dev \
localhost/bootupd:latest bash -c '
# Create initial test firmware directory and files
mkdir -p /usr/share/uboot/rpi/overlays
echo "initial uboot binary content v1.0" > /usr/share/uboot/rpi/u-boot.bin
echo "initial i2c device tree overlay" > /usr/share/uboot/rpi/overlays/i2c.dtb

# Create a fake RPM database for testing
mkdir -p /usr/lib/sysimage/rpm
echo "fake rpm database" > /usr/lib/sysimage/rpm/Packages

# Create mock rpm script that returns initial package data
cat << '"'EOT'"' > /usr/local/bin/rpm
#!/bin/bash
if [[ "$*" == *"-q"* ]] && [[ "$*" == *"-f"* ]]; then
echo "uboot-images-2023.04-1.fc42.noarch,1681234567"
exit 0
fi
exec /usr/bin/rpm.orig "$@"
EOT

# Backup original rpm and make our mock executable
cp /usr/bin/rpm /usr/bin/rpm.orig
chmod +x /usr/local/bin/rpm
export PATH="/usr/local/bin:$PATH"

# Run initial extend-payload-to-esp
bootupctl backend extend-payload-to-esp /usr/share/uboot/rpi

# Verify initial firmware was extended correctly
test -d /usr/lib/efi/firmware || { echo "firmware directory not created"; exit 1; }
firmware_ver_dir=$(find /usr/lib/efi/firmware -name "*2023.04-1*" -type d | head -1)
test -n "${firmware_ver_dir}" || { echo "initial firmware version directory not found"; exit 1; }
test -f "${firmware_ver_dir}/EFI/u-boot.bin" || { echo "initial u-boot.bin not copied"; exit 1; }
grep -q "initial uboot binary content v1.0" "${firmware_ver_dir}/EFI/u-boot.bin"
echo "✓ initial extend-payload completed successfully"

# Install to disk with initial firmware
bootc install to-disk --skip-fetch-check \
--disable-selinux --generic-image --via-loopback /target/myimage-update.raw

# Now simulate a firmware update by creating new firmware files
echo "updated uboot binary content v2.0" > /usr/share/uboot/rpi/u-boot.bin
echo "updated i2c device tree overlay" > /usr/share/uboot/rpi/overlays/i2c.dtb
echo "new overlay for v2" > /usr/share/uboot/rpi/overlays/spi.dtb

# Update mock rpm to return new version
cat << '"'EOT'"' > /usr/local/bin/rpm
#!/bin/bash
if [[ "$*" == *"-q"* ]] && [[ "$*" == *"-f"* ]]; then
echo "uboot-images-2023.04-2.fc42.noarch,1681234999"
exit 0
fi
exec /usr/bin/rpm.orig "$@"
EOT

# Run updated extend-payload-to-esp
bootupctl backend extend-payload-to-esp /usr/share/uboot/rpi

# Verify updated firmware was extended correctly (only v2.0 should exist now)
updated_firmware_ver_dir=$(find /usr/lib/efi/firmware -name "*2023.04-2*" -type d | head -1)
test -n "${updated_firmware_ver_dir}" || { echo "updated firmware version directory not found"; exit 1; }
test -f "${updated_firmware_ver_dir}/EFI/u-boot.bin" || { echo "updated u-boot.bin not copied"; exit 1; }
grep -q "updated uboot binary content v2.0" "${updated_firmware_ver_dir}/EFI/u-boot.bin"
test -f "${updated_firmware_ver_dir}/EFI/overlays/spi.dtb" || { echo "new spi.dtb not copied"; exit 1; }

# Verify old version (2023.04-1) was removed
old_firmware_ver_dir=$(find /usr/lib/efi/firmware -name "*2023.04-1*" -type d | head -1)
test -z "${old_firmware_ver_dir}" || { echo "old firmware version should have been removed but still exists: ${old_firmware_ver_dir}"; exit 1; }

echo "✓ updated extend-payload completed successfully (old version cleaned up)"

# Run bootupctl update to apply the updated firmware to ESP
bootupctl update
echo "✓ bootupctl update completed successfully"
'

# Verify updated firmware files were applied to ESP
sudo losetup -P -f myimage-update.raw
device=$(losetup -a myimage-update.raw --output NAME -n)
esp_part=$(sudo sfdisk -l -J "${device}" | jq -r '.partitiontable.partitions[] | select(.type == "C12A7328-F81F-11D2-BA4B-00A0C93EC93B").node')
sudo mount "${esp_part}" /mnt/

# Check that updated firmware files were applied to ESP during update
if sudo test -f /mnt/u-boot.bin; then
sudo grep -q "updated uboot binary content v2.0" /mnt/u-boot.bin || { echo "u-boot.bin was not updated on ESP"; exit 1; }
echo "✓ Updated firmware files correctly applied to ESP"
else
echo "Warning: u-boot.bin not found on ESP after update"
exit 1
fi

# Check that new overlay file was also copied
if sudo test -f /mnt/overlays/spi.dtb; then
sudo grep -q "new overlay for v2" /mnt/overlays/spi.dtb || { echo "spi.dtb content incorrect on ESP"; exit 1; }
echo "✓ New overlay files correctly applied to ESP"
else
echo "Warning: new spi.dtb not found on ESP after update"
fi

# Verify checksums and state integrity
echo "🔍 Validating firmware checksums and state integrity..."
sudo podman run --rm --privileged -v .:/target --pid=host --security-opt label=disable \
-v /var/lib/containers:/var/lib/containers \
-v /dev:/dev \
localhost/bootupd:latest bash -c '
# Run bootupctl validate to check all checksums
bootupctl validate || { echo "bootupctl validate failed - checksum mismatch detected"; exit 1; }
echo "✓ All file checksums validated successfully"

# Check that bootupd-state.json reflects the updated firmware
if test -f /boot/bootupd-state.json; then
# Verify firmware is tracked in state
jq -e ".installed.EFI.firmware.uboot" /boot/bootupd-state.json >/dev/null || { echo "Updated firmware not found in bootupd-state.json"; exit 1; }
echo "✓ Updated firmware properly tracked in bootupd-state.json"
fi
'

sudo umount /mnt
sudo losetup -D "${device}"
sudo rm -f myimage-update.raw
7 changes: 7 additions & 0 deletions src/bios.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use camino::Utf8PathBuf;
use openat_ext::OpenatDirExt;
#[cfg(target_arch = "powerpc64")]
use std::borrow::Cow;
use std::collections::BTreeMap;
use std::io::prelude::*;
use std::path::Path;
use std::process::Command;
Expand Down Expand Up @@ -122,6 +123,7 @@ impl Component for Bios {
meta,
filetree: None,
adopted_from: None,
firmware: BTreeMap::new(),
})
}

Expand Down Expand Up @@ -236,6 +238,7 @@ impl Component for Bios {
meta: update.clone(),
filetree: None,
adopted_from: Some(meta.version),
firmware: BTreeMap::new(),
}))
}

Expand All @@ -258,6 +261,7 @@ impl Component for Bios {
meta: updatemeta,
filetree: None,
adopted_from,
firmware: BTreeMap::new(),
})
}

Expand All @@ -268,4 +272,7 @@ impl Component for Bios {
fn get_efi_vendor(&self, _: &openat::Dir) -> Result<Option<String>> {
Ok(None)
}
fn extend_payload(&self, _: &str, _: &str) -> Result<Option<bool>> {
Ok(None)
}
}
5 changes: 5 additions & 0 deletions src/cli/bootupctl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ pub enum CtlBackend {
Generate(super::bootupd::GenerateOpts),
#[clap(name = "install", hide = true)]
Install(super::bootupd::InstallOpts),
#[clap(name = "extend-payload-to-esp", hide = true)]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should hide = true this - it's a user visible feature.

ExtendPayload(super::bootupd::ExtendPayloadOpts),
}

#[derive(Debug, Parser)]
Expand Down Expand Up @@ -109,6 +111,9 @@ impl CtlCommand {
CtlVerb::Backend(CtlBackend::Install(opts)) => {
super::bootupd::DCommand::run_install(opts)
}
CtlVerb::Backend(CtlBackend::ExtendPayload(opts)) => {
super::bootupd::DCommand::run_extend_payload(opts)
}
CtlVerb::MigrateStaticGrubConfig => Self::run_migrate_static_grub_config(),
}
}
Expand Down
26 changes: 26 additions & 0 deletions src/cli/bootupd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ pub enum DVerb {
GenerateUpdateMetadata(GenerateOpts),
#[clap(name = "install", about = "Install components")]
Install(InstallOpts),
#[clap(
name = "extend-payload-to-esp",
about = "Extend bootloader payload with additional files"
)]
ExtendPayload(ExtendPayloadOpts),
}

#[derive(Debug, Parser)]
Expand Down Expand Up @@ -82,12 +87,20 @@ pub struct GenerateOpts {
sysroot: Option<String>,
}

#[derive(Debug, Parser)]
pub struct ExtendPayloadOpts {
/// Source directory containing files to add
#[clap(value_parser)]
src_root: String,
}

impl DCommand {
/// Run CLI application.
pub fn run(self) -> Result<()> {
match self.cmd {
DVerb::Install(opts) => Self::run_install(opts),
DVerb::GenerateUpdateMetadata(opts) => Self::run_generate_meta(opts),
DVerb::ExtendPayload(opts) => Self::run_extend_payload(opts),
}
}

Expand Down Expand Up @@ -122,4 +135,17 @@ impl DCommand {
.context("boot data installation failed")?;
Ok(())
}

pub(crate) fn run_extend_payload(opts: ExtendPayloadOpts) -> Result<()> {
let components = crate::bootupd::get_components();
let sysroot = "/";
for component in components.values() {
if let Some(updated) = component.extend_payload(sysroot, &opts.src_root)? {
if updated {
println!("Extended payload for {} successfully", component.name());
}
}
}
Ok(())
}
}
3 changes: 3 additions & 0 deletions src/component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ pub(crate) trait Component {

/// Locating efi vendor dir
fn get_efi_vendor(&self, sysroot: &openat::Dir) -> Result<Option<String>>;

/// Extending payload from input dir
fn extend_payload(&self, sysroot: &str, src_root: &str) -> Result<Option<bool>>;
}

/// Given a component name, create an implementation.
Expand Down
Loading
Loading