aoh

A tool for running Ansible playbooks with `curl <url> | python` commands
git clone https://git.ashermorgan.net/aoh/
Log | Files | Refs | README

commit 0986c40b5077f1a08d01a7c44be600ae566a3172
parent 555df61ee112a712a11099699c348ac063360be9
Author: Asher Morgan <asher@ashermorgan.net>
Date:   Tue,  8 Sep 2026 20:02:25 -0700

Add logging in aoh/server.py

Also improve server error handling and add $AOH_LOG_LEVEL option.

Diffstat:
MREADME.md | 8+++++---
Maoh/__init__.py | 6++++++
Maoh/config.py | 2++
Maoh/server.py | 32++++++++++++++++++++++++++------
4 files changed, 39 insertions(+), 9 deletions(-)

diff --git a/README.md b/README.md @@ -53,14 +53,16 @@ environment variables or a `.env` file. - `AOH_DEBUG`: If set to `1`, the AoH server will copy Ansible logs to stdout. Defaults to `0`. +- `AOH_LOG_LEVEL`: The AoH logging threshold. Defaults to `ERROR`. + +- `AOH_MAX_RUNNERS`: The maximum number of concurrent runners, or `0` for no + limit. Defaults to `0`. + - `AOH_ORIGIN`: The origin of the AoH server, as referenced in the `/run.py` script (e.g. `https://aoh.example.com:5000`). This must be set if running AoH behind a proxy. Defaults to the origin on which requests for `/run.py` are received. -- `AOH_MAX_RUNNERS`: The maximum number of concurrent runners, or `0` for no - limit. Defaults to `0`. - - `AOH_PASSWORDS_FILE`: The file containing passwords for password-protected playbooks (see below). Defaults to `/aoh/passwords.yml` when running the Docker image and `./passwords.yml` otherwise. diff --git a/aoh/__init__.py b/aoh/__init__.py @@ -1,5 +1,11 @@ +import logging + +from .config import AOH_LOG_LEVEL from .server import app, scheduler +logging.basicConfig() +logging.getLogger(__name__).setLevel(AOH_LOG_LEVEL) + def create_app(): scheduler.start() diff --git a/aoh/config.py b/aoh/config.py @@ -10,6 +10,8 @@ load_dotenv() AOH_DEBUG = (os.getenv('AOH_DEBUG', '0') == '1') +AOH_LOG_LEVEL = os.getenv('AOH_LOG_LEVEL', 'ERROR') + try: AOH_MAX_RUNNERS = int(os.getenv('AOH_MAX_RUNNERS', '0')) except ValueError: diff --git a/aoh/server.py b/aoh/server.py @@ -1,11 +1,12 @@ +import logging import os from flask import Flask, abort, render_template, request, session from flask_apscheduler import APScheduler from .config import AOH_MAX_RUNNERS, AOH_ORIGIN -from .playbook import get_playbook -from .runner import Runner +from .playbook import PlaybookError, get_playbook +from .runner import Runner, RunnerError from .security import * _RUNNERS = {} @@ -20,6 +21,8 @@ app.secret_key = os.urandom(16) scheduler = APScheduler() scheduler.init_app(app) +logger = logging.getLogger(__name__) + @scheduler.task('interval', seconds=_GC_INTERVAL) def gc(): @@ -28,6 +31,8 @@ def gc(): runners = list(_RUNNERS.items()) for id, runner in runners: if runner and runner.has_timed_out(_GC_THRESHOLD): + logger.info(f'{runner.playbook.name} (#{id[:8]}): Runner torn down ' + 'due to inactivity.') runner.teardown() _RUNNERS.pop(id, None) @@ -39,6 +44,13 @@ def inject_stage_and_region(): } +@app.errorhandler(RunnerError) +@app.errorhandler(PlaybookError) +def error(e): + logger.error(e) + return {'msg': 'Internal server error'}, 500 + + @app.get('/run') @app.get('/run.py') def install(): @@ -51,10 +63,8 @@ def new_runner(path): if not playbook: return {'err': f'Playbook not found: {path}'}, 400 - if not validate_args(playbook, request.json['args']): - return {'err': 'Bad or banned arguments passed.'}, 400 - if AOH_MAX_RUNNERS != 0 and len(_RUNNERS) >= AOH_MAX_RUNNERS: + logger.info(f'{path}: No runners available.') return { 'err': 'No runners available.' }, 503 exp_pw_types = get_required_passwords(playbook, request.json['args']) @@ -68,16 +78,23 @@ def new_runner(path): }, 401 if not validate_aoh_password(playbook, act_pws.get('aoh_password')): + logger.info(f'{path}: Received incorrect password.') return {'err': 'Incorrect AoH password.'}, 401 act_pws.pop('aoh_password', None) # Don't pass AoH password on to runner + if not validate_args(playbook, request.json['args']): + logger.info(f"{path}: Received bad arguments: {request.json['args']}") + return {'err': 'Bad or banned arguments passed.'}, 400 + runner = Runner(playbook, request.json['host'], request.json['args'], act_pws) assert runner.id is not None _RUNNERS[runner.id] = runner session['runner'] = runner.id - short_id = runner.id.split('-')[0] + short_id = runner.id[:8] + + logger.info(f'{path} (#{short_id}): Created runner.') return {}, 201, {'Location': f'/runners/{path}/{short_id}'} @@ -95,9 +112,12 @@ def runner_update(path, short_id): if runner.playbook.name != path: return {'err': f'Playbook not found: {path}'}, 400 + logger.debug(f'{path} (#{short_id}): Received client message.') res = runner.process_message(request.json) if res['finished']: + logger.info(f'{runner.playbook.name} (#{short_id}): Runner torn down ' + 'due to completion.') runner.teardown() _RUNNERS.pop(id, None)