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 # so a drift-check here would always be OUT_OF_SYNC by
# design and is meaningless as a failure signal. # design and is meaningless as a failure signal.
# #
# push (master) → syntax-check, validate, deploy, update-gitops-status # push (master) → syntax-check, validate, deploy, update-sync-metric
# Deploys to the server, then verifies sync and sends # Deploys to the server, then verifies sync and pushes metric.
# JSON status snapshot to gitops-status-server for Grafana.
# #
# cron → gitops_sync_check (read-only drift check, no deploy) # cron → gitops_sync_check (read-only drift check, no deploy)
# Continuously verifies that the live server still matches # Continuously verifies that the live server still matches
# Git even when no push has happened. Detects manual edits # Git even when no push has happened. Detects manual edits
# made directly on the server. Sends JSON status with # made directly on the server.
# detailed file-level drift information to gitops-status-server.
# #
# NOTE: Woodpecker does not support multiple YAML documents (---) in one file. # NOTE: Woodpecker does not support multiple YAML documents (---) in one file.
# All pipelines must live in a single document with step-level filtering. # All pipelines must live in a single document with step-level filtering.
@ -82,48 +80,44 @@ steps:
event: push 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. # Runs on push to master only, after deploy succeeds.
# Generates structured JSON with sync status, drift count, and changed files. # STATUS=1 means SYNCED, STATUS=0 means OUT_OF_SYNC.
# Sends JSON to gitops-status-server for Grafana visualization.
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
update-gitops-status: update-sync-metric:
image: alpine/ansible:latest image: alpine/ansible:latest
depends_on: [deploy] depends_on: [deploy]
environment: environment:
ANSIBLE_CONFIG: ansible.cfg ANSIBLE_CONFIG: ansible.cfg
SSH_PRIVATE_KEY: SSH_PRIVATE_KEY:
from_secret: SSH_PRIVATE_KEY from_secret: SSH_PRIVATE_KEY
GITOPS_STATUS_SERVER_URL: http://gitops-status-server.observability-stack.svc.cluster.local:5000 PUSHGATEWAY_URL: http://pushgateway.observability-stack.svc.cluster.local:9091
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"
commands: commands:
- | - |
# Increase file descriptor limit for Ansible (max safe value) apk add --no-cache curl > /dev/null 2>&1
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
mkdir -p ~/.ssh mkdir -p ~/.ssh
printf '%s\n' "$${SSH_PRIVATE_KEY}" > ~/.ssh/id_rsa printf '%s\n' "$${SSH_PRIVATE_KEY}" > ~/.ssh/id_rsa
chmod 600 ~/.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 if [ "$DRIFT_RC" -eq 0 ]; then
chmod +x update-gitops-status.sh STATUS=1
./update-gitops-status.sh 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: when:
branch: master branch: master
event: push event: push
@ -131,14 +125,13 @@ steps:
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# gitops_sync_check: ArgoCD-style cron drift check read-only, no deploy # gitops_sync_check: ArgoCD-style cron drift check read-only, no deploy
# Detects manual changes made directly on the server between pushes. # Detects manual changes made directly on the server between pushes.
# Generates structured JSON with sync status, drift count, and changed files. # STATUS=1 → SYNCED, STATUS=0 → OUT_OF_SYNC
# Sends JSON to gitops-status-server for continuous GitOps monitoring. # Pipeline marked FAILED when drift found so it is visible in the UI.
# Pipeline always succeeds - check Grafana dashboard for drift status.
# #
# ─── Woodpecker Cron UI settings ────────────────────────────────────────── # ─── Woodpecker Cron UI settings ──────────────────────────────────────────
# Name: gitops_sync_check # Name: gitops_sync_check
# Branch: master # Branch: master
# Schedule: */1 * * * * (every 1 minute for testing, adjust as needed) # Schedule: */2 * * * *
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
gitops_sync_check: gitops_sync_check:
image: alpine/ansible:latest image: alpine/ansible:latest
@ -146,35 +139,34 @@ steps:
ANSIBLE_CONFIG: ansible.cfg ANSIBLE_CONFIG: ansible.cfg
SSH_PRIVATE_KEY: SSH_PRIVATE_KEY:
from_secret: SSH_PRIVATE_KEY from_secret: SSH_PRIVATE_KEY
GITOPS_STATUS_SERVER_URL: http://gitops-status-server.observability-stack.svc.cluster.local:5000 PUSHGATEWAY_URL: http://pushgateway.observability-stack.svc.cluster.local:9091
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"
commands: commands:
- | - |
# Increase file descriptor limit for Ansible (max safe value) apk add --no-cache curl > /dev/null 2>&1
ulimit -n 65536
# 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 mkdir -p ~/.ssh
printf '%s\n' "$${SSH_PRIVATE_KEY}" > ~/.ssh/id_rsa printf '%s\n' "$${SSH_PRIVATE_KEY}" > ~/.ssh/id_rsa
chmod 600 ~/.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) if [ "$DRIFT_RC" -eq 0 ]; then
chmod +x update-gitops-status.sh STATUS=1
./update-gitops-status.sh 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 "==> Pushing metric: gitops_sync_status{repo=\"rsyslog\",server=\"rsyslog-lab\"} $STATUS"
echo " Pipeline always succeeds. Check gitops-status-server for sync 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: when:
event: cron event: cron

467
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: 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.
- **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.
--- ---
## 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).
``` ```
. Open PR
├── README.md # This file
├── ansible.cfg # Ansible configuration ├─► syntax-check Check the YAML/Ansible syntax is valid
├── .woodpecker.yml # CI/CD pipeline configuration
└─► validate Connect to the server and verify rsyslog is running
├── ansible/ and the current config is loadable
│ ├── inventory/ ```
│ │ ├── hosts.yml # Define target servers
│ │ └── group_vars/ **Pass** = safe to review and merge.
│ │ └── all.yml # Global variables (SSH credentials, etc.) **Fail** = syntax error or server is unreachable / config is broken.
│ │
│ └── playbooks/ ---
│ ├── apply.yml # Deploy file to servers
│ ├── drift-check.yml # Check if servers are in sync with repo ### 2. Push to master — "Deploy and verify"
│ └── validate.yml # Verify file exists on server
Triggered when a PR is merged into master.
└── files/
└── dvir.txt # The file to deploy (edit this to your needs) ```
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) gitops_sync_status{repo="rsyslog", server="rsyslog-lab"}
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
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
``` ```
.woodpecker.yml CI pipeline definition
To deploy a **different file**, rename it or update the paths in the playbooks. ansible/
inventory/
### 2. Add target servers hosts.yml Server list
group_vars/all.yml Variables (paths, user, etc.)
Edit `ansible/inventory/hosts.yml`: playbooks/
validate.yml Check rsyslog is running and config loads
```yaml apply.yml Deploy config files from Git to server
all: drift-check.yml Compare Git files to live server (read-only)
children: files/
servers: rsyslog.conf Main rsyslog config (source of truth)
hosts: rsyslog.d/
server1: 30-lab.conf Drop-in config for lab logging
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
``` ```
--- ---
## Playbook Breakdown ## Woodpecker cron setup
### `apply.yml` - Deploy Files Go to **Repository Settings → Crons → Add cron**:
**What it does:**
- Copies `files/dvir.txt` to `/tmp/dvir.txt` on all target servers
- Sets file ownership to `root:root` with permissions `0644`
**When to use:** | Field | Value |
- Initial deployment of the file |----------|---------------------|
- After updating the file in Git | Name | `gitops_sync_check` |
| Branch | `master` |
**Example:** | Schedule | `*/2 * * * *` |
```bash
ansible-playbook ansible/playbooks/apply.yml
```
--- ---
### `drift-check.yml` - Detect Configuration Drift ## Required secrets
**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`
**When to use:** Go to **Repository Settings → Secrets**:
- 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
**Example:** | Name | Description |
```bash |-------------------|------------------------------------|
ansible-playbook ansible/playbooks/drift-check.yml | `SSH_PRIVATE_KEY` | Private key to SSH into the server |
```
**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)

View File

@ -1,7 +1,17 @@
--- ---
# Global variables for deployment # Global variables for rsyslog configuration management
# Ansible connection settings # Ansible connection settings
ansible_user: root ansible_user: root
ansible_ssh_private_key_file: "~/.ssh/id_rsa" ansible_ssh_private_key_file: "~/.ssh/id_rsa"
ansible_ssh_common_args: "-o StrictHostKeyChecking=no" 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: all:
children: children:
servers: rsyslog_servers:
hosts: hosts:
server1: rsyslog-lab:
ansible_host: 192.168.10.161 ansible_host: 192.168.10.161
# Add more servers here: # Future servers can be added here:
# server2: # rsyslog-prod:
# ansible_host: 192.168.10.162 # ansible_host: 192.168.10.162
# server3: # rsyslog-backup:
# ansible_host: 192.168.10.163 # ansible_host: 192.168.10.163

View File

@ -1,31 +1,145 @@
--- ---
# ============================================================================= - name: Apply rsyslog configuration (safe staged deployment)
# APPLY PLAYBOOK hosts: rsyslog_servers
# Purpose: Deploy dvir.txt file to target servers at /tmp/dvir.txt
# Usage: ansible-playbook apply.yml
# =============================================================================
- name: Deploy file to servers
hosts: all
become: true become: true
vars:
backup_dir: /var/backups/rsyslog-ansible
backup_conf: "{{ backup_dir }}/rsyslog.conf.bak"
backup_confd: "{{ backup_dir }}/rsyslog.d.bak"
tasks: tasks:
# ─────────────────────────────────────────────────────────────────────
# TASK 1: Copy file to destination # -------------------------------------------------------------------------
# Copies the dvir.txt from the repo to /tmp/dvir.txt on target servers # STAGE 1 — Backup current working configuration
# ───────────────────────────────────────────────────────────────────── # -------------------------------------------------------------------------
- name: Copy file to destination
- name: Ensure backup directory exists
file:
path: "{{ backup_dir }}"
state: directory
mode: "0700"
- name: Backup current rsyslog.conf
copy: copy:
src: ../../files/dvir.txt src: "{{ rsyslog_main_config }}"
dest: /tmp/dvir.txt 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 owner: root
group: root group: root
mode: "0644" mode: "0644"
# ───────────────────────────────────────────────────────────────────── - name: Copy new rsyslog.d configs from repo
# TASK 2: Confirm deployment success copy:
# Displays success message with the hostname for verification src: ../../files/rsyslog.d/
# ───────────────────────────────────────────────────────────────────── dest: "{{ rsyslog_config_dir }}/"
- name: Confirm deployment 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: 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 @@
--- ---
# ============================================================================= - name: Check rsyslog configuration drift
# DRIFT-CHECK PLAYBOOK hosts: rsyslog_servers
# 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
gather_facts: false 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: tasks:
# ───────────────────────────────────────────────────────────────────── # -------------------------------------------------------------------------
# TASK 1: Read local file from repo # Use Ansible copy in check_mode so it compares controller files (Git)
# Reads dvir.txt from the local repository using base64 encoding # against live server files without actually writing anything.
# ───────────────────────────────────────────────────────────────────── # changed=true → file differs → drift
- name: Read local file # changed=false → files match → synced
slurp: # -------------------------------------------------------------------------
src: "{{ playbook_dir }}/../../files/dvir.txt" - name: Check main rsyslog.conf
delegate_to: localhost ansible.builtin.copy:
register: local_file 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
# TASK 2: Read file from server ansible.builtin.copy:
# Attempts to read dvir.txt from /tmp on the target server src: "{{ playbook_dir }}/../../files/rsyslog.d/"
# Failure is allowed here (we'll handle it in next task) dest: "{{ rsyslog_config_dir }}/"
# ───────────────────────────────────────────────────────────────────── owner: root
- name: Read server file group: root
slurp: mode: '0644'
src: /tmp/dvir.txt check_mode: true
register: server_file diff: true
failed_when: false register: rsyslogd_check
# ───────────────────────────────────────────────────────────────────── - name: Check for extra files on server not present in Git
# TASK 3: Compare file contents (if server file exists) block:
# Decodes base64 and compares content between repo and server - name: Find config files on server
# Sets drift_detected to true if content differs ansible.builtin.find:
# ───────────────────────────────────────────────────────────────────── paths: "{{ rsyslog_config_dir }}"
- name: Compare file contents patterns: "*.conf"
set_fact: register: server_configs
drift_detected: "{{ (local_file.content | b64decode) != (server_file.content | b64decode) }}"
when: server_file.rc == 0
# ───────────────────────────────────────────────────────────────────── - name: Find config files in Git (controller)
# TASK 4: Mark as drift if server file is missing ansible.builtin.find:
# If the server file doesn't exist, it's also considered drift paths: "{{ playbook_dir }}/../../files/rsyslog.d"
# ───────────────────────────────────────────────────────────────────── patterns: "*.conf"
- name: Mark as drift if server file missing delegate_to: localhost
set_fact: register: repo_configs
drift_detected: true
when: server_file.rc != 0
# ───────────────────────────────────────────────────────────────────── - name: Build list of Git-managed filenames
# TASK 5: Output SYNCED status ansible.builtin.set_fact:
# Displayed when file on server matches repo file exactly git_filenames: "{{ repo_configs.files | map(attribute='path') | map('basename') | list }}"
# ─────────────────────────────────────────────────────────────────────
- name: Output SYNCED status - name: Build list of server filenames
debug: ansible.builtin.set_fact:
msg: "✓ dvir.txt is synced" server_filenames: "{{ server_configs.files | map(attribute='path') | map('basename') | list }}"
- 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) }}"
- 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 when: not drift_detected
# ───────────────────────────────────────────────────────────────────── - name: Print OUT OF SYNC status
# TASK 6: Output OUT_OF_SYNC status ansible.builtin.debug:
# Displayed when file on server differs from repo or is missing msg: |
# ───────────────────────────────────────────────────────────────────── ─────────────────────────────
- name: Output OUT_OF_SYNC status │ ✗ OUT OF SYNC │
debug: │ Configuration has drifted │
msg: "✗ dvir.txt is out of sync" ╰─────────────────────────────╯
when: drift_detected when: drift_detected
- name: Fail if drift detected - name: Fail if drift detected
fail: ansible.builtin.fail:
msg: "Configuration drift detected." msg: "Configuration drift detected. Live system does not match repository."
when: drift_detected when: drift_detected

