✨ Features: - Complete LEMP stack automation - WordPress with WP-CLI - Redis Object Cache (50% performance boost) - Multi-OS support - SSL/Let's Encrypt integration - Security hardening - Enterprise features - Docker testing environment - GitHub Actions CI/CD - Comprehensive documentation 🧪 Fully tested and production-ready Copyright (c) 2025 Sebastian Palencsár
180 lines
6.5 KiB
Python
Executable File
180 lines
6.5 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Ansible inventory validation script
|
|
Validates inventory files for common issues and best practices
|
|
"""
|
|
|
|
import argparse
|
|
import configparser
|
|
import ipaddress
|
|
import os
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
|
|
class InventoryValidator:
|
|
def __init__(self, inventory_file):
|
|
self.inventory_file = Path(inventory_file)
|
|
self.errors = []
|
|
self.warnings = []
|
|
|
|
def validate(self):
|
|
"""Run all validation checks"""
|
|
if not self.inventory_file.exists():
|
|
self.errors.append(f"Inventory file {self.inventory_file} does not exist")
|
|
return False
|
|
|
|
try:
|
|
config = configparser.ConfigParser(allow_no_value=True)
|
|
config.read(self.inventory_file)
|
|
|
|
self._validate_groups(config)
|
|
self._validate_hosts(config)
|
|
self._validate_variables(config)
|
|
self._validate_security(config)
|
|
|
|
except configparser.Error as e:
|
|
self.errors.append(f"INI parsing error: {e}")
|
|
|
|
return len(self.errors) == 0
|
|
|
|
def _validate_groups(self, config):
|
|
"""Validate group definitions"""
|
|
required_groups = ['wordpress_servers']
|
|
|
|
for group in required_groups:
|
|
if group not in config.sections():
|
|
self.errors.append(f"Required group '{group}' not found")
|
|
|
|
# Check for empty groups
|
|
for section in config.sections():
|
|
if not config.options(section):
|
|
self.warnings.append(f"Group '{section}' is empty")
|
|
|
|
def _validate_hosts(self, config):
|
|
"""Validate host definitions"""
|
|
host_pattern = re.compile(r'^[a-zA-Z0-9][a-zA-Z0-9\.-]*[a-zA-Z0-9]$')
|
|
|
|
for section in config.sections():
|
|
if ':vars' in section:
|
|
continue
|
|
|
|
for option in config.options(section):
|
|
host_line = f"{option} {config.get(section, option) or ''}"
|
|
host_name = option
|
|
|
|
# Validate hostname format
|
|
if not host_pattern.match(host_name) and not self._is_valid_ip(host_name):
|
|
self.warnings.append(f"Host '{host_name}' has unusual format")
|
|
|
|
# Check for common connection variables
|
|
if 'ansible_host=' in host_line:
|
|
ansible_host = self._extract_variable(host_line, 'ansible_host')
|
|
if ansible_host and not self._is_valid_ip(ansible_host) and not self._is_valid_hostname(ansible_host):
|
|
self.warnings.append(f"ansible_host '{ansible_host}' for host '{host_name}' looks invalid")
|
|
|
|
# Check for insecure practices
|
|
if 'ansible_ssh_pass=' in host_line:
|
|
self.warnings.append(f"Host '{host_name}' uses password authentication (consider SSH keys)")
|
|
|
|
if 'ansible_become_pass=' in host_line:
|
|
self.warnings.append(f"Host '{host_name}' has become password in inventory (consider vault)")
|
|
|
|
def _validate_variables(self, config):
|
|
"""Validate variable definitions"""
|
|
wordpress_vars = [
|
|
'mysql_root_password',
|
|
'wordpress_db_password',
|
|
'wp_admin_password'
|
|
]
|
|
|
|
for section in config.sections():
|
|
if ':vars' not in section:
|
|
continue
|
|
|
|
for option in config.options(section):
|
|
value = config.get(section, option)
|
|
|
|
# Check for hardcoded passwords
|
|
if any(var in option for var in wordpress_vars):
|
|
if value and len(value) < 8:
|
|
self.warnings.append(f"Variable '{option}' has weak password")
|
|
if value and value in ['password', 'admin', '123456']:
|
|
self.errors.append(f"Variable '{option}' has insecure default value")
|
|
|
|
def _validate_security(self, config):
|
|
"""Validate security-related settings"""
|
|
security_checks = {
|
|
'ansible_host_key_checking': 'false',
|
|
'ansible_ssh_common_args': '-o StrictHostKeyChecking=no'
|
|
}
|
|
|
|
for section in config.sections():
|
|
for option in config.options(section):
|
|
value = config.get(section, option) or ''
|
|
|
|
for check, insecure_value in security_checks.items():
|
|
if check in value and insecure_value in value.lower():
|
|
self.warnings.append(f"Insecure setting detected: {check}={insecure_value}")
|
|
|
|
def _is_valid_ip(self, ip_str):
|
|
"""Check if string is a valid IP address"""
|
|
try:
|
|
ipaddress.ip_address(ip_str)
|
|
return True
|
|
except ValueError:
|
|
return False
|
|
|
|
def _is_valid_hostname(self, hostname):
|
|
"""Check if string is a valid hostname"""
|
|
if len(hostname) > 255:
|
|
return False
|
|
hostname = hostname.rstrip('.')
|
|
allowed = re.compile(r'^[a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?$')
|
|
return all(allowed.match(part) for part in hostname.split('.'))
|
|
|
|
def _extract_variable(self, line, var_name):
|
|
"""Extract variable value from inventory line"""
|
|
pattern = fr'{var_name}=([^\s]+)'
|
|
match = re.search(pattern, line)
|
|
return match.group(1) if match else None
|
|
|
|
def print_results(self):
|
|
"""Print validation results"""
|
|
if self.errors:
|
|
print("❌ ERRORS:")
|
|
for error in self.errors:
|
|
print(f" - {error}")
|
|
|
|
if self.warnings:
|
|
print("⚠️ WARNINGS:")
|
|
for warning in self.warnings:
|
|
print(f" - {warning}")
|
|
|
|
if not self.errors and not self.warnings:
|
|
print("✅ Inventory validation passed!")
|
|
|
|
return len(self.errors) == 0
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description='Validate Ansible inventory files')
|
|
parser.add_argument('inventory', help='Path to inventory file')
|
|
parser.add_argument('--strict', action='store_true', help='Treat warnings as errors')
|
|
|
|
args = parser.parse_args()
|
|
|
|
validator = InventoryValidator(args.inventory)
|
|
is_valid = validator.validate()
|
|
validator.print_results()
|
|
|
|
if args.strict and validator.warnings:
|
|
is_valid = False
|
|
|
|
sys.exit(0 if is_valid else 1)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|