Skip to content

Conversation

@martinpitt
Copy link
Contributor

@martinpitt martinpitt commented May 14, 2025

Prerequisite for adding a backend with firewall-offline-cmd or XML editing. I'll experiment with that next. But I first want a complete test run to ensure I didn't break anything.

Summary by Sourcery

Introduce an OnlineAPIBackend to centralize all firewall operations via the FirewallClient API, streamline the module’s entry point, and drop legacy offline code paths.

Enhancements:

  • Centralize firewall operations in a new OnlineAPIBackend class using the FirewallClient API
  • Simplify main() to instantiate the backend and delegate all configuration methods, removing manual branching and version checks
  • Eliminate obsolete offline code and helper functions (e.g., handle_interface_permanent, offline version parsing)

Tests:

  • Remove legacy offline-specific unit tests and adapt the test suite to the new backend abstraction

@sourcery-ai
Copy link

sourcery-ai bot commented May 14, 2025

Reviewer's Guide

This PR refactors firewall_lib by introducing a unified OnlineAPIBackend for all FirewallClient operations, removing legacy offline support and related tests, and simplifying main() to delegate work to the new backend.

File-Level Changes

Change Details Files
Introduce OnlineAPIBackend to centralize all firewall operations
  • Implemented OnlineAPIBackend class with set_* methods for services, ipsets, ports, rules, etc.
  • Initialized FirewallClient in backend, set exception handler, tracked changed and reload flags
  • Added finalize() to apply settings and reload firewalld
library/firewall_lib.py
Remove legacy offline support code paths
  • Deleted handle_interface_permanent and set_the_default_zone helpers
  • Removed fw_offline detection, offline client imports and branches
  • Eliminated outdated offline-only version checks
library/firewall_lib.py
Simplify main() by delegating to OnlineAPIBackend
  • Replaced inline runtime/permanent logic with backend instantiation and method calls
  • Consolidated version checking into a single pre-run step
  • Removed manual changed/need_reload tracking in main()
library/firewall_lib.py
Reposition and retain utility definitions
  • Moved PCI_REGEX and lsr_parse_version below backend class
  • Ensured version parsing remains available for pre-run checks
library/firewall_lib.py
Clean up obsolete unit tests related to offline mode
  • Removed tests for handle_interface_permanent and offline version scenarios
  • Deleted offline-mode test blocks in test_firewall_lib.py
tests/unit/test_firewall_lib.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

@martinpitt
Copy link
Contributor Author

[citest]

@richm
Copy link
Contributor

richm commented May 14, 2025

lgtm

@martinpitt martinpitt force-pushed the backend-abstraction branch from 6ccefd5 to e5918cb Compare May 14, 2025 19:37
@martinpitt
Copy link
Contributor Author

I poured a bucket of black paint over this and fixed a typo, now it should pass the unit tests. Otherwise this was fairly green.

@codecov
Copy link

codecov bot commented May 14, 2025

Codecov Report

Attention: Patch coverage is 72.18935% with 141 lines in your changes missing coverage. Please review.

Project coverage is 61.16%. Comparing base (2d7c4ba) to head (7565dc4).
Report is 74 commits behind head on main.

Files with missing lines Patch % Lines
library/firewall_lib.py 72.18% 141 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #271      +/-   ##
==========================================
+ Coverage   61.09%   61.16%   +0.06%     
==========================================
  Files           2        2              
  Lines         910      927      +17     
==========================================
+ Hits          556      567      +11     
- Misses        354      360       +6     
Flag Coverage Δ
sanity ?

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ 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.

@martinpitt martinpitt marked this pull request as ready for review May 14, 2025 19:40
@martinpitt
Copy link
Contributor Author

[citest]

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 @martinpitt - I've reviewed your changes - here's some feedback:

  • The OnlineAPIBackend class is very large and handles many responsibilities—consider splitting out per-resource handlers or moving chunks into helper modules to improve maintainability.
  • You’ve duplicated definitions of PCI_REGEX and lsr_parse_version; consolidate those into a single definition at the top to avoid confusion.
  • You removed the offline tests but haven’t added coverage for the new backend methods—add focused unit tests for key OnlineAPIBackend.set_* methods (including check-mode vs normal mode) to validate the abstraction.
Here's what I looked at during the review
  • 🟡 General issues: 2 issues found
  • 🟢 Review instructions: all looks good
  • 🟢 Testing: all looks good
  • 🟢 Documentation: all looks good

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.

self.need_reload = True

def set_zone(self):
if self.state == "present" and not self.zone_exists:
Copy link

Choose a reason for hiding this comment

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

issue (bug_risk): Missing initialization of fw_zone and fw_settings after creating a zone

Initialize self.fw_zone and self.fw_settings after addZone(): fetch the new zone via config().getZoneByName() and call .getSettings() to avoid None errors.

fw_service_settings.addSourcePort(_port, _protocol)
self.changed = True
for _module in helper_module:
if fw_service_settings.queryModule(_module):
Copy link

Choose a reason for hiding this comment

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

issue (bug_risk): Incorrect condition for adding helper modules

Flip the condition to if not fw_service_settings.queryModule(_module): so modules are added only when they don’t already exist.

Comment on lines +487 to +501
if destination_ipv4:
if not fw_service_settings.queryDestination(
"ipv4", destination_ipv4
):
if not self.module.check_mode:
fw_service_settings.setDestination("ipv4", destination_ipv4)
self.changed = True
Copy link

Choose a reason for hiding this comment

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

suggestion (code-quality): Merge nested if conditions (merge-nested-ifs)

Suggested change
if destination_ipv4:
if not fw_service_settings.queryDestination(
"ipv4", destination_ipv4
):
if not self.module.check_mode:
fw_service_settings.setDestination("ipv4", destination_ipv4)
self.changed = True
if destination_ipv4 and not fw_service_settings.queryDestination(
"ipv4", destination_ipv4
):
if not self.module.check_mode:
fw_service_settings.setDestination("ipv4", destination_ipv4)
self.changed = True


ExplanationToo much nesting can make code difficult to understand, and this is especially
true in Python, where there are no brackets to help out with the delineation of
different nesting levels.

Reading deeply nested code is confusing, since you have to keep track of which
conditions relate to which levels. We therefore strive to reduce nesting where
possible, and the situation where two if conditions can be combined using
and is an easy win.

Comment on lines +494 to +508
if destination_ipv6:
if not fw_service_settings.queryDestination(
"ipv6", destination_ipv6
):
if not self.module.check_mode:
fw_service_settings.setDestination("ipv6", destination_ipv6)
self.changed = True
Copy link

Choose a reason for hiding this comment

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

suggestion (code-quality): Merge nested if conditions (merge-nested-ifs)

Suggested change
if destination_ipv6:
if not fw_service_settings.queryDestination(
"ipv6", destination_ipv6
):
if not self.module.check_mode:
fw_service_settings.setDestination("ipv6", destination_ipv6)
self.changed = True
if destination_ipv6 and not fw_service_settings.queryDestination(
"ipv6", destination_ipv6
):
if not self.module.check_mode:
fw_service_settings.setDestination("ipv6", destination_ipv6)
self.changed = True


ExplanationToo much nesting can make code difficult to understand, and this is especially
true in Python, where there are no brackets to help out with the delineation of
different nesting levels.

Reading deeply nested code is confusing, since you have to keep track of which
conditions relate to which levels. We therefore strive to reduce nesting where
possible, and the situation where two if conditions can be combined using
and is an easy win.

Comment on lines +531 to +545
if destination_ipv4:
if fw_service_settings.queryDestination("ipv4", destination_ipv4):
if not self.module.check_mode:
fw_service_settings.removeDestination(
"ipv4", destination_ipv4
)
self.changed = True
Copy link

Choose a reason for hiding this comment

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

suggestion (code-quality): Merge nested if conditions (merge-nested-ifs)