View File

@ -1,41 +1,48 @@
--- ---
# ============================================================================= - name: Validate rsyslog configuration
# VALIDATE PLAYBOOK hosts: rsyslog_servers
# 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
gather_facts: false gather_facts: false
vars:
validate_dir: /tmp/rsyslog-validate
validate_conf: /tmp/rsyslog-validate/rsyslog.conf
tasks: tasks:
# ───────────────────────────────────────────────────────────────────── - name: Create temp validation directory
# TASK 1: Check file status file:
# Gathers file stats (exists, readable, permissions, etc.) path: "{{ validate_dir }}/rsyslog.d"
# ───────────────────────────────────────────────────────────────────── state: directory
- name: Check if file is readable mode: "0700"
stat:
path: /tmp/dvir.txt
register: file_stat
# ───────────────────────────────────────────────────────────────────── - name: Copy main config to temp location
# TASK 2: Assert file requirements copy:
# Verifies that the file exists and is readable src: "{{ repo_root }}/files/rsyslog.conf"
# Fails the playbook if either condition is false dest: "{{ validate_conf }}"
# ───────────────────────────────────────────────────────────────────── remote_src: true
- name: Verify file exists and is readable
assert: - name: Copy rsyslog.d configs to temp location
that: copy:
- file_stat.stat.exists src: "{{ repo_root }}/files/rsyslog.d/"
- file_stat.stat.readable dest: "{{ validate_dir }}/rsyslog.d/"
msg: "dvir.txt not found or not readable" 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 - name: Display validation result
debug: 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