# 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](https://bootc-dev.github.io/bootc//bootc-install.html) and [Fedora's bare metal documentation](https://docs.fedoraproject.org/en-US/bootc/bare-metal/): #### 1. Core Commands ```bash 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](https://docs.fedoraproject.org/en-US/bootc/bare-metal/): ```bash podman run \ --rm --privileged \ --pid=host \ -v /dev:/dev \ -v /var/lib/containers:/var/lib/containers \ --security-opt label=type:unconfined_t \ \ 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](https://docs.fedoraproject.org/en-US/bootc/filesystem/) 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](https://docs.fedoraproject.org/en-US/bootc/authentication/): - **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 ```cpp class BootcInstallModule : public Calamares::Module { public: void init() override; QList 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 ```cpp 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 ```cpp QList BootcInstallModule::jobs() const { QList 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 ```cpp 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 ```cpp 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 ```cpp 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 ```cpp 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 ```cpp 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 ```cpp 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 ```cpp 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 ```cpp 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 ```cpp 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 ```yaml # 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 ```yaml # 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](https://docs.fedoraproject.org/en-US/bootc/bare-metal/) 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](https://docs.fedoraproject.org/en-US/bootc/bare-metal/) 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](https://docs.fedoraproject.org/en-US/bootc/bare-metal/) 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.