Skip to content

Instantly share code, notes, and snippets.

@ianballou
Last active November 21, 2025 14:59
Show Gist options
  • Select an option

  • Save ianballou/892866e4d953aee6126487d0143e9c29 to your computer and use it in GitHub Desktop.

Select an option

Save ianballou/892866e4d953aee6126487d0143e9c29 to your computer and use it in GitHub Desktop.
Claude generated solution for bootc package persistence in RHSM

Note: generated by Claude Code.

Package Persistence Field Implementation for bootc Systems

Overview

This document explains the implementation of a new persistence field in subscription-manager's package profile functionality. This field helps distinguish between different types of package installations on ostree-based systems like image mode machines.

Background: Why This Feature Was Needed

Traditional vs. ostree-based Systems

Traditional Linux systems (like regular RHEL, Fedora, Ubuntu) install packages directly onto the filesystem. All packages are essentially "permanent" until explicitly removed.

ostree-based systems work differently:

  • They have an immutable base image containing core system packages
  • Additional packages can be layered on top or installed transiently
  • This creates different "persistence" levels for packages

The Problem

Before this change, subscription-manager reported all packages the same way, regardless of how they were installed. Subscription services couldn't distinguish between:

  • Base image packages - Core packages that are part of the ostree commit
  • Layered packages - Packages added permanently via rpm-ostree install
  • Transient packages - Packages added temporarily via dnf --transient install

This information is valuable for understanding system configuration and compliance.

The Solution: Package Persistence Field

What Changed

The package profile now includes an optional persistence field with two possible values:

  • "persistent" - Package is part of the base ostree commit (immutable)
  • "transient" - Package was installed after the base commit (layered or transient)

When This Field Appears

  • Non-ostree systems (traditional Linux): Field is not included (maintains backward compatibility)
  • bootc systems: Field is always included for every package

Example Output

Before (all systems):

{
  "name": "vim-enhanced",
  "version": "9.1.083",
  "release": "6.el10_1",
  "arch": "x86_64",
  "epoch": 0,
  "vendor": "Red Hat, Inc."
}

After (ostree systems only):

{
  "name": "vim-enhanced",
  "version": "9.1.083",
  "release": "6.el10_1",
  "arch": "x86_64",
  "epoch": 0,
  "vendor": "Red Hat, Inc.",
  "persistence": "transient"
}

Technical Implementation

How It Works

The implementation follows these steps:

  1. Detect ostree system - Check if the system is running on ostree using the OSTree library
  2. Find base commit - Identify the original ostree commit without modifications
  3. Get base packages - Use rpm-ostree db list to get packages from the base commit
  4. Compare packages - Compare currently installed packages vs. base commit packages
  5. Assign persistence - Mark packages as persistent (in base) or transient (added later)

Key Technical Challenges Solved

Challenge 1: Database Isolation

Problem: On bootc systems in "transient unlock" mode, both the current and ostree deployment RPM databases contain identical packages.

Solution: Instead of reading RPM databases directly, use rpm-ostree db list <commit-hash> to get the true base commit packages.

Challenge 2: Finding the Right Base Commit

Problem: The booted deployment includes all changes, not just the base packages.

Solution: Use the rollback deployment or find a non-transient deployment as the baseline for comparison.

Code Structure

Package Class Changes

class Package:
    def __init__(self, name, version, release, arch, epoch=0, vendor=None, persistence=None):
        # ... existing fields ...
        self.persistence = persistence

    def to_dict(self):
        result = { /* existing fields */ }
        # Only include persistence if set (ostree systems only)
        if self.persistence is not None:
            result["persistence"] = self.persistence
        return result

Detection Logic

def _is_ostree_system():
    """Check if running on ostree using OSTree library"""

def _get_immutable_packages():
    """Get base commit packages using rpm-ostree db list"""

def _accumulate_profile(rpm_header_list):
    """Compare current vs base packages to determine persistence"""

Usage Examples

Scenario 1: Clean bootc System

# Fresh bootc system with only base packages
subscription-manager package-profile

Result: All ~480 packages marked as "persistent"

Scenario 2: System with Transient Packages

# Install a package transiently
sudo dnf --transient install vim-enhanced

subscription-manager package-profile

Result:

  • ~480 base packages marked as "persistent"
  • vim-enhanced marked as "transient"

Scenario 3: Traditional System

# Regular RHEL system
subscription-manager package-profile

Result: No persistence field included (backward compatible)

Benefits for Subscription Services

Enhanced Visibility

  • Compliance monitoring - Understand which packages are part of the approved base image
  • Configuration drift detection - Identify systems that have been modified from the standard
  • License tracking - Better understand actual vs. base package usage

Use Cases

  • Security auditing - Track non-standard package installations
  • Support efficiency - Quickly identify customized vs. standard systems
  • Inventory management - Understand true system composition

Backward Compatibility

For Subscription Services

  • Traditional systems continue to work exactly as before
  • New field is optional - existing parsing code won't break
  • Progressive enhancement - services can gradually adopt the new field

For Administrators

  • No behavior changes to existing workflows
  • No new dependencies on traditional systems
  • Graceful degradation if ostree detection fails

Testing and Validation

Test Coverage

  • ✅ Non-ostree systems (no persistence field)
  • ✅ Clean ostree systems (all persistent)
  • ✅ ostree systems with transient packages (mixed persistence)
  • ✅ Error handling (graceful fallback)
  • ✅ Package serialization/deserialization

Verification Commands

# Check if persistence detection is working
python3 -c "
from rhsm.profile import RPMProfile
profile = RPMProfile()
packages = profile.collect()
persistent = sum(1 for p in packages if p.get('persistence') == 'persistent')
transient = sum(1 for p in packages if p.get('persistence') == 'transient')
print(f'Persistent: {persistent}, Transient: {transient}')
"
@ianballou
Copy link
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment