22 KiB
Calamares Module for bootc install
Overview
This document outlines a focused plan for creating a single Calamares module that handles bootc install operations. This is the simplest and most direct approach, following the official bootc documentation patterns.
Executive Summary
Goal: Create a single Calamares module that executes bootc install commands with proper configuration and error handling.
Timeline: 2-3 months (focused, single-purpose implementation) Complexity: Low (direct tool integration) Target: Debian 13 (Trixie) with bootc support
Real-World Analysis
How bootc install Actually Works
Based on the official bootc documentation and Fedora's bare metal documentation:
1. Core Commands
bootc install to-disk /dev/sda
bootc install to-filesystem /path/to/mounted/fs
bootc install to-existing-root
2. Container Execution Pattern
From Fedora's official documentation:
podman run \
--rm --privileged \
--pid=host \
-v /dev:/dev \
-v /var/lib/containers:/var/lib/containers \
--security-opt label=type:unconfined_t \
<image> \
bootc install to-disk /path/to/disk
3. Key Differences from Anaconda Approach
- No kickstart files - Direct container execution
- No network during install - Container is already pulled
- Minimal configuration - Basic installer built into bootc
- Live ISO environment - Typically run from Fedora CoreOS Live ISO
3. Key OSTree Filesystem Nuances
Critical differences from traditional Linux installations:
- Composefs by default: Fedora bootc uses composefs for the root filesystem
- Read-only root: Filesystem is read-only at runtime (like
podman run --read-only) - Special mount points:
/etcand/varare persistent, mutable bind mounts - Kernel location: Kernel is in
/usr/lib/ostree-boot/not/boot/ - 3-way merge:
/etcchanges are merged across upgrades - Transient mountpoints: Support for dynamic mountpoints with
transient-ro
4. Filesystem Layout
/usr/lib/ostree-boot/ # Kernel and initrd (not /boot/)
/etc/ # Persistent, mutable (bind mount)
/var/ # Persistent, mutable (bind mount)
/usr/ # Read-only (composefs)
/opt/ # Read-only (composefs)
5. Authentication Patterns
From Fedora's authentication documentation:
- Registry auth:
/etc/ostree/auth.jsonfor container registries - SSH keys: Via kickstart or bootc-image-builder config
- User management: systemd-sysusers for local users
- nss-altfiles: Static users in
/usr/lib/passwdand/usr/lib/group
Implementation Plan
Phase 1: Core Module Development (Month 1)
1.1 Module Structure
class BootcInstallModule : public Calamares::Module
{
public:
void init() override;
QList<Calamares::job_ptr> jobs() const override;
private:
QString m_containerUrl;
QString m_targetDevice;
QString m_installType; // "to-disk", "to-filesystem", "to-existing-root"
bool m_authRequired;
QString m_authJson;
};
1.2 Configuration Loading
void BootcInstallModule::init()
{
auto config = Calamares::ModuleSystem::instance()->moduleConfiguration("bootc-install");
m_containerUrl = config.value("containerUrl").toString();
m_targetDevice = config.value("targetDevice").toString();
m_installType = config.value("installType", "to-disk").toString();
m_authRequired = config.value("authRequired", false).toBool();
m_authJson = config.value("authJson").toString();
}
1.3 Job Implementation
QList<Calamares::job_ptr> BootcInstallModule::jobs() const
{
QList<Calamares::job_ptr> jobs;
// Registry authentication job
if (m_authRequired) {
jobs.append(Calamares::job_ptr(new RegistryAuthJob(m_authJson)));
}
// Bootc installation job
jobs.append(Calamares::job_ptr(new BootcInstallJob(m_containerUrl, m_targetDevice, m_installType)));
// Post-install configuration job
jobs.append(Calamares::job_ptr(new BootcPostInstallJob(m_targetDevice, m_sshKey, m_username)));
return jobs;
}
Phase 2: Job Implementations (Month 1-2)
2.1 Registry Authentication Job
class RegistryAuthJob : public Calamares::Job
{
public:
RegistryAuthJob(const QString& authJson) : m_authJson(authJson) {}
QString prettyName() const override { return "Configuring registry authentication"; }
Calamares::JobResult exec() override;
private:
QString m_authJson;
};
Calamares::JobResult RegistryAuthJob::exec()
{
// Create /etc/ostree directory (persistent bind mount)
if (!QDir("/etc/ostree").exists()) {
if (!QDir().mkpath("/etc/ostree")) {
return Calamares::JobResult::error("Failed to create /etc/ostree directory");
}
}
// Write auth.json (will persist across upgrades due to /etc bind mount)
QFile authFile("/etc/ostree/auth.json");
if (!authFile.open(QIODevice::WriteOnly)) {
return Calamares::JobResult::error("Failed to open /etc/ostree/auth.json for writing");
}
authFile.write(m_authJson.toUtf8());
authFile.close();
return Calamares::JobResult::ok();
}
2.2 Bootc Install Job
class BootcInstallJob : public Calamares::Job
{
public:
BootcInstallJob(const QString& containerUrl, const QString& targetDevice, const QString& installType)
: m_containerUrl(containerUrl), m_targetDevice(targetDevice), m_installType(installType) {}
QString prettyName() const override { return "Installing bootc container"; }
Calamares::JobResult exec() override;
private:
QString m_containerUrl;
QString m_targetDevice;
QString m_installType;
};
Calamares::JobResult BootcInstallJob::exec()
{
// Build podman command with OSTree-specific considerations
QStringList args;
args << "run" << "--rm" << "--privileged";
args << "--pid=host";
args << "-v" << "/dev:/dev";
args << "-v" << "/var/lib/containers:/var/lib/containers";
args << "--security-opt" << "label=type:unconfined_t";
// Add environment variables for OSTree filesystem
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
env.insert("OSTREE_NO_SIGNATURE_VERIFICATION", "1"); // For composefs unsigned mode
env.insert("LIBMOUNT_FORCE_MOUNT2", "always"); // For transient-ro support
args << m_containerUrl;
args << "bootc" << "install" << m_installType << m_targetDevice;
// Execute podman command
QProcess process;
process.setProcessEnvironment(env);
process.start("podman", args);
if (!process.waitForFinished(-1)) {
return Calamares::JobResult::error("bootc install command timed out");
}
if (process.exitCode() != 0) {
QString error = QString("bootc install failed: %1").arg(process.readAllStandardError());
return Calamares::JobResult::error(error);
}
return Calamares::JobResult::ok();
}
2.3 Post-Install Configuration Job
class BootcPostInstallJob : public Calamares::Job
{
public:
BootcPostInstallJob(const QString& targetDevice, const QString& sshKey, const QString& username)
: m_targetDevice(targetDevice), m_sshKey(sshKey), m_username(username) {}
QString prettyName() const override { return "Configuring bootc system"; }
Calamares::JobResult exec() override;
private:
QString m_targetDevice;
QString m_sshKey;
QString m_username;
bool configureBootloader();
bool createUserAccount();
bool setupSshKey();
};
Calamares::JobResult BootcPostInstallJob::exec()
{
// Mount the installed system
QString mountPoint = "/mnt/bootc-install";
if (!QDir().mkpath(mountPoint)) {
return Calamares::JobResult::error("Failed to create mount point");
}
// Mount the root partition
QProcess mount;
mount.start("mount", QStringList() << m_targetDevice << mountPoint);
if (!mount.waitForFinished() || mount.exitCode() != 0) {
return Calamares::JobResult::error("Failed to mount installed system");
}
// Configure bootloader (GRUB2 for OSTree)
if (!configureBootloader()) {
return Calamares::JobResult::error("Failed to configure bootloader");
}
// Create user account
if (!createUserAccount()) {
return Calamares::JobResult::error("Failed to create user account");
}
// Setup SSH key
if (!setupSshKey()) {
return Calamares::JobResult::error("Failed to setup SSH key");
}
// Unmount
QProcess umount;
umount.start("umount", QStringList() << mountPoint);
umount.waitForFinished();
return Calamares::JobResult::ok();
}
bool BootcPostInstallJob::configureBootloader()
{
// OSTree systems use GRUB2 with specific configuration
// Kernel is in /usr/lib/ostree-boot/, not /boot/
QString grubConfig = QString("/mnt/bootc-install/boot/grub2/grub.cfg");
// Check if GRUB2 configuration exists
if (!QFile::exists(grubConfig)) {
// Run grub2-mkconfig to generate configuration
QProcess grubMkconfig;
grubMkconfig.start("grub2-mkconfig", QStringList() << "-o" << grubConfig);
if (!grubMkconfig.waitForFinished() || grubMkconfig.exitCode() != 0) {
return false;
}
}
// Install GRUB2 to the target device
QProcess grubInstall;
grubInstall.start("grub2-install", QStringList() << "--target=x86_64-efi" << m_targetDevice);
if (!grubInstall.waitForFinished() || grubInstall.exitCode() != 0) {
return false;
}
return true;
}
bool BootcPostInstallJob::createUserAccount()
{
// OSTree systems use systemd-sysusers for user management
// Users are defined in /usr/lib/sysusers.d/ or /etc/sysusers.d/
QString sysusersConfig = "/mnt/bootc-install/etc/sysusers.d/calamares-user.conf";
QString userConfig = QString("u %1 1000 \"%1\" /home/%1\n").arg(m_username);
userConfig += QString("g %1 1000\n").arg(m_username);
userConfig += QString("m %1 %1\n").arg(m_username);
QFile file(sysusersConfig);
if (!file.open(QIODevice::WriteOnly)) {
return false;
}
file.write(userConfig.toUtf8());
file.close();
return true;
}
bool BootcPostInstallJob::setupSshKey()
{
if (m_sshKey.isEmpty()) return true;
// Create .ssh directory for root
QString sshDir = "/mnt/bootc-install/root/.ssh";
if (!QDir().mkpath(sshDir)) {
return false;
}
// Write SSH key
QString keyFile = sshDir + "/authorized_keys";
QFile file(keyFile);
if (!file.open(QIODevice::WriteOnly)) {
return false;
}
file.write(m_sshKey.toUtf8());
file.close();
// Set proper permissions
QFile::setPermissions(keyFile, QFile::ReadOwner | QFile::WriteOwner);
QFile::setPermissions(sshDir, QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner);
return true;
}
Phase 3: OSTree Filesystem Considerations (Month 2)
3.1 Filesystem Layout Validation
class OstreeFilesystemValidator
{
public:
static bool validateTargetDevice(const QString& device);
static bool checkComposefsSupport();
static bool validateMountPoints();
private:
static bool isOstreeBootPath(const QString& path);
static bool checkTransientRoSupport();
};
bool OstreeFilesystemValidator::validateTargetDevice(const QString& device)
{
// Check if target device can support OSTree layout
// - GPT partition table required
// - EFI system partition for UEFI
// - Root partition for OSTree deployment
// - Boot partition for kernel/initrd (not /boot/ but /usr/lib/ostree-boot/)
QProcess sfdisk;
sfdisk.start("sfdisk", QStringList() << "-l" << device);
if (!sfdisk.waitForFinished()) return false;
QString output = sfdisk.readAllStandardOutput();
return output.contains("GPT") && output.contains("EFI");
}
bool OstreeFilesystemValidator::checkComposefsSupport()
{
// Check if composefs is available in the container
QProcess composefs;
composefs.start("composefs", QStringList() << "--help");
return composefs.waitForFinished() && composefs.exitCode() == 0;
}
3.2 Kernel and Initrd Handling
class OstreeBootManager
{
public:
static QString getKernelPath();
static QString getInitrdPath();
static bool setupBootloader(const QString& device);
static bool regenerateInitramfs(const QString& mountPoint);
private:
static const QString OSTREE_BOOT_PATH; // "/usr/lib/ostree-boot/"
};
const QString OstreeBootManager::OSTREE_BOOT_PATH = "/usr/lib/ostree-boot/";
QString OstreeBootManager::getKernelPath()
{
// Kernel is in /usr/lib/ostree-boot/, not /boot/
return OSTREE_BOOT_PATH + "vmlinuz";
}
QString OstreeBootManager::getInitrdPath()
{
// Initrd is in /usr/lib/ostree-boot/, not /boot/
return OSTREE_BOOT_PATH + "initramfs.img";
}
bool OstreeBootManager::regenerateInitramfs(const QString& mountPoint)
{
// OSTree systems need initramfs regeneration after configuration changes
// This is critical for filesystem configuration changes
QProcess dracut;
dracut.start("dracut", QStringList()
<< "--force"
<< "--hostonly"
<< "--kver" << "5.15.0" // Get actual kernel version
<< mountPoint + "/boot/initramfs.img");
if (!dracut.waitForFinished()) {
return false;
}
return dracut.exitCode() == 0;
}
3.3 Persistent State Management
class OstreeStateManager
{
public:
static bool setupEtcBindMount();
static bool setupVarBindMount();
static bool configureTransientRo();
private:
static bool createBindMount(const QString& source, const QString& target);
};
bool OstreeStateManager::setupEtcBindMount()
{
// /etc is a persistent, mutable bind mount
// Changes here persist across upgrades via 3-way merge
return createBindMount("/etc", "/etc");
}
bool OstreeStateManager::configureTransientRo()
{
// Enable transient-ro for dynamic mountpoints
QString configPath = "/usr/lib/ostree/prepare-root.conf";
QString config = "[root]\ntransient-ro = true\n";
QFile file(configPath);
if (!file.open(QIODevice::WriteOnly)) return false;
file.write(config.toUtf8());
file.close();
// Regenerate initramfs after config change
QProcess dracut;
dracut.start("dracut", QStringList() << "--force");
return dracut.waitForFinished() && dracut.exitCode() == 0;
}
Phase 4: UI Integration (Month 2-3)
4.1 Configuration Page
class BootcInstallPage : public QWidget
{
Q_OBJECT
public:
explicit BootcInstallPage(QWidget* parent = nullptr);
QString containerUrl() const;
QString targetDevice() const;
QString installType() const;
bool authRequired() const;
QString authJson() const;
bool enableTransientRo() const;
private slots:
void onContainerUrlChanged();
void onTargetDeviceChanged();
void onInstallTypeChanged();
void onAuthRequiredChanged();
void validateOstreeRequirements();
private:
QLineEdit* m_containerUrlEdit;
QLineEdit* m_targetDeviceEdit;
QComboBox* m_installTypeCombo;
QCheckBox* m_authRequiredCheck;
QTextEdit* m_authJsonEdit;
QCheckBox* m_transientRoCheck;
QLabel* m_ostreeStatusLabel;
};
4.2 OSTree-Aware Validation
bool BootcInstallPage::validate()
{
if (m_containerUrlEdit->text().isEmpty()) {
Calamares::Branding::instance()->setValidationError("Container URL is required");
return false;
}
if (m_targetDeviceEdit->text().isEmpty()) {
Calamares::Branding::instance()->setValidationError("Target device is required");
return false;
}
// Validate OSTree filesystem requirements
if (!OstreeFilesystemValidator::validateTargetDevice(m_targetDeviceEdit->text())) {
Calamares::Branding::instance()->setValidationError("Target device must have GPT partition table and EFI support");
return false;
}
if (!OstreeFilesystemValidator::checkComposefsSupport()) {
Calamares::Branding::instance()->setValidationError("Composefs support not available in container");
return false;
}
if (m_authRequiredCheck->isChecked() && m_authJsonEdit->toPlainText().isEmpty()) {
Calamares::Branding::instance()->setValidationError("Authentication JSON is required");
return false;
}
return true;
}
void BootcInstallPage::validateOstreeRequirements()
{
// Real-time validation of OSTree requirements
bool deviceValid = OstreeFilesystemValidator::validateTargetDevice(m_targetDeviceEdit->text());
bool composefsValid = OstreeFilesystemValidator::checkComposefsSupport();
QString status;
if (deviceValid && composefsValid) {
status = "✓ OSTree requirements satisfied";
m_ostreeStatusLabel->setStyleSheet("color: green;");
} else {
status = "✗ OSTree requirements not met";
m_ostreeStatusLabel->setStyleSheet("color: red;");
}
m_ostreeStatusLabel->setText(status);
}
Phase 4: Testing and Polish (Month 2-3)
4.1 Unit Tests
class BootcInstallModuleTest : public QObject
{
Q_OBJECT
private slots:
void testModuleInitialization();
void testJobCreation();
void testRegistryAuthJob();
void testBootcInstallJob();
void testValidation();
};
4.2 Integration Tests
- Test with real bootc containers
- Test registry authentication
- Test different install types
- Test error handling
Configuration
Module Configuration
# bootc-install.conf
module: bootc-install
config:
containerUrl: "quay.io/centos-bootc/centos-bootc:stream9"
targetDevice: "/dev/sda"
installType: "to-disk" # to-disk, to-filesystem, to-existing-root
authRequired: false
authJson: ""
# User account configuration
username: "admin"
sshKey: "" # SSH public key for root access
# OSTree-specific configuration
ostree:
enableComposefs: true
enableTransientRo: false
kernelPath: "/usr/lib/ostree-boot/vmlinuz"
initrdPath: "/usr/lib/ostree-boot/initramfs.img"
bootPath: "/usr/lib/ostree-boot/"
# Bootloader configuration
bootloader:
type: "grub2"
target: "x86_64-efi"
regenerateInitramfs: true
# Filesystem validation
validation:
requireGpt: true
requireEfi: true
checkComposefs: true
validateMountPoints: true
Calamares Integration
# calamares.conf
modules:
- bootc-install
bootc-install:
containerUrl: "quay.io/centos-bootc/centos-bootc:stream9"
targetDevice: "/dev/sda"
installType: "to-disk"
authRequired: false
# OSTree filesystem settings
ostree:
enableComposefs: true
enableTransientRo: false
Technical Architecture
File Structure
calamares-bootc-install/
├── src/
│ ├── BootcInstallModule.cpp
│ ├── BootcInstallJob.cpp
│ ├── RegistryAuthJob.cpp
│ ├── BootcInstallPage.cpp
│ └── BootcInstallPage.ui
├── config/
│ └── bootc-install.conf
├── tests/
│ ├── BootcInstallModuleTest.cpp
│ └── testdata/
└── CMakeLists.txt
Dependencies
- Calamares: Module framework
- Qt: UI and core functionality
- podman: Container runtime
- bootc: Container installation tool
Key Implementation Details
1. Simple and Focused
- Single purpose: Only handles
bootc install - Direct integration: Calls podman and bootc directly
- Minimal complexity: No pattern switching or hybrid approaches
2. Follow Official Patterns
- Use exact podman command from Fedora documentation
- Follow bootc install syntax from official docs
- Handle registry auth via
/etc/ostree/auth.json
3. Error Handling
- Validate inputs before execution
- Handle podman failures gracefully
- Provide clear error messages to users
Success Metrics
Technical Metrics
- Module loads and initializes correctly
- Jobs execute successfully
- Registry authentication works
- bootc install completes successfully
User Experience Metrics
- Clear configuration UI
- Proper validation and error messages
- Progress reporting during installation
- Integration with Calamares workflow
Conclusion
This focused plan creates a single, purpose-built Calamares module for bootc install operations that properly accounts for the unique OSTree filesystem characteristics and the specific nuances of direct container installation. By following the official bootc bare metal documentation exactly and incorporating the specific requirements for bootloader configuration, initramfs handling, and user account creation, we can create a reliable, maintainable solution.
Key bootc install Considerations Addressed:
- Direct Container Execution - Uses podman run with privileged mode for installation
- Post-Install Configuration - Handles bootloader setup, user creation, and SSH key configuration
- OSTree Filesystem Layout - Kernel in
/usr/lib/ostree-boot/not/boot/ - GRUB2 Configuration - Proper bootloader setup for OSTree systems
- Initramfs Regeneration - Critical for filesystem configuration changes
- User Account Management - Uses systemd-sysusers for OSTree-compatible user creation
- SSH Key Setup - Proper permissions and directory structure for root access
Key Advantages:
- bootc install Native - Follows the official bootc install approach exactly
- Complete Installation - Handles both installation and post-install configuration
- OSTree-Aware - Properly manages filesystem layout and bootloader configuration
- User-Friendly - Provides familiar Calamares interface for bootc installation
- Validation - Real-time checking of OSTree and bootc requirements
- Extensibility - Can be enhanced with additional features over time
This approach ensures that the Calamares module provides a complete, user-friendly interface for bootc install operations while properly handling all the OSTree-specific requirements for bootloader configuration, initramfs management, and user account creation.