Skip to content

fix: issues found via code review#170

Open
trgill wants to merge 5 commits intolinux-system-roles:mainfrom
trgill:misc-fixes
Open

fix: issues found via code review#170
trgill wants to merge 5 commits intolinux-system-roles:mainfrom
trgill:misc-fixes

Conversation

@trgill
Copy link
Copy Markdown
Collaborator

@trgill trgill commented May 5, 2026

Enhancement:

This PR addresses issues in the snapshot role. The fixes include:

  1. Tuple assignment bug - Fixed trailing commas that were creating tuples instead of strings for volume names
  2. Return statement syntax errors - Corrected return statements that were creating sets instead of dictionaries in check_mode paths
  3. Resource leak - Added proper file descriptor cleanup in the can_open() function
  4. Null pointer safety - Added null checks before calling .endswith() on potentially None values
  5. Typo corrections - Fixed user-facing error messages with spelling errors

Reason:

Code review, issues identified:

  • Data type errors: Volume names were being stored as tuples ('snapshot : vg/lv',) instead of strings, which could cause unexpected behavior in string operations and serialization
  • Return value errors: Check mode operations were returning sets {0, "message", False} instead of properly formatted dictionaries, causing the module to fail
  • Resource leaks: The find_unused_disk module was leaking file descriptors during disk scanning operations, potentially exhausting system resources on systems with many disks
  • Crash potential: Missing null checks could cause AttributeError exceptions when optional parameters are not provided
  • User experience: Typos in error messages ("emmpty", "snnapshot") reduce professionalism and clarity

Result:

All fixes maintain backward compatibility and do not change the external API or behavior of the role.

Issue Tracker Tickets (Jira or BZ if any):

Summary by Sourcery

Fix snapshot management role issues uncovered in review, correcting return structures, data types, error messages, and resource handling.

Bug Fixes:

  • Normalize check-mode return values to proper dictionaries instead of sets in snapshot manager commands.
  • Correct snapshot volume name handling to use strings instead of singleton tuples in LVM and validation helpers.
  • Prevent AttributeError by guarding snapshot name endswith checks when the parameter may be absent.
  • Fix user-facing error messages for snapshot operations, including typos and clearer text.
  • Eliminate a file descriptor leak in the test helper by closing device handles after exclusive open attempts.

Enhancements:

  • Initialize snapshot command return codes consistently before validation in snapshot manager logic.

Fixed volume["name"] assignments in lvm.py and validate.py where a
trailing comma was creating a tuple ('snapshot : vg/lv',) instead of
a string "snapshot : vg/lv".

Signed-off-by: Todd Gill <tgill@redhat.com>
@trgill trgill requested review from richm and spetrosi as code owners May 5, 2026 19:52
@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented May 5, 2026

Reviewer's guide (collapsed on small PRs)

Reviewer's Guide

Fixes several correctness and robustness issues in the snapshot role, including data type bugs, check-mode return value structure, resource cleanup, null safety, and typo corrections, while preserving external behavior.

Flow diagram for get_json_from_args null-safe snapshot detection

flowchart TD
  A[Start get_json_from_args] --> B[Iterate_volume_groups]
  B --> C[Iterate_logical_volumes]
  C --> D{snapshot_lvm_snapset_name_not_none}
  D -->|No| H[Skip_snapshot_name_suffix_check]
  D -->|Yes| E{lv_name_endswith_snapshot_lvm_snapset_name}
  E -->|Yes| F[Log_already_a_snapshot_for_lv]
  F --> C
  E -->|No| G[Validate_lv_and_vg]
  G --> I{Validation_passed}
  I -->|No| J[Log_and_continue_to_next_lv]
  J --> C
  I -->|Yes| K[Create_volume_dict]
  K --> L[Set_volume_name_as_string_snapshot_colon_vg_slash_lv]
  L --> M[Set_volume_vg]
  M --> N[Set_volume_lv]
  N --> C
  H --> G
  C --> O[End_when_all_lvs_processed]
Loading

File-Level Changes

