Compare commits

..

No commits in common. "master" and "1.0.0" have entirely different histories.

12 changed files with 597 additions and 497 deletions

View File

@ -8,15 +8,13 @@
# so a drift-check here would always be OUT_OF_SYNC by
# design and is meaningless as a failure signal.
#
# push (master) → syntax-check, validate, deploy, update-gitops-status
# Deploys to the server, then verifies sync and sends
# JSON status snapshot to gitops-status-server for Grafana.
# push (master) → syntax-check, validate, deploy, update-sync-metric
# Deploys to the server, then verifies sync and pushes metric.
#
# cron → gitops_sync_check (read-only drift check, no deploy)
# Continuously verifies that the live server still matches
# Git even when no push has happened. Detects manual edits
# made directly on the server. Sends JSON status with
# detailed file-level drift information to gitops-status-server.
# made directly on the server.
#
# NOTE: Woodpecker does not support multiple YAML documents (---) in one file.
# All pipelines must live in a single document with step-level filtering.
@ -82,48 +80,44 @@ steps:
event: push
# ---------------------------------------------------------------------------
# update-gitops-status: Post-deploy sync check + JSON status update
# update-sync-metric: Post-deploy sync check + Prometheus metric push
# Runs on push to master only, after deploy succeeds.
# Generates structured JSON with sync status, drift count, and changed files.
# Sends JSON to gitops-status-server for Grafana visualization.
# STATUS=1 means SYNCED, STATUS=0 means OUT_OF_SYNC.
# ---------------------------------------------------------------------------
update-gitops-status:
update-sync-metric:
image: alpine/ansible:latest
depends_on: [deploy]
environment:
ANSIBLE_CONFIG: ansible.cfg
SSH_PRIVATE_KEY:
from_secret: SSH_PRIVATE_KEY
GITOPS_STATUS_SERVER_URL: http://gitops-status-server.observability-stack.svc.cluster.local:5000
REPO_NAME: rsyslog
SERVER_NAME: rsyslog-lab
MODE: post-deploy
# Optimize Ansible for container environment
ANSIBLE_HOST_KEY_CHECKING: "False"
ANSIBLE_FORCE_COLOR: "False"
ANSIBLE_RETRY_FILES_ENABLED: "False"
ANSIBLE_UNSAFE_WRITES: "True"
ANSIBLE_FORKS: "1"
PUSHGATEWAY_URL: http://pushgateway.observability-stack.svc.cluster.local:9091
commands:
- |
# Increase file descriptor limit for Ansible (max safe value)
ulimit -n 65536
# Install dependencies: git for detecting deployed files, curl for HTTP requests, jq for JSON formatting
apk add --no-cache git curl jq > /dev/null 2>&1
# Setup SSH key for Ansible
apk add --no-cache curl > /dev/null 2>&1
mkdir -p ~/.ssh
printf '%s\n' "$${SSH_PRIVATE_KEY}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
echo "==> Running post-deploy GitOps status check..."
echo "==> Verifying post-deploy sync status..."
set +e
ansible-playbook -i ansible/inventory/hosts.yml ansible/playbooks/drift-check.yml
DRIFT_RC=$?
set -e
# Make script executable and run it
chmod +x update-gitops-status.sh
./update-gitops-status.sh
if [ "$DRIFT_RC" -eq 0 ]; then
STATUS=1
echo "==> SYNCED (1) server configuration matches Git"
else
STATUS=0
echo "==> OUT OF SYNC (0) drift detected after deploy"
fi
echo "==> JSON status update complete. Pipeline always succeeds."
printf 'gitops_sync_status{repo="rsyslog",server="rsyslog-lab"} %s\n' "$STATUS" | \
curl --silent --show-error --fail --data-binary @- \
"$${PUSHGATEWAY_URL}/metrics/job/gitops_rsyslog/instance/rsyslog-lab"
echo "==> Metric pushed. Pipeline always succeeds; sync status is in Prometheus."
when:
branch: master
event: push
@ -131,14 +125,13 @@ steps:
# ---------------------------------------------------------------------------
# gitops_sync_check: ArgoCD-style cron drift check read-only, no deploy
# Detects manual changes made directly on the server between pushes.
# Generates structured JSON with sync status, drift count, and changed files.
# Sends JSON to gitops-status-server for continuous GitOps monitoring.
# Pipeline always succeeds - check Grafana dashboard for drift status.
# STATUS=1 → SYNCED, STATUS=0 → OUT_OF_SYNC
# Pipeline marked FAILED when drift found so it is visible in the UI.
#
# ─── Woodpecker Cron UI settings ──────────────────────────────────────────
# Name: gitops_sync_check
# Branch: master
# Schedule: */1 * * * * (every 1 minute for testing, adjust as needed)
# Schedule: */2 * * * *
# ---------------------------------------------------------------------------
gitops_sync_check:
image: alpine/ansible:latest
@ -146,35 +139,34 @@ steps:
ANSIBLE_CONFIG: ansible.cfg
SSH_PRIVATE_KEY:
from_secret: SSH_PRIVATE_KEY
GITOPS_STATUS_SERVER_URL: http://gitops-status-server.observability-stack.svc.cluster.local:5000
REPO_NAME: rsyslog
SERVER_NAME: rsyslog-lab
# Optimize Ansible for container environment
ANSIBLE_HOST_KEY_CHECKING: "False"
ANSIBLE_FORCE_COLOR: "False"
ANSIBLE_RETRY_FILES_ENABLED: "False"
ANSIBLE_UNSAFE_WRITES: "True"
ANSIBLE_FORKS: "1"
PUSHGATEWAY_URL: http://pushgateway.observability-stack.svc.cluster.local:9091
commands:
- |
# Increase file descriptor limit for Ansible (max safe value)
ulimit -n 65536
apk add --no-cache curl > /dev/null 2>&1
# Install dependencies: curl for HTTP requests, jq for JSON formatting
apk add --no-cache curl jq bash > /dev/null 2>&1
# Setup SSH key for Ansible
mkdir -p ~/.ssh
printf '%s\n' "$${SSH_PRIVATE_KEY}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
echo "==> [cron] Running continuous GitOps drift check..."
echo "==> [cron] Running drift check against remote server..."
set +e
ansible-playbook -i ansible/inventory/hosts.yml ansible/playbooks/drift-check.yml
DRIFT_RC=$?
set -e
# Make script executable and run it (always succeeds - just updates JSON)
chmod +x update-gitops-status.sh
./update-gitops-status.sh
if [ "$DRIFT_RC" -eq 0 ]; then
STATUS=1
echo "==> STATUS: SYNCED (1) server configuration matches Git"
else
STATUS=0
echo "==> STATUS: OUT OF SYNC (0) manual drift detected on server"
fi
echo "==> Cron drift check complete. JSON status updated successfully."
echo " Pipeline always succeeds. Check gitops-status-server for sync status."
echo "==> Pushing metric: gitops_sync_status{repo=\"rsyslog\",server=\"rsyslog-lab\"} $STATUS"
printf 'gitops_sync_status{repo="rsyslog",server="rsyslog-lab"} %s\n' "$STATUS" | \
curl --silent --show-error --fail --data-binary @- \
"$${PUSHGATEWAY_URL}/metrics/job/gitops_rsyslog/instance/rsyslog-lab"
echo "==> Metric pushed. Pipeline always succeeds; sync status is in Prometheus."
when:
event: cron

463
README.md
View File

@ -1,348 +1,187 @@
# File Deployment & GitOps Management
# rsyslog GitOps
A simple, generic Ansible-based system to deploy and manage files on multiple servers using Git as the single source of truth.
Manage rsyslog configuration on Linux servers using Git as the single source of truth.
If it's not in Git, it doesn't belong on the server.
---
## Overview
## How it works in one sentence
This repository uses **Ansible** to:
- **Deploy** files from Git to target servers
- **Check Drift** to ensure servers stay in sync with the repository
- **Validate** that deployed files are correct
No rsyslog-specific code. Just simple file deployment that works for any file or service.
Every change goes through Git. The pipeline makes sure the server always matches what's in Git — and if someone changes the server directly, the system detects it automatically.
---
## Project Structure
## The three pipelines
### 1. Pull Request — "Is this config safe?"
Triggered when you open or update a pull request.
Does **not** touch the live server beyond a basic reachability check.
Does **not** compare the PR content to the server (they're expected to differ before merge).
```
.
├── README.md # This file
├── ansible.cfg # Ansible configuration
├── .woodpecker.yml # CI/CD pipeline configuration
Open PR
├── ansible/
│ ├── inventory/
│ │ ├── hosts.yml # Define target servers
│ │ └── group_vars/
│ │ └── all.yml # Global variables (SSH credentials, etc.)
│ │
│ └── playbooks/
│ ├── apply.yml # Deploy file to servers
│ ├── drift-check.yml # Check if servers are in sync with repo
│ └── validate.yml # Verify file exists on server
├─► syntax-check Check the YAML/Ansible syntax is valid
└── files/
└── dvir.txt # The file to deploy (edit this to your needs)
└─► validate Connect to the server and verify rsyslog is running
and the current config is loadable
```
**Pass** = safe to review and merge.
**Fail** = syntax error or server is unreachable / config is broken.
---
### 2. Push to master — "Deploy and verify"
Triggered when a PR is merged into master.
```
Merge to master
├─► syntax-check Same lint check as PR
├─► validate Same server check as PR
├─► deploy Copy the new config files from Git to the server
│ and restart rsyslog
└─► update-sync-metric Run a diff between Git and the live server
├─ Matches? → push metric 1 (SYNCED)
└─ Differs? → push metric 0 (OUT_OF_SYNC)
```
**Pass** = new config is live and the server matches Git.
The sync result is always sent to Prometheus regardless of outcome.
---
### 3. Cron — "Is the server still synced?"
Runs automatically every 2 minutes, **even with no new push**.
This is the ArgoCD-style continuous check.
It only reads — never deploys anything.
```
Every 2 minutes (cron)
└─► gitops_sync_check SSH to the server, compare every managed config
file against the latest Git commit
├─ Matches? → push metric 1 (SYNCED)
└─ Differs? → push metric 0 (OUT_OF_SYNC)
```
**Why this matters:** if someone edits `/etc/rsyslog.conf` directly on the server
(bypassing Git), the next cron run catches it within 2 minutes and marks OUT_OF_SYNC.
---
## Full flow diagram
```
Developer Woodpecker CI Linux Server Prometheus
│ │ │ │
│── open PR ───────────────►│ │ │
│ │── syntax-check │ │
│ │── validate ─────────────►│ │
│◄── PR ok / failed ────────│ │ │
│ │ │ │
│── merge to master ───────►│ │ │
│ │── syntax-check │ │
│ │── validate ─────────────►│ │
│ │── deploy ───────────────►│ write config │
│ │ │ restart rsyslog │
│ │── drift-check ──────────►│ compare files │
│ │ │◄────────────────────│
│ │── metric (1 or 0) ───────────────────────────►│
│ │ │ │
│ │ [every 2 min, no push] │ │
│ │── drift-check ──────────►│ compare files │
│ │── metric (1 or 0) ───────────────────────────►│
│ │ │ │
Someone edits the server directly (bad):
rogue admin Woodpecker CI Linux Server Prometheus
│ │ │ │
│── ssh rsyslog-lab │ │ │
│── vim /etc/rsyslog.conf ──────────────────────────► │ file changed │
│ │ │ │
│ [2 min later, cron runs] │ │
│ │── drift-check ──────────►│ diff detected │
│ │── metric 0 (OUT_OF_SYNC)────────────────────►│
│ │ │ alert fires
```
---
## How It Works (Simple Version)
## What is the sync metric?
1. **You edit the file** in `files/dvir.txt`
2. **You commit to Git** (the source of truth)
3. **Run `apply.yml`** to deploy to all servers
4. **Run `drift-check.yml`** anytime to verify servers match Git
5. **If drift is detected**, run `apply.yml` again to fix it
```
gitops_sync_status{repo="rsyslog", server="rsyslog-lab"}
```
That's it!
| Value | Meaning |
|-------|---------|
| `1` | Server config matches Git (SYNCED) |
| `0` | Server config differs from Git (OUT_OF_SYNC) |
Alert on `gitops_sync_status == 0` in Grafana/Alertmanager.
---
## Quick Start
## What drift-check actually compares
### 1. Edit the file you want to deploy
The drift-check playbook compares files **from the Woodpecker CI container** (always the latest Git commit) against the live server. It checks:
Open `files/dvir.txt` and add your content:
1. `/etc/rsyslog.conf` — must match `files/rsyslog.conf` in Git
2. `/etc/rsyslog.d/30-lab.conf` — must match `files/rsyslog.d/30-lab.conf` in Git
3. Any file managed by Git must not be missing from the server
Files on the server that are **not** in Git (e.g. `50-default.conf`, `20-ufw.conf`) are ignored — they are owned by the OS and are not our concern.
---
## Repository structure
```bash
nano files/dvir.txt
```
To deploy a **different file**, rename it or update the paths in the playbooks.
### 2. Add target servers
Edit `ansible/inventory/hosts.yml`:
```yaml
all:
children:
servers:
hosts:
server1:
ansible_host: 192.168.10.161
server2:
ansible_host: 192.168.10.162
```
### 3. Run the playbooks
#### Deploy the file:
```bash
ansible-playbook ansible/playbooks/apply.yml
```
#### Check if servers are in sync:
```bash
ansible-playbook ansible/playbooks/drift-check.yml
```
#### Validate file on server:
```bash
ansible-playbook ansible/playbooks/validate.yml
.woodpecker.yml CI pipeline definition
ansible/
inventory/
hosts.yml Server list
group_vars/all.yml Variables (paths, user, etc.)
playbooks/
validate.yml Check rsyslog is running and config loads
apply.yml Deploy config files from Git to server
drift-check.yml Compare Git files to live server (read-only)
files/
rsyslog.conf Main rsyslog config (source of truth)
rsyslog.d/
30-lab.conf Drop-in config for lab logging
```
---
## Playbook Breakdown
## Woodpecker cron setup
### `apply.yml` - Deploy Files
**What it does:**
- Copies `files/dvir.txt` to `/tmp/dvir.txt` on all target servers
- Sets file ownership to `root:root` with permissions `0644`
Go to **Repository Settings → Crons → Add cron**:
**When to use:**
- Initial deployment of the file
- After updating the file in Git
**Example:**
```bash
ansible-playbook ansible/playbooks/apply.yml
```
| Field | Value |
|----------|---------------------|
| Name | `gitops_sync_check` |
| Branch | `master` |
| Schedule | `*/2 * * * *` |
---
### `drift-check.yml` - Detect Configuration Drift
**What it does:**
- Reads the file from the Git repository (local)
- Reads the file from each target server (`/tmp/dvir.txt`)
- Compares the content byte-for-byte
- Reports `SYNCED` or `OUT_OF_SYNC`
## Required secrets
**When to use:**
- Verify servers match the repository state
- Detect if someone manually changed the file on the server
- Run periodically (via cron or CI/CD) to monitor compliance
Go to **Repository Settings → Secrets**:
**Example:**
```bash
ansible-playbook ansible/playbooks/drift-check.yml
```
**Output:**
```
✓ dvir.txt is synced # Files match
✗ dvir.txt is out of sync # Files differ or file missing
```
---
### `validate.yml` - Validate Deployment
**What it does:**
- Checks that `/tmp/dvir.txt` exists on the server
- Verifies the file is readable
- Fails if the file is missing or not readable
**When to use:**
- After running `apply.yml` to verify success
- To confirm the deployment was successful
**Example:**
```bash
ansible-playbook ansible/playbooks/validate.yml
```
---
## Configuration
### Inventory: `ansible/inventory/hosts.yml`
Define which servers to manage:
```yaml
all:
children:
servers:
hosts:
server1:
ansible_host: 192.168.10.161
server2:
ansible_host: 192.168.10.162
```
### Global Variables: `ansible/inventory/group_vars/all.yml`
SSH and connection settings:
```yaml
ansible_user: root
ansible_ssh_private_key_file: "~/.ssh/id_rsa"
ansible_ssh_common_args: "-o StrictHostKeyChecking=no"
```
### Ansible Config: `ansible.cfg`
Global Ansible settings (host checking, plugins, etc.)
---
## Workflow Examples
### Example 1: Deploy a new file
```bash
# 1. Edit the file
echo "new config content" > files/dvir.txt
# 2. Commit to Git
git add files/dvir.txt
git commit -m "Update deployment file"
git push
# 3. Deploy to servers
ansible-playbook ansible/playbooks/apply.yml
# 4. Verify success
ansible-playbook ansible/playbooks/validate.yml
```
### Example 2: Monitor for drift (continuous compliance)
```bash
# Run drift check periodically (cron job)
0 */6 * * * cd /path/to/repo && ansible-playbook ansible/playbooks/drift-check.yml
```
### Example 3: Detect and fix manual changes
```bash
# Someone manually edited /tmp/dvir.txt on server1
# Check for drift
ansible-playbook ansible/playbooks/drift-check.yml
# Output: ✗ dvir.txt is out of sync
# Restore from Git
ansible-playbook ansible/playbooks/apply.yml
# Verify it's fixed
ansible-playbook ansible/playbooks/drift-check.yml
# Output: ✓ dvir.txt is synced
```
---
## Prerequisites
- **Ansible** installed on your machine
- **SSH access** to all target servers (password or key-based)
- **Root or sudo access** on target servers (for writing to `/tmp`)
### Install Ansible
```bash
# macOS
brew install ansible
# Ubuntu/Debian
apt-get install ansible
# RHEL/CentOS
yum install ansible
# Python pip
pip install ansible
```
---
## Customization
### Deploy to a different path
Edit `ansible/playbooks/apply.yml`:
```yaml
- name: Copy file to destination
copy:
src: ../../files/dvir.txt
dest: /your/custom/path/filename.txt # ← Change this
owner: root
group: root
mode: "0644"
```
### Deploy multiple files
Add more tasks to `apply.yml`:
```yaml
- name: Copy file 1
copy:
src: ../../files/dvir.txt
dest: /tmp/dvir.txt
- name: Copy file 2
copy:
src: ../../files/another.txt
dest: /tmp/another.txt
```
### Use different target servers
Edit `hosts.yml` and use:
```bash
ansible-playbook ansible/playbooks/apply.yml -i ansible/inventory/hosts.yml --limit "server2"
```
---
## Troubleshooting
### "SSH connection refused"
- Check `ansible_host` is correct in `hosts.yml`
- Verify SSH key in `group_vars/all.yml`
- Test manually: `ssh -i ~/.ssh/id_rsa root@192.168.10.161`
### "Permission denied" on deployment
- Ensure `become: true` is in the playbook
- Verify user has sudo access
- Check SSH key has correct permissions: `chmod 600 ~/.ssh/id_rsa`
### Drift check shows "out of sync" but I didn't change anything
- File permissions or ownership might have changed
- Line endings (CRLF vs LF) might differ
- The server file might be missing
### Can't read file on server
- Check `/tmp/dvir.txt` exists: `ls -la /tmp/dvir.txt`
- Verify permissions: `stat /tmp/dvir.txt`
- Ensure file is readable: `cat /tmp/dvir.txt`
---
## Tips & Best Practices
1. **Always commit before deploying**
- Use Git as the single source of truth
- Never manually edit `/tmp/dvir.txt` on servers
2. **Run drift-check regularly**
- Use cron or CI/CD to monitor compliance
- Alert on `OUT_OF_SYNC` status
3. **Test in dev first**
- Add a `dev` group in `hosts.yml`
- Test playbooks on dev servers before prod
4. **Use version control for everything**
- Keep all changes in Git
- Easy rollback: just revert and re-run `apply.yml`
---
## License
MIT (or your preferred license)
| Name | Description |
|-------------------|------------------------------------|
| `SSH_PRIVATE_KEY` | Private key to SSH into the server |

View File

@ -1,7 +1,17 @@
---
# Global variables for deployment
# Global variables for rsyslog configuration management
# Ansible connection settings
ansible_user: root
ansible_ssh_private_key_file: "~/.ssh/id_rsa"
ansible_ssh_common_args: "-o StrictHostKeyChecking=no"
# Root directory of the rsyslog repository
repo_root: /root/rsyslog
# rsyslog service name
rsyslog_service: rsyslog
# Configuration paths
rsyslog_main_config: /etc/rsyslog.conf
rsyslog_config_dir: /etc/rsyslog.d

View File

@ -1,12 +1,12 @@
---
all:
children:
servers:
rsyslog_servers:
hosts:
server1:
rsyslog-lab:
ansible_host: 192.168.10.161
# Add more servers here:
# server2:
# Future servers can be added here:
# rsyslog-prod:
# ansible_host: 192.168.10.162
# server3:
# rsyslog-backup:
# ansible_host: 192.168.10.163

View File

@ -1,31 +1,145 @@
---
# =============================================================================
# APPLY PLAYBOOK
# Purpose: Deploy dvir.txt file to target servers at /tmp/dvir.txt
# Usage: ansible-playbook apply.yml
# =============================================================================
- name: Deploy file to servers
hosts: all
- name: Apply rsyslog configuration (safe staged deployment)
hosts: rsyslog_servers
become: true
vars:
backup_dir: /var/backups/rsyslog-ansible
backup_conf: "{{ backup_dir }}/rsyslog.conf.bak"
backup_confd: "{{ backup_dir }}/rsyslog.d.bak"
tasks:
# ─────────────────────────────────────────────────────────────────────
# TASK 1: Copy file to destination
# Copies the dvir.txt from the repo to /tmp/dvir.txt on target servers
# ─────────────────────────────────────────────────────────────────────
- name: Copy file to destination
# -------------------------------------------------------------------------
# STAGE 1 — Backup current working configuration
# -------------------------------------------------------------------------
- name: Ensure backup directory exists
file:
path: "{{ backup_dir }}"
state: directory
mode: "0700"
- name: Backup current rsyslog.conf
copy:
src: ../../files/dvir.txt
dest: /tmp/dvir.txt
src: "{{ rsyslog_main_config }}"
dest: "{{ backup_conf }}"
remote_src: true
mode: "0600"
- name: Remove stale rsyslog.d backup
file:
path: "{{ backup_confd }}"
state: absent
- name: Backup current rsyslog.d directory
copy:
src: "{{ rsyslog_config_dir }}/"
dest: "{{ backup_confd }}/"
remote_src: true
# -------------------------------------------------------------------------
# STAGE 2 — Deploy new configuration files from repo
# -------------------------------------------------------------------------
- name: Copy new rsyslog.conf from repo
copy:
src: ../../files/rsyslog.conf
dest: "{{ rsyslog_main_config }}"
owner: root
group: root
mode: "0644"
# ─────────────────────────────────────────────────────────────────────
# TASK 2: Confirm deployment success
# Displays success message with the hostname for verification
# ─────────────────────────────────────────────────────────────────────
- name: Confirm deployment
- name: Copy new rsyslog.d configs from repo
copy:
src: ../../files/rsyslog.d/
dest: "{{ rsyslog_config_dir }}/"
owner: root
group: root
mode: "0644"
# -------------------------------------------------------------------------
# STAGE 3 — Validate against the full real config tree on the remote host
# Runs rsyslogd -N1 against the actual /etc/rsyslog.conf so all includes,
# modules, and templates are resolved in the real environment.
# -------------------------------------------------------------------------
- name: Validate new configuration on remote host
command: rsyslogd -N1 -f "{{ rsyslog_main_config }}"
register: validation_result
changed_when: false
failed_when: false # We handle failure manually below
# -------------------------------------------------------------------------
# STAGE 4a — Validation FAILED: restore backup and abort
# -------------------------------------------------------------------------
- name: Print validation error output
debug:
msg: "✅ File deployed successfully to /tmp/dvir.txt on {{ inventory_hostname }}"
msg: |
##################################################
❌ RSYSLOG VALIDATION FAILED
##################################################
HOST : {{ inventory_hostname }} ({{ ansible_host }})
RC : {{ validation_result.rc }}
--- STDOUT ----------------------------------
{{ validation_result.stdout | default('(empty)') }}
--- STDERR ----------------------------------
{{ validation_result.stderr | default('(empty)') }}
##################################################
⚠ Rolling back to previous working configuration
##################################################
when: validation_result.rc != 0
- name: Restore rsyslog.conf from backup
copy:
src: "{{ backup_conf }}"
dest: "{{ rsyslog_main_config }}"
remote_src: true
mode: "0644"
when: validation_result.rc != 0
- name: Restore rsyslog.d from backup
copy:
src: "{{ backup_confd }}/"
dest: "{{ rsyslog_config_dir }}/"
remote_src: true
when: validation_result.rc != 0
- name: Fail pipeline — config restored to previous working state
fail:
msg: |
##################################################
❌ PIPELINE FAILED — rsyslog validation error
##################################################
Previous working config has been restored.
rsyslog was NOT restarted.
rc={{ validation_result.rc }}
stderr: {{ validation_result.stderr | default('(empty)') }}
##################################################
when: validation_result.rc != 0
# -------------------------------------------------------------------------
# STAGE 4b — Validation PASSED: restart rsyslog and report success
# -------------------------------------------------------------------------
- name: Restart rsyslog service
service:
name: "{{ rsyslog_service }}"
state: restarted
when: validation_result.rc == 0
- name: Print success status
debug:
msg: |
##################################################
✅ RSYSLOG CONFIGURATION DEPLOYED SUCCESSFULLY
##################################################
HOST : {{ inventory_hostname }} ({{ ansible_host }})
STATUS : Validation passed. Service restarted.
##################################################
when: validation_result.rc == 0

View File

@ -1,76 +1,101 @@
---
# =============================================================================
# DRIFT-CHECK PLAYBOOK
# Purpose: Compare file on repo vs server to detect if they're in sync
# Usage: ansible-playbook drift-check.yml
# Output: SYNCED or OUT_OF_SYNC status
# =============================================================================
- name: Check file drift
hosts: all
- name: Check rsyslog configuration drift
hosts: rsyslog_servers
gather_facts: false
# NOTE: src paths below resolve relative to the Ansible controller (the
# Woodpecker CI container), so they always reflect the latest Git commit
# NOT the server's local clone, which may be stale.
tasks:
# ─────────────────────────────────────────────────────────────────────
# TASK 1: Read local file from repo
# Reads dvir.txt from the local repository using base64 encoding
# ─────────────────────────────────────────────────────────────────────
- name: Read local file
slurp:
src: "{{ playbook_dir }}/../../files/dvir.txt"
# -------------------------------------------------------------------------
# Use Ansible copy in check_mode so it compares controller files (Git)
# against live server files without actually writing anything.
# changed=true → file differs → drift
# changed=false → files match → synced
# -------------------------------------------------------------------------
- name: Check main rsyslog.conf
ansible.builtin.copy:
src: "{{ playbook_dir }}/../../files/rsyslog.conf"
dest: "{{ rsyslog_main_config }}"
owner: root
group: root
mode: '0644'
check_mode: true
diff: true
register: main_config_check
- name: Check rsyslog.d config files
ansible.builtin.copy:
src: "{{ playbook_dir }}/../../files/rsyslog.d/"
dest: "{{ rsyslog_config_dir }}/"
owner: root
group: root
mode: '0644'
check_mode: true
diff: true
register: rsyslogd_check
- name: Check for extra files on server not present in Git
block:
- name: Find config files on server
ansible.builtin.find:
paths: "{{ rsyslog_config_dir }}"
patterns: "*.conf"
register: server_configs
- name: Find config files in Git (controller)
ansible.builtin.find:
paths: "{{ playbook_dir }}/../../files/rsyslog.d"
patterns: "*.conf"
delegate_to: localhost
register: local_file
register: repo_configs
# ─────────────────────────────────────────────────────────────────────
# TASK 2: Read file from server
# Attempts to read dvir.txt from /tmp on the target server
# Failure is allowed here (we'll handle it in next task)
# ─────────────────────────────────────────────────────────────────────
- name: Read server file
slurp:
src: /tmp/dvir.txt
register: server_file
failed_when: false
- name: Build list of Git-managed filenames
ansible.builtin.set_fact:
git_filenames: "{{ repo_configs.files | map(attribute='path') | map('basename') | list }}"
# ─────────────────────────────────────────────────────────────────────
# TASK 3: Compare file contents (if server file exists)
# Decodes base64 and compares content between repo and server
# Sets drift_detected to true if content differs
# ─────────────────────────────────────────────────────────────────────
- name: Compare file contents
set_fact:
drift_detected: "{{ (local_file.content | b64decode) != (server_file.content | b64decode) }}"
when: server_file.rc == 0
- name: Build list of server filenames
ansible.builtin.set_fact:
server_filenames: "{{ server_configs.files | map(attribute='path') | map('basename') | list }}"
# ─────────────────────────────────────────────────────────────────────
# TASK 4: Mark as drift if server file is missing
# If the server file doesn't exist, it's also considered drift
# ─────────────────────────────────────────────────────────────────────
- name: Mark as drift if server file missing
set_fact:
drift_detected: true
when: server_file.rc != 0
- name: Find server files that are managed by Git but missing on server
ansible.builtin.set_fact:
missing_on_server: "{{ git_filenames | difference(server_filenames) }}"
# ─────────────────────────────────────────────────────────────────────
# TASK 5: Output SYNCED status
# Displayed when file on server matches repo file exactly
# ─────────────────────────────────────────────────────────────────────
- name: Output SYNCED status
debug:
msg: "✓ dvir.txt is synced"
- name: Flag if any Git-managed file is missing from server
ansible.builtin.set_fact:
extra_files_on_server: true
when: missing_on_server | length > 0
- name: Show missing files
ansible.builtin.debug:
msg: "Files in Git but missing on server: {{ missing_on_server }}"
when: missing_on_server | length > 0
- name: Set overall drift flag
ansible.builtin.set_fact:
drift_detected: "{{ main_config_check.changed or rsyslogd_check.changed or (extra_files_on_server | default(false)) }}"
- name: Print SYNCED status
ansible.builtin.debug:
msg: |
╭─────────────────────────────╮
│ ✓ SYNCED │
│ Configuration is up-to-date │
╰─────────────────────────────╯
when: not drift_detected
# ─────────────────────────────────────────────────────────────────────
# TASK 6: Output OUT_OF_SYNC status
# Displayed when file on server differs from repo or is missing
# ─────────────────────────────────────────────────────────────────────
- name: Output OUT_OF_SYNC status
debug:
msg: "✗ dvir.txt is out of sync"
- name: Print OUT OF SYNC status
ansible.builtin.debug:
msg: |
─────────────────────────────
│ ✗ OUT OF SYNC │
│ Configuration has drifted │
╰─────────────────────────────╯
when: drift_detected
- name: Fail if drift detected
fail:
msg: "Configuration drift detected."
ansible.builtin.fail:
msg: "Configuration drift detected. Live system does not match repository."
when: drift_detected

View File

@ -1,41 +1,48 @@
---
# =============================================================================
# VALIDATE PLAYBOOK
# Purpose: Verify that dvir.txt exists and is readable on target servers
# Usage: ansible-playbook validate.yml
# Output: Success if file exists and is readable, Failure if not
# =============================================================================
- name: Validate file exists
hosts: all
- name: Validate rsyslog configuration
hosts: rsyslog_servers
gather_facts: false
vars:
validate_dir: /tmp/rsyslog-validate
validate_conf: /tmp/rsyslog-validate/rsyslog.conf
tasks:
# ─────────────────────────────────────────────────────────────────────
# TASK 1: Check file status
# Gathers file stats (exists, readable, permissions, etc.)
# ─────────────────────────────────────────────────────────────────────
- name: Check if file is readable
stat:
path: /tmp/dvir.txt
register: file_stat
- name: Create temp validation directory
file:
path: "{{ validate_dir }}/rsyslog.d"
state: directory
mode: "0700"
# ─────────────────────────────────────────────────────────────────────
# TASK 2: Assert file requirements
# Verifies that the file exists and is readable
# Fails the playbook if either condition is false
# ─────────────────────────────────────────────────────────────────────
- name: Verify file exists and is readable
assert:
that:
- file_stat.stat.exists
- file_stat.stat.readable
msg: "dvir.txt not found or not readable"
- name: Copy main config to temp location
copy:
src: "{{ repo_root }}/files/rsyslog.conf"
dest: "{{ validate_conf }}"
remote_src: true
- name: Copy rsyslog.d configs to temp location
copy:
src: "{{ repo_root }}/files/rsyslog.d/"
dest: "{{ validate_dir }}/rsyslog.d/"
remote_src: true
- name: Update IncludeConfig path to point to temp dir
replace:
path: "{{ validate_conf }}"
regexp: '^\$IncludeConfig\s+.*$'
replace: '$IncludeConfig {{ validate_dir }}/rsyslog.d/*.conf'
- name: Validate repo rsyslog configuration
command: rsyslogd -N1 -f "{{ validate_conf }}"
register: validate_result
failed_when: validate_result.rc != 0
changed_when: false
# ─────────────────────────────────────────────────────────────────────
# TASK 3: Display validation result
# Shows success message if all checks passed
# ─────────────────────────────────────────────────────────────────────
- name: Display validation result
debug:
msg: "✓ dvir.txt is valid and readable"
msg: "✓ rsyslog configuration is valid"
- name: Clean up temp validation directory
file:
path: "{{ validate_dir }}"
state: absent

16
apply.sh Executable file
View File

@ -0,0 +1,16 @@
#!/bin/bash
set -e
echo "Applying rsyslog config from git repo..."
cp rsyslog.conf /etc/rsyslog.conf
mkdir -p /etc/rsyslog.d
cp rsyslog.d/*.conf /etc/rsyslog.d/
echo "Validating config..."
rsyslogd -N1
echo "Restarting rsyslog..."
systemctl restart rsyslog
echo "Done."

39
drift-check.sh Executable file
View File

@ -0,0 +1,39 @@
#!/bin/bash
set -e
echo "Checking drift between git repo and live server..."
DIFF_FOUND=0
echo
echo "Comparing /etc/rsyslog.conf"
if ! diff -u rsyslog.conf /etc/rsyslog.conf; then
DIFF_FOUND=1
fi
echo
echo "Comparing rsyslog.d configs"
for file in rsyslog.d/*.conf; do
base=$(basename "$file")
target="/etc/rsyslog.d/$base"
if [ ! -f "$target" ]; then
echo "Missing on server: $target"
DIFF_FOUND=1
continue
fi
if ! diff -u "$file" "$target"; then
DIFF_FOUND=1
fi
done
if [ "$DIFF_FOUND" -eq 0 ]; then
echo
echo "No drift detected."
exit 0
else
echo
echo "Drift detected!"
exit 1
fi

View File

@ -1,3 +0,0 @@
This is a generic deployment file.
It can be used for any service or configuration.
Simply replace this with your own content as needed.

60
files/rsyslog.conf Normal file
View File

@ -0,0 +1,60 @@
# /etc/rsyslog.conf configuration file for rsyslog
#
# For more information install rsyslog-doc and see
# /usr/share/doc/rsyslog-doc/html/configuration/index.html
#
# Default logging rules can be found in /etc/rsyslog.d/50-default.conf
# TEST IDO
#################
#### MODULES ####
#################
module(load="imuxsock") # provides support for local system logging
#module(load="immark") # provides --MARK-- message capability
# provides UDP syslog reception
#module(load="imudp")
#input(type="imudp" port="514")
# provides TCP syslog reception
#module(load="imtcp")
#input(type="imtcp" port="514")
# provides kernel logging support and enable non-kernel klog messages
module(load="imklog" permitnonkernelfacility="on")
###########################
#### GLOBAL DIRECTIVES ####
###########################
# Use traditional timestamp format.
# To enable high precision timestamps, comment out the following line.
$ActionFileDefaultTemplate RSYSLOG_TraditionalFileFormat
# Filter duplicated messages
$RepeatedMsgReduction on
#
# Set the default permissions for all log files.
#
$FileOwner syslog
$FileGroup adm
$FileCreateMode 0640
$DirCreateMode 0755
$Umask 0022
$PrivDropToUser syslog
$PrivDropToGroup syslog
#
# Where to place spool and state files
#
$WorkDirectory /var/spool/rsyslog
#
# Include all config files in /etc/rsyslog.d/
#
$IncludeConfig /etc/rsyslog.d/*.conf
# Dvir was here

View File

@ -0,0 +1 @@
local0.* /var/log/lab.log