commit b32b1f96961f76d0cc3611856cd5b9570015e3b3
parent f7da3d7c2877d87ce3c9db091e00dd8bf553f679
Author: Asher Morgan <asher@ashermorgan.net>
Date: Sun, 16 Aug 2026 14:42:41 -0700
Rename project to AoH
Diffstat:
4 files changed, 165 insertions(+), 164 deletions(-)
diff --git a/README.md b/README.md
@@ -0,0 +1 @@
+# Ansible-over-HTTP (AoH)
diff --git a/connection_plugins/aoh.py b/connection_plugins/aoh.py
@@ -0,0 +1,161 @@
+# Copyright: (c) 2026, Asher Morgan <asher@ashermorgan.net>
+# GNU General Public License v3.0+
+
+DOCUMENTATION = r'''
+ name: aoh
+ short_description: Run tasks through an Ansible-over-HTTP (AoH) connection
+
+ options:
+ aoh_dir:
+ description: The AoH runner directory
+ required: true
+ vars:
+ - name: ansible_aoh_dir
+'''
+
+import base64
+import json
+import os
+import typing as t
+
+from ansible.errors import AnsibleError, AnsibleFileNotFound
+from ansible.plugins.connection import ConnectionBase
+from ansible.utils.display import Display
+
+display = Display()
+
+
+class Connection(ConnectionBase):
+ """Ansible-over-HTTP (AoH) connection."""
+
+ transport = 'aoh'
+ has_pipelining = False
+
+
+ def __init__(self, *args: t.Any, **kwargs: t.Any) -> None:
+ super().__init__(*args, **kwargs)
+
+ self.sendbuf = None
+ self.recvbuf = None
+
+ def _connect(self) -> Connection: # noqa: F821
+ """Connect to the host."""
+
+ if not self._connected:
+ AOH_DIR = self.get_option('aoh_dir')
+
+ display.vvv(f'ESTABLISH CONNECTION TO AOH RUNNER {AOH_DIR}',
+ host=self._play_context.remote_addr)
+
+ RECVBUF_PATH = f'{AOH_DIR}/recvbuf'
+ SENDBUF_PATH = f'{AOH_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') # noqa: SIM115
+ self.sendbuf = open(SENDBUF_PATH, 'w') # noqa: SIM115
+
+ self._connected = True
+
+ return self
+
+
+ def exec_command(self, cmd: str, in_data: bytes | None = None,
+ sudoable: bool = True) -> tuple[int, bytes, bytes]:
+ """Run a command on the host."""
+
+ super().exec_command(cmd, in_data=in_data, sudoable=sudoable)
+
+ display.vvv(f'EXEC {cmd}', host=self._play_context.remote_addr)
+
+ assert self.sendbuf is not None
+ assert self.recvbuf is not None
+ assert isinstance(cmd, str)
+ assert in_data is None
+ # assert sudoable is False
+
+ self.sendbuf.write(json.dumps({
+ 'exec': cmd,
+ }) + '\n')
+ self.sendbuf.flush()
+
+ res = json.loads(self.recvbuf.readline())
+ if 'err' in res:
+ raise AnsibleError(res['err'])
+ return (
+ res['returncode'],
+ res['stdout'].encode('latin1'),
+ res['stderr'].encode('latin1'),
+ )
+
+
+ def put_file(self, in_path: str, out_path: str) -> None:
+ """Transfer file to host."""
+
+ super().put_file(in_path, out_path)
+
+ display.vvv(f'PUT {in_path} TO {out_path}',
+ host=self._play_context.remote_addr)
+
+ assert self.sendbuf is not None
+ assert self.recvbuf is not None
+
+ if not os.path.exists(in_path):
+ raise AnsibleFileNotFound(f'file or module does not exist: {in_path}')
+ with open(in_path, 'rb') as f:
+ data = base64.b64encode(f.read()).decode()
+
+ self.sendbuf.write(json.dumps({
+ 'put': {
+ 'data': data,
+ 'dest': out_path,
+ },
+ }) + '\n')
+ self.sendbuf.flush()
+
+ res = json.loads(self.recvbuf.readline())
+ if 'err' in res:
+ raise AnsibleError(res['err'])
+
+
+ def fetch_file(self, in_path: str, out_path: str) -> None:
+ """Fetch file from host."""
+
+ super().fetch_file(in_path, out_path)
+
+ display.vvv(f'FETCH {in_path} TO {out_path}',
+ host=self._play_context.remote_addr)
+
+ assert self.sendbuf is not None
+ assert self.recvbuf is not None
+
+ self.sendbuf.write(json.dumps({
+ 'fetch': in_path,
+ }) + '\n')
+ self.sendbuf.flush()
+
+ res = json.loads(self.recvbuf.readline())
+ if 'err' in res:
+ raise AnsibleError(res['err'])
+ with open(out_path, 'rb') as f:
+ f.write(base64.b64decode(res['data']))
+
+
+ def close(self) -> None:
+ """Close connection."""
+
+ display.vvv('CLOSE CONNECTION FOR AOH 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().close()
diff --git a/connection_plugins/http.py b/connection_plugins/http.py
@@ -1,161 +0,0 @@
-# Copyright: (c) 2026, Asher Morgan <asher@ashermorgan.net>
-# GNU General Public License v3.0+
-
-DOCUMENTATION = r'''
- name: http
- short_description: Control hosts over a "reverse" HTTP connection
-
- options:
- runner:
- description: The artifact directory of the associated Ansible runner
- required: true
- vars:
- - name: ansible_http_runner
-'''
-
-import base64
-import json
-import os
-import typing as t
-
-from ansible.errors import AnsibleError, AnsibleFileNotFound
-from ansible.plugins.connection import ConnectionBase
-from ansible.utils.display import Display
-
-display = Display()
-
-
-class Connection(ConnectionBase):
- """Reverse-HTTP connection."""
-
- transport = 'http'
- has_pipelining = False
-
-
- def __init__(self, *args: t.Any, **kwargs: t.Any) -> None:
- super().__init__(*args, **kwargs)
-
- self.sendbuf = None
- self.recvbuf = None
-
- def _connect(self) -> Connection: # noqa: F821
- """Connect to the host (nop)."""
-
- if not self._connected:
- display.vvv('ESTABLISH HTTP CONNECTION FOR RUNNER '
- f'{self.get_option('runner')}',
- host=self._play_context.remote_addr)
-
- 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') # noqa: SIM115
- self.sendbuf = open(SENDBUF_PATH, 'w') # noqa: SIM115
-
- self._connected = True
-
- return self
-
-
- def exec_command(self, cmd: str, in_data: bytes | None = None,
- sudoable: bool = True) -> tuple[int, bytes, bytes]:
- """Run a command on the host."""
-
- super().exec_command(cmd, in_data=in_data, sudoable=sudoable)
-
- display.vvv(f'EXEC {cmd}', host=self._play_context.remote_addr)
-
- assert self.sendbuf is not None
- assert self.recvbuf is not None
- assert isinstance(cmd, str)
- assert in_data is None
- # assert sudoable is False
-
- self.sendbuf.write(json.dumps({
- 'exec': cmd,
- }) + '\n')
- self.sendbuf.flush()
-
- res = json.loads(self.recvbuf.readline())
- if 'err' in res:
- raise AnsibleError(res['err'])
- return (
- res['returncode'],
- res['stdout'].encode('latin1'),
- res['stderr'].encode('latin1'),
- )
-
-
- def put_file(self, in_path: str, out_path: str) -> None:
- """Transfer file to host."""
-
- super().put_file(in_path, out_path)
-
- display.vvv(f'PUT {in_path} TO {out_path}',
- host=self._play_context.remote_addr)
-
- assert self.sendbuf is not None
- assert self.recvbuf is not None
-
- if not os.path.exists(in_path):
- raise AnsibleFileNotFound(f'file or module does not exist: {in_path}')
- with open(in_path, 'rb') as f:
- data = base64.b64encode(f.read()).decode()
-
- self.sendbuf.write(json.dumps({
- 'put': {
- 'data': data,
- 'dest': out_path,
- },
- }) + '\n')
- self.sendbuf.flush()
-
- res = json.loads(self.recvbuf.readline())
- if 'err' in res:
- raise AnsibleError(res['err'])
-
-
- def fetch_file(self, in_path: str, out_path: str) -> None:
- """Fetch file from host."""
-
- super().fetch_file(in_path, out_path)
-
- display.vvv(f'FETCH {in_path} TO {out_path}',
- host=self._play_context.remote_addr)
-
- assert self.sendbuf is not None
- assert self.recvbuf is not None
-
- self.sendbuf.write(json.dumps({
- 'fetch': in_path,
- }) + '\n')
- self.sendbuf.flush()
-
- res = json.loads(self.recvbuf.readline())
- if 'err' in res:
- raise AnsibleError(res['err'])
- with open(out_path, 'rb') as f:
- f.write(base64.b64decode(res['data']))
-
-
- def close(self) -> None:
- """Close connection."""
-
- 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().close()
diff --git a/main.py b/main.py
@@ -38,7 +38,7 @@ class Runner:
self._logs = open(self._LOGS_PATH, 'r') # noqa: SIM115
# The FIFO buffers may not get created if Ansible crashes/exits
- # before calling the http connection plugin
+ # before calling the AoH connection plugin
while not os.path.exists(self._SENDBUF_PATH) and \
not self._runner_finished():
time.sleep(0.1)
@@ -68,8 +68,8 @@ class Runner:
'ANSIBLE_CONFIG': config,
}
vars = {
- 'ansible_http_runner': self._dir,
- 'ansible_connection': 'http',
+ 'ansible_aoh_dir': self._dir,
+ 'ansible_connection': 'aoh',
}
self._thread, self._runner = ansible_runner.run_async(