Skip to content

About

An ansible-runner project for collecting remote linux server configuration

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

Server Audit

A Python tool for collecting operating system and hardware information from Linux servers using Ansible in ad-hoc mode.

Features

  • Zero dependencies on remote hosts - Uses Ansible's raw module, no Python required on target servers
  • Supports older Linux distributions - Works with Ubuntu 18.04+ and other legacy systems
  • Comprehensive data collection:
    • OS distribution and version
    • Kernel version
    • CPU information (model, count, NUMA topology)
    • Memory (total, available, huge pages)
    • Disk information (mounts, capacity, SSD/HDD detection)
    • Network interfaces (IPv4, MAC addresses)
    • VM tuning settings (swappiness, overcommit, transparent hugepages)
    • Memory: every /proc/meminfo field and each swap area with the disk it lives on
    • Block devices (lsblk), with each mount and swap area mapped to its whole disks
    • GCP instance metadata: machine type, zone and attached disks by device name
    • Scheduled jobs: every cron table, the cron directories and systemd timers
    • MySQL, when installed: the options mysqld reads from its option files (my_print_defaults mysqld), with conflicting duplicates flagged, all global variables and status, persisted variables, memory instrumentation, and mysqld's process memory, allocator and systemd unit (see MySQL)
  • JSON output - Structured output for easy integration with other tools
  • Library and CLI - Use programmatically or from the command line

Requirements

  • Python 3.9+
  • SSH access to target hosts
  • Ansible Runner (ansible-runner>=2.3.0)

MySQL

On a MySQL host the audit adds a mysql object. On other hosts it is null. It has two parts.

config_files: what the option files say. This is the output of my_print_defaults mysqld:

  • options: the lines in the order mysqld reads them.
  • effective: each option's last value, which is the one mysqld uses. Names are normalized: dashes equal underscores and the loose- prefix is dropped.
  • duplicates: every option set more than once.
  • conflicts: the duplicates whose values differ. For example, explicit_defaults_for_timestamp=OFF followed later by =1 runs with it on.

my_print_defaults masks passwords. Any password or *_password value that an older client prints is masked by the parser.

runtime: what the running server uses. Every global variable (SHOW GLOBAL VARIABLES). A replacement host must match its source for the ones that change what the application sees: version, explicit_defaults_for_timestamp, sql_mode, character_set_server, collation_server, lower_case_table_names, time_zone, system_time_zone, transaction_isolation, auto_increment_increment, auto_increment_offset.

Where each setting came from, and what the server is doing:

Key Source Shape
global_status SHOW GLOBAL STATUS variables name → value
persisted_variables performance_schema.persisted_variables (SET PERSIST, mysqld-auto.cnf) joined with variables_info for VARIABLE_SOURCE, SET_TIME, SET_USER, SET_HOST rows
variables_info performance_schema.variables_info where VARIABLE_SOURCE is not COMPILED, with the current value and VARIABLE_PATH rows
memory_by_event top 30 of performance_schema.memory_summary_global_by_event_name by HIGH_NUMBER_OF_BYTES_USED rows
memory_global_total sys.memory_global_total and the byte sum of memory_summary_global_by_event_name rows
events information_schema.EVENTS rows
process /proc/<pid>/status, smaps_rollup, oom_score*, mapped libjemalloc/libtcmalloc, LD_PRELOAD, /etc/ld.so.preload, systemctl show and systemctl cat of mysqld's unit object

rows is a list of objects keyed by column name; SQL NULL is null. A query that returns no rows has status ok and an empty rows. A table the server does not have (MySQL 5.7 has no persisted_variables) gives unavailable with the client's error in messages. process.status is running or not_running.

Values of variables whose name ends in password, passwd, pwd, secret, token or ssl_key are masked, unless they are ON, OFF or a number. Passwords on cron lines and in systemd unit environments (--password=, -p<password> for MySQL tools, *PASSWORD*=, MYSQL_PWD=) are masked.

The queries run through the mysql client with the executing user's option files, so that user needs a working login, for example root's socket login when run with become. If the login fails, status is unavailable and messages holds the client's error. The rest of the audit still completes. Reading mysqld's maps, smaps_rollup and environ and other users' crontabs also needs root.

status config_files runtime, global_status and rows queries
ok options were read the query ran
empty no option file has a [mysqld] group —
error only error output —
unavailable — the client could not log in or query
not_installed no my_print_defaults on the host no mysql client on the host