Suggested change
if destination_ipv4:
if fw_service_settings.queryDestination("ipv4", destination_ipv4):
if not self.module.check_mode:
fw_service_settings.removeDestination(
"ipv4", destination_ipv4
)
self.changed = True
if destination_ipv4 and fw_service_settings.queryDestination("ipv4", destination_ipv4):
if not self.module.check_mode:
fw_service_settings.removeDestination(
"ipv4", destination_ipv4
)
self.changed = True


ExplanationToo much nesting can make code difficult to understand, and this is especially
true in Python, where there are no brackets to help out with the delineation of
different nesting levels.

Reading deeply nested code is confusing, since you have to keep track of which
conditions relate to which levels. We therefore strive to reduce nesting where
possible, and the situation where two if conditions can be combined using
and is an easy win.

err_str = "Permanent"

if not zone_exists and not zone_operation:
module.fail_json(msg="%s zone '%s' does not exist." % (err_str, zone))
Copy link

Choose a reason for hiding this comment

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

suggestion (code-quality): Replace interpolated string formatting with f-string (replace-interpolation-with-fstring)

Suggested change
module.fail_json(msg="%s zone '%s' does not exist." % (err_str, zone))
module.fail_json(msg=f"{err_str} zone '{zone}' does not exist.")

self.fw.setDefaultZone(zone)
self.changed = True

def set_service(
Copy link

Choose a reason for hiding this comment

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

issue (code-quality): We've found these issues:


Explanation

The quality score for this function is below the quality threshold of 25%.
This score is a combination of the method length, cognitive complexity and working memory.

How can you solve this?

It might be worth refactoring this function to make it shorter and more readable.

  • Reduce the function length by extracting pieces of functionality out into
    their own functions. This is the most important thing you can do - ideally a
    function should be less than 10 lines.
  • Reduce nesting, perhaps by introducing guard clauses to return early.
  • Ensure that variables are tightly scoped, so that code using related concepts
    sits together within the function rather than being scattered.

else:
self.module.fail_json(msg="INVALID SERVICE - " + item)

def set_ipset(self, ipset, description, short, ipset_type, ipset_entries):
Copy link

Choose a reason for hiding this comment

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

issue (code-quality): We've found these issues:


Explanation

The quality score for this function is below the quality threshold of 25%.
This score is a combination of the method length, cognitive complexity and working memory.

How can you solve this?

It might be worth refactoring this function to make it shorter and more readable.

  • Reduce the function length by extracting pieces of functionality out into
    their own functions. This is the most important thing you can do - ideally a
    function should be less than 10 lines.
  • Reduce nesting, perhaps by introducing guard clauses to return early.
  • Ensure that variables are tightly scoped, so that code using related concepts
    sits together within the function rather than being scattered.

Comment on lines +823 to +844
"%s does not exist - ensure it is defined in a previous task before running play outside check mode"
% item
Copy link

Choose a reason for hiding this comment

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

suggestion (code-quality): We've found these issues:

Suggested change
"%s does not exist - ensure it is defined in a previous task before running play outside check mode"
% item
f"{item} does not exist - ensure it is defined in a previous task before running play outside check mode"


Explanation
The quality score for this function is below the quality threshold of 25%.
This score is a combination of the method length, cognitive complexity and working memory.

How can you solve this?

It might be worth refactoring this function to make it shorter and more readable.

  • Reduce the function length by extracting pieces of functionality out into
    their own functions. This is the most important thing you can do - ideally a
    function should be less than 10 lines.
  • Reduce nesting, perhaps by introducing guard clauses to return early.
  • Ensure that variables are tightly scoped, so that code using related concepts
    sits together within the function rather than being scattered.

self.fw_settings.removeSource(item)
self.changed = True

def set_interface(self, interface):
Copy link

Choose a reason for hiding this comment

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

issue (code-quality): Low code quality found in OnlineAPIBackend.set_interface - 15% (low-code-quality)


ExplanationThe quality score for this function is below the quality threshold of 25%.
This score is a combination of the method length, cognitive complexity and working memory.

How can you solve this?

It might be worth refactoring this function to make it shorter and more readable.

  • Reduce the function length by extracting pieces of functionality out into
    their own functions. This is the most important thing you can do - ideally a
    function should be less than 10 lines.
  • Reduce nesting, perhaps by introducing guard clauses to return early.
  • Ensure that variables are tightly scoped, so that code using related concepts
    sits together within the function rather than being scattered.

@martinpitt
Copy link
Contributor Author

Yes yes sourcery.ai, I know the code isn't good. There are lots of issues, but I don't want to change the logic here. We can't use f-strings yet, this has to work on RHEL 7.

You’ve duplicated definitions of PCI_REGEX and lsr_parse_version

Erm, no, I didn't.

@richm
Copy link
Contributor

richm commented May 14, 2025

Yes yes sourcery.ai, I know the code isn't good. There are lots of issues, but I don't want to change the logic here. We can't use f-strings yet, this has to work on RHEL 7.

You’ve duplicated definitions of PCI_REGEX and lsr_parse_version

Erm, no, I didn't.

Yeah, just downvote or delete those annoying comments.

This code is very problematic:

 * It has not worked for years, at least not since RHEL 9 (probably
   earlier). If firewalld isn't running, this code already crashes in
   the `FirewallClient()` instantiation.

 * There haven't been any integration tests that cover this. The unit
   tests don't cover this as they mock the entire firewalld API, i.e.
   they test an API that simply does not exist.

 * `firewall.core.fw` is not supported API, but the internal CLI
   implementation. That can change at any time (and apparently did,
   assuming that the old code worked at *some* point). There is no
   offline API [1].

 * Even in the current state the implementation was very incomplete.
   None of the service/ipset/zone/etc. settings are implemented, only some
   parts of zone handling (and not even that, e.g. setting the default zone is
   missing as well).

Trying to implement the missing bits with the internal API would be a major
piece of work and go in the wrong direction. Let's rather use the official
methods (`firewall-offline-cmd` or XML editing).

To spare the next person from falling into the "oh, offline is supported"
pitfall, delete all the offline code.

[1] https://issues.redhat.com/browse/RHEL-88425?focusedId=27062772&page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel#comment-27062772
@martinpitt martinpitt force-pushed the backend-abstraction branch from e5918cb to cea4a88 Compare May 16, 2025 06:00
@martinpitt
Copy link
Contributor Author

I am getting quite far in my bootc-container-test branch which implements an offline backend, and now gained enough faith in this structure.

However, there are two global functions create_{service,ipset} which ought to move into the OnlineAPI class. Let's clean that up, pushed. I also rebased, so the interdiff above now also has #270 included (and that's good for making sure the merge works).

@martinpitt martinpitt force-pushed the backend-abstraction branch from cea4a88 to a74b4f9 Compare May 19, 2025 04:23
@martinpitt
Copy link
Contributor Author

Fixed an asymmetry, this makes the offline implementation a bit simpler. It is a no-op (by happenstance and a code smell) with the old code.

Introduce a backend class into firewall_lib, and move all `FirewallClient`
operations into it. This gets rid of some global variables, separates the
Ansible/parameter parsing from the firewalld operations, shrinks the massive
main() function, and allows us to add another implementation for offline mode
in the future (based on `firewalld-offline-cmd` or XML editing).

This does not change the logic or code much, other than adding a
truckload of `self.` prefixes and some `black` reformatting. There is
still way too much global state, which makes the code hard to
understand, but let's clean this up in steps.

The only other changes aside from moving are the elimination of
`handle_interface_permanent()` and `set_the_default_zone()` global functions.
They are FirewallClient API specific, only get called once (the former with two
unused parameters), and are simple enough to just get inlined.
@martinpitt martinpitt force-pushed the backend-abstraction branch from a74b4f9 to 7565dc4 Compare May 19, 2025 11:45
@martinpitt
Copy link
Contributor Author

[citest]

@martinpitt martinpitt merged commit 24620c0 into linux-system-roles:main May 19, 2025
35 checks passed
@martinpitt martinpitt deleted the backend-abstraction branch May 19, 2025 20:37
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.

2 participants