aoh

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

commit 4ad640c3f5948ce279908e12578a30e56b51bf64
parent ad83738738f59ac3ce5f10f3226eed82578c8449
Author: Asher Morgan <asher@ashermorgan.net>
Date:   Sat, 15 Aug 2026 19:56:28 -0700

Catch Ansible errors when http plugin not called

Previously the server hung on errors such as "no vault password", "no
hosts matched", etc. due to waiting for the http plugin to open the
other ends of the recvbuf/sendbuf pipes.

Diffstat:
Mconnection_plugins/http.py | 22+++++++++++++++++++---
Mmain.py | 86++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------
2 files changed, 78 insertions(+), 30 deletions(-)

diff --git a/connection_plugins/http.py b/connection_plugins/http.py @@ -50,8 +50,18 @@ class Connection(ConnectionBase): f'{self.get_option('runner')}', host=self._play_context.remote_addr) - self.sendbuf = open(f'{self.get_option('runner')}/sendbuf', 'w') - self.recvbuf = open(f'{self.get_option('runner')}/recvbuf', 'r') + RUNNER_DIR = self.get_option('runner') + RECVBUF_PATH = f'{RUNNER_DIR}/recvbuf' + SENDBUF_PATH = f'{RUNNER_DIR}/sendbuf' + + if not os.path.exists(RECVBUF_PATH): + os.mkfifo(RECVBUF_PATH) + if not os.path.exists(SENDBUF_PATH): + os.mkfifo(SENDBUF_PATH) + + self.recvbuf = open(RECVBUF_PATH, 'r') + self.sendbuf = open(SENDBUF_PATH, 'w') + self._connected = True return self @@ -141,9 +151,15 @@ class Connection(ConnectionBase): def close(self) -> None: """Close connection.""" - self._connected = False display.vvv('CLOSE HTTP CONNECTION FOR RUNNER ' f'{self.get_option('runner')}', host=self._play_context.remote_addr) + if self.recvbuf: + self.recvbuf.close() + if self.sendbuf: + self.sendbuf.close() + + self._connected = False + super(Connection, self).close() diff --git a/main.py b/main.py @@ -1,3 +1,4 @@ +import time import json import os import shutil @@ -20,21 +21,38 @@ class Runner: def __init__(self, config, playbook, host): self.id = str(uuid4()) self._dir = tempfile.mkdtemp() - self._SENDBUF_PATH = f'{self._dir}/sendbuf' - self._RECVBUF_PATH = f'{self._dir}/recvbuf' self._LOGS_PATH = f'{self._dir}/artifacts/{self.id}/stdout' - - os.mkfifo(self._SENDBUF_PATH) - os.mkfifo(self._RECVBUF_PATH) - - self._start(config, playbook, host) - - self._sendbuf = open(self._SENDBUF_PATH, 'r') - self._recvbuf = open(self._RECVBUF_PATH, 'w') - self._logs = open(self._LOGS_PATH, 'r') - - - def _start(self, config, playbook, host): + self._RECVBUF_PATH = f'{self._dir}/recvbuf' + self._SENDBUF_PATH = f'{self._dir}/sendbuf' + self._sendbuf = None + self._recvbuf = None + self._logs = None + self._thread = None + self._runner = None + + try: + self._start_runner(config, playbook, host) + + # We assume that ansible-runner will eventually create its log file + while not os.path.exists(self._LOGS_PATH): + time.sleep(0.1) + self._logs = open(self._LOGS_PATH, 'r') + + # The FIFO buffers may not get created if Ansible crashes/exits + # before calling the http connection plugin + while not os.path.exists(self._SENDBUF_PATH) and \ + not self._runner_finished(): + time.sleep(0.1) + if os.path.exists(self._RECVBUF_PATH): + # Note that recvbuf is created first, so sendbuf will exist too + self._recvbuf = open(self._RECVBUF_PATH, 'w') + self._sendbuf = open(self._SENDBUF_PATH, 'r') + except: + self.teardown() + raise + + + def _start_runner(self, config, playbook, host): raw_config = ansible_runner.get_ansible_config('dump', config, quiet=True)[0] @@ -55,7 +73,7 @@ class Runner: 'ansible_connection': 'http', } - self.thread, self.runner = ansible_runner.run_async( + self._thread, self._runner = ansible_runner.run_async( private_data_dir=self._dir, ident=self.id, envvars=env, @@ -71,7 +89,8 @@ class Runner: def _runner_finished(self): - return self.runner.status not in ['started', 'running'] + assert self._runner is not None + return self._runner.status not in ['started', 'running'] def process_client_request(self, req): @@ -79,25 +98,38 @@ class Runner: # We check runner status first, in case new logs come in afterwards res['finished'] = self._runner_finished() + assert self._logs is not None res['logs'] = self._logs.read() - if req: - self._recvbuf.write(json.dumps(req) + '\n') - self._recvbuf.flush() - - line = self._sendbuf.readline() - for key, val in json.loads(line or '{}').items(): - res[key] = val + try: + if req and self._recvbuf: + self._recvbuf.write(json.dumps(req) + '\n') + self._recvbuf.flush() + + if self._sendbuf: + line = self._sendbuf.readline() + for key, val in json.loads(line or '{}').items(): + res[key] = val + except Exception as e: + # This is probably a broken pipe, which indicates a fatal error. + # Then Ansible should exit soon. + if 'Broken pipe' not in str(e): + raise return res def teardown(self): - self.thread.join() self.id = None - self._sendbuf.close() - self._recvbuf.close() - self._logs.close() + if self._logs: + self._logs.close() + if self._recvbuf: + self._recvbuf.close() + if self._sendbuf: + self._sendbuf.close() + if self._thread: + # Closing the sendbuf should make Ansible exit if it hasn't already + self._thread.join() shutil.rmtree(self._dir)