Skip to content

feat: added package persistence info in packages profile#3684

Open
mjcr99 wants to merge 2 commits intomainfrom
macano/add-persistence-information-in-package-profile
Open

feat: added package persistence info in packages profile#3684
mjcr99 wants to merge 2 commits intomainfrom
macano/add-persistence-information-in-package-profile

Conversation

@mjcr99
Copy link
Contributor

@mjcr99 mjcr99 commented Jan 7, 2026

  • Card ID: RHEL-108712

Description

Hi team,

this PR introduces the tracking of persistence information for packages using rpm-ostree to check which packages are persistent or transient in a layered system.

Testing

RHEL 10

This development has been tested in the following machine:

Testing machine:

[vagrant@vm-rhel10 subscription-manager]$ cat /etc/os-release 
NAME="Red Hat Enterprise Linux"
VERSION="10.1 (Coughlan)"
ID="rhel"
ID_LIKE="centos fedora"
VERSION_ID="10.1"
PLATFORM_ID="platform:el10"
PRETTY_NAME="Red Hat Enterprise Linux 10.1 (Coughlan)"
ANSI_COLOR="0;31"
LOGO="fedora-logo-icon"
CPE_NAME="cpe:/o:redhat:enterprise_linux:10.1"
HOME_URL="https://www.redhat.com/"
VENDOR_NAME="Red Hat"
VENDOR_URL="https://www.redhat.com/"
DOCUMENTATION_URL="https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/10"
BUG_REPORT_URL="https://issues.redhat.com/"

REDHAT_BUGZILLA_PRODUCT="Red Hat Enterprise Linux 10"
REDHAT_BUGZILLA_PRODUCT_VERSION=10.1
REDHAT_SUPPORT_PRODUCT="Red Hat Enterprise Linux"
REDHAT_SUPPORT_PRODUCT_VERSION="10.1"

In this type of system, all the packages are understood to be persistent since there are no layers of installed packages.

The following command is run to upload the packages profile:

[vagrant@vm-rhel10 subscription-manager]$ sudo PYTHONPATH=./src python3 -m subscription_manager.scripts.package_profile_upload

Then the following information appears related to the persistence of the packages:

[vagrant@vm-rhel10 subscription-manager]$ sudo cat /var/lib/rhsm/cache/profile.json
...
{"name": "gcc", "version": "14.3.1", "release": "2.1.el10", "arch": "x86_64", "epoch": 0, "vendor": "Red Hat, Inc.", "persistence": "persistent"},
...

And as expected, no packages are shown as transient:

[vagrant@vm-rhel10 subscription-manager]$ sudo cat /var/lib/rhsm/cache/profile.json | grep transcient
[vagrant@vm-rhel10 subscription-manager]$ 
Fedora 43 CoreOS

To test this development

Testing machine:

vagrant@fedora41:~$ cat /etc/os-release 
NAME="Fedora Linux"
VERSION="43.20251120.3.0 (CoreOS)"
RELEASE_TYPE=stable
ID=fedora
VERSION_ID=43
VERSION_CODENAME=""
PRETTY_NAME="Fedora CoreOS 43.20251120.3.0"
ANSI_COLOR="0;38;2;60;110;180"
LOGO=fedora-logo-icon
CPE_NAME="cpe:/o:fedoraproject:fedora:43"
HOME_URL="https://getfedora.org/coreos/"
DOCUMENTATION_URL="https://docs.fedoraproject.org/en-US/fedora-coreos/"
SUPPORT_URL="https://github.com/coreos/fedora-coreos-tracker/"
BUG_REPORT_URL="https://github.com/coreos/fedora-coreos-tracker/"
REDHAT_BUGZILLA_PRODUCT="Fedora"
REDHAT_BUGZILLA_PRODUCT_VERSION=43
REDHAT_SUPPORT_PRODUCT="Fedora"
REDHAT_SUPPORT_PRODUCT_VERSION=43
SUPPORT_END=2026-12-02
VARIANT="CoreOS"
VARIANT_ID=coreos
OSTREE_VERSION='43.20251120.3.0'
IMAGE_VERSION='43.20251120.3.0'

