aoh

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

commit a3831d5ba913eee52f569cdf61fcac001c8375f6
parent 82216e676fa0f97d8f4887bac551c6e426ff2891
Author: Asher Morgan <asher@ashermorgan.net>
Date:   Tue,  8 Sep 2026 09:34:11 -0700

Minor cleanup

Diffstat:
M.editorconfig | 4+---
Maoh/client.py | 16++++++++--------
Maoh/connection_plugins/aoh.py | 9+++------
Maoh/playbook.py | 1-
Maoh/security.py | 8+++-----
Maoh/server.py | 6+++---
Mrequirements.txt | 2+-
7 files changed, 19 insertions(+), 27 deletions(-)

diff --git a/.editorconfig b/.editorconfig @@ -5,12 +5,10 @@ root = true [*] charset = utf-8 end_of_line = lf +indent_size = 4 indent_style = space insert_final_newline = true trim_trailing_whitespace = true - -[*.py] -indent_size = 4 max_line_length = 80 [*.yml] diff --git a/aoh/client.py b/aoh/client.py @@ -22,8 +22,8 @@ PASSWORD_PROMPTS = { 'vault_password': 'Vault password: ', } -# How long to sleep after receiving an empty responses -WAIT_INTERVAL = 0.1 +# Interval on which to poll the server when there's nothing to do, in seconds +POLL_INTERVAL = 0.1 class ClientError(Exception): @@ -204,7 +204,7 @@ def exec(args, keep_alive_interval, keep_alive_handler): 'stderr': (become_stderr + stderr).decode('latin1'), } except Exception as e: # noqa: BLE001 - return { 'err': str(e) } + return {'err': str(e)} def put(args): @@ -214,9 +214,9 @@ def put(args): with open(args['dest'], 'wb') as f: f.write(base64.b64decode(args['data'])) except Exception as e: # noqa: BLE001 - return { 'err': str(e) } + return {'err': str(e)} else: - return { 'ok': True } + return {'ok': True} def fetch(args): @@ -228,7 +228,7 @@ def fetch(args): 'data': base64.b64encode(f.read()).decode(), } except Exception as e: # noqa: BLE001 - return { 'err': str(e) } + return {'err': str(e)} def runner_loop(runner_url, cookies): @@ -243,7 +243,7 @@ def runner_loop(runner_url, cookies): keep_alive_handler = lambda res=res: send_message( runner_url, cookies, - { 'id': res['id'], 'keep-alive': True }, + {'id': res['id'], 'keep-alive': True}, keep_alive=True ) req = exec(res['exec'], res['keep-alive-interval'], @@ -257,7 +257,7 @@ def runner_loop(runner_url, cookies): req['id'] = res['id'] else: req = {} - time.sleep(WAIT_INTERVAL) + time.sleep(POLL_INTERVAL) def create_runner(playbook, args): diff --git a/aoh/connection_plugins/aoh.py b/aoh/connection_plugins/aoh.py @@ -1,6 +1,3 @@ -# Copyright: (c) 2026, Asher Morgan <asher@ashermorgan.net> -# GNU General Public License v3.0+ - DOCUMENTATION = """ name: aoh short_description: Run tasks through an Ansible-over-HTTP (AoH) connection @@ -159,7 +156,7 @@ class Connection(ConnectionBase): if not res.get('keep-alive'): return res - self._send_message_async({ 'id': id, 'keep-alive': True }) + self._send_message_async({'id': id, 'keep-alive': True}) def exec_command(self, cmd: str, in_data: bytes | None = None, @@ -182,7 +179,7 @@ class Connection(ConnectionBase): 'success': self.become.success, } - res = self._send_message_sync({ 'exec': msg }) + res = self._send_message_sync({'exec': msg}) if not all(k in res for k in ['returncode', 'stdout', 'stderr']): raise AnsibleError('Received invalid AoH EXEC response') return ( @@ -219,7 +216,7 @@ class Connection(ConnectionBase): display.vvv(f'FETCH {in_path} TO {out_path}', host=self._play_context.remote_addr) - res = self._send_message_sync({ 'fetch': in_path }) + res = self._send_message_sync({'fetch': in_path}) if 'data' not in res: raise AnsibleError('Received AoH FETCH response without data') try: diff --git a/aoh/playbook.py b/aoh/playbook.py @@ -6,7 +6,6 @@ _PLAYBOOKS_FILE = os.getenv('AOH_PLAYBOOKS_FILE', 'playbooks.yml') _PLAYBOOKS_DIR = os.path.dirname(_PLAYBOOKS_FILE) - class PlaybookError(Exception): """Raised for AoH playbook configuration errors.""" diff --git a/aoh/security.py b/aoh/security.py @@ -5,8 +5,6 @@ import ansible_runner import bcrypt import yaml -_PASSWORDS_FILE = os.getenv('AOH_PASSWORDS_FILE', 'passwords.yml') - _CLI_OPT_WHITELIST = [ # These options should be safe for clients to invoke '--ask-become-pass', '-K', @@ -14,7 +12,6 @@ _CLI_OPT_WHITELIST = [ '--ask-vault-password', '--ask-vault-pass', '-J', '--check', '-C', '--diff', '-D', - '--help', '-h', '--limit', '-l', '--list-tags', '--skip-tags', @@ -25,8 +22,7 @@ _CLI_OPT_WHITELIST = [ ] _CLI_OPT_BLACKLIST = [ - # These options are not supported at all by AoH - '--step', # Interactive + '--step', # Interactive, not supported ] _CLI_PASSWORD_OPTS = { @@ -47,6 +43,8 @@ _CONFIG_PASSWORD_OPTS = { 'DEFAULT_BECOME_ASK_PASS': 'become_password', } +_PASSWORDS_FILE = os.getenv('AOH_PASSWORDS_FILE', 'passwords.yml') + def _get_opts(args): """Identify options present in a list of CLI arguments.""" diff --git a/aoh/server.py b/aoh/server.py @@ -48,10 +48,10 @@ def install(): def new_runner(path): playbook = get_playbook(path) if not playbook: - return { 'err': f'Playbook not found: {path}' }, 400 + return {'err': f'Playbook not found: {path}'}, 400 if not validate_args(playbook, request.json['args']): - return { 'err': 'Bad or banned arguments passed.' }, 400 + return {'err': 'Bad or banned arguments passed.'}, 400 exp_pw_types = get_required_passwords(playbook, request.json['args']) act_pws = request.json.get('passwords', {}) @@ -64,7 +64,7 @@ def new_runner(path): }, 401 if not validate_aoh_password(playbook, act_pws.get('aoh_password')): - return { 'err': 'Incorrect AoH password.' }, 401 + return {'err': 'Incorrect AoH password.'}, 401 act_pws.pop('aoh_password', None) # Don't pass AoH password on to runner runner = Runner(playbook, request.json['host'], request.json['args'], diff --git a/requirements.txt b/requirements.txt @@ -1,8 +1,8 @@ ansible ansible_runner +bcrypt flask flask-apscheduler gunicorn python-dotenv pyyaml -bcrypt