Change Details Files
Normalize check-mode return structures to proper dictionaries and initialize return code in snapshot manager commands.
  • Initialize rc with SnapshotStatus.SNAPSHOT_OK before validation in mgr_snapshot_cmd
  • Replace set-style check_mode returns in mgr_snapshot_cmd, mgr_extend_cmd, and mgr_revert_cmd with dictionaries containing return_code, errors, and changed keys
  • Ensure check-mode branches include changed=False for consistency
  • Fix message construction for resize/revert operations to use proper string concatenation and joining
module_utils/snapshot_lsr/snapmgr.py
Correct snapshot volume naming types and add null-safe checks around snapshot name matching in LVM helpers.
  • Guard calls to .endswith() with a truthiness check on module_args['snapshot_lvm_snapset_name'] to avoid None dereference
  • Change volume['name'] from a single-element tuple to a plain string when constructing snapshot volume descriptors in get_json_from_args
module_utils/snapshot_lsr/lvm.py
module_utils/snapshot_lsr/validate.py
Tighten robustness and polish by avoiding file descriptor leaks and fixing user-facing typos.
  • Capture file descriptor from os.open() in can_open(), then close it to prevent leaks
  • Correct spelling in error messages from 'snnapshot'/'emmpty' to 'snapshot'/'empty' for mount/umount and extend paths
module_utils/snapshot_lsr/snapmgr.py
tests/library/find_unused_disk.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

logger.info("mgr_snapshot_cmd: %s", snapset_name)
changed = False
message = ""
rc = SnapshotStatus.SNAPSHOT_OK
Copy link
Copy Markdown

@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:

  • In the updated .endswith() guards, consider using module_args.get("snapshot_lvm_snapset_name") is not None (or similar) instead of a generic truthiness check so that an empty string doesn’t silently skip matches and to avoid KeyError if the key is missing.
  • In the check-mode return paths (e.g., mgr_snapshot_cmd, mgr_extend_cmd, mgr_revert_cmd), you’re using the "errors" field to carry informational messages; if these are not true errors, you may want to introduce a separate field (like "message") to distinguish normal check-mode output from actual error conditions.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In the updated `.endswith()` guards, consider using `module_args.get("snapshot_lvm_snapset_name") is not None` (or similar) instead of a generic truthiness check so that an empty string doesn’t silently skip matches and to avoid `KeyError` if the key is missing.
- In the check-mode return paths (e.g., `mgr_snapshot_cmd`, `mgr_extend_cmd`, `mgr_revert_cmd`), you’re using the `"errors"` field to carry informational messages; if these are not true errors, you may want to introduce a separate field (like `"message"`) to distinguish normal check-mode output from actual error conditions.

## Individual Comments

### Comment 1
<location path="module_utils/snapshot_lsr/lvm.py" line_range="99" />
<code_context>
         for lv in lv_list:

-            if lv["lv_name"].endswith(module_args["snapshot_lvm_snapset_name"]):
+            if module_args["snapshot_lvm_snapset_name"] and lv["lv_name"].endswith(module_args["snapshot_lvm_snapset_name"]):
                 logger.info(
                     "get_json_from_args: already a snapshot for %s", lv["lv_name"]
</code_context>
<issue_to_address>
**question (bug_risk):** Guarding by truthiness may change behavior when `snapshot_lvm_snapset_name` is an empty string

Previously, an empty `snapshot_lvm_snapset_name` caused `lv["lv_name"].endswith(...)` to match all LVs; now the condition is skipped in that case. If an empty string is supposed to mean “match everything,” this changes behavior. If you only want to check when a non-empty name is given, this is fine; otherwise, guard explicitly (e.g., `is not None`) or use a sentinel default to distinguish “no filter” from “empty string.”
</issue_to_address>

### Comment 2
<location path="module_utils/snapshot_lsr/validate.py" line_range="49" />
<code_context>
         for lv in lv_list:

-            if lv["lv_name"].endswith(module_args["snapshot_lvm_snapset_name"]):
+            if module_args["snapshot_lvm_snapset_name"] and lv["lv_name"].endswith(module_args["snapshot_lvm_snapset_name"]):
                 logger.info(
                     "get_json_from_args: already a snapshot for %s", lv["lv_name"]
</code_context>
<issue_to_address>
**question:** Validate that the new truthiness guard matches the intended semantics for `snapshot_lvm_snapset_name`

Because this now short-circuits on generic truthiness, any falsy value (like an empty string) will skip the `endswith` check. Please confirm that’s the desired behavior for “unset” names; if not, narrow the guard to only the specific values you consider as not configured (e.g. explicitly checking for `None`).
</issue_to_address>

### Comment 3
<location path="tests/library/find_unused_disk.py" line_range="120-121" />
<code_context>
     """Return true if the device can be opened with exclusive access."""
     try:
-        os.open(disk_path, os.O_EXCL)
+        fd = os.open(disk_path, os.O_EXCL)
+        os.close(fd)
         return True
     except OSError:
</code_context>
<issue_to_address>
**suggestion (testing):** Consider adding tests for `can_open` to verify file descriptor cleanup and error handling.

The fix removes the leak, but there are still no tests covering `can_open`. Please add tests that:

1. Verify it returns `True` when `os.open` succeeds and that `os.open`/`os.close` are called as expected (no extra FDs left open).
2. Verify it returns `False` when `os.open` raises `OSError`.

This will protect against regressions in both behavior and resource handling.

Suggested implementation:

```python
def can_open(disk_path):
    """Return true if the device can be opened with exclusive access."""
    try:
        fd = os.open(disk_path, os.O_EXCL)
        os.close(fd)
        return True
    except OSError:
        return False


def test_can_open_returns_true_and_closes_fd(monkeypatch):
    """can_open returns True and closes the file descriptor when os.open succeeds."""
    calls = {"open": [], "close": []}

    def fake_open(path, flags):
        calls["open"].append((path, flags))
        return 42

    def fake_close(fd):
        calls["close"].append(fd)

    monkeypatch.setattr(os, "open", fake_open)
    monkeypatch.setattr(os, "close", fake_close)

    result = can_open("/dev/fake-disk")

    # Behavior: returns True on success
    assert result is True
    # Resource handling: exactly one open and one close with the correct FD
    assert calls["open"] == [("/dev/fake-disk", os.O_EXCL)]
    assert calls["close"] == [42]


def test_can_open_returns_false_on_oserror(monkeypatch):
    """can_open returns False and does not close any FD when os.open raises OSError."""
    def fake_open(path, flags):
        raise OSError("cannot open")

    close_calls = []

    def fake_close(fd):
        close_calls.append(fd)

    monkeypatch.setattr(os, "open", fake_open)
    monkeypatch.setattr(os, "close", fake_close)

    result = can_open("/dev/fake-disk")

    # Behavior: returns False on OSError
    assert result is False
    # Resource handling: os.close must not be called when open fails
    assert close_calls == []

```

- Ensure `os` is imported at the top of `tests/library/find_unused_disk.py` (if it is not already): `import os`.
- These tests assume pytest is used and that the `monkeypatch` fixture is available in your test configuration.
</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 thread module_utils/snapshot_lsr/lvm.py Outdated
for lv in lv_list:

if lv["lv_name"].endswith(module_args["snapshot_lvm_snapset_name"]):
if module_args["snapshot_lvm_snapset_name"] and lv["lv_name"].endswith(module_args["snapshot_lvm_snapset_name"]):
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

question (bug_risk): Guarding by truthiness may change behavior when snapshot_lvm_snapset_name is an empty string

Previously, an empty snapshot_lvm_snapset_name caused lv["lv_name"].endswith(...) to match all LVs; now the condition is skipped in that case. If an empty string is supposed to mean “match everything,” this changes behavior. If you only want to check when a non-empty name is given, this is fine; otherwise, guard explicitly (e.g., is not None) or use a sentinel default to distinguish “no filter” from “empty string.”

Comment thread module_utils/snapshot_lsr/validate.py Outdated
for lv in lv_list:

if lv["lv_name"].endswith(module_args["snapshot_lvm_snapset_name"]):
if module_args["snapshot_lvm_snapset_name"] and lv["lv_name"].endswith(module_args["snapshot_lvm_snapset_name"]):
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

question: Validate that the new truthiness guard matches the intended semantics for snapshot_lvm_snapset_name

Because this now short-circuits on generic truthiness, any falsy value (like an empty string) will skip the endswith check. Please confirm that’s the desired behavior for “unset” names; if not, narrow the guard to only the specific values you consider as not configured (e.g. explicitly checking for None).

Comment on lines +120 to +121
fd = os.open(disk_path, os.O_EXCL)
os.close(fd)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion (testing): Consider adding tests for can_open to verify file descriptor cleanup and error handling.

The fix removes the leak, but there are still no tests covering can_open. Please add tests that:

  1. Verify it returns True when os.open succeeds and that os.open/os.close are called as expected (no extra FDs left open).
  2. Verify it returns False when os.open raises OSError.

This will protect against regressions in both behavior and resource handling.

Suggested implementation:

def can_open(disk_path):
    """Return true if the device can be opened with exclusive access."""
    try:
        fd = os.open(disk_path, os.O_EXCL)
        os.close(fd)
        return True
    except OSError:
        return False


def test_can_open_returns_true_and_closes_fd(monkeypatch):
    """can_open returns True and closes the file descriptor when os.open succeeds."""
    calls = {"open": [], "close": []}

    def fake_open(path, flags):
        calls["open"].append((path, flags))
        return 42

    def fake_close(fd):
        calls["close"].append(fd)

    monkeypatch.setattr(os, "open", fake_open)
    monkeypatch.setattr(os, "close", fake_close)

    result = can_open("/dev/fake-disk")

    # Behavior: returns True on success
    assert result is True
    # Resource handling: exactly one open and one close with the correct FD
    assert calls["open"] == [("/dev/fake-disk", os.O_EXCL)]
    assert calls["close"] == [42]


def test_can_open_returns_false_on_oserror(monkeypatch):
    """can_open returns False and does not close any FD when os.open raises OSError."""
    def fake_open(path, flags):
        raise OSError("cannot open")

    close_calls = []

    def fake_close(fd):
        close_calls.append(fd)

    monkeypatch.setattr(os, "open", fake_open)
    monkeypatch.setattr(os, "close", fake_close)

    result = can_open("/dev/fake-disk")

    # Behavior: returns False on OSError
    assert result is False
    # Resource handling: os.close must not be called when open fails
    assert close_calls == []
  • Ensure os is imported at the top of tests/library/find_unused_disk.py (if it is not already): import os.
  • These tests assume pytest is used and that the monkeypatch fixture is available in your test configuration.

trgill added 4 commits May 5, 2026 15:55
Fixed three locations where return statements were creating sets instead
of dicts due to missing key-value syntax. All check_mode returns now
properly return {"return_code": rc, "errors": message, "changed": False}.

Also initialized rc variable at the start of mgr_snapshot_cmd to ensure
it's defined for all code paths.

Signed-off-by: Todd Gill <tgill@redhat.com>
The can_open() function was opening file descriptors with os.open()
but never closing them, causing a resource leak. Now properly closes
the file descriptor after verifying exclusive access is possible.

Signed-off-by: Todd Gill <tgill@redhat.com>
Added null checks in lvm.py and validate.py to prevent AttributeError
when snapshot_lvm_snapset_name is None. The code now verifies the value
exists before calling .endswith() on it.

Signed-off-by: Todd Gill <tgill@redhat.com>
Fixed three typos in snapmgr.py error messages:
- "emmpty" → "empty"
- "snnapshot" → "snapshot" (2 occurrences)

Also improved formatting by adding space after colon in error message.

Signed-off-by: Todd Gill <tgill@redhat.com>
@trgill trgill changed the title Fix issues found via code review fix: issues found via code review May 5, 2026
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