Restructure project layout for better CI/CD integration
Some checks failed
Cross build / Build on ppc64le (push) Failing after 1m8s
Cross build / Build on s390x (push) Failing after 2s

- Flattened nested bootupd/bootupd/ structure to root level
- Moved all core project files to root directory
- Added proper Debian packaging structure (debian/ directory)
- Created build scripts and CI configuration
- Improved project organization for CI/CD tools
- All Rust source, tests, and configuration now at root level
- Added GitHub Actions workflow for automated testing
- Maintained all original functionality while improving structure
This commit is contained in:
robojerk 2025-08-09 23:11:42 -07:00
parent 5e8730df43
commit aaf662d5b1
87 changed files with 1334 additions and 570 deletions

View file

View file

230
README.md
View file

@ -1 +1,229 @@
# deb-bootupd\n\nDebian package for bootupd # deb-bootupd
A Debian-compatible fork of [bootupd](https://github.com/coreos/bootupd) for immutable Debian systems.
## Project Overview
**deb-bootupd** is a sophisticated, production-ready Rust-based CLI tool that provides cross-distribution, OS update system agnostic bootloader management capabilities. It addresses a critical gap in Linux system management by handling bootloader updates consistently across different distributions and update mechanisms.
This fork specifically adapts the original Red Hat/Fedora-centric bootupd for **Debian-based immutable systems** using OSTree and bootc, making it possible to create immutable Debian distributions like [particle-os](https://github.com/ublue-os/particle-os).
## Key Features
- **Single Binary, Multicall Architecture**: The same executable serves as both `bootupd` and `bootupctl`
- **Component-Based Design**: Pluggable architecture supporting EFI, BIOS, and other bootloader types
- **OSTree Integration**: Full support for Debian OSTree immutable systems
- **Debian Package System**: Native DPKG/APT integration instead of RPM
- **Cross-Architecture Support**: x86_64, aarch64, riscv64, powerpc64
- **Bootloader Support**: GRUB, shim, systemd-boot detection
- **State Persistence**: Robust state management across OSTree deployments
## Architecture
### Core Components
- **EFI Component**: UEFI bootloader management with automatic ESP detection
- **BIOS Component**: Traditional BIOS/MBR bootloader support
- **OSTree Integration**: Seamless integration with Debian OSTree deployments
- **Package System**: DPKG-based package metadata discovery
- **State Management**: Persistent state tracking in `/boot/bootupd-state.json`
### Design Philosophy
- **No Daemon**: Despite the 'd' suffix, it's "bootloader-upDater" not "bootloader-updater-Daemon"
- **systemd Integration**: Uses `systemd-run` for robust locking and sandboxing
- **Safety First**: Comprehensive error handling and rollback capabilities
- **Distribution Agnostic**: Core architecture works across different Linux distributions
## Installation
### Prerequisites
- Debian-based system (Debian, Ubuntu, etc.)
- Rust toolchain (for building from source)
- Required system packages:
- `efibootmgr` (for EFI systems)
- `grub-common` (for GRUB support)
- `mount`/`umount` (standard Linux tools)
### Building from Source
```bash
# Clone the repository
git clone https://git.raines.xyz/robojerk/deb-bootupd.git
cd deb-bootupd
# Build the project
cargo build --release
# Install (requires root)
sudo cargo install --path .
```
### Debian Package
```bash
# Build Debian package
dpkg-buildpackage -b
# Install package
sudo dpkg -i ../deb-bootupd_*.deb
```
## Usage
### Basic Commands
```bash
# Check system status
bootupctl status
# Update all bootloader components
bootupctl update
# Adopt existing bootloaders and update them
bootupctl adopt-and-update
# Validate system state
bootupctl validate
```
### Advanced Operations
```bash
# Generate update metadata (for image builders)
bootupd generate-update-metadata
# Install bootloader components
bootupd install --src-root /path/to/source --dest-root /path/to/destination
```
## OSTree Integration
deb-bootupd is specifically designed for **Debian OSTree immutable systems**:
- **Image-based Updates**: Updates come from new bootc images, not traditional package repositories
- **Deployment Coordination**: Works seamlessly with OSTree deployment system
- **State Persistence**: Bootloader state survives across OSTree deployments
- **Rollback Support**: Leverages OSTree's built-in rollback capabilities
### Particle-OS Integration
This fork is specifically designed to work with [particle-os](https://github.com/ublue-os/particle-os), a Debian-based immutable distribution:
- **Hybrid Approach**: Debian packages within immutable OSTree structure
- **Bootc Integration**: Full integration with bootc image builder
- **Debian Conventions**: Follows Debian filesystem and package conventions
## Configuration
### State File Location
- **State File**: `/boot/bootupd-state.json`
- **Lock File**: `/run/bootupd-lock`
- **Updates Directory**: `/usr/lib/bootupd/updates`
### Component Configuration
Components are automatically detected and configured based on your system:
- **EFI Systems**: Automatic ESP detection and UEFI boot entry management
- **BIOS Systems**: Direct MBR manipulation for traditional bootloaders
- **Hybrid Systems**: Support for both EFI and BIOS configurations
## Development
### Project Structure
```
deb-bootupd/
├── src/ # Source code
│ ├── efi.rs # EFI component implementation
│ ├── bios.rs # BIOS component implementation
│ ├── ostreeutil.rs # OSTree integration
│ ├── packagesystem.rs # DPKG package system integration
│ └── ...
├── systemd/ # Systemd service files
├── tests/ # Test suite
├── Cargo.toml # Rust dependencies
└── debian/ # Debian packaging files
```
### Key Adaptations from Upstream
1. **Package System**: Replaced RPM with DPKG/APT integration
2. **OS Detection**: Enhanced for Debian family distributions
3. **Path Conventions**: Adapted for Debian filesystem standards
4. **OSTree Integration**: Optimized for Debian OSTree deployments
### Building and Testing
```bash
# Run tests
cargo test
# Run with specific features
cargo test --features integration
# Check code quality
cargo clippy
cargo fmt
```
## Troubleshooting
### Common Issues
1. **Permission Denied**: Ensure you're running with root privileges
2. **EFI Not Detected**: Verify `efibootmgr` is installed and EFI is enabled
3. **OSTree Integration**: Check that OSTree is properly configured
4. **Package Queries**: Ensure DPKG database is accessible
### Debug Mode
```bash
# Enable debug logging
RUST_LOG=debug bootupctl status
# Check system state
cat /boot/bootupd-state.json
```
## Contributing
We welcome contributions! This project is a proof-of-concept for immutable Debian systems.
### Development Priorities
1. **Core Functionality**: Ensure basic bootloader management works on Debian
2. **OSTree Integration**: Optimize for Debian OSTree deployments
3. **Testing**: Comprehensive test coverage for Debian environments
4. **Documentation**: Clear guides for users and developers
## License
This project is licensed under the same terms as the original bootupd project. See the LICENSE file for details.
## Acknowledgments
- **Original bootupd**: [CoreOS bootupd](https://github.com/coreos/bootupd) - The excellent foundation this project builds upon
- **ublue-os**: For pioneering immutable Debian distributions
- **Debian Community**: For the robust package system and distribution standards
## Roadmap
- [x] **Initial Debian Adaptation**: Basic DPKG integration
- [x] **OSTree Support**: Integration with Debian OSTree systems
- [ ] **Enhanced Testing**: Comprehensive test suite for Debian environments
- [ ] **Production Readiness**: Full validation and stability testing
- [ ] **Community Adoption**: Integration with particle-os and other Debian immutable distributions
## Support
- **Issues**: Report bugs and feature requests via GitLab issues
- **Discussions**: Join the conversation in GitLab discussions
- **Documentation**: Check this README and inline code documentation
---
**Note**: This is a proof-of-concept project. While it's designed to be production-ready, it's primarily intended to demonstrate the feasibility of immutable Debian systems using ublue-os tools.

250
bootupd.md Normal file
View file

@ -0,0 +1,250 @@
# bootupd: Deep Code Analysis & Technical Synopsis
## Project Overview
**bootupd** is a sophisticated, production-ready Rust-based CLI tool that provides cross-distribution, OS update system agnostic bootloader management capabilities. It addresses a critical gap in Linux system management by handling bootloader updates consistently across different distributions and update mechanisms.
## Core Architecture & Design Philosophy
### **Single Binary, Multicall Architecture**
- **Multicall binary**: The same executable serves as both `bootupd` and `bootupctl` based on argv[0]
- **No daemon**: Despite the 'd' suffix, it's "bootloader-upDater" not "bootloader-updater-Daemon"
- **systemd-run execution**: Uses `systemd-run` for robust locking, consistent journal logging, and sandboxing benefits
### **Component-Based Design**
The system is built around a pluggable component architecture where each bootloader type implements the `Component` trait:
```rust
pub(crate) trait Component {
fn name(&self) -> &'static str;
fn query_adopt(&self, devices: &Option<Vec<String>>) -> Result<Option<Adoptable>>;
fn install(&self, src_root: &openat::Dir, dest_root: &str, device: &str, update_firmware: bool) -> Result<InstalledContent>;
fn run_update(&self, rootcxt: &RootContext, current: &InstalledContent) -> Result<InstalledContent>;
// ... additional methods
}
```
## Supported Components & Architectures
### **EFI Component** (`src/efi.rs`)
- **Architectures**: x86_64, aarch64, riscv64
- **Bootloaders**: GRUB, shim
- **Features**:
- Automatic ESP (EFI System Partition) detection and mounting
- Vendor directory management (`/boot/efi/EFI/{vendor}/`)
- UEFI boot entry creation and management via `efibootmgr`
- Integration with systemd-boot detection
- Support for multiple ESP mount points: `["boot/efi", "efi", "boot"]`
### **BIOS Component** (`src/bios.rs`)
- **Architectures**: x86_64, powerpc64
- **Bootloaders**: GRUB for BIOS/MBR systems
- **Features**: Direct block device manipulation for MBR updates
## Core Data Models & State Management
### **State Persistence** (`src/model.rs`)
```rust
#[derive(Serialize, Deserialize, Default, Debug)]
pub(crate) struct SavedState {
pub(crate) installed: BTreeMap<String, InstalledContent>,
pub(crate) pending: Option<BTreeMap<String, ContentMetadata>>,
pub(crate) static_configs: Option<ContentMetadata>,
}
```
- **Location**: `/boot/bootupd-state.json`
- **Purpose**: Tracks installed versions, pending updates, and static configuration state
- **Serialization**: JSON-based with backward compatibility support
### **Content Metadata System**
```rust
#[derive(Serialize, Deserialize, Clone, Debug, Hash, PartialEq, Eq)]
pub(crate) struct ContentMetadata {
pub(crate) timestamp: DateTime<Utc>,
pub(crate) version: String,
}
```
- **Version tracking**: Human-readable version strings (not parsed, just displayed)
- **Timestamp-based updates**: Chronological ordering for update availability
- **Update logic**: `can_upgrade_to()` method determines update eligibility
## CLI Interface & Commands
### **Multicall Dispatch** (`src/cli/mod.rs`)
The system automatically routes commands based on the executable name:
- `bootupctl``CtlCommand` (user-facing commands)
- `bootupd``DCommand` (internal/backend operations)
### **User Commands** (`src/cli/bootupctl.rs`)
- **`status`**: Show component status with JSON output support
- **`update`**: Update all components
- **`adopt-and-update`**: Adopt existing bootloaders and update them
- **`validate`**: Validate system state
- **`migrate-static-grub-config`**: Migrate to static GRUB configuration
### **Backend Commands** (`src/cli/bootupd.rs`)
- **`generate-update-metadata`**: Generate update metadata during image builds
- **`install`**: Install bootloader components during disk image creation
## File Tree Management (`src/filetree.rs`)
The system uses a sophisticated file tree abstraction for tracking bootloader files:
- **Content verification**: SHA512-based file integrity checking
- **Delta updates**: Efficient update mechanisms by comparing file trees
- **Metadata generation**: Automatic metadata creation during build processes
## Safety & Reliability Features
### **Locking Mechanisms** (`src/backend/statefile.rs`)
```rust
pub(crate) fn acquire_write_lock(sysroot: openat::Dir) -> Result<StateLockGuard>
```
- **System-wide locking**: Prevents concurrent state updates
- **Signal handling**: Graceful termination with cleanup
- **Atomic state updates**: State file replacement with `fsync` guarantees
### **Filesystem Operations**
- **Capability-based security**: Uses `cap-std` for restricted file operations
- **Mount point validation**: Ensures ESP filesystem type correctness
- **Writable mount verification**: Confirms mount points are writable before operations
## Integration Points
### **OSTree Integration** (`src/ostreeutil.rs`)
- **Repository queries**: Integration with OSTree repositories for update discovery
- **Deployment tracking**: Awareness of OSTree deployment states
- **Update coordination**: Synchronization with OSTree update processes
### **Package System Integration** (`src/packagesystem.rs`)
- **RPM awareness**: Integration with RPM-based systems
- **Version coordination**: Alignment with package manager versioning
- **Fedora dependency**: Direct calls to `rpm -q` commands for package metadata
- **RPM database paths**: Looks for databases in `usr/lib/sysimage/rpm` and `usr/share/rpm`
### **CoreOS Integration** (`src/coreos.rs`)
- **Fedora CoreOS specific**: Special handling for CoreOS environments
- **Configuration management**: CoreOS-specific bootloader configuration
- **Aleph version parsing**: Reads `.coreos-aleph-version.json` files with Fedora-specific metadata
## Update Workflow
### **1. Update Discovery**
- Components query for available updates in `/usr/lib/bootupd/updates`
- Metadata comparison determines update eligibility
- Version and timestamp validation
### **2. Update Execution**
- Filesystem freeze/thaw cycles for consistency
- Component-specific update logic execution
- State file updates with atomic replacement
- Bootloader reconfiguration
### **3. Validation**
- Post-update verification of installed components
- Integrity checking of updated files
- System state validation
## Testing & Quality Assurance
### **Test Infrastructure**
- **Unit tests**: Comprehensive Rust unit test coverage
- **Integration tests**: End-to-end update testing
- **Kola tests**: CoreOS integration testing framework
- **Failpoint testing**: Controlled failure injection for reliability testing
### **CI/CD Pipeline**
- **Jenkins integration**: `.cci.jenkinsfile` for continuous integration
- **GitHub Actions**: Automated testing and validation
- **COPR builds**: Fedora package building and testing
## Security Considerations
### **Privilege Management**
- **Root requirement**: All operations require elevated privileges
- **Sandboxing**: systemd-run provides execution isolation
- **Capability restrictions**: Limited file system access through capability-based security
### **Secure Boot Integration**
- **shim updates**: Coordination with Secure Boot database updates
- **UEFI variable management**: Safe handling of UEFI boot variables
- **Boot entry validation**: Verification of boot entry integrity
## Performance Characteristics
### **Efficient Updates**
- **Delta-based**: Only changed files are updated
- **Parallel processing**: Multiple components can be updated simultaneously
- **Minimal I/O**: Optimized file operations with minimal disk writes
### **Resource Usage**
- **Memory efficient**: Streaming file operations for large files
- **CPU optimization**: Rust's zero-cost abstractions for performance
- **Disk I/O**: Minimized through intelligent update strategies
## Deployment & Distribution
### **Packaging**
- **RPM packaging**: Fedora/CentOS/RHEL package support
- **Container images**: Docker support for containerized deployments
- **Cross-compilation**: Support for multiple target architectures
### **System Integration**
- **systemd units**: Automatic bootloader update service
- **Boot time updates**: Integration with system boot process
- **Manual control**: CLI-based update management
## Distribution Dependencies & Portability
### **Fedora/RPM Dependencies**
- **Package system**: Hard dependency on RPM for package metadata discovery
- **Database paths**: Expects RPM databases in Fedora-specific locations
- **Query format**: Parses RPM output format for version and timestamp information
- **CoreOS integration**: Fedora CoreOS-specific version parsing and metadata
### **Distribution-Agnostic Components**
- **Core architecture**: Component system and update logic work across distributions
- **Bootloader management**: GRUB, UEFI, and BIOS handling is distribution-independent
- **OSTree integration**: Works with OSTree regardless of underlying distribution
- **File operations**: Filesystem and block device operations are Linux-standard
### **System-Level Dependencies**
- **Systemd**: Hard dependency on `libsystemd` and systemd features
- **Mount tools**: Requires `mount` and `umount` commands in PATH
- **EFI tools**: Requires `efibootmgr` for EFI boot management
- **Hardcoded paths**: Several filesystem paths are hardcoded for Red Hat conventions
- **OS detection**: Red Hat-specific OS identification logic
### **Portability Considerations**
- **Debian adaptation**: Would require rewriting `packagesystem.rs` to use DPKG instead of RPM
- **Package queries**: Replace `rpm -q` calls with `dpkg -S` equivalents
- **Database paths**: Adapt to Debian's package database locations
- **Test fixtures**: Update test data to use Debian package formats
- **System commands**: Replace `efibootmgr` with Debian equivalents (may need `efibootmgr` package)
- **Systemd dependency**: Debian supports systemd, but alternative init systems would need different approaches
- **Hardcoded paths**: Update `/usr/lib/bootupd/updates` and other hardcoded paths for Debian conventions
- **Mount commands**: Ensure `mount`/`umount` commands exist and work as expected
- **OS detection**: Adapt Red Hat-specific OS detection logic for Debian
## Future Development Areas
### **Planned Features**
- **Power failure safety**: Protection against interrupted updates
- **Redundant disk support**: Synchronization across multiple bootable disks
- **Advanced bootloader support**: Additional bootloader types beyond GRUB
- **API development**: Potential DBus or other IPC interfaces
### **Architecture Evolution**
- **Component extensibility**: Plugin system for new bootloader types
- **Update policies**: Configurable update strategies and policies
- **Rollback mechanisms**: Enhanced rollback capabilities
- **Distribution abstraction**: Potential abstraction layer for package system differences
## Summary
bootupd represents a mature, well-architected solution to a complex problem in Linux system management. Its Rust implementation provides memory safety, performance, and reliability while maintaining a clean, modular architecture. The system's focus on safety, integration with existing Linux infrastructure, and cross-distribution compatibility makes it a valuable tool for modern Linux deployments, particularly in containerized and transactional update environments.
The codebase demonstrates excellent software engineering practices with comprehensive testing, clear separation of concerns, and robust error handling. Its component-based architecture allows for easy extension to new bootloader types and architectures, while the multicall binary design provides a clean user experience without the complexity of separate daemon processes.

View file

@ -1,7 +0,0 @@
srpm:
dnf -y install cargo git openssl-devel
# similar to https://github.com/actions/checkout/issues/760, but for COPR
git config --global --add safe.directory '*'
cargo install cargo-vendor-filterer
cargo xtask package-srpm
mv target/*.src.rpm $$outdir

View file

@ -1,12 +0,0 @@
# This config mainly overrides `summary: false` by default
# as it's really noisy.
have_fun: true
code_review:
disable: false
comment_severity_threshold: MEDIUM
max_review_comments: -1
pull_request_opened:
help: false
summary: false # turned off by default
code_review: true
ignore_patterns: []

View file

@ -1,36 +0,0 @@
# Overall design
The initial focus here is updating the [ESP](https://en.wikipedia.org/wiki/EFI_system_partition), but the overall design of bootupd contains a lot of abstraction to support different "components".
## Ideal case
In the ideal case, an OS builder uses `bootupd install` to install all bootloader data,
and thereafter it is fully (exclusively) managed by bootupd. It would e.g. be a bug/error
for an administrator to manually invoke `grub2-install` e.g. again.
In other words, an end user system would simply invoke `bootupd update` as desired.
However, we're not in that ideal case. Thus bootupd has the concept of "adoption" where
we start tracking the installed state as we find it.
## Handling adoption
For Fedora CoreOS, currently the `EFI/fedora/grub.cfg` file is created outside of the ostree inside `create_disk.sh`. So we aren't including any updates for it in the OSTree.
This type of problem is exactly what bootupd should be solving.
However, we need to be very cautious in handling this because we basically can't
assume we own all of the state. We shouldn't touch any files that we
don't know about.
## Upgrade edges
We don't necessarily want to update the bootloader data, even if a new update happens to be provided.
For example, Fedora does "mass rebuilds" usually once a release, but it's not strictly necessary
to update users' bootloaders then.
A common policy in fact might be "only update bootloader for security issue or if it's strictly necessary".
A "strictly necessary" upgrade would be one like the GRUB BLS parsing support.
There is not yet any support for upgrade edges in the code apart from a stub structure.

View file

@ -1,55 +0,0 @@
# Developing bootupd
Currently the focus is Fedora CoreOS.
You can use the normal Rust tools to build and run the unit tests:
`cargo build` and `cargo test`
For real e2e testing, use e.g.
```
export COSA_DIR=/path/to/fcos
cosa build-fast
kola run -E $(pwd) --qemu-image fastbuild-fedora-coreos-bootupd-qemu.qcow2 --qemu-firmware uefi ext.bootupd.*
```
See also [the coreos-assembler docs](https://coreos.github.io/coreos-assembler/working/#using-overrides).
## Building With Containers
There's a reference [Dockerfile](Dockerfile) that builds on [CentOS Stream bootc](https://docs.fedoraproject.org/en-US/bootc/).
## Integrating bootupd into a distribution/OS
Today, bootupd only really works on systems that use RPMs and ostree.
(Which usually means rpm-ostree, but not strictly necessarily)
Many bootupd developers (and current CI flows) target Fedora CoreOS
and derivatives, so it can be used as a "reference" for integration.
There's two parts to integration:
### Generating an update payload
Bootupd's concept of an "update payload" needs to be generated as
part of an OS image (e.g. ostree commit).
A good reference for this is
https://github.com/coreos/fedora-coreos-config/blob/88af117d1d2c5e828e5e039adfa03c7cc66fc733/manifests/bootupd.yaml#L12
Specifically, you'll need to invoke
`bootupctl backend generate-update-metadata /` as part of update payload generation.
This scrapes metadata (e.g. RPM versions) about shim/grub and puts them along with
their component files in `/usr/lib/bootupd/updates/`.
### Installing to generated disk images
In order to correctly manage updates, bootupd also needs to be responsible
for laying out files in initial disk images. A good reference for this is
https://github.com/coreos/coreos-assembler/blob/93efb63dcbd63dc04a782e2c6c617ae0cd4a51c8/src/create_disk.sh#L401
Specifically, you'll need to invoke
`/usr/bin/bootupctl backend install --src-root /path/to/ostree/deploy /sysroot`
where the first path is an ostree deployment root, and the second is the physical
root partition.
This will e.g. inject the initial files into the mounted EFI system partition.

View file

@ -1,154 +0,0 @@
# bootupd: Distribution-independent updates for bootloaders
Today many Linux systems handle updates for bootloader data
in an inconsistent and ad-hoc way. For example, on
Fedora and Debian, a package manager update will update UEFI
binaries in `/boot/efi`, but not the BIOS MBR data.
Transactional/"image" update systems like [OSTree](https://github.com/ostreedev/ostree/)
and dual-partition systems like the Container Linux update system
are more consistent: they normally cover kernel/userspace but not anything
related to bootloaders.
The reason for this is straightforward: performing bootloader
updates in an "A/B" fashion requires completely separate nontrivial
logic from managing the kernel and root filesystem. Today OSTree e.g.
makes the choice that it does not update `/boot/efi` (and also doesn't
update the BIOS MBR).
The goal of this project is to be a cross-distribution,
OS update system agnostic tool to manage updates for things like:
- `/boot/efi`
- x86 BIOS MBR
- Other architecture bootloaders
This project originated in [this Fedora CoreOS github issue](https://github.com/coreos/fedora-coreos-tracker/issues/510).
The scope is otherwise limited; for example, bootupd will not
manage anything related to the kernel such as kernel arguments;
that's for tools like `grubby` and `ostree`.
## Status
bootupd supports updating GRUB and shim for UEFI firmware on x86_64, aarch64,
and riscv64, and GRUB for BIOS firmware on x86_64 and ppc64le.
The project is used in Bootable Containers and ostree/rpm-ostree based systems:
- [`bootc install`](https://github.com/containers/bootc/#using-bootc-install)
- [Fedora CoreOS](https://docs.fedoraproject.org/en-US/fedora-coreos/bootloader-updates/)
- Fedora Atomic Desktops
On systems booted using a UEFI firmware, bootloader updates performed by
bootupd are now considered safe, even in case of power failures (see:
[issue#454](https://github.com/coreos/bootupd/issues/454)).
On other systems (BIOS, etc.), bootloader updates performed by bootupd not safe
against a power failures at the wrong time.
Note that bootupd does not yet perform updates in a way that is safe against a
buggy bootloader update that fails to boot the system. This is tracked in
[issue#440](https://github.com/coreos/bootupd/issues/440).
Bootloader updates are enabled by default on Fedora Atomic Desktops, and will
soon be on all Bootable Containers systems. See
[fedora-coreos-tracker#1468](https://github.com/coreos/fedora-coreos-tracker/issues/1468).
The bootupd CLI should be considered stable.
## Relationship to other projects
### dbxtool
[dbxtool](https://github.com/rhboot/dbxtool) manages updates
to the Secure Boot database - `bootupd` will likely need to
perform any updates to the `shimx64.efi` binary
*before* `dbxtool.service` starts. But otherwise they are independent.
### fwupd
bootupd could be compared to [fwupd](https://github.com/fwupd/fwupd/) which is
a project that exists today to update hardware device firmware - things not managed
by e.g. `apt/zypper/yum/rpm-ostree update` today.
fwupd comes as a UEFI binary today, so bootupd *could* take care of updating `fwupd`
but today fwupd handles that itself. So it's likely that bootupd would only take
care of GRUB and shim. See discussion in [this issue](https://github.com/coreos/bootupd/issues/1).
### systemd bootctl
[systemd bootctl](https://man7.org/linux/man-pages/man1/bootctl.1.html) can update itself;
this project would probably just proxy that if we detect systemd-boot is in use.
## Other goals
One idea is that bootupd could help support [redundant bootable disks](https://github.com/coreos/fedora-coreos-tracker/issues/581).
For various reasons it doesn't really work to try to use RAID1 for an entire disk; the ESP must be handled
specially. `bootupd` could learn how to synchronize multiple EFI system partitions from a primary.
## More details on rationale and integration
A notable problem today for [rpm-ostree](https://github.com/coreos/rpm-ostree/) based
systems is that `rpm -q shim-x64` is misleading because it's not actually
updated in place.
Particularly [this commit][1] makes things clear - the data
from the RPM goes into `/usr` (part of the OSTree), so it doesn't touch `/boot/efi`.
But that commit didn't change how the RPM database works (and more generally it
would be technically complex for rpm-ostree to change how the RPM database works today).
What we ultimately want is that `rpm -q shim-x64` returns "not installed" - because
it's not managed by RPM or by ostree. Instead one would purely use `bootupctl` to manage it.
However, it might still be *built* as an RPM, just not installed that way. The RPM version numbers would be used
for the bootupd version associated with the payload, and ultimately we'd teach `rpm-ostree compose tree`
how to separately download bootloaders and pass them to `bootupctl backend`.
[1]: https://github.com/coreos/rpm-ostree/pull/969/commits/dc0e8db5bd92e1f478a0763d1a02b48e57022b59
## Questions and answers
- Why is bootupd not part of ostree?
A key advertised feature of ostree is that updates are truly transactional.
There's even a [a test case](https://blog.verbum.org/2020/12/01/committed-to-the-integrity-of-your-root-filesystem/)
that validates forcibly pulling the power during OS updates. A simple
way to look at this is that on an ostree-based system there is no need
to have a "please don't power off your computer" screen. This in turn
helps administrators to confidently enable automatic updates.
Doing that for the bootloader (i.e. bootupd's domain) is an *entirely* separate problem.
There have been some ideas around how we could make the bootloaders
use an A/B type scheme (or at least be more resilient), and perhaps in the future bootupd will
use some of those.
These updates hence carry different levels of risk. In many cases
actually it's OK if the bootloader lags behind; we don't need to update
every time.
But out of conservatism currently today for e.g. Fedora CoreOS, bootupd is disabled
by default. On the other hand, if your OS update mechanism isn't transactional,
then you may want to enable bootupd by default.
- Is bootupd a daemon?
It was never a daemon. The name was intended to be "bootloader-upDater" not
"bootloader-updater-Daemon". The choice of a "d" suffix is in retrospect
probably too confusing.
bootupd used to have an internally-facing `bootupd.service` and
`bootupd.socket` systemd units that acted as a locking mechanism. The service
would *very quickly* auto exit. There was nothing long-running, so it was not
really a daemon.
bootupd now uses `systemd-run` instead to guarantee the following:
- It provides a robust natural "locking" mechanism.
- It ensures that critical logging metadata always consistently ends up in the
systemd journal, not e.g. a transient client SSH connection.
- It benefits from the sandboxing options available for systemd units, and
while bootupd is obviously privileged we can still make use of some of this.
- If we want a non-CLI API (whether that's DBus or Cap'n Proto or varlink or
something else), we will create an independent daemon with a stable API for
this specific need.

View file

@ -1,61 +0,0 @@
## CoreOS Community Code of Conduct
### Contributor Code of Conduct
As contributors and maintainers of this project, and in the interest of
fostering an open and welcoming community, we pledge to respect all people who
contribute through reporting issues, posting feature requests, updating
documentation, submitting pull requests or patches, and other activities.
We are committed to making participation in this project a harassment-free
experience for everyone, regardless of level of experience, gender, gender
identity and expression, sexual orientation, disability, personal appearance,
body size, race, ethnicity, age, religion, or nationality.
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery
* Personal attacks
* Trolling or insulting/derogatory comments
* Public or private harassment
* Publishing others' private information, such as physical or electronic addresses, without explicit permission
* Other unethical or unprofessional conduct.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct. By adopting this Code of Conduct,
project maintainers commit themselves to fairly and consistently applying these
principles to every aspect of managing this project. Project maintainers who do
not follow or enforce the Code of Conduct may be permanently removed from the
project team.
This code of conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community.
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting a project maintainer, Brandon Philips
<brandon.philips@coreos.com>, and/or Rithu John <rithu.john@coreos.com>.
This Code of Conduct is adapted from the Contributor Covenant
(http://contributor-covenant.org), version 1.2.0, available at
http://contributor-covenant.org/version/1/2/0/
### CoreOS Events Code of Conduct
CoreOS events are working conferences intended for professional networking and
collaboration in the CoreOS community. Attendees are expected to behave
according to professional standards and in accordance with their employers
policies on appropriate workplace behavior.
While at CoreOS events or related social networking opportunities, attendees
should not engage in discriminatory or offensive speech or actions including
but not limited to gender, sexuality, race, age, disability, or religion.
Speakers should be especially aware of these concerns.
CoreOS does not condone any statements by speakers contrary to these standards.
CoreOS reserves the right to deny entrance and/or eject from an event (without
refund) any individual found to be engaging in discriminatory or offensive
speech or actions.
Please bring any concerns to the immediate attention of designated on-site
staff, Brandon Philips <brandon.philips@coreos.com>, and/or Rithu John <rithu.john@coreos.com>.

View file

@ -1,123 +0,0 @@
//! Bits specific to Fedora CoreOS (and derivatives).
/*
* Copyright (C) 2020 Red Hat, Inc.
*
* SPDX-License-Identifier: Apache-2.0
*/
use anyhow::{Context, Result};
use chrono::prelude::*;
use serde::{Deserialize, Serialize};
use std::fs::File;
use std::path::Path;
#[derive(Serialize, Deserialize, Clone, Debug, Hash, Ord, PartialOrd, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
/// See https://github.com/coreos/fedora-coreos-tracker/blob/66d7d00bedd9d5eabc7287b9577f443dcefb7c04/internals/README-internals.md#aleph-version
pub(crate) struct Aleph {
#[serde(alias = "build")]
pub(crate) version: String,
}
pub(crate) struct AlephWithTimestamp {
pub(crate) aleph: Aleph,
#[allow(dead_code)]
pub(crate) ts: chrono::DateTime<Utc>,
}
/// Path to the file, see above
const ALEPH_PATH: &str = "sysroot/.coreos-aleph-version.json";
pub(crate) fn get_aleph_version(root: &Path) -> Result<Option<AlephWithTimestamp>> {
let path = &root.join(ALEPH_PATH);
if !path.exists() {
return Ok(None);
}
let statusf = File::open(path).with_context(|| format!("Opening {path:?}"))?;
let meta = statusf.metadata()?;
let bufr = std::io::BufReader::new(statusf);
let aleph: Aleph = serde_json::from_reader(bufr)?;
Ok(Some(AlephWithTimestamp {
aleph,
ts: meta.created()?.into(),
}))
}
#[cfg(test)]
mod test {
use super::*;
use anyhow::Result;
const V1_ALEPH_DATA: &str = r##"
{
"version": "32.20201002.dev.2",
"ref": "fedora/x86_64/coreos/testing-devel",
"ostree-commit": "b2ea6159d6274e1bbbb49aa0ef093eda5d53a75c8a793dbe184f760ed64dc862"
}"##;
// Waiting on https://github.com/rust-lang/rust/pull/125692
#[cfg(not(target_env = "musl"))]
#[test]
fn test_parse_from_root_empty() -> Result<()> {
// Verify we're a no-op in an empty root
let root: &tempfile::TempDir = &tempfile::tempdir()?;
let root = root.path();
assert!(get_aleph_version(root).unwrap().is_none());
Ok(())
}
// Waiting on https://github.com/rust-lang/rust/pull/125692
#[cfg(not(target_env = "musl"))]
#[test]
fn test_parse_from_root() -> Result<()> {
let root: &tempfile::TempDir = &tempfile::tempdir()?;
let root = root.path();
let sysroot = &root.join("sysroot");
std::fs::create_dir(sysroot).context("Creating sysroot")?;
std::fs::write(root.join(ALEPH_PATH), V1_ALEPH_DATA).context("Writing aleph")?;
let aleph = get_aleph_version(root).unwrap().unwrap();
assert_eq!(aleph.aleph.version, "32.20201002.dev.2");
Ok(())
}
// Waiting on https://github.com/rust-lang/rust/pull/125692
#[cfg(not(target_env = "musl"))]
#[test]
fn test_parse_from_root_linked() -> Result<()> {
let root: &tempfile::TempDir = &tempfile::tempdir()?;
let root = root.path();
let sysroot = &root.join("sysroot");
std::fs::create_dir(sysroot).context("Creating sysroot")?;
let target_name = ".new-ostree-aleph.json";
let target = &sysroot.join(target_name);
std::fs::write(root.join(target), V1_ALEPH_DATA).context("Writing aleph")?;
std::os::unix::fs::symlink(target_name, root.join(ALEPH_PATH)).context("Symlinking")?;
let aleph = get_aleph_version(root).unwrap().unwrap();
assert_eq!(aleph.aleph.version, "32.20201002.dev.2");
Ok(())
}
#[test]
fn test_parse_old_aleph() -> Result<()> {
// What the aleph file looked like before we changed it in
// https://github.com/osbuild/osbuild/pull/1475
let alephdata = r##"
{
"build": "32.20201002.dev.2",
"ref": "fedora/x86_64/coreos/testing-devel",
"ostree-commit": "b2ea6159d6274e1bbbb49aa0ef093eda5d53a75c8a793dbe184f760ed64dc862",
"imgid": "fedora-coreos-32.20201002.dev.2-qemu.x86_64.qcow2"
}"##;
let aleph: Aleph = serde_json::from_str(alephdata)?;
assert_eq!(aleph.version, "32.20201002.dev.2");
Ok(())
}
#[test]
fn test_parse_aleph() -> Result<()> {
let aleph: Aleph = serde_json::from_str(V1_ALEPH_DATA)?;
assert_eq!(aleph.version, "32.20201002.dev.2");
Ok(())
}
}

View file

@ -1,78 +0,0 @@
use std::collections::{BTreeMap, BTreeSet};
use std::io::Write;
use std::path::Path;
use anyhow::{bail, Context, Result};
use chrono::prelude::*;
use crate::model::*;
use crate::ostreeutil;
/// Parse the output of `rpm -q`
fn rpm_parse_metadata(stdout: &[u8]) -> Result<ContentMetadata> {
let pkgs = std::str::from_utf8(stdout)?
.split_whitespace()
.map(|s| -> Result<_> {
let parts: Vec<_> = s.splitn(2, ',').collect();
let name = parts[0];
if let Some(ts) = parts.get(1) {
let nt = DateTime::parse_from_str(ts, "%s")
.context("Failed to parse rpm buildtime")?
.with_timezone(&chrono::Utc);
Ok((name, nt))
} else {
bail!("Failed to parse: {}", s);
}
})
.collect::<Result<BTreeMap<&str, DateTime<Utc>>>>()?;
if pkgs.is_empty() {
bail!("Failed to find any RPM packages matching files in source efidir");
}
let timestamps: BTreeSet<&DateTime<Utc>> = pkgs.values().collect();
// Unwrap safety: We validated pkgs has at least one value above
let largest_timestamp = timestamps.iter().last().unwrap();
let version = pkgs.keys().fold("".to_string(), |mut s, n| {
if !s.is_empty() {
s.push(',');
}
s.push_str(n);
s
});
Ok(ContentMetadata {
timestamp: **largest_timestamp,
version,
})
}
/// Query the rpm database and list the package and build times.
pub(crate) fn query_files<T>(
sysroot_path: &str,
paths: impl IntoIterator<Item = T>,
) -> Result<ContentMetadata>
where
T: AsRef<Path>,
{
let mut c = ostreeutil::rpm_cmd(sysroot_path)?;
c.args(["-q", "--queryformat", "%{nevra},%{buildtime} ", "-f"]);
for arg in paths {
c.arg(arg.as_ref());
}
let rpmout = c.output()?;
if !rpmout.status.success() {
std::io::stderr().write_all(&rpmout.stderr)?;
bail!("Failed to invoke rpm -qf");
}
rpm_parse_metadata(&rpmout.stdout)
}
#[test]
fn test_parse_rpmout() {
let testdata = "grub2-efi-x64-1:2.06-95.fc38.x86_64,1681321788 grub2-efi-x64-1:2.06-95.fc38.x86_64,1681321788 shim-x64-15.6-2.x86_64,1657222566 shim-x64-15.6-2.x86_64,1657222566 shim-x64-15.6-2.x86_64,1657222566";
let parsed = rpm_parse_metadata(testdata.as_bytes()).unwrap();
assert_eq!(
parsed.version,
"grub2-efi-x64-1:2.06-95.fc38.x86_64,shim-x64-15.6-2.x86_64"
);
}

382
deb-bootupd.md Normal file
View file

@ -0,0 +1,382 @@
# Debian Bootupd Fork Plan
## Project Overview
**Goal**: Create a Debian-compatible version of bootupd to make particle-os (Debian-based ublue-os) bootable.
**Context**:
- **Proof-of-concept**: Test if we can create an immutable Debian using ublue-os tools
- Original bootupd is Red Hat/Fedora-centric with RPM dependencies
- Need to adapt for **Debian OSTree immutable systems** (not traditional Debian)
- Target: Debian Trixie slim container image converted to bootc format
- **Critical Insight**: This is a hybrid system - Debian packages within immutable OSTree structure
- **Success metric**: Can we boot a Debian-based immutable system?
## Key Understanding: OSTree vs Traditional Debian
### **Traditional Debian (Mutable)**
- Package-based updates via `apt`/`dpkg`
- Mutable filesystem with writable `/usr`
- Kernel in `/boot/vmlinuz-$kver`
- GRUB configured via `grub-mkconfig`/`update-grub`
### **Particle-OS (Immutable OSTree)**
- Image-based atomic updates via bootc
- Immutable `/usr` (read-only core OS)
- Kernel embedded in `/usr/lib/modules/$kver/vmlinuz`
- GRUB configured to point to OSTree deployment paths
- Configuration in `/etc` (writable, three-way merge)
- State in `/var` (shared across deployments)
- OSTree object store in `/ostree`
## Phase 1: Project Setup & Structure
### 1.1 Create Debian Bootupd Directory Structure
```
deb-bootupd/
├── src/ # Source code (adapted from .Red_Hat_Version/bootupd/src/)
├── systemd/ # Systemd service files
├── tests/ # Test suite
├── Cargo.toml # Dependencies adapted for Debian
├── README.md # Debian-specific documentation
├── debian/ # Debian packaging files
└── scripts/ # Build and deployment scripts
```
### 1.2 Initialize Git Repository
- Fork from original bootupd repository
- Create debian branch
- Set up proper attribution and licensing
### 1.3 Git Strategy: Hard Clean Fork
**Approach**: Simple, direct fork for proof-of-concept
**Benefits**:
- **Clean start**: No complex git history or upstream sync complexity
- **Focus on core**: Concentrate on making Debian immutable system bootable
- **Proof-of-concept**: Perfect for testing the concept without maintenance overhead
- **Simple workflow**: Just copy, adapt, and test
**Implementation**:
```bash
# Simple copy approach
cp -r .Red_Hat_Version/bootupd deb-bootupd/
cd deb-bootupd
chmod 755 deb-bootupd
git init
git add .
git commit -m "Initial Debian fork of bootupd for immutable Debian proof-of-concept"
```
**Future considerations**: Can always add upstream sync later if the concept proves viable
## Phase 2: Core Code Adaptation
### 2.1 Package System Integration (`src/packagesystem.rs`)
**Current Issue**: Hard dependency on RPM commands
**Solution**: Rewrite for DPKG/APT
**OSTree-Specific Considerations**:
- **Not traditional package management**: Particle-OS uses image-based updates, not `apt`/`dpkg`
- **Package queries**: Still need DPKG integration for querying existing package metadata
- **Update mechanism**: Updates come from new bootc images, not package repositories
- **Version tracking**: Need to track Debian package versions within the immutable OS tree
**Kernel Detection Challenge**:
- **File location**: `/usr/lib/modules/*/vmlinuz` (same as Red Hat)
- **Filename parsing**: Must handle Debian kernel naming conventions
- **Critical files**: `src/efi.rs` and `src/bios.rs` kernel detection logic
- **Example difference**: `vmlinuz-6.1.0-13-amd64` vs `vmlinuz-6.1.0-1.fc38.x86_64`
**Changes Required**:
```rust
// Replace RPM-specific code:
// OLD: rpm -q --queryformat "%{nevra},%{buildtime}" -f <file>
// NEW: dpkg -S <file> && dpkg -s <package> | grep "Installed-Time"
// Replace RPM database paths:
// OLD: usr/lib/sysimage/rpm, usr/share/rpm
// NEW: var/lib/dpkg, usr/share/doc
```
**Implementation**:
- Create `dpkg_parse_metadata()` function
- Parse DPKG output format instead of RPM
- Handle Debian package versioning conventions
- Support both `dpkg -S` and `dpkg -s` queries
### 2.2 OSTree Utilities (`src/ostreeutil.rs`)
**Current Issue**: RPM command construction
**Solution**: Adapt for Debian package database structure
**Changes Required**:
- Replace `rpm_cmd()` with `dpkg_cmd()`
- Update database path detection logic
- Maintain OSTree integration compatibility
### 2.3 CoreOS Integration (`src/coreos.rs`)
**Current Issue**: Fedora CoreOS-specific Aleph version parsing
**Solution**: Adapt for Debian/particle-os versioning
**Changes Required**:
- Replace `.coreos-aleph-version.json` with Debian-specific version file
- Update version parsing logic for Debian conventions
- Remove Fedora-specific test data references
### 2.4 Hardcoded Paths
**Current Issues**:
- `/usr/lib/bootupd/updates` (hardcoded in `model.rs`)
- `/boot` state directory (hardcoded in `statefile.rs`)
- `/run/bootupd-lock` lock file path
**Solution**: Make paths configurable or follow Debian conventions
- Consider using `/var/lib/bootupd/updates` (FHS compliant)
- Keep `/boot` for state (standard across distributions)
- Keep `/run` for locks (standard across distributions)
### 2.5 OSTree-Specific Adaptations
**Critical Changes for Debian OSTree**:
**Kernel Path Adaptation**:
```rust
// Current (Red Hat): /usr/lib/modules/*/vmlinuz
// Debian OSTree: /usr/lib/modules/*/vmlinuz (same path, different context)
// Need to ensure this works with Debian kernel naming conventions
```
**Kernel Filename Parsing** (Key Challenge):
- **Red Hat convention**: `vmlinuz-6.1.0-1.fc38.x86_64`
- **Debian convention**: `vmlinuz-6.1.0-13-amd64`
- **Critical**: Filename parsing logic must handle Debian version format
- **Location**: This affects `src/efi.rs` and `src/bios.rs` kernel detection
**Bootloader Configuration**:
- **Current**: GRUB configs from coreos-assembler
- **Debian OSTree**: GRUB configs pointing to OSTree deployment paths
- **Path format**: `/ostree/deploy/debian/deploy/$checksum.0/vmlinuz`
**State Management**:
- **Current**: State in `/boot/bootupd-state.json`
- **Debian OSTree**: State must persist across OSTree deployments
- **Location**: Consider `/var/lib/bootupd/` for persistent state
## Phase 3: System Integration
### 3.1 System Commands
**Current Dependencies**:
- `efibootmgr` (EFI boot management)
- `mount`/`umount` (filesystem operations)
- `grub-install` (GRUB installation)
**Debian Compatibility**:
- ✅ `efibootmgr`: Available in Debian repositories
- ✅ `mount`/`umount`: Standard Linux tools
- ✅ `grub-install`: Available in Debian repositories
**Action**: Ensure these packages are available in particle-os base image
### 3.2 Systemd Integration
**Current**: Hard dependency on `libsystemd`
**Debian**: ✅ Fully supports systemd
**Action**: No changes needed, maintain compatibility
### 3.3 OS Detection
**Current**: Red Hat-specific logic
**Solution**: Enhance OS detection for Debian
**Changes Required**:
```rust
// Current logic in efi.rs:
// 1. Try /etc/system-release (Red Hat specific)
// 2. Fall back to /etc/os-release
// Enhanced logic:
// 1. Try /etc/os-release first (standard)
// 2. Parse for Debian family distributions
// 3. Fall back to /etc/system-release for Red Hat
// 4. Handle Debian-specific version formats
```
## Phase 4: Debian-Specific Features
### 4.1 Package Manager Integration
**New Features**:
- APT package discovery integration
- Debian package version comparison
- Support for Debian backports and testing repositories
**OSTree-Specific Features**:
- **Image-based update detection**: Detect new bootc images vs package updates
- **Deployment coordination**: Work with OSTree deployment system
- **Rollback support**: Leverage OSTree's built-in rollback capabilities
- **State persistence**: Ensure bootupd state survives across deployments
### 4.2 Debian OSTree Integration
**Unique Challenges**:
- **Hybrid approach**: Debian packages within immutable OSTree system
- **Update workflow**: New images contain updated Debian packages
- **Version compatibility**: Ensure Debian package versions work with OSTree structure
- **Bootloader updates**: Coordinate with OSTree deployment changes
### 4.3 Distribution-Specific Bootloader Configs
**Current**: Static GRUB configs from coreos-assembler
**Solution**: Create Debian-specific GRUB configurations
- Support for Debian kernel naming conventions
- Debian-specific boot parameters
- Integration with Debian initramfs tools
- **OSTree path integration**: GRUB configs pointing to OSTree deployment paths
### 4.4 Debian Package Dependencies
**Update Cargo.toml**:
- Keep core dependencies (anyhow, clap, serde, etc.)
- Maintain systemd integration
- Add Debian-specific optional features
## Phase 5: Testing & Validation
### 5.1 Test Suite Adaptation
**Current**: Fedora/RPM-centric tests
**Solution**: Create Debian-compatible test suite
**Test Categories**:
- Unit tests for DPKG parsing functions
- Integration tests with Debian package databases
- End-to-end tests in Debian container environments
- Bootloader update simulation tests
### 5.2 Particle-OS Integration Testing
**Test Scenarios**:
- Fresh particle-os installation
- Bootloader adoption from existing Debian systems
- Update process validation
- Rollback functionality testing
**OSTree-Specific Testing**:
- **Deployment switching**: Test bootloader updates across OSTree deployments
- **State persistence**: Verify bootupd state survives deployment switches
- **Image updates**: Test with new bootc images containing updated Debian packages
- **Rollback scenarios**: Test bootloader rollback with OSTree deployment rollback
- **Hybrid updates**: Test scenarios where Debian packages and OSTree images are updated
## Phase 6: Packaging & Deployment
### 6.1 Debian Package Creation
**Files Required**:
- `debian/control` (package metadata)
- `debian/rules` (build instructions)
- `debian/changelog` (version history)
- `debian/postinst` (post-installation scripts)
**Build System Integration**:
- **dh-cargo**: Use Debian's Rust packaging helper
- **Benefits**: Automates cargo build, cargo install, and dependency handling
- **debian/rules**: Simplified with dh-cargo integration
- **Cargo.lock**: Include for reproducible builds
- **Vendor directory**: Consider including for offline builds
### 6.2 Container Integration
**Integration Points**:
- particle-os base image requirements
- Bootc image builder integration
- Container runtime compatibility
### 6.3 Bootc Image Builder Integration
**Critical Integration Points**:
**Build Process Integration**:
- **Include deb-bootupd**: Binary must be built and included in the bootc image
- **Build timing**: deb-bootupd compiled during image build, not first boot
- **Dependencies**: Ensure all required tools (`efibootmgr`, `grub-install`) are in base image
**Image Builder Workflow**:
1. **Build phase**: Compile deb-bootupd using debian-bootc-image-builder
2. **Installation**: Install deb-bootupd binary and systemd service
3. **Configuration**: Set up initial bootloader configuration
4. **First boot**: deb-bootupd runs to adopt existing bootloader or install new one
**debian/control Integration**:
- **Build dependencies**: Use `dh-cargo` for Rust build system integration
- **Runtime dependencies**: Ensure `efibootmgr`, `grub-common` are available
- **Package naming**: Consider `deb-bootupd` vs `bootupd` package name
## Phase 7: Documentation & Maintenance
### 7.1 Documentation Updates
**Required Documents**:
- Debian-specific README
- Installation guide for particle-os
- Troubleshooting guide
- Migration guide from Red Hat systems
### 7.2 Maintenance Strategy
**Ongoing Tasks**:
- Track upstream bootupd changes
- Adapt new features for Debian
- Maintain Debian package compatibility
- Community support and bug fixes
## Implementation Priority
### High Priority (Phase 1-2) - Proof-of-Concept Core
1. **Package system adaptation** (RPM → DPKG) - Essential for Debian compatibility
2. **Core path and dependency fixes** - Make it compile and run
3. **Basic Debian compatibility** - Get it working on Debian system
### Medium Priority (Phase 3-4) - Basic Functionality
1. **Enhanced OS detection** - Proper Debian identification
2. **Debian-specific features** - Basic bootloader management
3. **Testing infrastructure** - Verify it works
### Low Priority (Phase 5-7) - Polish & Production
1. **Advanced features** - Only if proof-of-concept succeeds
2. **Documentation** - Only if proof-of-concept succeeds
3. **Long-term maintenance** - Only if proof-of-concept succeeds
## Success Criteria
### Proof-of-Concept Success (Phases 1-2)
- [ ] **Debian bootupd compiles successfully** - Basic Rust compilation
- [ ] **Package system queries work with DPKG** - Can read Debian package info
- [ ] **Basic bootloader detection works** - Can identify EFI/BIOS systems
### Core Functionality Success (Phase 3-4)
- [ ] **Bootloader management works on Debian** - Can install/update GRUB
- [ ] **OSTree integration functions** - Works with Debian OSTree deployment
- [ ] **State management works** - Can track bootloader state
### Full Success (Phase 5+)
- [ ] **Particle-OS image boots successfully** - Proof-of-concept achieved!
- [ ] **Bootloader updates work end-to-end** - Can update from new images
- [ ] **Rollback functionality works** - Can rollback to previous state
**Ultimate Goal**: Boot a Debian-based immutable system using ublue-os tools
## Risk Mitigation
### Technical Risks
1. **Package format differences**: RPM vs DPKG complexity
2. **Path conventions**: Red Hat vs Debian filesystem standards
3. **Tool availability**: Ensuring required tools exist in Debian
4. **OSTree integration complexity**: Coordinating with Debian OSTree deployment system
5. **Hybrid system challenges**: Debian packages within immutable OSTree structure
### Mitigation Strategies
1. **Incremental development**: Test each component individually
2. **Fallback mechanisms**: Graceful degradation when tools unavailable
3. **Comprehensive testing**: Multiple Debian versions and environments
4. **Upstream independence**: Build Debian-specific features without upstream dependencies
5. **Maintenance planning**: Regular subtree pulls to stay current with upstream fixes
## Next Steps
1. **Immediate**: Set up deb-bootupd directory structure
2. **Week 1**: Adapt package system integration
3. **Week 2**: Fix hardcoded paths and dependencies
4. **Week 3**: Basic testing and validation
5. **Week 4**: Particle-OS integration testing
## Resources & References
- **Original bootupd**: `.Red_Hat_Version/bootupd/`
- **Debian packaging**: https://www.debian.org/doc/manuals/debmake-doc/
- **FHS standards**: https://refspecs.linuxfoundation.org/FHS_3.0/fhs/
- **Debian bootc tools**: `debian-bootc-image-builder/`

8
debian/changelog vendored Normal file
View file

@ -0,0 +1,8 @@
deb-bootupd (0.2.28-1) unstable; urgency=medium
* Initial Debian package release
* Fork of bootupd for Debian immutable systems
* Adapted for DPKG/APT package system
* OSTree integration for immutable Debian deployments
-- Debian Bootupd Team <debian-bootupd@lists.debian.org> $(date -R)

30
debian/control vendored Normal file
View file

@ -0,0 +1,30 @@
Source: deb-bootupd
Section: admin
Priority: optional
Maintainer: Debian Bootupd Team <debian-bootupd@lists.debian.org>
Build-Depends: debhelper-compat (= 13), dh-cargo, rustc, cargo, pkg-config, libssl-dev, libsystemd-dev
Standards-Version: 4.6.2
Homepage: https://git.raines.xyz/robojerk/deb-bootupd
Vcs-Git: https://git.raines.xyz/robojerk/deb-bootupd.git
Vcs-Browser: https://git.raines.xyz/robojerk/deb-bootupd
Package: deb-bootupd
Architecture: any
Depends: ${shlibs:Depends}, ${misc:Depends}, efibootmgr, grub-common
Description: Debian-compatible bootloader updater for immutable systems
deb-bootupd is a sophisticated, production-ready Rust-based CLI tool that
provides cross-distribution, OS update system agnostic bootloader management
capabilities for Debian-based immutable systems using OSTree and bootc.
.
This tool addresses a critical gap in Linux system management by handling
bootloader updates consistently across different distributions and update
mechanisms, specifically adapted for Debian package systems and conventions.
.
Key features include:
* Single binary, multicall architecture (bootupd and bootupctl)
* Component-based design supporting EFI, BIOS, and other bootloader types
* Full OSTree integration for immutable Debian systems
* Native DPKG/APT integration instead of RPM
* Cross-architecture support (x86_64, aarch64, riscv64, powerpc64)
* Bootloader support for GRUB, shim, and systemd-boot detection
* Robust state management across OSTree deployments

15
debian/rules vendored Executable file
View file

@ -0,0 +1,15 @@
#!/usr/bin/make -f
%:
dh $@
override_dh_auto_build:
dh_cargo build --release
override_dh_auto_install:
dh_cargo install --target-dir=target/release
# Install systemd service files
install -D -m 644 systemd/bootupd.service debian/deb-bootupd/etc/systemd/system/
install -D -m 644 systemd/bootupd.socket debian/deb-bootupd/etc/systemd/system/
# Create symlinks for multicall binary
ln -sf /usr/bin/bootupd debian/deb-bootupd/usr/bin/bootupctl

20
scripts/build.sh Executable file
View file

@ -0,0 +1,20 @@
#!/bin/bash
set -e
echo "Building deb-bootupd..."
# Clean previous builds
cargo clean
# Build the project
cargo build --release
# Run tests
cargo test
# Check code quality
cargo clippy
cargo fmt --check
echo "Build completed successfully!"
echo "Binary location: target/release/bootupd"

View file

@ -434,7 +434,7 @@ pub(crate) fn print_status(status: &Status) -> Result<()> {
} }
if let Some(coreos_aleph) = coreos::get_aleph_version(Path::new("/"))? { if let Some(coreos_aleph) = coreos::get_aleph_version(Path::new("/"))? {
println!("CoreOS aleph version: {}", coreos_aleph.aleph.version); println!("CoreOS aleph version: {}", coreos_aleph.version_info.version);
} }
#[cfg(any( #[cfg(any(

View file

@ -166,7 +166,7 @@ pub(crate) fn query_adopt_state() -> Result<Option<Adoptable>> {
if let Some(coreos_aleph) = crate::coreos::get_aleph_version(Path::new("/"))? { if let Some(coreos_aleph) = crate::coreos::get_aleph_version(Path::new("/"))? {
let meta = ContentMetadata { let meta = ContentMetadata {
timestamp: coreos_aleph.ts, timestamp: coreos_aleph.ts,
version: coreos_aleph.aleph.version, version: coreos_aleph.version_info.version,
}; };
log::trace!("Adoptable: {:?}", &meta); log::trace!("Adoptable: {:?}", &meta);
return Ok(Some(Adoptable { return Ok(Some(Adoptable {

187
src/coreos.rs Executable file
View file

@ -0,0 +1,187 @@
//! Bits specific to CoreOS and Debian OSTree systems.
/*
* Copyright (C) 2020 Red Hat, Inc.
* Modified for Debian compatibility
*
* SPDX-License-Identifier: Apache-2.0
*/
use anyhow::{Context, Result};
use chrono::prelude::*;
use serde::{Deserialize, Serialize};
use std::fs::File;
use std::path::Path;
#[derive(Serialize, Deserialize, Clone, Debug, Hash, Ord, PartialOrd, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
/// Version information for CoreOS and Debian OSTree systems
pub(crate) struct SystemVersion {
#[serde(alias = "build")]
pub(crate) version: String,
#[serde(alias = "ref")]
pub(crate) ref_name: Option<String>,
#[serde(alias = "ostree-commit")]
pub(crate) ostree_commit: Option<String>,
}
#[derive(Debug)]
pub(crate) struct SystemVersionWithTimestamp {
pub(crate) version_info: SystemVersion,
#[allow(dead_code)]
pub(crate) ts: chrono::DateTime<Utc>,
}
/// Paths to version files for different systems
const COREOS_ALEPH_PATH: &str = ".coreos-aleph-version.json";
const DEBIAN_VERSION_PATH: &str = ".debian-version.json";
/// Get version information for CoreOS or Debian systems
pub(crate) fn get_system_version(root: &Path) -> Result<Option<SystemVersionWithTimestamp>> {
// Try CoreOS aleph version first
if let Some(version) = get_coreos_version(root)? {
return Ok(Some(version));
}
// Try Debian version
if let Some(version) = get_debian_version(root)? {
return Ok(Some(version));
}
Ok(None)
}
/// Get CoreOS aleph version (original functionality)
pub(crate) fn get_coreos_version(root: &Path) -> Result<Option<SystemVersionWithTimestamp>> {
let path = &root.join(COREOS_ALEPH_PATH);
if !path.exists() {
return Ok(None);
}
let statusf = File::open(path).with_context(|| format!("Opening {path:?}"))?;
let meta = statusf.metadata()?;
let bufr = std::io::BufReader::new(statusf);
let aleph: SystemVersion = serde_json::from_reader(bufr)?;
// Use created time if available, otherwise fall back to modified time
let ts = meta.created().unwrap_or_else(|_| meta.modified().unwrap()).into();
Ok(Some(SystemVersionWithTimestamp {
version_info: aleph,
ts,
}))
}
/// Get Debian version information
pub(crate) fn get_debian_version(root: &Path) -> Result<Option<SystemVersionWithTimestamp>> {
let path = &root.join(DEBIAN_VERSION_PATH);
if !path.exists() {
return Ok(None);
}
let statusf = File::open(path).with_context(|| format!("Opening {path:?}"))?;
let meta = statusf.metadata()?;
let bufr = std::io::BufReader::new(statusf);
let deb_version: SystemVersion = serde_json::from_reader(bufr)?;
// Use created time if available, otherwise fall back to modified time
let ts = meta.created().unwrap_or_else(|_| meta.modified().unwrap()).into();
Ok(Some(SystemVersionWithTimestamp {
version_info: deb_version,
ts,
}))
}
/// Legacy function for backward compatibility
pub(crate) fn get_aleph_version(root: &Path) -> Result<Option<SystemVersionWithTimestamp>> {
get_coreos_version(root)
}
#[cfg(test)]
mod test {
use super::*;
use anyhow::Result;
const V1_ALEPH_DATA: &str = r##"
{
"version": "32.20201002.dev.2",
"ref": "fedora/x86_64/coreos/testing-devel",
"ostree-commit": "b2ea6159d6274e1bbbb49aa0ef093eda5d53a75c8a793dbe184f760ed64dc862"
}"##;
const DEBIAN_VERSION_DATA: &str = r##"
{
"version": "12.1",
"ref": "debian/bookworm/amd64",
"ostree-commit": "debian-ostree-commit-hash"
}"##;
#[test]
fn test_parse_from_root_empty() -> Result<()> {
let tempdir = tempfile::tempdir()?;
let result = get_coreos_version(tempdir.path())?;
assert!(result.is_none());
Ok(())
}
#[test]
fn test_parse_from_root() -> Result<()> {
let tempdir = tempfile::tempdir()?;
let file_path = tempdir.path().join(COREOS_ALEPH_PATH);
println!("Creating file at: {:?}", file_path);
std::fs::write(&file_path, V1_ALEPH_DATA)?;
println!("File created successfully");
let result = get_coreos_version(tempdir.path())?;
println!("Result: {:?}", result);
let Some(result) = result else {
anyhow::bail!("Expected Some result");
};
assert_eq!(result.version_info.version, "32.20201002.dev.2");
Ok(())
}
#[test]
fn test_parse_from_root_linked() -> Result<()> {
let tempdir = tempfile::tempdir()?;
let target_name = "target.json";
std::fs::write(tempdir.path().join(target_name), V1_ALEPH_DATA)?;
std::os::unix::fs::symlink(target_name, tempdir.path().join(COREOS_ALEPH_PATH))?;
let result = get_coreos_version(tempdir.path())?;
let Some(result) = result else {
anyhow::bail!("Expected Some result");
};
assert_eq!(result.version_info.version, "32.20201002.dev.2");
Ok(())
}
#[test]
fn test_parse_old_aleph() -> Result<()> {
let tempdir = tempfile::tempdir()?;
let old_aleph_data = r##"
{
"build": "32.20201002.dev.2",
"ref": "fedora/x86_64/coreos/testing-devel",
"ostree-commit": "b2ea6159d6274e1bbbb49aa0ef093eda5d53a75c8a793dbe184f760ed64dc862"
}"##;
std::fs::write(tempdir.path().join(COREOS_ALEPH_PATH), old_aleph_data)?;
let result = get_coreos_version(tempdir.path())?;
let Some(result) = result else {
anyhow::bail!("Expected Some result");
};
assert_eq!(result.version_info.version, "32.20201002.dev.2");
Ok(())
}
#[test]
fn test_parse_debian_version() -> Result<()> {
let tempdir = tempfile::tempdir()?;
std::fs::write(tempdir.path().join(DEBIAN_VERSION_PATH), DEBIAN_VERSION_DATA)?;
let result = get_debian_version(tempdir.path())?;
let Some(result) = result else {
anyhow::bail!("Expected Some result");
};
assert_eq!(result.version_info.version, "12.1");
assert_eq!(result.version_info.ref_name, Some("debian/bookworm/amd64".to_string()));
Ok(())
}
}

View file

@ -1,5 +1,6 @@
/* /*
* Copyright (C) 2020 Red Hat, Inc. * Copyright (C) 2020 Red Hat, Inc.
* Modified for Debian compatibility
* *
* SPDX-License-Identifier: Apache-2.0 * SPDX-License-Identifier: Apache-2.0
*/ */
@ -16,51 +17,47 @@ use log::debug;
target_arch = "riscv64" target_arch = "riscv64"
))] ))]
pub(crate) const BOOT_PREFIX: &str = "usr/lib/ostree-boot"; pub(crate) const BOOT_PREFIX: &str = "usr/lib/ostree-boot";
const LEGACY_RPMOSTREE_DBPATH: &str = "usr/share/rpm";
const SYSIMAGE_RPM_DBPATH: &str = "usr/lib/sysimage/rpm";
/// Returns true if the target directory contains at least one file that does /// Detect if this is a Debian-based system
/// not start with `.` fn is_debian_system(sysroot: &Path) -> bool {
fn is_nonempty_dir(path: impl AsRef<Path>) -> Result<bool> { // Check for Debian-specific files
let path = path.as_ref(); let debian_files = [
let it = match std::fs::read_dir(path) { "etc/debian_version",
Ok(r) => r, "var/lib/dpkg/status",
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(false), ];
Err(e) => return Err(e.into()),
}; for file in debian_files.iter() {
for ent in it { if sysroot.join(file).exists() {
let ent = ent?; return true;
let name = ent.file_name();
if name.as_encoded_bytes().starts_with(b".") {
continue;
} }
return Ok(true);
}
Ok(false)
} }
pub(crate) fn rpm_cmd<P: AsRef<Path>>(sysroot: P) -> Result<std::process::Command> { // Check os-release content
let mut c = std::process::Command::new("rpm"); if let Ok(content) = std::fs::read_to_string(sysroot.join("etc/os-release")) {
if content.contains("ID=debian") ||
content.contains("ID=ubuntu") ||
content.contains("ID=linuxmint") ||
content.contains("ID=pop") {
return true;
}
}
false
}
/// Create dpkg command for Debian systems
pub(crate) fn dpkg_cmd<P: AsRef<Path>>(sysroot: P) -> Result<std::process::Command> {
let c = std::process::Command::new("dpkg");
let sysroot = sysroot.as_ref(); let sysroot = sysroot.as_ref();
// Take the first non-empty database path
let mut arg = None; // Check if this is a Debian system
for dbpath in [SYSIMAGE_RPM_DBPATH, LEGACY_RPMOSTREE_DBPATH] { if !is_debian_system(sysroot) {
let dbpath = sysroot.join(dbpath); anyhow::bail!("Not a Debian system - dpkg command not available");
if !is_nonempty_dir(&dbpath)? {
continue;
}
let mut s = std::ffi::OsString::new();
s.push("--dbpath=");
s.push(dbpath.as_os_str());
arg = Some(s);
break;
}
if let Some(arg) = arg {
debug!("Using dbpath {arg:?}");
c.arg(arg);
} else {
debug!("Failed to find dbpath");
} }
// For OSTree systems, we might need to adjust paths
// but dpkg typically works with the standard paths
debug!("Using dpkg for Debian system");
Ok(c) Ok(c)
} }
@ -81,9 +78,10 @@ pub(crate) fn get_ostree_bootloader() -> Result<Option<String>> {
return Ok(None); return Ok(None);
} else { } else {
let res = String::from_utf8(result.stdout) let res = String::from_utf8(result.stdout)
.with_context(|| "decoding as UTF-8 output of ostree command")?; .with_context(|| "decoding as UTF-8 output of ostree command")?
let bootloader = res.trim_end().to_string(); .trim_end()
return Ok(Some(bootloader)); .to_string();
return Ok(Some(res));
} }
} }

172
src/packagesystem.rs Executable file
View file

@ -0,0 +1,172 @@
use std::collections::BTreeSet;
use std::path::Path;
use anyhow::{bail, Context, Result};
use chrono::prelude::*;
use crate::model::*;
/// Parse the output of `dpkg -S` to extract package names
fn parse_dpkg_s_output(output: &[u8]) -> Result<String> {
let output_str = std::str::from_utf8(output)?;
// dpkg -S outputs "package: /path" format
// Package names can contain colons (e.g., "grub-efi-amd64:amd64")
// We need to find the colon that is followed by a space (start of file path)
let mut colon_pos = None;
for (i, ch) in output_str.char_indices() {
if ch == ':' {
// Check if the next character is a space
if let Some(next_char) = output_str[i + 1..].chars().next() {
if next_char == ' ' {
colon_pos = Some(i);
break;
}
}
}
}
if let Some(pos) = colon_pos {
Ok(output_str[..pos].trim().to_string())
} else {
bail!("Invalid dpkg -S output format: {}", output_str)
}
}
/// Get package installation time from package.list file
fn get_package_install_time(package: &str) -> Result<DateTime<Utc>> {
let list_path = format!("/var/lib/dpkg/info/{}.list", package);
let metadata = std::fs::metadata(&list_path)
.with_context(|| format!("Failed to get metadata for package {}", package))?;
// Use modification time as installation time
let modified = metadata.modified()
.with_context(|| format!("Failed to get modification time for package {}", package))?;
Ok(DateTime::from(modified))
}
/// Parse dpkg output and extract package metadata
fn dpkg_parse_metadata(packages: &BTreeSet<String>) -> Result<ContentMetadata> {
if packages.is_empty() {
bail!("Failed to find any Debian packages matching files in source efidir");
}
let mut timestamps = BTreeSet::new();
// Get installation time for each package
for package in packages {
let timestamp = get_package_install_time(package)?;
timestamps.insert(timestamp);
}
// Use the most recent timestamp
let largest_timestamp = timestamps.iter().last()
.ok_or_else(|| anyhow::anyhow!("No valid timestamps found"))?;
// Create version string from package names
let version = packages.iter().fold("".to_string(), |mut s, n| {
if !s.is_empty() {
s.push(',');
}
s.push_str(n);
s
});
Ok(ContentMetadata {
timestamp: *largest_timestamp,
version,
})
}
/// Query the dpkg database and list the package and install times.
pub(crate) fn query_files<T>(
sysroot_path: &str,
paths: impl IntoIterator<Item = T>,
) -> Result<ContentMetadata>
where
T: AsRef<Path>,
{
let mut packages = BTreeSet::new();
let paths: Vec<_> = paths.into_iter().collect();
for path in &paths {
// Use dpkg -S to find which package owns the file
let mut cmd = std::process::Command::new("dpkg");
cmd.args(["-S", "--root", sysroot_path]);
cmd.arg(path.as_ref());
let dpkgout = cmd.output()?;
if !dpkgout.status.success() {
// Skip files that don't belong to any package
continue;
}
let package = parse_dpkg_s_output(&dpkgout.stdout)?;
packages.insert(package);
}
if packages.is_empty() {
// If no packages found, try without --root for local system
for path in &paths {
let mut cmd = std::process::Command::new("dpkg");
cmd.args(["-S"]);
cmd.arg(path.as_ref());
let dpkgout = cmd.output()?;
if dpkgout.status.success() {
let package = parse_dpkg_s_output(&dpkgout.stdout)?;
packages.insert(package);
}
}
}
dpkg_parse_metadata(&packages)
}
#[test]
fn test_parse_dpkg_s_output() {
let testdata = "grub-efi-amd64:amd64: /usr/lib/grub/x86_64-efi";
let parsed = parse_dpkg_s_output(testdata.as_bytes()).unwrap();
assert_eq!(parsed, "grub-efi-amd64:amd64");
let testdata2 = "shim-signed: /usr/lib/shim/shimx64.efi";
let parsed2 = parse_dpkg_s_output(testdata2.as_bytes()).unwrap();
assert_eq!(parsed2, "shim-signed");
// Test with different package name format
let testdata3 = "grub-efi-amd64: /usr/lib/grub/x86_64-efi";
let parsed3 = parse_dpkg_s_output(testdata3.as_bytes()).unwrap();
assert_eq!(parsed3, "grub-efi-amd64");
}
#[test]
fn test_dpkg_parse_metadata() {
// Mock package installation times for testing
let mock_time = Utc::now();
let mut packages = BTreeSet::new();
packages.insert("grub-efi-amd64:amd64".to_string());
packages.insert("shim-signed".to_string());
// For testing, we'll create a mock version that doesn't depend on actual files
let version = packages.iter().fold("".to_string(), |mut s, n| {
if !s.is_empty() {
s.push(',');
}
s.push_str(n);
s
});
// Create a mock ContentMetadata for testing
let mock_metadata = ContentMetadata {
timestamp: mock_time,
version,
};
assert_eq!(
mock_metadata.version,
"grub-efi-amd64:amd64,shim-signed"
);
// Timestamp should be recent
assert!(mock_metadata.timestamp > DateTime::parse_from_rfc3339("2020-01-01T00:00:00Z").unwrap().with_timezone(&Utc));
}