Skip to content

feat: Write roles fingerprints to /var/log/sysroles.jsonl [citest_skip] - #325

Merged
richm merged 2 commits into
mainfrom
fingerprint-write-to-file
Aug 6, 2026
Merged

feat: Write roles fingerprints to /var/log/sysroles.jsonl [citest_skip]#325
richm merged 2 commits into
mainfrom
fingerprint-write-to-file

Conversation

@spetrosi

@spetrosi spetrosi commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

Feature: Write roles fingerprints to /var/log/sysroles.jsonl [citest_skip]

Reason: By default logs are printed to rsyslog. This change adds a possibility to write logs to a file on the system for the downstream users.

Result: For the upstream, this makes rsyslog log message more detailed. For the downstream - also writes logs to /var/log/sysroles.jsonl

@spetrosi spetrosi self-assigned this Aug 6, 2026
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Ignore keyword(s) in the title.

⛔ Ignored keywords (1)
  • [citest_skip]

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: cd758b28-b39a-4618-9283-f77439bd8f7e

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Changes

The module now collects structured role fingerprints with host and distribution metadata. It supports deterministic syslog output, optional JSONL logging, file-size trimming, locking, validation, and check-mode reporting.

Role fingerprint logging

Layer / File(s) Summary
Fingerprint contract and execution flow
library/sr_fingerprint.py, tests/unit/test_sr_fingerprint.py
The module defines structured arguments, collects fingerprint metadata, formats syslog and JSONL records, and returns structured check-mode results. Tests cover field handling, formatting, validation, errors, and timestamps.
JSONL persistence and retention
library/sr_fingerprint.py, tests/unit/test_sr_fingerprint.py
The module creates parent directories, appends JSONL records with exclusive locking, preserves value types, and removes oldest records when the size limit is exceeded. Tests cover append, directory creation, and trimming behavior.
🚥 Pre-merge checks | ✅ 5 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description Format ⚠️ Warning The PR description includes Reason, Result, and Signed-off-by with an email, but it lacks the required Enhancement: or Feature: section. Add an Enhancement: or Feature: section that describes the change, while keeping the existing required sections.
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title follows Conventional Commits format and accurately describes the fingerprint JSONL logging change.
Description check ✅ Passed The description covers the feature, reason, and result, although it uses Feature instead of the template's Enhancement heading.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (4)
tests/unit/test_sr_fingerprint.py (4)

328-350: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Strengthen the _handle_fingerprint coverage.

Two gaps exist in this group of tests.

First, test_handle_fingerprint_check_mode_with_log_file does not assert that check mode writes nothing. That is the core guarantee of the check-mode path, so assert that log_path does not exist after the call.

Second, no test covers the normal execution path. _FakeModule.logged at Line 36 collects the syslog output, but no test reads it. The syslog record is the primary output of this module. Add a test that runs _handle_fingerprint with check_mode=False and write_log_file=False, then asserts the logged line equals _format_fingerprint_syslog(...) of the returned fingerprint.

💚 Proposed additions
         parsed = json.loads(result["jsonl_row"])
         self.assertEqual(parsed["role_name"], "systemd")
+        self.assertFalse(os.path.exists(log_path))
+
+    def test_handle_fingerprint_logs_syslog_line(self):
+        module = _FakeModule(
+            {
+                "status": "success",
+                "write_log_file": False,
+                "max_log_size": 2000000,
+                "role_name": "systemd",
+                "role_path": "/usr/share/ansible/roles/linux-system-roles.systemd",
+                "ansible_play_hosts_all": ["host1", "host2"],
+                "distribution": "RedHat",
+                "distribution_version": "9.4",
+            },
+            check_mode=False,
+        )
+        with self.assertRaises(_ExitJsonException) as ctx:
+            sr_fingerprint._handle_fingerprint(module)
+        result = ctx.exception.kwargs
+        self.assertEqual(len(module.logged), 1)
+        self.assertEqual(
+            module.logged[0],
+            sr_fingerprint._format_fingerprint_syslog(result["fingerprint"]),
+        )
+        self.assertIn("status=success", module.logged[0])
+        self.assertIn("play_hosts_number=2", module.logged[0])
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/test_sr_fingerprint.py` around lines 328 - 350, Strengthen the
_handle_fingerprint tests by asserting
test_handle_fingerprint_check_mode_with_log_file leaves log_path nonexistent
after execution. Add a normal-mode test using check_mode=False and
write_log_file=False, then verify _FakeModule.logged contains a line equal to
_format_fingerprint_syslog(...) for the returned fingerprint.

172-177: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use shutil.rmtree so cleanup cannot mask a test failure.

If _write_jsonl_log raises before it creates subdir, os.listdir(subdir) raises FileNotFoundError inside the finally block. That second exception replaces the original failure, so the report hides the real cause. shutil.rmtree handles the nested tree in one call and tolerates a partially created directory.

♻️ Proposed cleanup fix
         finally:
-            subdir = os.path.dirname(log_file)
-            for name in os.listdir(subdir):
-                os.unlink(os.path.join(subdir, name))
-            os.rmdir(subdir)
-            os.rmdir(tmpdir)
+            shutil.rmtree(tmpdir, ignore_errors=True)

Add the import:

 import re
+import shutil
 import tempfile
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/test_sr_fingerprint.py` around lines 172 - 177, Replace the manual
cleanup loop and separate os.rmdir calls in the finally block of the test with
shutil.rmtree on tmpdir, and add the required shutil import. Ensure cleanup
tolerates a missing or partially created subdir without masking the original
test failure.

133-135: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Run Black on the wrapped assignments.

Lines 133-135 wrap a single string literal in redundant parentheses. Black removes such parentheses when the joined line fits in the line limit. The same pattern appears at Lines 206-208 and Lines 246-248. Run the formatter so CI does not reject the diff.

As per path instructions: "Must follow PEP 8 and be formatted with Python Black" and "Run tox -e black,flake8 before committing".

#!/bin/bash
# Description: Report Black formatting differences for the changed Python files.
set -euo pipefail

