Compare commits
21
Commits
b27d20fb52
...
weekly-ci
+36
-2
@@ -39,12 +39,14 @@ reviews:
|
|||||||
mode: "warning"
|
mode: "warning"
|
||||||
requirements: |
|
requirements: |
|
||||||
PR title MUST follow Conventional Commits format:
|
PR title MUST follow Conventional Commits format:
|
||||||
- Format: <type>: <description> or <type>!: <description> for breaking changes
|
- Format: <required type><optional (scope)><optional !>: <required description>
|
||||||
- Valid types: Refer to the 'type-enum' rule in .commitlintrc.js file for the complete list of allowed types
|
- Valid types: Refer to https://github.com/linux-system-roles/auto-maintenance/blob/main/pr_title_lint.py#L23
|
||||||
- Examples:
|
- Examples:
|
||||||
- "feat: Add backup functionality"
|
- "feat: Add backup functionality"
|
||||||
- "fix: Correct OSTree package installation"
|
- "fix: Correct OSTree package installation"
|
||||||
- "fix!: Remove deprecated variable (breaking change)"
|
- "fix!: Remove deprecated variable (breaking change)"
|
||||||
|
- "chore(formatting): Fix indentation in my_module.py"
|
||||||
|
- "refactor(python)!: Use new python library thispy which has breaking api changes"
|
||||||
|
|
||||||
custom_checks:
|
custom_checks:
|
||||||
- mode: "warning"
|
- mode: "warning"
|
||||||
@@ -55,6 +57,8 @@ reviews:
|
|||||||
- Must contain "Reason:" section explaining why the change was needed
|
- Must contain "Reason:" section explaining why the change was needed
|
||||||
- Must contain "Result:" section describing the outcome or impact
|
- Must contain "Result:" section describing the outcome or impact
|
||||||
- Can contain optional "Issue Tracker Tickets (Jira or BZ if any):" section
|
- Can contain optional "Issue Tracker Tickets (Jira or BZ if any):" section
|
||||||
|
- Must contain "Signed-off-by:" section with your name and email address - use git commit -s
|
||||||
|
- Can contain optional "Assisted-by:" section with name of the AI coding assistant and models used
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
```
|
```
|
||||||
@@ -65,6 +69,34 @@ reviews:
|
|||||||
Result: Users can now set aide_secure_logging: false for debugging while maintaining secure defaults.
|
Result: Users can now set aide_secure_logging: false for debugging while maintaining secure defaults.
|
||||||
|
|
||||||
Issue Tracker Tickets (Jira or BZ if any): RHEL-12345
|
Issue Tracker Tickets (Jira or BZ if any): RHEL-12345
|
||||||
|
|
||||||
|
Signed-off-by: John Doe john.doe@example.com
|
||||||
|
|
||||||
|
Assisted-by: Fish 4.3 using model Swim 6.2
|
||||||
|
```
|
||||||
|
|
||||||
|
For PRs that are bug fixes, you can use the following template:
|
||||||
|
- Must contain "Cause:" section explaining the root cause of the bug
|
||||||
|
- Must contain "Consequences:" section explaining the impact of the bug and how it appears to users
|
||||||
|
- Must contain "Fix:" section explaining the fix for the bug and how it fixes the bug
|
||||||
|
- Must contain "Result:" section describing the outcome or impact of the fix
|
||||||
|
- Can contain optional "Issue Tracker Tickets (Jira or BZ if any):" section
|
||||||
|
- Must contain "Signed-off-by:" section with your name and email address - use git commit -s
|
||||||
|
- Can contain optional "Assisted-by:" section with name of the AI coding assistant and models used
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```
|
||||||
|
Cause: The variable rolename_user_name was being checked for a `none` value but was not being checked for string length greater than 0 if a string.
|
||||||
|
|
||||||
|
Consequences: The role allowed empty user names to be configured which caused the daemon to report "User not found".
|
||||||
|
|
||||||
|
Fix: The variable rolename_user_name is now checked for string length if not `none`, and will report an error in that case if the length is 0.
|
||||||
|
|
||||||
|
Result: The role will not allow the user to provide an empty user name and will report an error if the rolename_user_name length is 0.
|
||||||
|
|
||||||
|
Signed-off-by: John Doe john.doe@example.com
|
||||||
|
|
||||||
|
Assisted-by: Fish 4.3 using model Swim 6.2
|
||||||
```
|
```
|
||||||
|
|
||||||
path_instructions:
|
path_instructions:
|
||||||
@@ -180,6 +212,8 @@ reviews:
|
|||||||
- Tests should verify both success and failure scenarios
|
- Tests should verify both success and failure scenarios
|
||||||
- Use assert module to verify expected state after role execution
|
- Use assert module to verify expected state after role execution
|
||||||
- Include cleanup tasks to ensure tests are rerunnable
|
- Include cleanup tasks to ensure tests are rerunnable
|
||||||
|
- Tests should be run in a block with an always section that runs the test cleanup to ensure that the cleanup is always run.
|
||||||
|
- The cleanup tasks should be tagged with `tests::cleanup` so that the cleanup can be skipped for debug purposes.
|
||||||
- Tests should be idempotent - running twice should not cause failures
|
- Tests should be idempotent - running twice should not cause failures
|
||||||
- Example verification:
|
- Example verification:
|
||||||
```yaml
|
```yaml
|
||||||
|
|||||||
@@ -1,141 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
parserPreset: 'conventional-changelog-conventionalcommits',
|
|
||||||
rules: {
|
|
||||||
'body-leading-blank': [1, 'always'],
|
|
||||||
'body-max-line-length': [2, 'always', 100],
|
|
||||||
'footer-leading-blank': [1, 'always'],
|
|
||||||
'footer-max-line-length': [2, 'always', 100],
|
|
||||||
'header-max-length': [2, 'always', 100],
|
|
||||||
'subject-case': [
|
|
||||||
2,
|
|
||||||
'never',
|
|
||||||
['start-case', 'pascal-case', 'upper-case'],
|
|
||||||
],
|
|
||||||
'subject-empty': [2, 'never'],
|
|
||||||
'subject-full-stop': [2, 'never', '.'],
|
|
||||||
'type-case': [2, 'always', 'lower-case'],
|
|
||||||
'type-empty': [2, 'never'],
|
|
||||||
'type-enum': [
|
|
||||||
2,
|
|
||||||
'always',
|
|
||||||
[
|
|
||||||
'build',
|
|
||||||
'chore',
|
|
||||||
'ci',
|
|
||||||
'docs',
|
|
||||||
'feat',
|
|
||||||
'fix',
|
|
||||||
'perf',
|
|
||||||
'refactor',
|
|
||||||
'revert',
|
|
||||||
'style',
|
|
||||||
'test',
|
|
||||||
'tests',
|
|
||||||
],
|
|
||||||
],
|
|
||||||
},
|
|
||||||
prompt: {
|
|
||||||
questions: {
|
|
||||||
type: {
|
|
||||||
description: "Select the type of change that you're committing",
|
|
||||||
enum: {
|
|
||||||
feat: {
|
|
||||||
description: 'A new feature',
|
|
||||||
title: 'Features',
|
|
||||||
emoji: '✨',
|
|
||||||
},
|
|
||||||
fix: {
|
|
||||||
description: 'A bug fix',
|
|
||||||
title: 'Bug Fixes',
|
|
||||||
emoji: '🐛',
|
|
||||||
},
|
|
||||||
docs: {
|
|
||||||
description: 'Documentation only changes',
|
|
||||||
title: 'Documentation',
|
|
||||||
emoji: '📚',
|
|
||||||
},
|
|
||||||
style: {
|
|
||||||
description:
|
|
||||||
'Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)',
|
|
||||||
title: 'Styles',
|
|
||||||
emoji: '💎',
|
|
||||||
},
|
|
||||||
refactor: {
|
|
||||||
description:
|
|
||||||
'A code change that neither fixes a bug nor adds a feature',
|
|
||||||
title: 'Code Refactoring',
|
|
||||||
emoji: '📦',
|
|
||||||
},
|
|
||||||
perf: {
|
|
||||||
description: 'A code change that improves performance',
|
|
||||||
title: 'Performance Improvements',
|
|
||||||
emoji: '🚀',
|
|
||||||
},
|
|
||||||
test: {
|
|
||||||
description: 'Adding missing tests or correcting existing tests',
|
|
||||||
title: 'Tests',
|
|
||||||
emoji: '🚨',
|
|
||||||
},
|
|
||||||
tests: {
|
|
||||||
description: 'Adding missing tests or correcting existing tests',
|
|
||||||
title: 'Tests',
|
|
||||||
emoji: '🚨',
|
|
||||||
},
|
|
||||||
build: {
|
|
||||||
description:
|
|
||||||
'Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)',
|
|
||||||
title: 'Builds',
|
|
||||||
emoji: '🛠',
|
|
||||||
},
|
|
||||||
ci: {
|
|
||||||
description:
|
|
||||||
'Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs)',
|
|
||||||
title: 'Continuous Integrations',
|
|
||||||
emoji: '⚙️',
|
|
||||||
},
|
|
||||||
chore: {
|
|
||||||
description: "Other changes that don't modify src or test files",
|
|
||||||
title: 'Chores',
|
|
||||||
emoji: '♻️',
|
|
||||||
},
|
|
||||||
revert: {
|
|
||||||
description: 'Reverts a previous commit',
|
|
||||||
title: 'Reverts',
|
|
||||||
emoji: '🗑',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
scope: {
|
|
||||||
description:
|
|
||||||
'What is the scope of this change (e.g. component or file name)',
|
|
||||||
},
|
|
||||||
subject: {
|
|
||||||
description:
|
|
||||||
'Write a short, imperative tense description of the change',
|
|
||||||
},
|
|
||||||
body: {
|
|
||||||
description: 'Provide a longer description of the change',
|
|
||||||
},
|
|
||||||
isBreaking: {
|
|
||||||
description: 'Are there any breaking changes?',
|
|
||||||
},
|
|
||||||
breakingBody: {
|
|
||||||
description:
|
|
||||||
'A BREAKING CHANGE commit requires a body. Please enter a longer description of the commit itself',
|
|
||||||
},
|
|
||||||
breaking: {
|
|
||||||
description: 'Describe the breaking changes',
|
|
||||||
},
|
|
||||||
isIssueAffected: {
|
|
||||||
description: 'Does this change affect any open issues?',
|
|
||||||
},
|
|
||||||
issuesBody: {
|
|
||||||
description:
|
|
||||||
'If issues are closed, the commit requires a body. Please enter a longer description of the commit itself',
|
|
||||||
},
|
|
||||||
issues: {
|
|
||||||
description: 'Add issue references (e.g. "fix #123", "re #123".)',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -6,4 +6,4 @@ updates:
|
|||||||
schedule:
|
schedule:
|
||||||
interval: monthly
|
interval: monthly
|
||||||
commit-message:
|
commit-message:
|
||||||
prefix: ci
|
prefix: "ci: [citest_skip] "
|
||||||
|
|||||||
@@ -44,10 +44,10 @@ jobs:
|
|||||||
- name: Install tox, tox-lsr
|
- name: Install tox, tox-lsr
|
||||||
run: |
|
run: |
|
||||||
set -euxo pipefail
|
set -euxo pipefail
|
||||||
pip3 install "git+https://github.com/linux-system-roles/tox-lsr@3.18.1"
|
pip3 install "git+https://github.com/linux-system-roles/tox-lsr@3.20.1"
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v6
|
uses: actions/setup-python@v7
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.versions.python }}
|
python-version: ${{ matrix.versions.python }}
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ jobs:
|
|||||||
- name: Install tox, tox-lsr
|
- name: Install tox, tox-lsr
|
||||||
run: |
|
run: |
|
||||||
set -euxo pipefail
|
set -euxo pipefail
|
||||||
pip3 install "git+https://github.com/linux-system-roles/tox-lsr@3.18.1"
|
pip3 install "git+https://github.com/linux-system-roles/tox-lsr@3.20.1"
|
||||||
|
|
||||||
- name: Run ansible-plugin-scan
|
- name: Run ansible-plugin-scan
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -47,10 +47,10 @@ jobs:
|
|||||||
- name: Install tox, tox-lsr
|
- name: Install tox, tox-lsr
|
||||||
run: |
|
run: |
|
||||||
set -euxo pipefail
|
set -euxo pipefail
|
||||||
pip3 install "git+https://github.com/linux-system-roles/tox-lsr@3.18.1"
|
pip3 install "git+https://github.com/linux-system-roles/tox-lsr@3.20.1"
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v6
|
uses: actions/setup-python@v7
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.versions.python }}
|
python-version: ${{ matrix.versions.python }}
|
||||||
|
|
||||||
|
|||||||
@@ -22,11 +22,11 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Install conventional-commit linter
|
- name: Install pr_title_lint.py
|
||||||
run: npm install @commitlint/config-conventional @commitlint/cli
|
run: curl -o pr_title_lint.py https://raw.githubusercontent.com/linux-system-roles/auto-maintenance/main/pr_title_lint.py
|
||||||
|
|
||||||
- name: Run commitlint on PR title
|
- name: Run pr_title_lint.py on PR title
|
||||||
env:
|
env:
|
||||||
PR_TITLE: ${{ github.event.pull_request.title }}
|
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||||
# Echo from env variable to avoid bash errors with extra characters
|
# Echo from env variable to avoid bash errors with extra characters
|
||||||
run: echo "$PR_TITLE" | npx commitlint --verbose
|
run: python3 pr_title_lint.py "${PR_TITLE}"
|
||||||
|
|||||||
@@ -30,8 +30,8 @@ jobs:
|
|||||||
# QEMU
|
# QEMU
|
||||||
- { image: "centos-9", env: "qemu-ansible-core-2-16" }
|
- { image: "centos-9", env: "qemu-ansible-core-2-16" }
|
||||||
- { image: "centos-10", env: "qemu-ansible-core-2-17" }
|
- { image: "centos-10", env: "qemu-ansible-core-2-17" }
|
||||||
- { image: "fedora-42", env: "qemu-ansible-core-2-19" }
|
|
||||||
- { image: "fedora-43", env: "qemu-ansible-core-2-20" }
|
- { image: "fedora-43", env: "qemu-ansible-core-2-20" }
|
||||||
|
- { image: "fedora-44", env: "qemu-ansible-core-2-21" }
|
||||||
- { image: "leap-15.6", env: "qemu-ansible-core-2-18" }
|
- { image: "leap-15.6", env: "qemu-ansible-core-2-18" }
|
||||||
|
|
||||||
# container
|
# container
|
||||||
@@ -40,10 +40,11 @@ jobs:
|
|||||||
# broken on non-running dbus
|
# broken on non-running dbus
|
||||||
# - { image: "centos-10", env: "container-ansible-core-2-17" }
|
# - { image: "centos-10", env: "container-ansible-core-2-17" }
|
||||||
- { image: "centos-10-bootc", env: "container-ansible-core-2-17" }
|
- { image: "centos-10-bootc", env: "container-ansible-core-2-17" }
|
||||||
- { image: "fedora-42", env: "container-ansible-core-2-17" }
|
|
||||||
- { image: "fedora-43", env: "container-ansible-core-2-20" }
|
- { image: "fedora-43", env: "container-ansible-core-2-20" }
|
||||||
- { image: "fedora-42-bootc", env: "container-ansible-core-2-17" }
|
- { image: "fedora-44", env: "container-ansible-core-2-21" }
|
||||||
- { image: "fedora-43-bootc", env: "container-ansible-core-2-20" }
|
- { image: "fedora-43-bootc", env: "container-ansible-core-2-20" }
|
||||||
|
# ansible-core 2.21 cannot enable services using service module in bootc images
|
||||||
|
- { image: "fedora-44-bootc", env: "container-ansible-core-2-20" }
|
||||||
|
|
||||||
env:
|
env:
|
||||||
TOX_ARGS: "--skip-tags tests::infiniband,tests::nvme,tests::scsi"
|
TOX_ARGS: "--skip-tags tests::infiniband,tests::nvme,tests::scsi"
|
||||||
@@ -110,29 +111,24 @@ jobs:
|
|||||||
python3 -m pip install --upgrade pip
|
python3 -m pip install --upgrade pip
|
||||||
sudo apt update
|
sudo apt update
|
||||||
sudo apt install -y --no-install-recommends git ansible-core genisoimage qemu-system-x86
|
sudo apt install -y --no-install-recommends git ansible-core genisoimage qemu-system-x86
|
||||||
pip3 install "git+https://github.com/linux-system-roles/tox-lsr@3.18.1"
|
pip3 install "git+https://github.com/linux-system-roles/tox-lsr@3.20.1"
|
||||||
|
|
||||||
# HACK: Drop this when moving this workflow to 26.04 LTS
|
- name: Check for podman version 5 or higher
|
||||||
- name: Update podman to 5.x for compatibility with bootc-image-builder's podman 5
|
id: check_podman_version
|
||||||
if: steps.check_platform.outputs.supported && endsWith(matrix.scenario.image, '-bootc')
|
if: steps.check_platform.outputs.supported
|
||||||
run: |
|
run: |
|
||||||
sed 's/noble/plucky/g' /etc/apt/sources.list.d/ubuntu.sources | sudo tee /etc/apt/sources.list.d/plucky.sources >/dev/null
|
podman_version=$(podman version -f '{{.Client.Version}}')
|
||||||
cat <<EOF | sudo tee /etc/apt/preferences.d/podman.pref >/dev/null
|
podman_major_version="${podman_version%%.*}"
|
||||||
Package: podman buildah golang-github-containers-common crun libgpgme11t64 libgpg-error0 golang-github-containers-image catatonit conmon containers-storage
|
echo "Podman version: $podman_version"
|
||||||
Pin: release n=plucky
|
if [ "$podman_major_version" -lt 5 ]; then
|
||||||
Pin-Priority: 991
|
echo "need_podman_update=1" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "need_podman_update=0" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
|
|
||||||
Package: libsubid4 netavark passt aardvark-dns containernetworking-plugins libslirp0 slirp4netns
|
- name: Ensure use of podman 5
|
||||||
Pin: release n=plucky
|
if: steps.check_platform.outputs.supported && steps.check_podman_version.outputs.need_podman_update == 1
|
||||||
Pin-Priority: 991
|
uses: redhat-actions/podman-install@main
|
||||||
|
|
||||||
Package: *
|
|
||||||
Pin: release n=plucky
|
|
||||||
Pin-Priority: 400
|
|
||||||
EOF
|
|
||||||
|
|
||||||
sudo apt update
|
|
||||||
sudo apt install -y podman crun conmon containers-storage
|
|
||||||
|
|
||||||
- name: Configure tox-lsr
|
- name: Configure tox-lsr
|
||||||
if: steps.check_platform.outputs.supported
|
if: steps.check_platform.outputs.supported
|
||||||
|
|||||||
@@ -72,8 +72,8 @@ jobs:
|
|||||||
meta_main=meta/main.yml
|
meta_main=meta/main.yml
|
||||||
# All Fedora are supported, add latest Fedora versions to supported_platforms
|
# All Fedora are supported, add latest Fedora versions to supported_platforms
|
||||||
if yq '.galaxy_info.galaxy_tags[]' "$meta_main" | grep -qi fedora$; then
|
if yq '.galaxy_info.galaxy_tags[]' "$meta_main" | grep -qi fedora$; then
|
||||||
supported_platforms+=" Fedora-42"
|
|
||||||
supported_platforms+=" Fedora-43"
|
supported_platforms+=" Fedora-43"
|
||||||
|
supported_platforms+=" Fedora-44"
|
||||||
fi
|
fi
|
||||||
# Specific Fedora versions supported
|
# Specific Fedora versions supported
|
||||||
if yq '.galaxy_info.galaxy_tags[]' "$meta_main" | grep -qiP 'fedora\d+$'; then
|
if yq '.galaxy_info.galaxy_tags[]' "$meta_main" | grep -qiP 'fedora\d+$'; then
|
||||||
@@ -99,10 +99,10 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
# Ensure ansible version is a string!
|
# Ensure ansible version is a string!
|
||||||
- platform: Fedora-42
|
|
||||||
ansible_version: "2.19"
|
|
||||||
- platform: Fedora-43
|
- platform: Fedora-43
|
||||||
ansible_version: "2.20"
|
ansible_version: "2.20"
|
||||||
|
- platform: Fedora-44
|
||||||
|
ansible_version: "2.21"
|
||||||
- platform: CentOS-7-latest
|
- platform: CentOS-7-latest
|
||||||
ansible_version: "2.9"
|
ansible_version: "2.9"
|
||||||
- platform: CentOS-Stream-8
|
- platform: CentOS-Stream-8
|
||||||
|
|||||||
@@ -1,6 +1,33 @@
|
|||||||
Changelog
|
Changelog
|
||||||
=========
|
=========
|
||||||
|
|
||||||
|
[1.4.0] - 2026-08-06
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
### New Features
|
||||||
|
|
||||||
|
- feat: Write roles fingerprints to /var/log/sysroles.jsonl [citest_skip] (#100)
|
||||||
|
|
||||||
|
### Other Changes
|
||||||
|
|
||||||
|
- ci: bump actions/setup-python from 6 to 7 (#98)
|
||||||
|
- ci: ensure dependabot updates do not invoke ci tests [citest_skip] (#99)
|
||||||
|
|
||||||
|
[1.3.2] - 2026-07-27
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- fix: Better support for check mode (#93)
|
||||||
|
|
||||||
|
### Other Changes
|
||||||
|
|
||||||
|
- ci: use gha checkout v7, codecov v7 [citest_skip] (#90)
|
||||||
|
- ci: Use our own pr_title_lint.py instead of NPM commitlint [citest_skip] (#91)
|
||||||
|
- ci: bump actions/checkout from 6 to 7 (#92)
|
||||||
|
- ci: bump tox-lsr version to 3.20.0 to fix tox 4.58 api breakage [citest_skip] (#94)
|
||||||
|
- ci: Add support for Fedora 44 and drop Fedora 42 - use ansible-core 2.21 [citest_skip] (#96)
|
||||||
|
|
||||||
[1.3.1] - 2026-06-24
|
[1.3.1] - 2026-06-24
|
||||||
--------------------
|
--------------------
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ aide_init: false
|
|||||||
# Fetch db
|
# Fetch db
|
||||||
aide_fetch_db: false
|
aide_fetch_db: false
|
||||||
|
|
||||||
|
# Upload db
|
||||||
|
aide_upload_db: false
|
||||||
|
|
||||||
# Enable check database phase
|
# Enable check database phase
|
||||||
aide_check: false
|
aide_check: false
|
||||||
|
|
||||||
|
|||||||
+303
-25
@@ -7,29 +7,148 @@ __metaclass__ = type
|
|||||||
DOCUMENTATION = """
|
DOCUMENTATION = """
|
||||||
---
|
---
|
||||||
module: sr_fingerprint
|
module: sr_fingerprint
|
||||||
short_description: Write a message string to syslog using Ansible C(module.log) function.
|
short_description: Write role fingerprint data to syslog and optionally to a JSONL log file
|
||||||
description:
|
description:
|
||||||
- Writes the given string to the system log using Ansible C(module.log) function.
|
- Collects role fingerprint data into a canonical record and writes it to
|
||||||
|
syslog using Ansible C(module.log) as C(key=value) pairs.
|
||||||
|
- Optionally appends the same record as a JSON line to a log file
|
||||||
|
(one JSON object per line, JSONL format), by default
|
||||||
|
C(/var/log/sysroles.jsonl).
|
||||||
|
- Playbook variables are not available inside modules automatically. Roles
|
||||||
|
pass C(role_name), C(role_path), C(ansible_play_hosts_all),
|
||||||
|
C(distribution), and C(distribution_version) from the task.
|
||||||
|
- C(ansible_check_mode) is collected from the module execution context.
|
||||||
- Intended for role-internal or diagnostic use.
|
- Intended for role-internal or diagnostic use.
|
||||||
author: Rich Megginson (@richm)
|
author: Rich Megginson (@richm)
|
||||||
options:
|
options:
|
||||||
sr_message:
|
status:
|
||||||
description: Text to record in syslog.
|
description: Role execution status.
|
||||||
type: str
|
type: str
|
||||||
required: true
|
required: true
|
||||||
|
choices:
|
||||||
|
- begin
|
||||||
|
- success
|
||||||
|
write_log_file:
|
||||||
|
description: >-
|
||||||
|
If C(true), append fingerprint data to the JSONL log file.
|
||||||
|
Defaults to C(false).
|
||||||
|
type: bool
|
||||||
|
default: false
|
||||||
|
log_file:
|
||||||
|
description: >-
|
||||||
|
Path to the JSONL log file. A lock sidecar (C(<log_file>.lock))
|
||||||
|
is created next to the log file for cross-process safety.
|
||||||
|
type: path
|
||||||
|
default: /var/log/sysroles.jsonl
|
||||||
|
max_log_size:
|
||||||
|
description: >-
|
||||||
|
Maximum log file size in bytes. When appending a new record
|
||||||
|
would exceed this limit, the oldest records are removed first.
|
||||||
|
Set to C(0) to disable trimming.
|
||||||
|
type: int
|
||||||
|
default: 2000000
|
||||||
|
role_name:
|
||||||
|
description: Name of the role, typically C({{ role_name }}).
|
||||||
|
type: str
|
||||||
|
required: true
|
||||||
|
role_path:
|
||||||
|
description: Path to the role, typically C({{ role_path }}).
|
||||||
|
type: path
|
||||||
|
required: true
|
||||||
|
ansible_play_hosts_all:
|
||||||
|
description: >-
|
||||||
|
All hosts in the play, typically C({{ ansible_play_hosts_all }}).
|
||||||
|
Used to derive C(play_hosts_number).
|
||||||
|
type: list
|
||||||
|
elements: str
|
||||||
|
required: true
|
||||||
|
distribution:
|
||||||
|
description: >-
|
||||||
|
OS distribution name, typically
|
||||||
|
C({{ ansible_facts["distribution"] }}).
|
||||||
|
type: str
|
||||||
|
default: ""
|
||||||
|
distribution_version:
|
||||||
|
description: >-
|
||||||
|
OS distribution version, typically
|
||||||
|
C({{ ansible_facts["distribution_version"] }}).
|
||||||
|
type: str
|
||||||
|
default: ""
|
||||||
"""
|
"""
|
||||||
|
|
||||||
EXAMPLES = """
|
EXAMPLES = """
|
||||||
- name: Record a fingerprint message in syslog
|
- name: Record role begin fingerprint to syslog only (not log file)
|
||||||
sr_fingerprint:
|
sr_fingerprint:
|
||||||
sr_message: "system_role:ROLENAME"
|
status: begin
|
||||||
|
role_name: bootloader
|
||||||
|
role_path: "{{ role_path }}"
|
||||||
|
ansible_play_hosts_all: "{{ ansible_play_hosts_all }}"
|
||||||
|
distribution: "{{ ansible_facts['distribution'] }}"
|
||||||
|
distribution_version: "{{ ansible_facts['distribution_version'] }}"
|
||||||
|
write_log_file: false
|
||||||
|
|
||||||
|
- name: Record role success fingerprint
|
||||||
|
sr_fingerprint:
|
||||||
|
status: success
|
||||||
|
role_name: bootloader
|
||||||
|
role_path: "{{ role_path }}"
|
||||||
|
ansible_play_hosts_all: "{{ ansible_play_hosts_all }}"
|
||||||
|
distribution: "{{ ansible_facts['distribution'] }}"
|
||||||
|
distribution_version: "{{ ansible_facts['distribution_version'] }}"
|
||||||
|
write_log_file: true
|
||||||
"""
|
"""
|
||||||
|
|
||||||
RETURN = r""" # """
|
RETURN = r"""
|
||||||
|
fingerprint:
|
||||||
|
description: The fingerprint record written to syslog and optionally to the log file.
|
||||||
|
returned: always
|
||||||
|
type: dict
|
||||||
|
sample:
|
||||||
|
date: "2026-08-03T10:15:00+02:00"
|
||||||
|
role_name: network
|
||||||
|
role_path: /usr/share/ansible/roles/linux-system-roles.network
|
||||||
|
status: success
|
||||||
|
ansible_version: "2.16.3"
|
||||||
|
managed_node_distro: RedHat-9.4
|
||||||
|
play_hosts_number: 3
|
||||||
|
ansible_check_mode: false
|
||||||
|
message:
|
||||||
|
description: Informational message shown in check mode.
|
||||||
|
returned: check mode
|
||||||
|
type: str
|
||||||
|
sample: "Check mode: message not logged - [date=... role_name=...]"
|
||||||
|
jsonl_row:
|
||||||
|
description: The JSON line that would be appended to the log file.
|
||||||
|
returned: check mode and O(write_log_file=true)
|
||||||
|
type: str
|
||||||
|
log_file:
|
||||||
|
description: Path to the log file that would be written.
|
||||||
|
returned: check mode and O(write_log_file=true)
|
||||||
|
type: str
|
||||||
|
"""
|
||||||
|
|
||||||
from ansible.module_utils.basic import AnsibleModule
|
from ansible.module_utils.basic import AnsibleModule
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
|
import errno
|
||||||
|
import fcntl
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import stat
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
FINGERPRINT_FIELDS = (
|
||||||
|
"date",
|
||||||
|
"role_name",
|
||||||
|
"role_path",
|
||||||
|
"status",
|
||||||
|
"ansible_version",
|
||||||
|
"managed_node_distro",
|
||||||
|
"play_hosts_number",
|
||||||
|
"ansible_check_mode",
|
||||||
|
)
|
||||||
|
|
||||||
|
FINGERPRINT_SYSLOG_SEPARATOR = " "
|
||||||
|
|
||||||
|
|
||||||
def _local_iso8601_no_microseconds():
|
def _local_iso8601_no_microseconds():
|
||||||
@@ -51,9 +170,184 @@ def _local_iso8601_no_microseconds():
|
|||||||
return datetime.datetime.now(utc).astimezone().replace(microsecond=0).isoformat()
|
return datetime.datetime.now(utc).astimezone().replace(microsecond=0).isoformat()
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_parent_dir(path):
|
||||||
|
parent = os.path.dirname(path)
|
||||||
|
if not parent:
|
||||||
|
return
|
||||||
|
if os.path.isdir(parent):
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
os.makedirs(parent)
|
||||||
|
except OSError as exc:
|
||||||
|
# another process may have created the directory
|
||||||
|
if exc.errno != errno.EEXIST or not os.path.isdir(parent):
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def _format_fingerprint_jsonl(record):
|
||||||
|
"""Format the canonical fingerprint record as a single JSON line."""
|
||||||
|
return json.dumps(record, separators=(",", ":"), sort_keys=False)
|
||||||
|
|
||||||
|
|
||||||
|
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))
|
||||||
|
orig_stat = os.stat(log_file)
|
||||||
|
dir_name = os.path.dirname(log_file) or "."
|
||||||
|
fd, tmp_path = tempfile.mkstemp(dir=dir_name, suffix=".tmp")
|
||||||
|
try:
|
||||||
|
os.fchmod(fd, stat.S_IMODE(orig_stat.st_mode))
|
||||||
|
try:
|
||||||
|
os.fchown(fd, orig_stat.st_uid, orig_stat.st_gid)
|
||||||
|
except OSError:
|
||||||
|
# not running as root; keep default ownership
|
||||||
|
pass
|
||||||
|
with os.fdopen(fd, "w") as tmp_fd:
|
||||||
|
tmp_fd.writelines(lines)
|
||||||
|
tmp_fd.flush()
|
||||||
|
os.fsync(tmp_fd.fileno())
|
||||||
|
os.rename(tmp_path, log_file)
|
||||||
|
except BaseException:
|
||||||
|
try:
|
||||||
|
os.unlink(tmp_path)
|
||||||
|
except OSError:
|
||||||
|
# already removed or never created
|
||||||
|
pass
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def _write_jsonl_log(log_file, record, max_size=0):
|
||||||
|
_ensure_parent_dir(log_file)
|
||||||
|
new_line = _format_fingerprint_jsonl(record) + "\n"
|
||||||
|
lock_path = log_file + ".lock"
|
||||||
|
lock_fd = open(lock_path, "w")
|
||||||
|
try:
|
||||||
|
fcntl.flock(lock_fd, fcntl.LOCK_EX)
|
||||||
|
try:
|
||||||
|
cur_size = os.path.getsize(log_file)
|
||||||
|
except OSError:
|
||||||
|
# file does not exist yet
|
||||||
|
cur_size = 0
|
||||||
|
if max_size > 0 and cur_size + len(new_line) > max_size and cur_size > 0:
|
||||||
|
_trim_log_file(log_file, len(new_line))
|
||||||
|
with open(log_file, "a") as log_fd:
|
||||||
|
log_fd.write(new_line)
|
||||||
|
finally:
|
||||||
|
fcntl.flock(lock_fd, fcntl.LOCK_UN)
|
||||||
|
lock_fd.close()
|
||||||
|
|
||||||
|
|
||||||
|
def _get_managed_node_distro(distribution, distribution_version):
|
||||||
|
if distribution and distribution_version:
|
||||||
|
return "%s-%s" % (distribution, distribution_version)
|
||||||
|
return "unknown"
|
||||||
|
|
||||||
|
|
||||||
|
def _get_play_hosts_number(play_hosts_all):
|
||||||
|
return len(play_hosts_all)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_ansible_version(module):
|
||||||
|
version = getattr(module, "ansible_version", None)
|
||||||
|
if version:
|
||||||
|
return version
|
||||||
|
return "unknown"
|
||||||
|
|
||||||
|
|
||||||
|
def _get_check_mode(module):
|
||||||
|
return bool(getattr(module, "check_mode", False))
|
||||||
|
|
||||||
|
|
||||||
|
def _collect_fingerprint_record(module, status):
|
||||||
|
"""Build the canonical fingerprint record used by all output formatters."""
|
||||||
|
return {
|
||||||
|
"date": _local_iso8601_no_microseconds(),
|
||||||
|
"role_name": module.params["role_name"],
|
||||||
|
"role_path": module.params["role_path"],
|
||||||
|
"status": status,
|
||||||
|
"ansible_version": _get_ansible_version(module),
|
||||||
|
"managed_node_distro": _get_managed_node_distro(
|
||||||
|
module.params["distribution"], module.params["distribution_version"]
|
||||||
|
),
|
||||||
|
"play_hosts_number": _get_play_hosts_number(
|
||||||
|
module.params["ansible_play_hosts_all"]
|
||||||
|
),
|
||||||
|
"ansible_check_mode": _get_check_mode(module),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _fingerprint_record_items(record):
|
||||||
|
return [(field, record[field]) for field in FINGERPRINT_FIELDS]
|
||||||
|
|
||||||
|
|
||||||
|
def _format_fingerprint_key_value(field, value):
|
||||||
|
text = "" if value is None else str(value)
|
||||||
|
if any(char in text for char in ' "='):
|
||||||
|
return '%s="%s"' % (field, text.replace('"', '""'))
|
||||||
|
return "%s=%s" % (field, text)
|
||||||
|
|
||||||
|
|
||||||
|
def _format_fingerprint_syslog(record):
|
||||||
|
"""Format the canonical fingerprint record as key=value syslog text."""
|
||||||
|
pairs = [
|
||||||
|
_format_fingerprint_key_value(field, value)
|
||||||
|
for field, value in _fingerprint_record_items(record)
|
||||||
|
]
|
||||||
|
return FINGERPRINT_SYSLOG_SEPARATOR.join(pairs)
|
||||||
|
|
||||||
|
|
||||||
|
def _handle_fingerprint(module):
|
||||||
|
max_log_size = module.params["max_log_size"]
|
||||||
|
if max_log_size < 0:
|
||||||
|
module.fail_json(
|
||||||
|
msg="max_log_size must be 0 or a positive integer, got %d" % max_log_size
|
||||||
|
)
|
||||||
|
|
||||||
|
fingerprint_record = _collect_fingerprint_record(module, module.params["status"])
|
||||||
|
log_message = _format_fingerprint_syslog(fingerprint_record)
|
||||||
|
|
||||||
|
if module.check_mode:
|
||||||
|
result = dict(
|
||||||
|
changed=False,
|
||||||
|
message="Check mode: message not logged - [%s]" % log_message,
|
||||||
|
fingerprint=fingerprint_record,
|
||||||
|
)
|
||||||
|
if module.params["write_log_file"]:
|
||||||
|
result["jsonl_row"] = _format_fingerprint_jsonl(fingerprint_record)
|
||||||
|
result["log_file"] = module.params["log_file"]
|
||||||
|
module.exit_json(**result)
|
||||||
|
|
||||||
|
module.log(log_message)
|
||||||
|
|
||||||
|
if module.params["write_log_file"]:
|
||||||
|
log_file = module.params["log_file"]
|
||||||
|
try:
|
||||||
|
_write_jsonl_log(
|
||||||
|
log_file, fingerprint_record, module.params["max_log_size"]
|
||||||
|
)
|
||||||
|
except (IOError, OSError) as exc:
|
||||||
|
module.fail_json(
|
||||||
|
msg="Failed to write fingerprint log file %s: %s" % (log_file, exc)
|
||||||
|
)
|
||||||
|
|
||||||
|
module.exit_json(changed=False, fingerprint=fingerprint_record)
|
||||||
|
|
||||||
|
|
||||||
def run_module():
|
def run_module():
|
||||||
module_args = dict(
|
module_args = dict(
|
||||||
sr_message=dict(type="str", required=True),
|
status=dict(type="str", required=True, choices=["begin", "success"]),
|
||||||
|
write_log_file=dict(type="bool", default=False),
|
||||||
|
log_file=dict(type="path", default="/var/log/sysroles.jsonl"),
|
||||||
|
max_log_size=dict(type="int", default=2000000),
|
||||||
|
role_name=dict(type="str", required=True),
|
||||||
|
role_path=dict(type="path", required=True),
|
||||||
|
ansible_play_hosts_all=dict(type="list", elements="str", required=True),
|
||||||
|
distribution=dict(type="str", default=""),
|
||||||
|
distribution_version=dict(type="str", default=""),
|
||||||
)
|
)
|
||||||
|
|
||||||
module = AnsibleModule(
|
module = AnsibleModule(
|
||||||
@@ -61,23 +355,7 @@ def run_module():
|
|||||||
supports_check_mode=True,
|
supports_check_mode=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
log_message = "%s %s" % (
|
_handle_fingerprint(module)
|
||||||
module.params["sr_message"],
|
|
||||||
_local_iso8601_no_microseconds(),
|
|
||||||
)
|
|
||||||
|
|
||||||
if module.check_mode:
|
|
||||||
module.exit_json(
|
|
||||||
changed=False,
|
|
||||||
message="Check mode: message not logged - [%s]" % log_message,
|
|
||||||
)
|
|
||||||
|
|
||||||
module.log(log_message)
|
|
||||||
|
|
||||||
# we don't actually change anything, so we're not changed - writing a log message
|
|
||||||
# is not considered a change
|
|
||||||
# also, we don't want to report changed every time the role runs
|
|
||||||
module.exit_json(changed=False)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
|||||||
+147
-122
@@ -10,137 +10,162 @@
|
|||||||
state: present
|
state: present
|
||||||
use: "{{ (__aide_is_ostree | d(false)) |
|
use: "{{ (__aide_is_ostree | d(false)) |
|
||||||
ternary('ansible.posix.rhel_rpm_ostree', omit) }}"
|
ternary('ansible.posix.rhel_rpm_ostree', omit) }}"
|
||||||
|
register: __aide_install_packages
|
||||||
|
|
||||||
- name: Get AIDE version
|
- name: Packages are installed
|
||||||
ansible.builtin.command:
|
# either run mode or check mode and no changes to packages
|
||||||
cmd: aide --version
|
when: not ansible_check_mode or (ansible_check_mode and not __aide_install_packages.changed)
|
||||||
register: __aide_version_register
|
|
||||||
changed_when: false
|
|
||||||
|
|
||||||
# assumes the version starts with a digit and goes to the end of the line
|
|
||||||
- name: Set AIDE version
|
|
||||||
set_fact:
|
|
||||||
aide_version: "{{ __output | regex_search('(?m)^A[iI][dD][eE] (\\d.*)$', '\\1') | first }}"
|
|
||||||
vars:
|
|
||||||
__output: "{{ __aide_version_register.stdout if __aide_version_register.stdout | length > 0
|
|
||||||
else __aide_version_register.stderr }}"
|
|
||||||
|
|
||||||
- name: Ensure required services are enabled and started
|
|
||||||
ansible.builtin.service:
|
|
||||||
name: "{{ item }}"
|
|
||||||
state: started
|
|
||||||
enabled: true
|
|
||||||
loop: "{{ __aide_services }}"
|
|
||||||
|
|
||||||
- name: Generate "/etc/{{ __aide_config }}"
|
|
||||||
ansible.builtin.template:
|
|
||||||
src: "{{ aide_config_template }}"
|
|
||||||
dest: "/etc/{{ __aide_config }}"
|
|
||||||
mode: "0400"
|
|
||||||
when: aide_config_template is not none
|
|
||||||
|
|
||||||
# - name: Print Header
|
|
||||||
# ansible.builtin.command: head /etc/aide.conf || true
|
|
||||||
|
|
||||||
- name: Initialize AIDE database
|
|
||||||
when: aide_init | bool
|
|
||||||
block:
|
block:
|
||||||
- name: Initialize AIDE database
|
- name: Get AIDE version
|
||||||
ansible.builtin.command:
|
ansible.builtin.command:
|
||||||
cmd: aide --init
|
cmd: aide --version
|
||||||
changed_when: true
|
check_mode: false
|
||||||
|
register: __aide_version_register
|
||||||
- name: Copy AIDE reference database
|
|
||||||
ansible.builtin.copy:
|
|
||||||
remote_src: true
|
|
||||||
src: "{{ __aide_db_new_name }}"
|
|
||||||
dest: "{{ __aide_db_name }}"
|
|
||||||
owner: root
|
|
||||||
group: root
|
|
||||||
mode: "0440"
|
|
||||||
force: true
|
|
||||||
when: not aide_fetch_db | bool
|
|
||||||
|
|
||||||
- name: Remove remote AIDE database file
|
|
||||||
ansible.builtin.file:
|
|
||||||
path: "{{ __aide_db_new_name }}"
|
|
||||||
state: absent
|
|
||||||
when: not aide_fetch_db | bool
|
|
||||||
|
|
||||||
- name: Fetch AIDE database
|
|
||||||
when: aide_fetch_db | bool
|
|
||||||
block:
|
|
||||||
- name: Fetch AIDE database
|
|
||||||
ansible.builtin.fetch:
|
|
||||||
src: "{{ __aide_db_new_name }}"
|
|
||||||
dest: "{{ aide_db_fetch_dir }}"
|
|
||||||
|
|
||||||
- name: Remove remote AIDE database file
|
|
||||||
ansible.builtin.file:
|
|
||||||
path: "{{ __aide_db_new_name }}"
|
|
||||||
state: absent
|
|
||||||
|
|
||||||
- name: Check AIDE integrity
|
|
||||||
when: aide_check | bool
|
|
||||||
block:
|
|
||||||
- name: Copy AIDE reference database
|
|
||||||
ansible.builtin.copy:
|
|
||||||
src: >-
|
|
||||||
{{ aide_db_fetch_dir }}/{{ inventory_hostname }}{{ __aide_db_new_name }}
|
|
||||||
dest: "{{ __aide_db_name }}"
|
|
||||||
owner: root
|
|
||||||
group: root
|
|
||||||
mode: "0440"
|
|
||||||
when: aide_fetch_db | bool
|
|
||||||
|
|
||||||
- name: Check against AIDE reference database
|
|
||||||
ansible.builtin.command:
|
|
||||||
cmd: aide --check
|
|
||||||
changed_when: false
|
changed_when: false
|
||||||
|
|
||||||
- name: Update AIDE database and fetch it
|
# assumes the version starts with a digit and goes to the end of the line
|
||||||
when: aide_update | bool
|
- name: Set AIDE version
|
||||||
block:
|
set_fact:
|
||||||
- name: Update AIDE database
|
aide_version: "{{ __output | regex_search('(?m)^A[iI][dD][eE] (\\d.*)$', '\\1') | first }}"
|
||||||
ansible.builtin.command:
|
|
||||||
cmd: aide --update
|
|
||||||
register: __aide_update_result
|
|
||||||
failed_when: __msg not in __aide_update_result.stdout
|
|
||||||
changed_when: true
|
|
||||||
vars:
|
vars:
|
||||||
__msg: >-
|
__output: "{{ __aide_version_register.stdout if __aide_version_register.stdout | length > 0
|
||||||
AIDE found NO differences between database and filesystem. Looks okay!!
|
else __aide_version_register.stderr }}"
|
||||||
|
|
||||||
|
- name: Ensure required services are enabled and started
|
||||||
|
ansible.builtin.service:
|
||||||
|
name: "{{ item }}"
|
||||||
|
state: started
|
||||||
|
enabled: true
|
||||||
|
loop: "{{ __aide_services }}"
|
||||||
|
|
||||||
|
- name: Generate "/etc/{{ __aide_config }}"
|
||||||
|
ansible.builtin.template:
|
||||||
|
src: "{{ aide_config_template }}"
|
||||||
|
dest: "/etc/{{ __aide_config }}"
|
||||||
|
mode: "0400"
|
||||||
|
when: aide_config_template is not none
|
||||||
|
|
||||||
|
# - name: Print Header
|
||||||
|
# ansible.builtin.command: head /etc/aide.conf || true
|
||||||
|
|
||||||
|
- name: Initialize AIDE database
|
||||||
|
when:
|
||||||
|
- not ansible_check_mode
|
||||||
|
- aide_init | bool
|
||||||
|
block:
|
||||||
|
- name: Initialize AIDE database
|
||||||
|
ansible.builtin.command:
|
||||||
|
cmd: aide --init
|
||||||
|
changed_when: true
|
||||||
|
|
||||||
|
- name: Copy AIDE reference database
|
||||||
|
ansible.builtin.copy:
|
||||||
|
remote_src: true
|
||||||
|
src: "{{ __aide_db_new_name }}"
|
||||||
|
dest: "{{ __aide_db_name }}"
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: "0440"
|
||||||
|
force: true
|
||||||
|
when: not aide_fetch_db | bool
|
||||||
|
|
||||||
|
- name: Remove remote AIDE database file
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "{{ __aide_db_new_name }}"
|
||||||
|
state: absent
|
||||||
|
when: not aide_fetch_db | bool
|
||||||
|
|
||||||
- name: Fetch AIDE database
|
- name: Fetch AIDE database
|
||||||
ansible.builtin.fetch:
|
when:
|
||||||
src: "{{ __aide_db_new_name }}"
|
- not ansible_check_mode
|
||||||
dest: "{{ aide_db_fetch_dir }}"
|
- aide_fetch_db | bool
|
||||||
|
block:
|
||||||
|
- name: Fetch AIDE database
|
||||||
|
ansible.builtin.fetch:
|
||||||
|
src: "{{ __aide_db_new_name }}"
|
||||||
|
dest: "{{ aide_db_fetch_dir }}"
|
||||||
|
|
||||||
- name: Remove remote AIDE database file
|
- name: Remove remote AIDE database file
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
path: "{{ __aide_db_new_name }}"
|
path: "{{ __aide_db_new_name }}"
|
||||||
|
state: absent
|
||||||
|
|
||||||
|
- name: Check AIDE integrity
|
||||||
|
when:
|
||||||
|
- not ansible_check_mode
|
||||||
|
- aide_check | bool
|
||||||
|
block:
|
||||||
|
- name: Copy AIDE reference database
|
||||||
|
ansible.builtin.copy:
|
||||||
|
src: >-
|
||||||
|
{{ aide_db_fetch_dir }}/{{ inventory_hostname }}{{ __aide_db_new_name }}
|
||||||
|
dest: "{{ __aide_db_name }}"
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: "0440"
|
||||||
|
when: aide_upload_db | bool
|
||||||
|
|
||||||
|
- name: Check against AIDE reference database
|
||||||
|
ansible.builtin.command:
|
||||||
|
cmd: aide --check
|
||||||
|
register: __aide_check_result
|
||||||
|
changed_when: __aide_check_result.rc > 1 and __aide_check_result.rc <= 7
|
||||||
|
failed_when: __aide_check_result.rc >= 14
|
||||||
|
|
||||||
|
- name: Update AIDE database and fetch it
|
||||||
|
when:
|
||||||
|
- not ansible_check_mode
|
||||||
|
- aide_update | bool
|
||||||
|
block:
|
||||||
|
- name: Update AIDE database
|
||||||
|
ansible.builtin.command:
|
||||||
|
cmd: aide --update
|
||||||
|
register: __aide_update_result
|
||||||
|
changed_when: __aide_update_result.rc > 1 and __aide_update_result.rc <= 7
|
||||||
|
failed_when: __aide_update_result.rc >= 14
|
||||||
|
|
||||||
|
- name: Fetch AIDE database
|
||||||
|
ansible.builtin.fetch:
|
||||||
|
src: "{{ __aide_db_new_name }}"
|
||||||
|
dest: "{{ aide_db_fetch_dir }}"
|
||||||
|
|
||||||
|
- name: Remove remote AIDE database file
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "{{ __aide_db_new_name }}"
|
||||||
|
state: absent
|
||||||
|
|
||||||
|
- name: Update aide check cron configuration if necessary
|
||||||
|
ansible.builtin.lineinfile:
|
||||||
|
path: /etc/crontab
|
||||||
|
regexp: "^.* root {{ __aide_bin_path }} --check"
|
||||||
|
line: "{{ aide_cron_interval }} root {{ __aide_bin_path }} --check"
|
||||||
|
when:
|
||||||
|
- aide_cron_check is not none
|
||||||
|
- aide_cron_check | bool
|
||||||
|
|
||||||
|
- name: Remove aide check cron configuration if necessary
|
||||||
|
ansible.builtin.lineinfile:
|
||||||
|
path: /etc/crontab
|
||||||
state: absent
|
state: absent
|
||||||
|
regexp: "^.* root {{ __aide_bin_path }} --check"
|
||||||
|
when:
|
||||||
|
- aide_cron_check is not none
|
||||||
|
- not aide_cron_check | bool
|
||||||
|
|
||||||
- name: Update aide check cron configuration if necessary
|
- name: Fetch AIDE logs
|
||||||
ansible.builtin.lineinfile:
|
ansible.builtin.fetch:
|
||||||
path: /etc/crontab
|
src: "/var/log/aide/aide.log"
|
||||||
regexp: "^.* root {{ __aide_bin_path }} --check"
|
dest: "{{ aide_db_fetch_dir }}"
|
||||||
line: "{{ aide_cron_interval }} root {{ __aide_bin_path }} --check"
|
owner: root
|
||||||
when:
|
group: root
|
||||||
- aide_cron_check is not none
|
mode: "0640"
|
||||||
- aide_cron_check | bool
|
|
||||||
|
|
||||||
- name: Remove aide check cron configuration if necessary
|
- name: Record role success fingerprint
|
||||||
ansible.builtin.lineinfile:
|
sr_fingerprint:
|
||||||
path: /etc/crontab
|
status: success
|
||||||
state: absent
|
role_name: aide
|
||||||
regexp: "^.* root {{ __aide_bin_path }} --check"
|
role_path: "{{ role_path }}"
|
||||||
when:
|
ansible_play_hosts_all: "{{ ansible_play_hosts_all }}"
|
||||||
- aide_cron_check is not none
|
distribution: "{{ ansible_facts['distribution'] }}"
|
||||||
- not aide_cron_check | bool
|
distribution_version: "{{ ansible_facts['distribution_version'] }}"
|
||||||
|
write_log_file: "{{ __aide_write_log_file }}"
|
||||||
- name: Record role success fingerprint
|
|
||||||
sr_fingerprint:
|
|
||||||
sr_message: >-
|
|
||||||
success system_role:aide ansible_version={{ ansible_version.full }}
|
|
||||||
{{ ansible_facts['distribution'] }}-{{ ansible_facts['distribution_version'] }}
|
|
||||||
|
|||||||
+7
-3
@@ -7,9 +7,13 @@
|
|||||||
|
|
||||||
- name: Record role begin fingerprint
|
- name: Record role begin fingerprint
|
||||||
sr_fingerprint:
|
sr_fingerprint:
|
||||||
sr_message: >-
|
status: begin
|
||||||
begin system_role:aide ansible_version={{ ansible_version.full }}
|
role_name: aide
|
||||||
{{ ansible_facts['distribution'] }}-{{ ansible_facts['distribution_version'] }}
|
role_path: "{{ role_path }}"
|
||||||
|
ansible_play_hosts_all: "{{ ansible_play_hosts_all }}"
|
||||||
|
distribution: "{{ ansible_facts['distribution'] }}"
|
||||||
|
distribution_version: "{{ ansible_facts['distribution_version'] }}"
|
||||||
|
write_log_file: "{{ __aide_write_log_file }}"
|
||||||
|
|
||||||
- name: Determine if system is ostree and set flag
|
- name: Determine if system is ostree and set flag
|
||||||
when: not __aide_is_ostree is defined
|
when: not __aide_is_ostree is defined
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (C) 2023, Red Hat, Inc.
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
from __future__ import absolute_import, division, print_function
|
||||||
|
|
||||||
|
__metaclass__ = type
|
||||||
|
|
||||||
|
DOCUMENTATION = """
|
||||||
|
author: Rich Megginson
|
||||||
|
name: dump_packages
|
||||||
|
type: aggregate
|
||||||
|
short_description: dump arguments to package module
|
||||||
|
description:
|
||||||
|
- Dump arguments to package module to get list of packages.
|
||||||
|
- Used in conjunction with CI testing to get the packages used
|
||||||
|
- with all combinations of: distribution/version/role arguments
|
||||||
|
- Used to generate lists of packages for ostree image builds.
|
||||||
|
requirements:
|
||||||
|
- None
|
||||||
|
"""
|
||||||
|
|
||||||
|
from ansible.plugins.callback import CallbackBase # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
class CallbackModule(CallbackBase):
|
||||||
|
"""
|
||||||
|
Dump packages.
|
||||||
|
"""
|
||||||
|
|
||||||
|
CALLBACK_VERSION = 2.0
|
||||||
|
CALLBACK_TYPE = "aggregate"
|
||||||
|
CALLBACK_NAME = "dump_packages"
|
||||||
|
# needed for 2.9 compatibility
|
||||||
|
CALLBACK_NEEDS_WHITELIST = False # wokeignore:rule=whitelist
|
||||||
|
CALLBACK_NEEDS_ENABLED = False
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(CallbackModule, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def v2_runner_on_ok(self, result):
|
||||||
|
fields = result._task_fields
|
||||||
|
if (
|
||||||
|
fields["action"] in ["package", "dnf", "yum"]
|
||||||
|
and fields["args"].get("state") != "absent"
|
||||||
|
):
|
||||||
|
packages = set()
|
||||||
|
if "invocation" in result._result:
|
||||||
|
results = [result._result]
|
||||||
|
elif "results" in result._result and isinstance(
|
||||||
|
result._result["results"], list
|
||||||
|
):
|
||||||
|
results = result._result["results"]
|
||||||
|
for item in results:
|
||||||
|
pkgs = item["invocation"]["module_args"]["name"]
|
||||||
|
if isinstance(pkgs, list):
|
||||||
|
for ii in pkgs:
|
||||||
|
packages.add(ii)
|
||||||
|
else:
|
||||||
|
packages.add(pkgs)
|
||||||
|
# tell python black that this line is ok
|
||||||
|
# fmt: off
|
||||||
|
self._display.display("lsrpackages: " + " ".join(sorted(list(packages))))
|
||||||
|
# fmt: on
|
||||||
+36
-6
@@ -15,22 +15,52 @@
|
|||||||
|
|
||||||
- name: Run the role
|
- name: Run the role
|
||||||
include_tasks: tasks/run_role_with_clear_facts.yml
|
include_tasks: tasks/run_role_with_clear_facts.yml
|
||||||
|
vars:
|
||||||
|
__aide_write_log_file: true
|
||||||
|
|
||||||
# look for the exact module invocation, not some other message that might contain the string
|
# look for the exact module invocation, not some other message that might contain the string
|
||||||
- name: Check system journal contains role fingerprints
|
- name: Get fingerprint entries from journal
|
||||||
ansible.builtin.shell:
|
ansible.builtin.shell:
|
||||||
executable: /bin/bash
|
executable: /bin/bash
|
||||||
cmd: >-
|
cmd: >-
|
||||||
set -eo pipefail;
|
set -eo pipefail;
|
||||||
journalctl --since "{{ __journal_start_time }}" --no-pager |
|
journalctl --since "{{ __journal_start_time }}" --no-pager |
|
||||||
grep -v " Invoked with" | grep "sr_fingerprint.*begin system_role:aide" ||
|
grep -v " Invoked with" |
|
||||||
{ echo ERROR: BEGIN fingerprint not found; exit 1; };
|
grep "sr_fingerprint.*role_name=aide"
|
||||||
journalctl --since "{{ __journal_start_time }}" --no-pager |
|
register: __register_journal_fingerprints
|
||||||
grep -v " Invoked with" | grep "sr_fingerprint.*success system_role:aide" ||
|
|
||||||
{ echo ERROR: SUCCESS fingerprint not found; exit 1; }
|
|
||||||
changed_when: false
|
changed_when: false
|
||||||
when: __register_dev_log.stat.exists
|
when: __register_dev_log.stat.exists
|
||||||
|
|
||||||
|
- name: Check that the log file was written
|
||||||
|
ansible.builtin.slurp:
|
||||||
|
path: /var/log/sysroles.jsonl
|
||||||
|
register: __register_log_file
|
||||||
|
|
||||||
|
- name: Verify log file and journal fingerprints
|
||||||
|
when: __register_dev_log.stat.exists
|
||||||
|
vars:
|
||||||
|
__journal_lines: "{{ __register_journal_fingerprints.stdout_lines }}"
|
||||||
|
__journal_begin: "{{ __journal_lines | select('search', 'status=begin') | list }}"
|
||||||
|
__journal_success: "{{ __journal_lines | select('search', 'status=success') | list }}"
|
||||||
|
__begin_date: "{{ (__journal_begin[0] | regex_search('date=([^ ]+)', '\\1'))[0] }}"
|
||||||
|
__success_date: "{{ (__journal_success[0] | regex_search('date=([^ ]+)', '\\1'))[0] }}"
|
||||||
|
__file_content: "{{ __register_log_file.content | b64decode }}"
|
||||||
|
block:
|
||||||
|
- name: Print contents of logs
|
||||||
|
debug:
|
||||||
|
var: item
|
||||||
|
loop:
|
||||||
|
- "{{ __file_content }}"
|
||||||
|
- "{{ __journal_lines }}"
|
||||||
|
|
||||||
|
- name: Assert content is correct
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- __journal_begin | length > 0
|
||||||
|
- __journal_success | length > 0
|
||||||
|
- __begin_date in __file_content
|
||||||
|
- __success_date in __file_content
|
||||||
|
|
||||||
- name: Check if the file exists
|
- name: Check if the file exists
|
||||||
ansible.builtin.stat:
|
ansible.builtin.stat:
|
||||||
path: /etc/aide.conf
|
path: /etc/aide.conf
|
||||||
|
|||||||
@@ -0,0 +1,414 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright: (c) 2026, Red Hat, Inc.
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
"""Unit tests for sr_fingerprint module helpers."""
|
||||||
|
|
||||||
|
from __future__ import absolute_import, division, print_function
|
||||||
|
|
||||||
|
__metaclass__ = type
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
import sr_fingerprint
|
||||||
|
|
||||||
|
|
||||||
|
class _ExitJsonException(Exception):
|
||||||
|
def __init__(self, kwargs):
|
||||||
|
self.kwargs = kwargs
|
||||||
|
|
||||||
|
|
||||||
|
class _FailJsonException(Exception):
|
||||||
|
def __init__(self, kwargs):
|
||||||
|
self.kwargs = kwargs
|
||||||
|
|
||||||
|
|
||||||
|
class _FakeModule(object):
|
||||||
|
ansible_version = "2.16.3"
|
||||||
|
|
||||||
|
def __init__(self, params=None, check_mode=False):
|
||||||
|
self.params = params or {}
|
||||||
|
self.check_mode = check_mode
|
||||||
|
self.logged = []
|
||||||
|
|
||||||
|
def log(self, msg):
|
||||||
|
self.logged.append(msg)
|
||||||
|
|
||||||
|
def exit_json(self, **kwargs):
|
||||||
|
raise _ExitJsonException(kwargs)
|
||||||
|
|
||||||
|
def fail_json(self, **kwargs):
|
||||||
|
raise _FailJsonException(kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def _cleanup_log(log_file):
|
||||||
|
for path in (log_file, log_file + ".lock"):
|
||||||
|
try:
|
||||||
|
os.unlink(path)
|
||||||
|
except OSError:
|
||||||
|
# file may not exist
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _sample_fingerprint_record():
|
||||||
|
return {
|
||||||
|
"date": "2026-06-10T12:00:00+00:00",
|
||||||
|
"role_name": "systemd",
|
||||||
|
"role_path": "/usr/share/ansible/roles/linux-system-roles.systemd",
|
||||||
|
"status": "begin",
|
||||||
|
"ansible_version": "2.16.3",
|
||||||
|
"managed_node_distro": "RedHat-9.4",
|
||||||
|
"play_hosts_number": 3,
|
||||||
|
"ansible_check_mode": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class TestSrFingerprint(unittest.TestCase):
|
||||||
|
def test_fingerprint_fields_match_record_keys(self):
|
||||||
|
record = _sample_fingerprint_record()
|
||||||
|
self.assertEqual(set(sr_fingerprint.FINGERPRINT_FIELDS), set(record.keys()))
|
||||||
|
|
||||||
|
def test_format_fingerprint_syslog(self):
|
||||||
|
record = _sample_fingerprint_record()
|
||||||
|
message = sr_fingerprint._format_fingerprint_syslog(record)
|
||||||
|
self.assertEqual(
|
||||||
|
message,
|
||||||
|
"date=2026-06-10T12:00:00+00:00 role_name=systemd "
|
||||||
|
"role_path=/usr/share/ansible/roles/linux-system-roles.systemd status=begin "
|
||||||
|
"ansible_version=2.16.3 managed_node_distro=RedHat-9.4 "
|
||||||
|
"play_hosts_number=3 ansible_check_mode=False",
|
||||||
|
)
|
||||||
|
for field in sr_fingerprint.FINGERPRINT_FIELDS:
|
||||||
|
self.assertIn("%s=" % field, message)
|
||||||
|
|
||||||
|
def test_format_fingerprint_jsonl(self):
|
||||||
|
record = _sample_fingerprint_record()
|
||||||
|
line = sr_fingerprint._format_fingerprint_jsonl(record)
|
||||||
|
parsed = json.loads(line)
|
||||||
|
self.assertEqual(parsed, record)
|
||||||
|
|
||||||
|
def test_collect_fingerprint_record_from_passed_inputs(self):
|
||||||
|
module = _FakeModule(
|
||||||
|
{
|
||||||
|
"role_name": "systemd",
|
||||||
|
"role_path": "/usr/share/ansible/roles/linux-system-roles.systemd",
|
||||||
|
"ansible_play_hosts_all": ["host1", "host2", "host3"],
|
||||||
|
"distribution": "RedHat",
|
||||||
|
"distribution_version": "9.4",
|
||||||
|
},
|
||||||
|
check_mode=True,
|
||||||
|
)
|
||||||
|
record = sr_fingerprint._collect_fingerprint_record(module, "begin")
|
||||||
|
self.assertEqual(record["role_name"], "systemd")
|
||||||
|
self.assertEqual(
|
||||||
|
record["role_path"], "/usr/share/ansible/roles/linux-system-roles.systemd"
|
||||||
|
)
|
||||||
|
self.assertEqual(record["managed_node_distro"], "RedHat-9.4")
|
||||||
|
self.assertEqual(record["play_hosts_number"], 3)
|
||||||
|
self.assertTrue(record["ansible_check_mode"])
|
||||||
|
self.assertEqual(
|
||||||
|
set(record.keys()),
|
||||||
|
set(sr_fingerprint.FINGERPRINT_FIELDS),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_get_managed_node_distro_from_params(self):
|
||||||
|
distro = sr_fingerprint._get_managed_node_distro("Fedora", "42")
|
||||||
|
self.assertEqual(distro, "Fedora-42")
|
||||||
|
|
||||||
|
def test_get_managed_node_distro_missing(self):
|
||||||
|
self.assertEqual(sr_fingerprint._get_managed_node_distro("", ""), "unknown")
|
||||||
|
|
||||||
|
def test_get_play_hosts_number(self):
|
||||||
|
self.assertEqual(
|
||||||
|
sr_fingerprint._get_play_hosts_number(["a", "b"]),
|
||||||
|
2,
|
||||||
|
)
|
||||||
|
self.assertEqual(sr_fingerprint._get_play_hosts_number([]), 0)
|
||||||
|
|
||||||
|
def test_format_fingerprint_syslog_quotes_values_with_spaces(self):
|
||||||
|
record = _sample_fingerprint_record()
|
||||||
|
record["role_path"] = (
|
||||||
|
"/usr/share/ansible/roles/linux-system-roles.systemd extra"
|
||||||
|
)
|
||||||
|
message = sr_fingerprint._format_fingerprint_syslog(record)
|
||||||
|
self.assertIn(
|
||||||
|
'role_path="/usr/share/ansible/roles/linux-system-roles.systemd extra"',
|
||||||
|
message,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_write_jsonl_log_appends_valid_json_lines(self):
|
||||||
|
with tempfile.NamedTemporaryFile(delete=False, suffix=".jsonl") as tmp:
|
||||||
|
log_file = tmp.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
record = _sample_fingerprint_record()
|
||||||
|
sr_fingerprint._write_jsonl_log(log_file, record)
|
||||||
|
sr_fingerprint._write_jsonl_log(log_file, record)
|
||||||
|
|
||||||
|
with open(log_file, "r") as log_fd:
|
||||||
|
lines = log_fd.read().splitlines()
|
||||||
|
|
||||||
|
self.assertEqual(len(lines), 2)
|
||||||
|
for line in lines:
|
||||||
|
parsed = json.loads(line)
|
||||||
|
self.assertEqual(parsed, record)
|
||||||
|
finally:
|
||||||
|
_cleanup_log(log_file)
|
||||||
|
|
||||||
|
def test_write_jsonl_log_creates_parent_dir(self):
|
||||||
|
tmpdir = tempfile.mkdtemp()
|
||||||
|
log_file = os.path.join(tmpdir, "subdir", "fingerprint.jsonl")
|
||||||
|
|
||||||
|
try:
|
||||||
|
record = _sample_fingerprint_record()
|
||||||
|
sr_fingerprint._write_jsonl_log(log_file, record)
|
||||||
|
|
||||||
|
with open(log_file, "r") as log_fd:
|
||||||
|
parsed = json.loads(log_fd.readline())
|
||||||
|
self.assertEqual(parsed["role_name"], "systemd")
|
||||||
|
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)
|
||||||
|
|
||||||
|
def test_write_jsonl_log_preserves_types(self):
|
||||||
|
with tempfile.NamedTemporaryFile(delete=False, suffix=".jsonl") as tmp:
|
||||||
|
log_file = tmp.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
record = _sample_fingerprint_record()
|
||||||
|
sr_fingerprint._write_jsonl_log(log_file, record)
|
||||||
|
|
||||||
|
with open(log_file, "r") as log_fd:
|
||||||
|
parsed = json.loads(log_fd.readline())
|
||||||
|
|
||||||
|
self.assertIsInstance(parsed["play_hosts_number"], int)
|
||||||
|
self.assertIsInstance(parsed["ansible_check_mode"], bool)
|
||||||
|
finally:
|
||||||
|
_cleanup_log(log_file)
|
||||||
|
|
||||||
|
def test_trim_removes_oldest_lines(self):
|
||||||
|
with tempfile.NamedTemporaryFile(delete=False, suffix=".jsonl") as tmp:
|
||||||
|
log_file = tmp.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
record = _sample_fingerprint_record()
|
||||||
|
sample = dict(record, role_name="role_0")
|
||||||
|
line_size = len(sr_fingerprint._format_fingerprint_jsonl(sample) + "\n")
|
||||||
|
max_size = line_size * 5
|
||||||
|
for _i in range(10):
|
||||||
|
record_copy = dict(record, role_name="role_%d" % _i)
|
||||||
|
sr_fingerprint._write_jsonl_log(
|
||||||
|
log_file, record_copy, max_size=max_size
|
||||||
|
)
|
||||||
|
|
||||||
|
with open(log_file, "r") as log_fd:
|
||||||
|
lines = log_fd.read().splitlines()
|
||||||
|
|
||||||
|
self.assertEqual(len(lines), 5)
|
||||||
|
first = json.loads(lines[0])
|
||||||
|
last = json.loads(lines[-1])
|
||||||
|
self.assertEqual(first["role_name"], "role_5")
|
||||||
|
self.assertEqual(last["role_name"], "role_9")
|
||||||
|
finally:
|
||||||
|
_cleanup_log(log_file)
|
||||||
|
|
||||||
|
def test_trim_multiple_lines(self):
|
||||||
|
with tempfile.NamedTemporaryFile(delete=False, suffix=".jsonl") as tmp:
|
||||||
|
log_file = tmp.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
record = _sample_fingerprint_record()
|
||||||
|
sample = dict(record, role_name="role_0")
|
||||||
|
line_size = len(sr_fingerprint._format_fingerprint_jsonl(sample) + "\n")
|
||||||
|
# Log contains more than 3 lines.
|
||||||
|
n_initial = 4
|
||||||
|
max_size = line_size * n_initial
|
||||||
|
for _i in range(n_initial):
|
||||||
|
sr_fingerprint._write_jsonl_log(
|
||||||
|
log_file,
|
||||||
|
dict(record, role_name="role_%d" % _i),
|
||||||
|
max_size=max_size,
|
||||||
|
)
|
||||||
|
|
||||||
|
with open(log_file, "r") as log_fd:
|
||||||
|
initial_lines = log_fd.read().splitlines()
|
||||||
|
self.assertEqual(len(initial_lines), n_initial)
|
||||||
|
|
||||||
|
# New record larger than one existing line (up to two) via a very
|
||||||
|
# long role_path, so trim removes exactly two oldest records.
|
||||||
|
base_record = dict(record, role_name="role_long", role_path="")
|
||||||
|
base_size = len(
|
||||||
|
sr_fingerprint._format_fingerprint_jsonl(base_record) + "\n"
|
||||||
|
)
|
||||||
|
path_len = 2 * line_size - base_size
|
||||||
|
self.assertGreater(path_len, 0)
|
||||||
|
long_path = "x" * path_len
|
||||||
|
long_record = dict(record, role_name="role_long", role_path=long_path)
|
||||||
|
new_line = sr_fingerprint._format_fingerprint_jsonl(long_record) + "\n"
|
||||||
|
self.assertGreater(len(new_line), line_size)
|
||||||
|
self.assertLessEqual(len(new_line), 2 * line_size)
|
||||||
|
|
||||||
|
sr_fingerprint._write_jsonl_log(log_file, long_record, max_size=max_size)
|
||||||
|
|
||||||
|
with open(log_file, "r") as log_fd:
|
||||||
|
lines = log_fd.read().splitlines()
|
||||||
|
|
||||||
|
# Exactly two oldest records removed; new record appended.
|
||||||
|
self.assertEqual(len(lines), n_initial - 2 + 1)
|
||||||
|
parsed = [json.loads(line) for line in lines]
|
||||||
|
role_names = [entry["role_name"] for entry in parsed]
|
||||||
|
self.assertEqual(role_names, ["role_2", "role_3", "role_long"])
|
||||||
|
self.assertEqual(parsed[-1], long_record)
|
||||||
|
self.assertEqual(parsed[-1]["role_path"], long_path)
|
||||||
|
self.assertNotIn("role_0", role_names)
|
||||||
|
self.assertNotIn("role_1", role_names)
|
||||||
|
finally:
|
||||||
|
_cleanup_log(log_file)
|
||||||
|
|
||||||
|
def test_trim_disabled_when_zero(self):
|
||||||
|
with tempfile.NamedTemporaryFile(delete=False, suffix=".jsonl") as tmp:
|
||||||
|
log_file = tmp.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
record = _sample_fingerprint_record()
|
||||||
|
for _i in range(20):
|
||||||
|
sr_fingerprint._write_jsonl_log(log_file, record, max_size=0)
|
||||||
|
|
||||||
|
with open(log_file, "r") as log_fd:
|
||||||
|
lines = log_fd.read().splitlines()
|
||||||
|
|
||||||
|
self.assertEqual(len(lines), 20)
|
||||||
|
finally:
|
||||||
|
_cleanup_log(log_file)
|
||||||
|
|
||||||
|
def test_trim_no_op_when_under_limit(self):
|
||||||
|
with tempfile.NamedTemporaryFile(delete=False, suffix=".jsonl") as tmp:
|
||||||
|
log_file = tmp.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
record = _sample_fingerprint_record()
|
||||||
|
for _i in range(3):
|
||||||
|
sr_fingerprint._write_jsonl_log(log_file, record, max_size=2000000)
|
||||||
|
|
||||||
|
with open(log_file, "r") as log_fd:
|
||||||
|
lines = log_fd.read().splitlines()
|
||||||
|
|
||||||
|
self.assertEqual(len(lines), 3)
|
||||||
|
finally:
|
||||||
|
_cleanup_log(log_file)
|
||||||
|
|
||||||
|
def test_handle_fingerprint_check_mode_without_log_file(self):
|
||||||
|
module = _FakeModule(
|
||||||
|
{
|
||||||
|
"status": "begin",
|
||||||
|
"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"],
|
||||||
|
"distribution": "RedHat",
|
||||||
|
"distribution_version": "9.4",
|
||||||
|
},
|
||||||
|
check_mode=True,
|
||||||
|
)
|
||||||
|
with self.assertRaises(_ExitJsonException) as ctx:
|
||||||
|
sr_fingerprint._handle_fingerprint(module)
|
||||||
|
result = ctx.exception.kwargs
|
||||||
|
self.assertFalse(result["changed"])
|
||||||
|
self.assertIn("Check mode", result["message"])
|
||||||
|
self.assertIn("fingerprint", result)
|
||||||
|
self.assertNotIn("jsonl_row", result)
|
||||||
|
|
||||||
|
def test_handle_fingerprint_check_mode_with_log_file(self):
|
||||||
|
log_path = os.path.join(tempfile.gettempdir(), "test_sr_fingerprint.jsonl")
|
||||||
|
module = _FakeModule(
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"write_log_file": True,
|
||||||
|
"log_file": log_path,
|
||||||
|
"max_log_size": 2000000,
|
||||||
|
"role_name": "systemd",
|
||||||
|
"role_path": "/usr/share/ansible/roles/linux-system-roles.systemd",
|
||||||
|
"ansible_play_hosts_all": ["host1"],
|
||||||
|
"distribution": "RedHat",
|
||||||
|
"distribution_version": "9.4",
|
||||||
|
},
|
||||||
|
check_mode=True,
|
||||||
|
)
|
||||||
|
with self.assertRaises(_ExitJsonException) as ctx:
|
||||||
|
sr_fingerprint._handle_fingerprint(module)
|
||||||
|
result = ctx.exception.kwargs
|
||||||
|
self.assertIn("jsonl_row", result)
|
||||||
|
self.assertEqual(result["log_file"], log_path)
|
||||||
|
parsed = json.loads(result["jsonl_row"])
|
||||||
|
self.assertEqual(parsed["role_name"], "systemd")
|
||||||
|
|
||||||
|
def test_handle_fingerprint_write_failure_calls_fail_json(self):
|
||||||
|
log_path = os.path.join(tempfile.gettempdir(), "test_write_fail.jsonl")
|
||||||
|
module = _FakeModule(
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"write_log_file": True,
|
||||||
|
"log_file": log_path,
|
||||||
|
"max_log_size": 2000000,
|
||||||
|
"role_name": "systemd",
|
||||||
|
"role_path": "/usr/share/ansible/roles/linux-system-roles.systemd",
|
||||||
|
"ansible_play_hosts_all": ["host1"],
|
||||||
|
"distribution": "RedHat",
|
||||||
|
"distribution_version": "9.4",
|
||||||
|
},
|
||||||
|
check_mode=False,
|
||||||
|
)
|
||||||
|
original = sr_fingerprint._write_jsonl_log
|
||||||
|
|
||||||
|
def _raise_ioerror(*args, **kwargs):
|
||||||
|
raise IOError("disk full")
|
||||||
|
|
||||||
|
sr_fingerprint._write_jsonl_log = _raise_ioerror
|
||||||
|
try:
|
||||||
|
with self.assertRaises(_FailJsonException) as ctx:
|
||||||
|
sr_fingerprint._handle_fingerprint(module)
|
||||||
|
self.assertIn(
|
||||||
|
"Failed to write fingerprint log file", ctx.exception.kwargs["msg"]
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
sr_fingerprint._write_jsonl_log = original
|
||||||
|
|
||||||
|
def test_handle_fingerprint_rejects_negative_max_log_size(self):
|
||||||
|
module = _FakeModule(
|
||||||
|
{
|
||||||
|
"status": "begin",
|
||||||
|
"write_log_file": False,
|
||||||
|
"max_log_size": -1,
|
||||||
|
"role_name": "systemd",
|
||||||
|
"role_path": "/usr/share/ansible/roles/linux-system-roles.systemd",
|
||||||
|
"ansible_play_hosts_all": ["host1"],
|
||||||
|
"distribution": "RedHat",
|
||||||
|
"distribution_version": "9.4",
|
||||||
|
},
|
||||||
|
check_mode=False,
|
||||||
|
)
|
||||||
|
with self.assertRaises(_FailJsonException) as ctx:
|
||||||
|
sr_fingerprint._handle_fingerprint(module)
|
||||||
|
self.assertIn(
|
||||||
|
"max_log_size must be 0 or a positive integer",
|
||||||
|
ctx.exception.kwargs["msg"],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_local_iso8601_no_microseconds_has_no_fraction(self):
|
||||||
|
timestamp = sr_fingerprint._local_iso8601_no_microseconds()
|
||||||
|
match = re.match(
|
||||||
|
r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[+-]\d{2}:?\d{2}$", timestamp
|
||||||
|
)
|
||||||
|
self.assertIsNotNone(match)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -23,6 +23,8 @@ __aide_required_facts:
|
|||||||
__aide_required_facts_subsets: "{{ ['!all', '!min'] +
|
__aide_required_facts_subsets: "{{ ['!all', '!min'] +
|
||||||
__aide_required_facts }}"
|
__aide_required_facts }}"
|
||||||
|
|
||||||
|
__aide_write_log_file: false
|
||||||
|
|
||||||
# BEGIN - DO NOT EDIT THIS BLOCK - rh distros variables
|
# BEGIN - DO NOT EDIT THIS BLOCK - rh distros variables
|
||||||
# Ansible distribution identifiers that the role treats like RHEL
|
# Ansible distribution identifiers that the role treats like RHEL
|
||||||
__aide_rh_distros:
|
__aide_rh_distros:
|
||||||
|
|||||||
Reference in New Issue
Block a user