This system is layered and uses the rpm-ostree tool to install packages in layers that can be rolled back to older versions, allowing the user to better manage the installed packages.

This system, as seen below, does not allow the user to install, by default, packages using dnf:

vagrant@fedora41:~$ sudo dnf install tree
Updating and loading repositories:
 Fedora 43 - x86_64                                                                                                                                   100% |  15.9 KiB/s |  22.3 KiB |  00m01s
 Fedora 43 - x86_64 - Updates Archive                                                                                                                 100% |  12.6 KiB/s |   3.4 KiB |  00m00s
 Fedora 43 - x86_64 - Updates                                                                                                                         100% |   8.3 KiB/s |   7.1 KiB |  00m01s
Repositories loaded.
Package                                                          Arch           Version                                                          Repository                               Size
Installing:
 tree                                                            x86_64         2.2.1-2.fc43                                                     fedora                              112.2 KiB

Transaction Summary:
 Installing:         1 package

Total size of inbound packages is 61 KiB. Need to download 61 KiB.
After this operation, 112 KiB extra will be used (install 112 KiB, remove 0 B).
Error: this bootc system is configured to be read-only. For more information, run `bootc --help`.

To allow the installation of packages with dnf, it's required to run the following command:

vagrant@fedora41:~$ sudo rpm-ostree usroverlay
Development mode enabled.  A writable overlayfs is now mounted on /usr.
All changes there will be discarded on reboot.

As seen in the command output, the installed packages using dnf will be present in the system until it's rebooted, this means these packages are the ones to be marked as transient.

The tree package is installed to test if the implemented mechanism is able to detect it as a transient package:

vagrant@fedora41:~$ sudo dnf install tree
Updating and loading repositories:                                                                                                                                                               
Repositories loaded.
Package                                                          Arch           Version                                                          Repository                               Size
Installing:
 tree                                                            x86_64         2.2.1-2.fc43                                                     fedora                              112.2 KiB

Transaction Summary:
 Installing:         1 package

Total size of inbound packages is 61 KiB. Need to download 61 KiB.
After this operation, 112 KiB extra will be used (install 112 KiB, remove 0 B).
Is this ok [y/N]: y
[1/1] tree-0:2.2.1-2.fc43.x86_64                                                                                                                      100% | 631.5 KiB/s |  61.3 KiB |  00m00s
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
[1/1] Total                                                                                                                                           100% |  76.4 KiB/s |  61.3 KiB |  00m01s
Running transaction
Importing OpenPGP key 0x31645531:
 UserID     : "Fedora (43) <fedora-43-primary@fedoraproject.org>"
 Fingerprint: C6E7F081CF80E13146676E88829B606631645531
 From       : file:///etc/pki/rpm-gpg/RPM-GPG-KEY-fedora-43-x86_64
Is this ok [y/N]: y
The key was successfully imported.
[1/3] Verify package files                                                                                                                            100% | 500.0   B/s |   1.0   B |  00m00s
[2/3] Prepare transaction                                                                                                                             100% |  14.0   B/s |   1.0   B |  00m00s
[3/3] Installing tree-0:2.2.1-2.fc43.x86_64                                                                                                           100% | 326.3 KiB/s | 113.6 KiB |  00m00s
Complete!

To test the implemented functions easily, the following script is provided:

test.py
import rpm

def parse_rpm_string(rpm_string: str) -> dict | None:
    """
    Parses a standard RPM package string into its NVR (Name, Version, Release) components.
    This function uses a greedy regular expression to correctly handle package names 
    that contain hyphens (e.g., 'amd-ucode-firmware'). It follows the RPM naming 
    convention where the version and release are separated by the last two 
    hyphens before the architecture suffix.
    Args:
        rpm_string (str): The full package string to parse. 
            Example: "NetworkManager-cloud-setup-1:1.54.0-2.fc43.x86_64"
    Returns:
        dict | None: A dictionary with the following keys if the match is successful:
            - 'name': The package name (including any internal hyphens).
            - 'ver': The version string (including Epoch if present).
            - 'rel': The release string (e.g., '2.fc43').
            - 'arch': The architecture (e.g., 'x86_64', 'noarch').
            Returns None if the string does not follow the expected RPM format.
    Example:
        >>> parse_rpm_string("bash-completion-1:2.16-2.fc43.noarch")
        {'name': 'bash-completion', 'ver': '1:2.16', 'rel': '2.fc43', 'arch': 'noarch'}
    """
    from re import match

    # Regex Logic:
    # ^\s* -> Ignore leading whitespace.
    # (?P<name>.+)     -> Greedy match for the name (captures all hyphens until the last two).
    # -                -> Separator between name and version.
    # (?P<ver>[^-]+)   -> Version: matches everything until the next hyphen.
    # -                -> Separator between version and release.
    # (?P<rel>[^-]+)   -> Release: matches everything until the last dot.
    # \.               -> The dot separating release and architecture.
    # (?P<arch>[^.]+)$ -> Architecture: everything after the last dot.

    regex_pattern = r"^\s*(?P<name>.+)-(?P<ver>[^-]+)-(?P<rel>[^-]+)\.(?P<arch>[^.]+)$"

    result = match(regex_pattern, rpm_string.strip())
    if result:
        return result.groupdict()
    return None

def _get_immutable_packages() -> set:
    """
    Get the set of packages from the immutable ostree deployment.
    For bootc systems, uses rpm-ostree to get the true base commit packages.
    Returns a set of (name, version, release, arch, epoch) tuples.
    """
    immutable_packages = set()

    try:
        import subprocess, json

        # Get rpm-ostree status to find the base commits
        result = subprocess.run(['rpm-ostree', 'status', '--json'],
                              capture_output=True, text=True, check=True)
        status = json.loads(result.stdout)

        deployments = status.get('deployments', [])
        if not deployments:
            print("No deployments found in rpm-ostree status")
            return immutable_packages

        # Use deployments[0] since it's the most recent
        base_checksum = deployments[0].get('checksum')
        if not base_checksum:
            print("No base checksum found")
            return immutable_packages

        print(f"Using checksum: {base_checksum[:10]} to get for immutable packages")

        # Use rpm-ostree db list to get packages from the base_checksum
        result = subprocess.run(['rpm-ostree', 'db', 'list', base_checksum],
                              capture_output=True, text=True, check=True)

        # Skip first line since there is returned the consulted base_checksum
        for line in result.stdout.strip().split('\n')[1:]:
            line = line.strip()

            try:
                package_dict = parse_rpm_string(line)
                immutable_packages.add(package_dict["name"])

            except (ValueError, IndexError) as e:
                print(f"Failed to parse package line '{line}': {e}")
                continue

        print(f"Found {len(immutable_packages)} packages in base ostree commit {base_checksum[:10]}")

    except subprocess.CalledProcessError as e:
        print(f"rpm-ostree command failed: {e}")
    except ImportError:
        print("subprocess module not available")
    except Exception as e:
        print(f"Failed to get immutable packages via rpm-ostree: {e}")

    return immutable_packages

ts = rpm.TransactionSet()
ts.setVSFlags(-1)
db = ts.dbMatch()
packages = []

for p in db:
#    print(p["name"])
    packages.append(p["name"])

p_not_in_inmutable_p = []
p_inmutable_not_in_p = []

inmutable_p = _get_immutable_packages()
print("Number of persistent packages: ", len(inmutable_p))

for p in packages:
   if p not in inmutable_p:
      p_not_in_inmutable_p.append(p)

print("p_not_in_inmutable_p (transient): ", p_not_in_inmutable_p)

After the previous package installation, the testing script is run and the obtained result is the expected:

vagrant@fedora41:~$ python3 test.py 
Using checksum: d91a98a1e9 to get for immutable packages
Found 461 packages in base ostree commit d91a98a1e9
Number of persistent packages:  461
p_not_in_inmutable_p (transient):  ['gpg-pubkey', 'tree']

@sourcery-ai
Copy link

sourcery-ai bot commented Jan 7, 2026

Reviewer's Guide

Adds persistence metadata to package profiles and implements ostree-aware detection of persistent vs transient packages using rpm-ostree on layered systems.

Sequence diagram for ostree-aware package persistence detection

sequenceDiagram
    actor SystemAdmin
    participant PackageProfileUploadScript
    participant RPMProfile
    participant OstreeHelpers
    participant RpmOstree as rpm_ostree
    participant ProfileJson as profile_json_file

    SystemAdmin->>PackageProfileUploadScript: run package_profile_upload
    PackageProfileUploadScript->>RPMProfile: construct RPMProfile(from_file=None)
    PackageProfileUploadScript->>RPMProfile: _accumulate_profile(rpm_header_list)

    RPMProfile->>OstreeHelpers: _is_ostree_system()
    alt ostree system
        OstreeHelpers->>OstreeHelpers: _get_immutable_packages()
        OstreeHelpers->>RpmOstree: rpm-ostree status --json
        RpmOstree-->>OstreeHelpers: deployments with checksum
        OstreeHelpers->>RpmOstree: rpm-ostree db list base_checksum
        RpmOstree-->>OstreeHelpers: package list text
        OstreeHelpers-->>RPMProfile: immutable_packages set
    else non ostree system
        OstreeHelpers-->>RPMProfile: empty immutable_packages set
    end

    loop for each rpm_header
        RPMProfile->>RPMProfile: determine persistence
        alt name not in immutable_packages and is_ostree
            RPMProfile->>RPMProfile: persistence = transient
        else
            RPMProfile->>RPMProfile: persistence = persistent
        end
        RPMProfile->>RPMProfile: create Package with persistence
    end

    RPMProfile-->>PackageProfileUploadScript: list of Package
    PackageProfileUploadScript->>PackageProfileUploadScript: serialize to dict list with persistence
    PackageProfileUploadScript->>ProfileJson: write profile.json
    ProfileJson-->>SystemAdmin: profile includes persistence for each package
Loading

Updated class diagram for package persistence in profile generation

classDiagram

class Package {
    +str name
    +str version
    +str release
    +str arch
    +int epoch
    +str vendor
    +str persistence
    +to_dict() dict
    +__eq__(other Package) bool
    +_normalize_string(value str) str
}

class RPMProfile {
    +__init__(from_file TextIOWrapper)
    +to_dict() dict
    -_accumulate_profile(rpm_header_list List~dict~) List~Package~
}

class parse_rpm_string {
    +parse_rpm_string(rpm_string str) dict
}

class _is_ostree_system {
    +_is_ostree_system() bool
}

class _get_immutable_packages {
    +_get_immutable_packages() set
}

class OSTreeLibrary {
    +Sysroot new_default()
    +Sysroot load(cancellable)
    +Sysroot get_booted_deployment()
}

class RpmOstreeCli {
    +status_json()
    +db_list(base_checksum str)
}

RPMProfile ..> Package : creates
RPMProfile ..> _is_ostree_system : uses
RPMProfile ..> _get_immutable_packages : uses
_get_immutable_packages ..> parse_rpm_string : uses
_is_ostree_system ..> OSTreeLibrary : uses when gi_available
_get_immutable_packages ..> RpmOstreeCli : uses
Loading

File-Level Changes

Change Details Files
Extend Package model and serialized profile format to include package persistence metadata.
  • Add a persistence attribute to Package objects with constructor wiring and equality semantics.
  • Include persistence in the serialized dict representation of packages written to the profile cache.
  • Adjust RPMProfile deserialization to read persistence values from existing profile data.
src/rhsm/profile.py
Detect whether the system is ostree-based and resolve immutable base packages via rpm-ostree.
  • Introduce OSTree introspection via gobject-introspection, guarded by availability checks, to detect ostree systems.
  • Add _is_ostree_system helper to determine if the host is ostree/bootc-like, falling back gracefully when OSTree is unavailable.
  • Implement _get_immutable_packages to call rpm-ostree status --json and rpm-ostree db list and derive the set of base (persistent) package names, with debug logging and robust error handling.
src/rhsm/profile.py
Mark packages as persistent or transient when building the RPM profile on ostree systems.
  • Have RPMProfile._accumulate_profile detect ostree systems and precompute the immutable package name set when applicable.
  • Skip gpg-pubkey pseudo-packages while iterating headers, as before.
  • Assign persistence="transient" to packages not in the immutable set on ostree systems and persistence="persistent" otherwise, ensuring all reported packages carry persistence status.
src/rhsm/profile.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@github-actions
Copy link

github-actions bot commented Jan 7, 2026

Coverage

Coverage (computed on Fedora latest) •
FileStmtsMissCoverMissing
rhsm
   profile.py2979269%29–30, 34–35, 46, 48, 55, 75, 115–119, 121–125, 127–129, 131–138, 157–159, 163–165, 203, 216, 230–231, 241–242, 248, 256, 268, 278–279, 281–282, 291, 354, 370, 404, 456–457, 462–463, 480, 490–492, 498–501, 531–535, 545, 548–552, 554–555, 559, 561–562, 564, 577, 588–591, 598, 603, 618–621
TOTAL17293447474% 

Tests Skipped Failures Errors Time
2455 14 💤 0 ❌ 0 🔥 23.273s ⏱️

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 3 issues, and left some high level feedback:

  • When loading an RPMProfile from an existing file, pkg_dict["persistence"] will raise a KeyError for previously cached profiles that don't have this field; consider using pkg_dict.get("persistence") with a sensible default (e.g. "persistent" or None).
  • parse_rpm_string can return None, but _get_immutable_packages assumes it always returns a dict and accesses package_dict["name"]; guard against a None return or broaden the exception handling to avoid unexpected TypeErrors.
  • In Package.__eq__, persistence is compared directly while other string fields are normalized; it may be safer to normalize persistence too (or treat None consistently) to avoid equality differences due to representation.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- When loading an RPMProfile from an existing file, `pkg_dict["persistence"]` will raise a KeyError for previously cached profiles that don't have this field; consider using `pkg_dict.get("persistence")` with a sensible default (e.g. `"persistent"` or `None`).
- `parse_rpm_string` can return `None`, but `_get_immutable_packages` assumes it always returns a dict and accesses `package_dict["name"]`; guard against a `None` return or broaden the exception handling to avoid unexpected TypeErrors.
- In `Package.__eq__`, `persistence` is compared directly while other string fields are normalized; it may be safer to normalize `persistence` too (or treat `None` consistently) to avoid equality differences due to representation.

## Individual Comments

### Comment 1
<location> `src/rhsm/profile.py:512` </location>
<code_context>
                         arch=pkg_dict["arch"],
                         epoch=pkg_dict["epoch"],
                         vendor=pkg_dict["vendor"],
+                        persistence=pkg_dict["persistence"]
                     )
                 )
</code_context>

<issue_to_address>
**issue (bug_risk):** Directly indexing 'persistence' breaks backward compatibility with existing profile files

Older profile files won’t have a `"persistence"` key, so `pkg_dict["persistence"]` will raise a `KeyError` when loading them. Use `pkg_dict.get("persistence", <sensible_default>)` instead so deserialization remains backward compatible.
</issue_to_address>

### Comment 2
<location> `src/rhsm/profile.py:40-49` </location>
<code_context>
 except ImportError:
     yum = None

+try:
+    import gi
+    gi.require_version("OSTree", "1.0")
</code_context>

<issue_to_address>
**suggestion:** parse_rpm_string(None) is not handled, so a bad line can abort the whole rpm-ostree parsing loop

If `parse_rpm_string(line)` returns `None` for an unexpected format, `package_dict["name"]` will raise a `TypeError`. Because the inner handler only catches `ValueError` and `IndexError`, this `TypeError` will fall through to the outer `except Exception` and stop processing further lines. Please either skip `None` results before indexing (e.g. `if not package_dict: continue`) or add `TypeError` to the inner `except` so a single malformed line doesn’t abort the loop.