Memory, disks, cloud and schedules

  • memory.meminfo: every /proc/meminfo field, in kB (HugePages_* are page counts).
  • memory.swaps: each line of /proc/swaps (filename, type, size_kb, used_kb, priority), with block_device and parent_disks. A swap file maps through the mount that holds it.
  • vm_settings also has page_cluster, min_free_kbytes, zone_reclaim_mode and vfs_cache_pressure.
  • block_devices: lsblk rows (name, kname, pkname, type, size in bytes, rotational, mountpoint, model, serial). A device with several parents (a striped LV) has one row per parent.
  • disks[].block_device and disks[].parent_disks: the mounted device and the whole disks under it, through partitions, LVM and RAID.
  • cloud: on GCP, machine_type, zone, the metadata server's disks and device_names (GCP device name → kernel name, from /dev/disk/by-id/google-*). The metadata server does not give the disk type (pd-ssd, pd-balanced) or size; get them with gcloud compute disks list and join on the device name. status is not_cloud off GCP.
  • scheduled_jobs: timezone, cron_files (path → non-comment lines, for /etc/crontab, /etc/cron.d/* and every user's crontab), cron_dirs (/etc/cron.daily etc. → scripts) and timers (unit, activates, active_state, calendar, monotonic, last_trigger, next_elapse, from systemctl show of each timer).

These keys are null in output parsed from a payload that did not collect them.

Installation

# Clone the repository
git clone <repository-url>
cd audit

# Install in development mode
pip install -e .

# Or install with dev dependencies
pip install -e ".[dev]"

Usage

Command Line Interface

# Audit all hosts in inventory, output to directory (one file per host)
server-audit -i inventory/hosts -o output/

# Audit specific host group
server-audit -i inventory/hosts -o output/ --hosts webservers

# Audit with host pattern
server-audit -i inventory/hosts -o output/ --hosts "db*"

# Output to single combined JSON file
server-audit -i inventory/hosts -o audit_results.json

# Verbose mode
server-audit -i inventory/hosts -o output/ -v

# SSH password login (prompts once, like ansible -k; needs sshpass on this host)
server-audit -i inventory/hosts -o output/ --hosts databases -k

CLI Options

Option Description
-i, --inventory PATH Path to Ansible inventory file (required)
-o, --output PATH Output path - directory for per-host files, or .json file for combined output (required)
--hosts PATTERN Host pattern to audit (default: all)
-k, --ask-pass Ask for the SSH password once and give it to Ansible, as ansible -k does. Needs sshpass on the control host
-v, --verbose Enable verbose output
-V, --version Show version

Library Usage

from server_audit import run_audit, AuditResult

# Run audit and get results as AuditResult objects
results = run_audit("inventory/hosts", hosts="all")

for hostname, result in results.items():
    print(f"{hostname}: {result.os_info.distribution} {result.os_info.version}")
    print(f"  CPU: {result.hardware.cpu_count} cores")
    print(f"  Memory: {result.hardware.memory_total}")
from server_audit.runner import run_audit_to_json

# Run audit and save directly to JSON files
files = run_audit_to_json(
    inventory_path="inventory/hosts",
    output_path="output/",
    hosts="webservers"
)

print(f"Created {len(files)} audit files")

Inventory File

Use standard Ansible inventory format:

[webservers]
web1 ansible_host=192.168.1.10 ansible_user=admin
web2 ansible_host=192.168.1.11 ansible_user=admin

[databases]
db1 ansible_host=192.168.1.20 ansible_user=dba

[all:vars]
ansible_ssh_common_args='-o StrictHostKeyChecking=no'

For password authentication, add ansible_password:

[servers]
server1 ansible_host=192.168.1.10 ansible_user=test ansible_password=secret

Output Format

Each audit produces a JSON file with the following structure:

{
    "hostname": "web1",
    "audit_timestamp": "2026-01-24T10:30:00+00:00",
    "os_info": {
        "distribution": "Ubuntu",
        "version": "24.04",
        "kernel": "6.8.0-45-generic"
    },
    "hardware": {
        "memory_total": "15.6 GB",
        "cpu_count": 8,
        "cpu_model": "Intel(R) Xeon(R) CPU E5-2680 v4 @ 2.40GHz",
        "disk_types": {
            "sda": "SSD",
            "sdb": "HDD"
        },
        "numa": {
            "NUMA node(s)": "2",
            "NUMA node0 CPU(s)": "0-3",
            "NUMA node1 CPU(s)": "4-7"
        }
    },
    "disks": [
        {
            "mount": "/",
            "device": "/dev/sda1",
            "size_total": 107374182400,
            "size_available": 53687091200,
            "fstype": "ext4",
            "block_device": "sda1",
            "parent_disks": ["sda"]
        }
    ],
    "networks": {
        "eth0": {
            "ipv4": "192.168.1.10",
            "mac": "00:11:22:33:44:55"
        }
    },
    "vm_settings": {
        "swappiness": "60",
        "overcommit_memory": "0",
        "dirty_ratio": "20",
        "dirty_background_ratio": "10",
        "transparent_hugepages": "always",
        "huge_pages_raw": ["HugePages_Total: 0", "HugePages_Free: 0"],
        "semaphores": {
            "semmsl": "32000",
            "semmns": "1024000000",
            "semopm": "500",
            "semmni": "32000"
        },
        "page_cluster": "3",
        "min_free_kbytes": "67584",
        "zone_reclaim_mode": "0",
        "vfs_cache_pressure": "100"
    },
    "memory": {
        "meminfo": {"MemTotal": 16337788, "SwapTotal": 8388604, "...": 0},
        "swaps": [
            {
                "filename": "/swap.img",
                "type": "file",
                "size_kb": 8388604,
                "used_kb": 0,
                "priority": -2,
                "mount": "/",
                "block_device": "sda1",
                "parent_disks": ["sda"]
            }
        ]
    },
    "block_devices": [
        {"name": "sda", "kname": "sda", "pkname": null, "type": "disk", "size": 107374182400,
         "rotational": true, "mountpoint": null, "model": "PersistentDisk", "serial": "persistent-disk-0"}
    ],
    "cloud": null,
    "scheduled_jobs": {"timezone": "UTC +0000", "cron_files": {}, "cron_dirs": {}, "timers": [], "messages": []},
    "mysql": null
}

Project Structure

audit/
├── src/server_audit/
│   ├── __init__.py      # Public API exports
│   ├── cli.py           # Command-line interface
│   ├── exceptions.py    # Custom exceptions
│   ├── models.py        # Data models (AuditResult, etc.)
│   ├── parsers.py       # Output parsing logic
│   ├── payload.py       # Shell command payload
│   └── runner.py        # Ansible runner orchestration
├── tests/               # Test suite
├── inventory/           # Sample inventory files
├── output/              # Default output directory
└── docker/              # Docker test environment

License

MIT

About

An ansible-runner project for collecting remote linux server configuration

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages