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

730 lines
No EOL
22 KiB
Markdown

# 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 \
<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](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<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
```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<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
```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.