Using systemd's .path unit to run a Python script whenever a httpd config file is changed. The Python script creates a backup of the config and logs the differences of the newly modified and backup file. After that it checks the syntax of the httpd config and gracefully reloads the web server service.
The Python script:
#!/path/to/python
import difflib
import logging
import shutil
import subprocess
import sys
from pathlib import Path
import yaml
logging.basicConfig(format='%(levelname)s - %(message)s', level=logging.INFO)
class HttpdMonitor:
def __init__(self, config_file):
self.config_file = config_file
self.config = self.load_config()
self.httpd_conf = self.config['httpd-conf']
self.backup_file = self.config['httpd-conf'] + '.bak'
self.syntax_check_cmd = self.config['syntax-check-cmd'].split()
self.reload_cmd = self.config['reload-cmd'].split()
def load_config(self):
with open(self.config_file, 'r') as stream:
return yaml.safe_load(stream)
def make_backup(self):
try:
shutil.copy2(self.httpd_conf, self.backup_file)
except IOError as e:
logging.exception("Making backup failed: %s", e)
def find_diff(self):
with open(self.httpd_conf, 'r') as f, open(self.backup_file, 'r') as bak:
diffs = list(
difflib.unified_diff(f.readlines(), bak.readlines(), fromfile=self.httpd_conf, tofile=self.backup_file))
return diffs
def log_diff(self):
try:
diffs = self.find_diff()
logging.info('Differences found: \n%s', ''.join(diffs))
except IOError as e:
logging.exception('Finding diff failed: %s', e)
def call_httpd_commands(self):
subprocess.run(self.syntax_check_cmd, check=True)
subprocess.run(self.reload_cmd, check=True)
def reload_httpd(self):
try:
self.call_httpd_commands()
except subprocess.CalledProcessError as e:
logging.exception("Reloading failed: %s", e)
def run(self):
if not Path(self.backup_file).is_file():
self.make_backup()
return
self.log_diff()
self.make_backup()
self.reload_httpd()
if __name__ == '__main__':
httpd_monitor = HttpdMonitor(sys.argv[1])
httpd_monitor.run()
This uses a YAML file to specify the httpd config file and also the commands to run, sort of like this:
---
httpd-conf: /path/to/file
syntax-check-cmd: httpd -t
reload-cmd: httpd -k graceful
Using systemd's .path unit the script get fired whenever there's something changed in the httpd config file. For example httpd-monitor.path:
[Unit]
Description=Monitor the httpd conf for changes
[Path]
PathChanged=/path/to/file
Unit=httpd-monitor.service
[Install]
WantedBy=multi-user.target
And httpd-monitor.service:
[Unit]
Description=Do backup, diff changes and reload httpd
[Service]
Restart=no
Type=simple
ExecStart=/path/to/httpd_monitor.py /path/to/httpd-monitor.yaml
[Install]
WantedBy=multi-user.target
So I'd first run systemd start httpd-monitor.service just to create the initial backup (the if condition within the run() method should take care of it) and then just systemd enable --now httpd-monitor.path to enable the .path unit.
Probably not the simplest of solutions so I'm very open to any feedback!