Skip to content

Conversation

@richm
Copy link
Contributor

@richm richm commented Sep 30, 2025

Feature: Support running the timesync role during container builds.

Reason: This is particularly useful for building bootc derivative OSes.

Result: These flags enable running the bootc container scenarios in CI,
which ensures that the role works in buildah build environment. This
allows us to officially support this role for image mode builds.

Signed-off-by: Rich Megginson rmeggins@redhat.com

Summary by Sourcery

Enable offline NetworkManager support to allow the role to run in container build environments, refactor the command API and provider switching logic, and overhaul the test suite with a reusable harness and helper tasks to streamline CI testing

New Features:

  • Introduce an offline NetworkManager provider (nm_offline) for container build environments
  • Enable support for running the role under Buildah/container scenarios by switching to offline mode when the system is not booted

Enhancements:

  • Add a check_rc flag to run_command and rename the ifcfg_header property to managed_file_header
  • Automatically detect booted systems (__network_is_booted) and switch to nm_offline when necessary

CI:

  • Remove branch filters from the QEMU KVM integration tests workflow

Tests:

  • Refactor test playbooks to use a unified run_test.yml harness with modular setup, assert, and cleanup tasks
  • Add new test helper tasks (assert_connection_settings.yml, assert_command_output.yml, assert_nm_connection_status.yml, and handlers)
  • Update tests to conditionally skip or adjust behavior in buildah environments and increase the default test timeout

@sourcery-ai
Copy link

sourcery-ai bot commented Sep 30, 2025

Reviewer's Guide

Enables offline operation of the network role in buildah environments by introducing an nm_offline provider and boot detection, refactors library methods for command execution and file header handling, updates defaults and main tasks for offline support, and restructures the integration tests and CI workflow with a unified run_test harness to handle build and boot scenarios.

Class diagram for new and updated command provider classes

classDiagram
    class Cmd {
        +managed_file_header
        +run_command(argv, encoding=None, check_rc=False)
        +log(...)
    }
    class Cmd_nm {
        +managed_file_header
        +run_command(argv, encoding=None, check_rc=False)
    }
    class Cmd_nm_offline {
        +profile_path(name)
        +run_prepare()
        +connection_create(connections, idx)
        +run_action_present(idx)
        +run_action_absent(idx)
        +run_action_up(idx)
        +run_action_down(idx)
    }
    class Cmd_initscripts {
        +managed_file_header
        +run_command(argv, encoding=None, check_rc=False)
    }
    Cmd <|-- Cmd_nm
    Cmd <|-- Cmd_nm_offline
    Cmd <|-- Cmd_initscripts
Loading

File-Level Changes

Change Details Files
Refactor command execution API and header property
  • Renamed ifcfg_header to managed_file_header in base and subclasses
  • Extended run_command signature to accept a new check_rc parameter
library/network_connections.py
Introduce offline NM backend for container builds
  • Added Cmd_nm_offline provider class with fully offline connection_create and action methods
  • Updated provider factory to include nm_offline option
library/network_connections.py
Automatic switch to nm_offline when system not booted
  • Added systemd boot detection in tasks/set_facts.yml to set __network_is_booted
  • Set network_provider to nm_offline in main tasks when __network_is_booted is false
tasks/set_facts.yml
tasks/main.yml
Adjust service tasks and defaults for nm_offline support
  • Omitted starting services when not booted via conditional state in tasks/main.yml
  • Defined nm_offline under __network_provider_setup in defaults/main.yml
tasks/main.yml
defaults/main.yml
Refactor integration tests with unified run_test harness
  • Created tasks/run_test.yml and handler scripts (handle_setup_task.yml, handle_test_task.yml, handle_assert_task.yml, handle_cleanup_task.yml)
  • Refactored playbooks to invoke run_test abstraction and added buildah/boot conditional logic
tests/playbooks/*.yml
tests/tasks/run_test.yml
tests/tasks/handle_*.yml
Add test helper task modules for assertions
  • Added assert_connection_settings.yml, assert_command_output.yml, assert_nm_connection_status.yml for reusable checks
  • Introduced collection-requirements.yml to pull community.general
tests/tasks/assert_connection_settings.yml
tests/tasks/assert_command_output.yml
tests/tasks/assert_nm_connection_status.yml
tests/collection-requirements.yml
Update CI workflow for bootc integration
  • Removed branch filter from qemu-kvm-integration-tests workflow
  • Included bootc container images in the test matrix
.github/workflows/qemu-kvm-integration-tests.yml

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

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 there - I've reviewed your changes and they look great!

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location> `library/network_connections.py:3120-3121` </location>
<code_context>
+            return
+
+        path = self.profile_path(connection["name"])
+        if self.check_mode == CheckMode.REAL_RUN:
+            with open(path, "w") as f:
+                f.write(self.run_env.managed_file_header)
+                f.write(out.decode("UTF-8"))
+            os.chmod(path, stat.S_IRUSR | stat.S_IWUSR)
+            os.chown(path, 0, 0)
+
+        # nmcli always generates a new UUID, so comparing with existing file is moot
</code_context>

<issue_to_address>
**suggestion:** os.chown may fail in non-root or container environments.

Catch PermissionError from os.chown or skip this step if not running as root to avoid failures in restricted environments.

```suggestion
            os.chmod(path, stat.S_IRUSR | stat.S_IWUSR)
            # Only attempt chown if running as root, and catch PermissionError
            if hasattr(os, "geteuid") and os.geteuid() == 0:
                try:
                    os.chown(path, 0, 0)
                except PermissionError:
                    # Log or silently skip if chown fails due to permissions
                    pass
```
</issue_to_address>

### Comment 2
<location> `library/network_connections.py:3099-3102` </location>
<code_context>
+            self.log_error(idx, "Connection 'type' not specified")
+            return
+
+        # DEBUG, drop for final commit
+        self.log_info(
+            idx, "XXX nm_offline provider action_present connection: %r" % connection
</code_context>

<issue_to_address>
**suggestion:** Remove debug logging before final merge.

Please remove the 'log_info' statements marked as debug to prevent extra log output in production.

```suggestion
```
</issue_to_address>

### Comment 3
<location> `library/network_connections.py:3109` </location>
<code_context>
+            self.connection_create(self.connections, idx), check_rc=True
+        )
+        # Should Not Happen™, but make sure
+        if not out.strip():
+            self.log_error(
+                idx, "nmcli --offline returned no output (rc %d); err: %s" % (rc, err)
</code_context>

<issue_to_address>
**suggestion:** Consider handling out as bytes or str consistently.

Since out is used with both strip() and decode('UTF-8'), ensure run_command consistently returns bytes, or add type checks to prevent type errors.
</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.

Comment on lines +3120 to +3125
os.chmod(path, stat.S_IRUSR | stat.S_IWUSR)
os.chown(path, 0, 0)
Copy link

Choose a reason for hiding this comment

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

suggestion: os.chown may fail in non-root or container environments.

Catch PermissionError from os.chown or skip this step if not running as root to avoid failures in restricted environments.

Suggested change
os.chmod(path, stat.S_IRUSR | stat.S_IWUSR)
os.chown(path, 0, 0)
os.chmod(path, stat.S_IRUSR | stat.S_IWUSR)
# Only attempt chown if running as root, and catch PermissionError
if hasattr(os, "geteuid") and os.geteuid() == 0:
try:
os.chown(path, 0, 0)
except PermissionError:
# Log or silently skip if chown fails due to permissions
pass

Comment on lines +3099 to +3106
# DEBUG, drop for final commit
self.log_info(
idx, "XXX nm_offline provider action_present connection: %r" % connection
)
Copy link

Choose a reason for hiding this comment

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

suggestion: Remove debug logging before final merge.

Please remove the 'log_info' statements marked as debug to prevent extra log output in production.

Suggested change
# DEBUG, drop for final commit
self.log_info(
idx, "XXX nm_offline provider action_present connection: %r" % connection
)

self.connection_create(self.connections, idx), check_rc=True
)
# Should Not Happen™, but make sure
if not out.strip():
Copy link

Choose a reason for hiding this comment

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

suggestion: Consider handling out as bytes or str consistently.

Since out is used with both strip() and decode('UTF-8'), ensure run_command consistently returns bytes, or add type checks to prevent type errors.

class Cmd_nm_offline(Cmd):
def __init__(self, **kwargs):
Cmd.__init__(self, **kwargs)
self.validate_one_type = ArgValidator_ListConnections.VALIDATE_ONE_MODE_NM

Check warning

Code scanning / CodeQL

Overwriting attribute in super-class or sub-class Warning

Assignment overwrites attribute validate_one_type, which was previously defined in superclass
Cmd
.
@codecov
Copy link

codecov bot commented Sep 30, 2025

Codecov Report

❌ Patch coverage is 5.13699% with 277 lines in your changes missing coverage. Please review.
✅ Project coverage is 39.90%. Comparing base (1b57520) to head (e8a3ff4).
⚠️ Report is 57 commits behind head on main.

Files with missing lines Patch % Lines
library/network_connections.py 5.13% 277 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #814      +/-   ##
==========================================
- Coverage   43.11%   39.90%   -3.22%     
==========================================
  Files          12       12              
  Lines        3124     3408     +284     
==========================================
+ Hits         1347     1360      +13     
- Misses       1777     2048     +271     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@richm richm removed the request for review from liangwen12year September 30, 2025 20:13
@richm richm force-pushed the bootc branch 2 times, most recently from 0e4baf0 to 3dca657 Compare October 7, 2025 18:16
if "nm.exists" not in connection:
connection["nm.exists"] = True
argv = [
"nmcli",
Copy link
Collaborator

Choose a reason for hiding this comment

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

Instead of spawning nmcli, did you consider using libnm to create the connection in memory (similarly to NMUtil.connection_create) and then using nm_keyfile_write() to write it to disk? Probably that would allow some code reuse.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Instead of spawning nmcli, did you consider using libnm to create the connection in memory (similarly to NMUtil.connection_create) and then using nm_keyfile_write() to write it to disk? Probably that would allow some code reuse.

I tried this - I got this error - fatal error: failure loading libnm library: g-io-error-quark: Could not connect: No such file or directory (1) - I'm assuming in order to do it the way you suggested, dbus needs to be running? and/or NetworkMananger? If so - that's not going to work during image build or bootc build - is there a way to tell the api to work in offline mode?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I suppose you called NM.Client.new(None)? That creates a client object that tries to connect to NM over D-Bus.

For offline operations, don't create a client object. Instead, build the NM.SimpleConnection object and then write it with NM.keyfile_write(). Like in this sample code:

#!/usr/bin/env python
# SPDX-License-Identifier: LGPL-2.1-or-later

import sys
import os
import uuid

import gi

gi.require_version("NM", "1.0")
from gi.repository import NM, GLib


def print_values(setting, key, value, flags, data):
    print("  %s.%s: %s" % (setting.get_name(), key, value))


def create_profile(name):
    profile = NM.SimpleConnection.new()

    s_con = NM.SettingConnection.new()
    s_con.set_property(NM.SETTING_CONNECTION_ID, name)
    s_con.set_property(NM.SETTING_CONNECTION_UUID, str(uuid.uuid4()))
    s_con.set_property(NM.SETTING_CONNECTION_TYPE, "802-3-ethernet")

    s_wired = NM.SettingWired.new()

    s_ip4 = NM.SettingIP4Config.new()
    s_ip4.set_property(NM.SETTING_IP_CONFIG_METHOD, "auto")

    s_ip6 = NM.SettingIP6Config.new()
    s_ip6.set_property(NM.SETTING_IP_CONFIG_METHOD, "auto")

    profile.add_setting(s_con)
    profile.add_setting(s_ip4)
    profile.add_setting(s_ip6)
    profile.add_setting(s_wired)

    print(" *** Created connection profile:")
    profile.for_each_setting_value(print_values, None)

    return profile


def kf_handler_write(connection, keyfile, handler_type, handler_data, user_data):
    globals()["kf_handler_write_cnt"] = kf_handler_write_cnt

    [kf_group, kf_key, cur_setting, cur_property] = handler_data.get_context()

    print(f" - handler: {kf_group} {kf_key} {cur_setting}.{cur_property}\n");

    if handler_type == NM.KeyfileHandlerType.WRITE_CERT:
        # TODO handle writing certificates
        return False
    return False


if __name__ == "__main__":
    profile = create_profile("test")

    try:
        print("")
        print(" *** Writing keyfile...")
        keyfile = NM.keyfile_write(profile, NM.KeyfileHandlerFlags.NONE, kf_handler_write, 0)
    except Exception as e:
        print("Write failed: %r" % (e))
        raise

    [kf_string,_] = keyfile.to_data()
    print("")
    print(f" *** Keyfile:\n{kf_string}")

Or have a look at https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/blob/main/examples/python/gi/nm-keyfile.py

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I suppose you called NM.Client.new(None)? That creates a client object that tries to connect to NM over D-Bus.

Yes - https://github.com/linux-system-roles/network/blob/main/module_utils/network_lsr/nm/client.py#L47

def get_client():
    return NM.Client.new()

For offline operations, don't create a client object. Instead, build the NM.SimpleConnection object and then write it with NM.keyfile_write(). Like in this sample code:

ok - I'll try it

#!/usr/bin/env python
# SPDX-License-Identifier: LGPL-2.1-or-later

import sys
import os
import uuid

import gi

gi.require_version("NM", "1.0")
from gi.repository import NM, GLib


def print_values(setting, key, value, flags, data):
    print("  %s.%s: %s" % (setting.get_name(), key, value))


def create_profile(name):
    profile = NM.SimpleConnection.new()

    s_con = NM.SettingConnection.new()
    s_con.set_property(NM.SETTING_CONNECTION_ID, name)
    s_con.set_property(NM.SETTING_CONNECTION_UUID, str(uuid.uuid4()))
    s_con.set_property(NM.SETTING_CONNECTION_TYPE, "802-3-ethernet")

    s_wired = NM.SettingWired.new()

    s_ip4 = NM.SettingIP4Config.new()
    s_ip4.set_property(NM.SETTING_IP_CONFIG_METHOD, "auto")

    s_ip6 = NM.SettingIP6Config.new()
    s_ip6.set_property(NM.SETTING_IP_CONFIG_METHOD, "auto")

    profile.add_setting(s_con)
    profile.add_setting(s_ip4)
    profile.add_setting(s_ip6)
    profile.add_setting(s_wired)

    print(" *** Created connection profile:")
    profile.for_each_setting_value(print_values, None)

    return profile


def kf_handler_write(connection, keyfile, handler_type, handler_data, user_data):
    globals()["kf_handler_write_cnt"] = kf_handler_write_cnt

    [kf_group, kf_key, cur_setting, cur_property] = handler_data.get_context()

    print(f" - handler: {kf_group} {kf_key} {cur_setting}.{cur_property}\n");

    if handler_type == NM.KeyfileHandlerType.WRITE_CERT:
        # TODO handle writing certificates
        return False
    return False


if __name__ == "__main__":
    profile = create_profile("test")

    try:
        print("")
        print(" *** Writing keyfile...")
        keyfile = NM.keyfile_write(profile, NM.KeyfileHandlerFlags.NONE, kf_handler_write, 0)
    except Exception as e:
        print("Write failed: %r" % (e))
        raise

    [kf_string,_] = keyfile.to_data()
    print("")
    print(f" *** Keyfile:\n{kf_string}")

Or have a look at https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/blob/main/examples/python/gi/nm-keyfile.py

@richm
Copy link
Contributor Author

richm commented Oct 17, 2025

[citest]

@richm
Copy link
Contributor Author

richm commented Oct 17, 2025

[citest]

@richm
Copy link
Contributor Author

richm commented Oct 18, 2025

[citest]

Feature: Support running the timesync role during container builds.

Reason: This is particularly useful for building bootc derivative OSes.

Result: These flags enable running the bootc container scenarios in CI,
which ensures that the role works in buildah build environment. This
allows us to officially support this role for image mode builds.

Signed-off-by: Rich Megginson <rmeggins@redhat.com>
@richm
Copy link
Contributor Author

richm commented Oct 20, 2025

[citest]

1 similar comment
@richm
Copy link
Contributor Author

richm commented Oct 20, 2025

[citest]

@stale
Copy link

stale bot commented Dec 18, 2025

Thank you for your contribution! There was no activity in this pull request recently. To avoid pull requests to pile up, an automated process marked this pull request as stale. It will close this pull request if no further activity occurs. The current policy is available at: https://github.com//linux-system-roles/network/blob/main/.github/stale.yml

@stale stale bot added the stale label Dec 18, 2025
@richm richm removed the stale label Dec 18, 2025
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.

3 participants