pip install --quiet black >/dev/null 2>&1 || true
black --check --diff library/sr_fingerprint.py tests/unit/test_sr_fingerprint.py || true
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/test_sr_fingerprint.py` around lines 133 - 135, Run Black on
tests/unit/test_sr_fingerprint.py and apply its formatting to the wrapped
assignments, including the assignments around record["role_path"] and the
matching occurrences near the other noted locations. Remove redundant
parentheses where the string literal fits on one line, then ensure the file
passes Black and flake8.

Source: Path instructions


131-140: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for the quote and equals-sign escaping branch.

test_format_fingerprint_syslog_quotes_values_with_spaces covers only the space case. The text.replace('"', '""') branch in _format_fingerprint_key_value stays untested, as does the = trigger. Downstream parsers depend on that escaping contract, so pin it.

💚 Proposed additional tests
+    def test_format_fingerprint_syslog_escapes_quotes(self):
+        record = _sample_fingerprint_record()
+        record["role_path"] = '/tmp/we"ird'
+        message = sr_fingerprint._format_fingerprint_syslog(record)
+        self.assertIn('role_path="/tmp/we""ird"', message)
+
+    def test_format_fingerprint_syslog_quotes_values_with_equals(self):
+        record = _sample_fingerprint_record()
+        record["role_path"] = "/tmp/a=b"
+        message = sr_fingerprint._format_fingerprint_syslog(record)
+        self.assertIn('role_path="/tmp/a=b"', message)
+
+    def test_get_managed_node_distro_partial(self):
+        self.assertEqual(sr_fingerprint._get_managed_node_distro("Fedora", ""), "unknown")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/test_sr_fingerprint.py` around lines 131 - 140, Extend
test_format_fingerprint_syslog_quotes_values_with_spaces to include a
fingerprint value containing a double quote and an equals sign, then assert the
formatted output uses the expected doubled-quote escaping and quoted
representation. Ensure the test exercises both the text.replace('"', '""')
branch and the equals-sign trigger in _format_fingerprint_key_value.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@library/sr_fingerprint.py`:
- Around line 191-197: Update _trim_log_file and its caller to accept max_size,
and trim based on the current file size so existing oversized content is removed
until the remaining log plus the pending new line fits within max_size. Preserve
removal of oldest records and ensure the caller passes the configured limit
rather than only the new line’s size.

In `@tests/unit/test_sr_fingerprint.py`:
- Line 17: Update the import in test_sr_fingerprint.py and the corresponding
package layout so sr_fingerprint is imported through its package-qualified path
rather than as a bare top-level module. Ensure the package is discoverable
through the repository’s normal configuration without modifying PYTHONPATH or
injecting library/ into sys.path.

---

Nitpick comments:
In `@tests/unit/test_sr_fingerprint.py`:
- Around line 328-350: Strengthen the _handle_fingerprint tests by asserting
test_handle_fingerprint_check_mode_with_log_file leaves log_path nonexistent
after execution. Add a normal-mode test using check_mode=False and
write_log_file=False, then verify _FakeModule.logged contains a line equal to
_format_fingerprint_syslog(...) for the returned fingerprint.
- Around line 172-177: Replace the manual cleanup loop and separate os.rmdir
calls in the finally block of the test with shutil.rmtree on tmpdir, and add the
required shutil import. Ensure cleanup tolerates a missing or partially created
subdir without masking the original test failure.
- Around line 133-135: Run Black on tests/unit/test_sr_fingerprint.py and apply
its formatting to the wrapped assignments, including the assignments around
record["role_path"] and the matching occurrences near the other noted locations.
Remove redundant parentheses where the string literal fits on one line, then
ensure the file passes Black and flake8.
- Around line 131-140: Extend
test_format_fingerprint_syslog_quotes_values_with_spaces to include a
fingerprint value containing a double quote and an equals sign, then assert the
formatted output uses the expected doubled-quote escaping and quoted
representation. Ensure the test exercises both the text.replace('"', '""')
branch and the equals-sign trigger in _format_fingerprint_key_value.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: c663ac99-6fdf-43ee-8bf1-89a5765ac517

📥 Commits

Reviewing files that changed from the base of the PR and between caaa6ea and 083004c.

📒 Files selected for processing (2)
  • library/sr_fingerprint.py
  • tests/unit/test_sr_fingerprint.py

Comment thread library/sr_fingerprint.py
Comment on lines +191 to +197
def _trim_log_file(log_file, size_needed):
"""Remove oldest records until the file can accommodate size_needed bytes."""
with open(log_file, "r") as log_fd:
lines = log_fd.readlines()
size_removed = 0
while lines and size_removed < size_needed:
size_removed += len(lines.pop(0))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Trim frees only one line worth of bytes, so the file can stay above max_log_size.

_trim_log_file stops as soon as size_removed >= size_needed, where size_needed is the length of the single new line. The caller invokes it whenever cur_size + len(new_line) > max_size. If cur_size is already greater than max_size, removing only len(new_line) bytes leaves the file above the limit on every subsequent write. The file then never returns to the documented cap.

This happens when an operator lowers max_log_size between runs, or when the file grew under a previous larger limit. Pass max_size into the trim function and remove lines until the remaining content plus the new line fits.

🐛 Proposed fix to converge on the configured limit
-def _trim_log_file(log_file, size_needed):
-    """Remove oldest records until the file can accommodate size_needed bytes."""
+def _trim_log_file(log_file, size_needed, max_size=0):
+    """Remove oldest records until the file can accommodate size_needed bytes."""
     with open(log_file, "r") as log_fd:
         lines = log_fd.readlines()
-    size_removed = 0
-    while lines and size_removed < size_needed:
-        size_removed += len(lines.pop(0))
+    remaining = sum(len(line) for line in lines)
+    if max_size > 0:
+        target = max_size - size_needed
+    else:
+        target = remaining - size_needed
+    while lines and remaining > target:
+        remaining -= len(lines.pop(0))

Update the call site:

         if max_size > 0 and cur_size + len(new_line) > max_size and cur_size > 0:
-            _trim_log_file(log_file, len(new_line))
+            _trim_log_file(log_file, len(new_line), max_size=max_size)
🧰 Tools
🪛 ast-grep (0.45.0)

[warning] 192-192: File path is request-/variable-derived; validate and normalize to prevent path traversal.
Context: open(log_file, "r")
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').

(open-filename-from-request)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@library/sr_fingerprint.py` around lines 191 - 197, Update _trim_log_file and
its caller to accept max_size, and trim based on the current file size so
existing oversized content is removed until the remaining log plus the pending
new line fits within max_size. Preserve removal of oldest records and ensure the
caller passes the configured limit rather than only the new line’s size.

import tempfile
import unittest

import sr_fingerprint

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check how the unit tests resolve the library/ import path.
set -euo pipefail

fd -t f 'conftest.py'
fd -t f 'tox.ini|pytest.ini|setup.cfg|pyproject.toml' -d 2

rg -n -C3 'PYTHONPATH|sys\.path|rootdir|testpaths|library' --glob 'tox.ini' --glob 'conftest.py' --glob 'pytest.ini' --glob 'setup.cfg' --glob 'pyproject.toml'

Repository: linux-system-roles/kernel_settings

Length of output: 180


Prefer importing sr_fingerprint as a package.

tests/unit/test_sr_fingerprint.py imports library/sr_fingerprint.py as a bare top-level module, but the tracked config does not set test PYTHONPATH or inject library/ into sys.path. Change the package layout/import path instead of relying on runner-specific path setup.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/test_sr_fingerprint.py` at line 17, Update the import in
test_sr_fingerprint.py and the corresponding package layout so sr_fingerprint is
imported through its package-qualified path rather than as a bare top-level
module. Ensure the package is discoverable through the repository’s normal
configuration without modifying PYTHONPATH or injecting library/ into sys.path.

Comment thread library/sr_fingerprint.py
os.fchmod(fd, stat.S_IMODE(orig_stat.st_mode))
try:
os.fchown(fd, orig_stat.st_uid, orig_stat.st_gid)
except OSError:
Comment thread library/sr_fingerprint.py
except BaseException:
try:
os.unlink(tmp_path)
except OSError:
for path in (log_file, log_file + ".lock"):
try:
os.unlink(path)
except OSError:
@spetrosi spetrosi changed the title feat: Write roles fingerprints to /var/log/sysroles.jsonl feat: Write roles fingerprints to /var/log/sysroles.jsonl [citest_skip] Aug 6, 2026
Feature: Write roles fingerprints to /var/log/sysroles.jsonl [citest_skip]

Reason: By default logs are printed to rsyslog. This change adds a possibility to write logs to a file on the system for the downstream users.

Result: For the upstream, this makes rsyslog log message more detailed. For the downstream - also writes logs to /var/log/sysroles.jsonl
Signed-off-by: Sergei Petrosian <spetrosi@redhat.com>
@spetrosi
spetrosi force-pushed the fingerprint-write-to-file branch from 083004c to 0535194 Compare August 6, 2026 15:02
The sr_fingerprint module was rewritten to accept structured parameters
(status, role_name, role_path, etc.) instead of a free-form sr_message.
Update the role tasks and tests to match the new module interface.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@spetrosi

spetrosi commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator Author

[citest]

@richm
richm merged commit a2404c5 into main Aug 6, 2026
12 of 13 checks passed
@richm
richm deleted the fingerprint-write-to-file branch August 6, 2026 22:09
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