deb-bootc-image-builder-new/docs/calmares-plan.md
2025-09-05 07:10:12 -07:00

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: /etc and /var are persistent, mutable bind mounts
  • Kernel location: Kernel is in /usr/lib/ostree-boot/ not /boot/
  • 3-way merge: /etc changes 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.json for 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/passwd and /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:

  1. Direct Container Execution - Uses podman run with privileged mode for installation
  2. Post-Install Configuration - Handles bootloader setup, user creation, and SSH key configuration
  3. OSTree Filesystem Layout - Kernel in /usr/lib/ostree-boot/ not /boot/
  4. GRUB2 Configuration - Proper bootloader setup for OSTree systems
  5. Initramfs Regeneration - Critical for filesystem configuration changes
  6. User Account Management - Uses systemd-sysusers for OSTree-compatible user creation
  7. SSH Key Setup - Proper permissions and directory structure for root access

Key Advantages:

  1. bootc install Native - Follows the official bootc install approach exactly
  2. Complete Installation - Handles both installation and post-install configuration
  3. OSTree-Aware - Properly manages filesystem layout and bootloader configuration
  4. User-Friendly - Provides familiar Calamares interface for bootc installation
  5. Validation - Real-time checking of OSTree and bootc requirements
  6. 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.