Suggested implementation:

```python
    for line in rpm_output.splitlines():
        try:
            package_dict = parse_rpm_string(line)
            if not package_dict:
                # parse_rpm_string returned None or an empty dict; skip this malformed line
                continue
            package = {
                "name": package_dict["name"],
                "version": package_dict["version"],
                "release": package_dict["release"],
                "arch": package_dict["arch"],
            }
            packages.append(package)
        except (ValueError, IndexError):
            log.debug("Skipping unparsable rpm line: %s", line)

```

I inferred the surrounding loop and variable names (`rpm_output`, `packages`, and `log`) from your description, since that part of the file wasn’t shown. You will need to adjust the `SEARCH` block to exactly match the existing loop that calls `parse_rpm_string(line)` in `src/rhsm/profile.py` (e.g. the iterable, the container type being appended to, and the logging call), and then apply the same `if not package_dict: continue` pattern immediately after the `parse_rpm_string` call.
</issue_to_address>

### Comment 3
<location> `src/rhsm/profile.py:434-438` </location>
<code_context>
+        return False
+    
+
+def _get_immutable_packages() -> set:
+    """
+    Get the set of packages from the immutable ostree deployment.
+    For bootc systems, uses rpm-ostree to get the true base commit packages.
+    Returns a set of (name, version, release, arch, epoch) tuples.
+    """
+    immutable_packages = set()
</code_context>

<issue_to_address>
**suggestion (bug_risk):** Return type and docstring describe tuples but the implementation returns a set of names

This currently only adds `package_dict['name']` to the set. Either update the docstring/return type to reflect a set of names, or change the implementation to store full `(name, version, release, arch, epoch)` tuples as documented.

Suggested implementation:

```python
    immutable_packages = set()

```

```python
                immutable_packages.add((
                    package_dict.get("name"),
                    package_dict.get("version"),
                    package_dict.get("release"),
                    package_dict.get("arch"),
                    package_dict.get("epoch"),
                ))

```

If there are any other places in `_get_immutable_packages()` (or related helper functions) where `immutable_packages.add(...)` is called with only the name or any subset of the fields, update them to add the same 5-tuple `(name, version, release, arch, epoch)` for consistency with the docstring.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@mjcr99 mjcr99 force-pushed the macano/add-persistence-information-in-package-profile branch 2 times, most recently from ef972db to f3f10f5 Compare January 8, 2026 13:23
@mjcr99 mjcr99 requested a review from jirihnidek January 8, 2026 15:38
@mjcr99 mjcr99 force-pushed the macano/add-persistence-information-in-package-profile branch 2 times, most recently from 870c5cd to 8077735 Compare January 9, 2026 09:34
@jsefler
Copy link
Contributor

jsefler commented Jan 9, 2026

While doing some preliminary testing with a new build of subscription-manager for this pull request, I discovered that attempts to install a transient package on an image mode system with selinux turned on (Enforcing) will trigger the following selinux denials...

type=AVC msg=audit(1767908617.062:9249): avc: denied { getattr } for pid=49079 comm="rhsm-package-pr" path="/run/ostree-booted" dev="tmpfs" ino=557 scontext=system_u:system_r:rhsmcertd_t:s0 tcontext=system_u:object_r:var_run_t:s0 tclass=file permissive=0

type=AVC msg=audit(1767908617.066:9250): avc: denied { read } for pid=49079 comm="rhsm-package-pr" name="ostree-booted" dev="tmpfs" ino=557 scontext=system_u:system_r:rhsmcertd_t:s0 tcontext=system_u:object_r:var_run_t:s0 tclass=file permissive=0

As a result, the package profile "persistence" attribute for the transiently installed package will be "persistent" instead of "transient". When selinux is turned off (Permissive), the selinux denials are not encountered and the expected "transient" value is set in the package profile. Hence it appears that a new selinux-policy is needed for this new feature to work as desired on an image mode (layered) system.

Copy link
Contributor

@jirihnidek jirihnidek left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR. I can confirm that it works as expected. I have some comments and requests.

In general you mix usage of OSTree Python module and parsing of rpm-ostree output. It seems sub-optimal. Maybe, there is some technical reason for it, but I do not see it. If there is any, then it should be mentioned in some comment. If there is no technical reason for using both approaches, then I would try to use only one approach Python API or parsing rpm-ostree.

@ianballou
Copy link
Contributor

I gave this a test on a system registered to Satellite. Newly installed packages are being marked as transient, but updated ones seems to still be marked as persistent. For example:

Installing:
 vim-enhanced                          x86_64                        2:9.1.083-6.el10_1                        rhel-10-for-x86_64-appstream-rpms                        1.9 M
Upgrading:
 vim-data                              noarch                        2:9.1.083-6.el10_1                        rhel-10-for-x86_64-baseos-rpms                            17 k
 vim-minimal                           x86_64                        2:9.1.083-6.el10_1                        rhel-10-for-x86_64-baseos-rpms                           799 k
Installing dependencies:
 vim-common                            x86_64                        2:9.1.083-6.el10_1                        rhel-10-for-x86_64-appstream-rpms                        7.7 M
 vim-filesystem                        noarch                        2:9.1.083-6.el10_1                        rhel-10-for-x86_64-baseos-rpms                            16 k
 xxd     

...
image

and

image

I'd expect updated packages to be transient as well, is this unexpected?

@mjcr99 mjcr99 force-pushed the macano/add-persistence-information-in-package-profile branch from 68ae996 to 1509dcf Compare January 13, 2026 11:43
@mjcr99
Copy link
Contributor Author

mjcr99 commented Jan 13, 2026

Hi @ianballou, thanks for testing it.
The behavior you obtained was expected. I have changed the transient detection logic to meet your expectations. Now, transiently updated packages should also be marked as transient

@ianballou
Copy link
Contributor

Hi @ianballou, thanks for testing it. The behavior you obtained was expected. I have changed the transient detection logic to meet your expectations. Now, transiently updated packages should also be marked as transient

Great, this is working as I expected now 👍

@zpytela
Copy link

zpytela commented Jan 14, 2026

The /run/ostree-booted file has an incorrect context. It needs to be troubleshooted how it was created.

Subsequently, rhsmcertd needs to be allowed the needed permissions.

@jsefler
Copy link
Contributor

jsefler commented Jan 14, 2026

Seeking clarification from @zpytela

The /run/ostree-booted file has an incorrect context. It needs to be troubleshooted how it was created.

Subsequently, rhsmcertd needs to be allowed the needed permissions.

Are you saying that context system_u:object_r:var_run_t:s0 on file /run/ostree-booted is wrong? What context are you expecting? Let's troubleshoot. Here is exactly what I get when I reserve a RHEL-10.2-image-mode machine in Testing Farm...

jsefler@jseflerP1Gen4:~$ NO_TTY=1 testing-farm reserve --compose RHEL-10.2-image-mode --arch x86_64 --duration 720 --ssh-public-key ~/.ssh/rhsm-qe.pub
💻 RHEL-10.2-image-mode on x86_64 
🕗 Reserved for 720 minutes
⏳ Maximum reservation time is 720 minutes
🔎 https://api.dev.testing-farm.io/v0.1/requests/6c798ad4-8d41-4996-833e-41a3369f80b5
🌎 ssh root@10.31.11.179
Warning: Permanently added '10.31.11.179' (ED25519) to the list of known hosts.
Welcome to Testing Farm!

Machine is reserved until Thu Jan 15 05:07:26 UTC 2026.

To extend the reservation, run 'extend-reservation MINUTES'.

To return machine back, run 'return2testingfarm'.

To reboot the machine, use 'reboot'.

To prevent returning machine, create the file '/var/tmp/.testing-farm-keep'.
This can be useful if you play around with system time and you want to prevent the system to be returned.
Register this system with Red Hat Lightspeed: rhc connect

Example:
# rhc connect --activation-key <key> --organization <org>

The rhc client and Red Hat Lightspeed will enable analytics and additional
management capabilities on your system.
View your connected systems at https://console.redhat.com/insights

You can learn more about how to register your system 
using rhc at https://red.ht/registration
Last login: Wed Jan 14 17:07:26 2026 from 10.31.11.245
[root@cdc25a10-4768-433d-b5a0-d90ed7e4fb17 ~]# bootc status
● Booted image: containers-storage:localhost/tmt/bootc/6d2cb92b-fb74-4212-8e9f-467ce3ef2d75
        Digest: sha256:56b33495654d2008f25873b0edd98b4e60dbbc1adb4ff41878ac617736bc7c4d (amd64)
       Version: 10.2 (2026-01-14T17:06:07Z)

  Rollback image: images.paas.redhat.com/testingfarm/rhel-bootc:10.2
          Digest: sha256:aaf4e4b78815e1ddd4f701ae33ea57ecd4c12d039111fb0db6efeda2a5ae9d46 (amd64)
         Version: 10.2 (2026-01-12T07:54:55Z)
[root@cdc25a10-4768-433d-b5a0-d90ed7e4fb17 ~]# 
[root@cdc25a10-4768-433d-b5a0-d90ed7e4fb17 ~]# ls -Z /run/ostree-booted
system_u:object_r:var_run_t:s0 /run/ostree-booted
[root@cdc25a10-4768-433d-b5a0-d90ed7e4fb17 ~]# 
[root@cdc25a10-4768-433d-b5a0-d90ed7e4fb17 ~]# matchpathcon -V /run/ostree-booted
/run/ostree-booted verified.
[root@cdc25a10-4768-433d-b5a0-d90ed7e4fb17 ~]# 

As you see above, the system_u:object_r:var_run_t:s0 context is what I get immediately out of the box. What context are you expecting?

@jsefler
Copy link
Contributor

jsefler commented Jan 15, 2026

The SELinux conversation has moved to RHEL-141391.

Copy link
Contributor

@jirihnidek jirihnidek left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have a few comments and small requests.

)

except (ValueError, IndexError) as e:
log.debug(f"Failed to parse package line '{line}': {e}")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The severity of this log message should be at least warning or error.

immutable_packages = set()
if is_ostree:
immutable_packages = _get_immutable_packages()
log.debug(f"Running on ostree system with {len(immutable_packages)} persistent packages")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would write here the debug message here in the else case, when the system is not detected as ostree based system. It could be important to print here for troubleshooting some edge cases (like SELinux issues, etc.).

result = _get_immutable_packages()

self.assertIsInstance(result, set)
self.assertEqual(len(result), 0)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is not unit test for the case, when there is at least one transient package.

@jirihnidek
Copy link
Contributor

@ianballou Do you really need only "persistent" and "transient" states? What about the use cases, when ostree based system is used and some package is installed using rpm-ostree install <PKG_NAME>, but the system has not been restarted yet? Shouldn't be the <PKG_NAME> listed as "pending" or something like this?

@ianballou
Copy link
Contributor

@ianballou Do you really need only "persistent" and "transient" states? What about the use cases, when ostree based system is used and some package is installed using rpm-ostree install <PKG_NAME>, but the system has not been restarted yet? Shouldn't be the <PKG_NAME> listed as "pending" or something like this?

@jirihnidek my focus has been the "dnf --transient" for bootc use case. DNF's documentation mentions this persistence feature is for bootc machines specifically, so I didn't want to impose a new concept onto "vanilla" ostree. From a technical standpoint, bootc users shouldn't be using rpm-ostree.

I can understand though wanting to also have this feature make some sense for vanilla ostree hosts. I don't know enough about how rpm-ostree works to say if its behavior would mix well with the bootc persistent principles being followed here.

Something to consider: bootc machines are expected to work well with subscription-manager due to the dnf connection and a wish to reduce differences with normal EL machines. Do ostree systems have a similar expectation?

@mjcr99 mjcr99 force-pushed the macano/add-persistence-information-in-package-profile branch from 1509dcf to 44d77c6 Compare February 17, 2026 13:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants