aoh

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

commit a1a4b9ccebaa9c9635edaa8449975f0e064cc222
parent b661fa39ccb4e112ab9cb0c490e6f08f6af97995
Author: Asher Morgan <asher@ashermorgan.net>
Date:   Sun,  4 Oct 2026 16:32:42 -0700

Add basic Windows support

Largely based off of the Windows-specific code in the official ssh and winrm
connection plugins.

Diffstat:
Maoh/client.py | 3++-
Maoh/connection_plugins/aoh.py | 49++++++++++++++++++++++++++++++++++++++++++-------
Maoh/runner.py | 4+++-
Maoh/server.py | 2+-
Mtests/e2e/test_connection.yml | 5++---
5 files changed, 50 insertions(+), 13 deletions(-)

diff --git a/aoh/client.py b/aoh/client.py @@ -272,9 +272,10 @@ def create_runner(playbook, args): color |= os.getenv('ANSIBLE_FORCE_COLOR', '').lower() in TRUTHY_VALUES req = { - 'host': platform.node(), 'args': args, 'color': color, + 'host': platform.node(), + 'is_windows': sys.platform == 'win32', } status, headers, res = send_json_request(url, req, 'POST') diff --git a/aoh/connection_plugins/aoh.py b/aoh/connection_plugins/aoh.py @@ -9,8 +9,6 @@ DOCUMENTATION = """ required: true env: - name: ANSIBLE_AOH_DIR - vars: - - name: ansible_aoh_dir aoh_timeout: description: The timeout for AoH responses, in seconds type: integer @@ -32,6 +30,12 @@ DOCUMENTATION = """ - name: ANSIBLE_AOH_KEEP_ALIVE_COUNT vars: - name: ansible_aoh_keep_alive_count + aoh_is_windows: + description: Whether the client host runs Windows + type: bool + default: false + env: + - name: ANSIBLE_AOH_IS_WINDOWS """ import base64 @@ -42,12 +46,15 @@ import select import typing as t from uuid import uuid4 +from ansible._internal._powershell import _clixml from ansible.errors import ( AnsibleConnectionFailure, AnsibleError, AnsibleFileNotFound, ) +from ansible.plugins import AnsiblePlugin from ansible.plugins.connection import ConnectionBase +from ansible.plugins.shell.cmd import ShellModule as CmdShellModule from ansible.utils.display import Display display = Display() @@ -60,10 +67,27 @@ class Connection(ConnectionBase): has_pipelining = True has_tty = False + _shell: CmdShellModule def __init__(self, *args: t.Any, **kwargs: t.Any) -> None: + AnsiblePlugin.__init__(self) # enable get_option() + self.is_windows = self.get_option('aoh_is_windows') + + if self.is_windows: + self._shell_type = 'cmd' # must be set before super().__init__() + super().__init__(*args, **kwargs) + if self.is_windows and self._shell.SHELL_FAMILY != 'cmd': + display.warning( + msg='The aoh connection plugin should have the shell type of ' \ + f'cmd and not {self._shell.SHELL_FAMILY}. This may ' \ + 'result in an error when attempting to run more complex ' \ + 'commands.', + help_text='Unset ansible_shell_type or set to cmd to use the ' \ + 'required cmd shell.', + ) + self.timeout = self.get_option('aoh_timeout') self.keep_alive_count = self.get_option('aoh_keep_alive_count') self.keep_alive_interval = round(self.timeout/self.keep_alive_count, 1) @@ -73,6 +97,11 @@ class Connection(ConnectionBase): self._recvpoll = None self._connected = False + if self.is_windows: + # ty: ignore[invalid-assignment] + self.module_implementation_preferences = ('.ps1', '.exe', '') + self.allow_executable = False # Prevent use of "/bin/sh -c ..." + def _connect(self) -> Connection: # noqa: F821 """Connect to the host.""" @@ -167,6 +196,9 @@ class Connection(ConnectionBase): display.vvv(f'EXEC {cmd}', host=self._play_context.remote_addr) + if self.is_windows: + sudoable = False + msg = { 'cmd': cmd, 'stdin': in_data and in_data.decode('latin1'), @@ -182,11 +214,14 @@ class Connection(ConnectionBase): 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 ( - res['returncode'], - res['stdout'].encode('latin1'), - res['stderr'].encode('latin1'), - ) + + stdout = res['stdout'].encode('latin1') + stderr = res['stderr'].encode('latin1') + + if self.is_windows: + stderr = _clixml.replace_stderr_clixml(stderr) + + return (res['returncode'], stdout, stderr) def put_file(self, in_path: str, out_path: str) -> None: diff --git a/aoh/runner.py b/aoh/runner.py @@ -60,13 +60,14 @@ def get_config_values(playbook, keys): class Runner: """An Ansible-over-HTTP playbook runner.""" - def __init__(self, playbook, host, args, color, passwords): + def __init__(self, playbook, host, args, is_windows, color, passwords): """Create a new AoH runner.""" self.id = str(uuid.uuid4()) # Runner ID (set to None after teardown) self.playbook = playbook self.host = self.playbook.host or host or 'aoh_node' self.args = args + self.is_windows = is_windows self.color = color self.passwords = passwords @@ -122,6 +123,7 @@ class Runner: for key, val in self.playbook.env.items(): env[key] = val env['ANSIBLE_AOH_DIR'] = self._DIR + env['ANSIBLE_AOH_IS_WINDOWS'] = '1' if self.is_windows else '0' env['ANSIBLE_CONNECTION_PLUGINS'] = ':'.join( [_CONNECTION_PLUGIN_DIR] + config['DEFAULT_CONNECTION_PLUGIN_PATH'] diff --git a/aoh/server.py b/aoh/server.py @@ -102,7 +102,7 @@ def new_runner(path): return {'err': 'Bad or banned arguments passed'}, 400 runner = Runner(playbook, request.json['host'], request.json['args'], - request.json['color'], act_pws) + request.json['is_windows'], request.json['color'], act_pws) assert runner.id is not None _RUNNERS[runner.id] = runner diff --git a/tests/e2e/test_connection.yml b/tests/e2e/test_connection.yml @@ -35,11 +35,10 @@ - name: raw with unicode arg and output raw: "{{ echo_commands[action_prefix ~ ansible_connection ~ '_' ~ (ansible_shell_type|default(''))] | default(echo_commands['default']) }}" vars: - # Windows over SSH does not have a way to set the console codepage to allow UTF-8. We need to + # Windows over AoH does not have a way to set the console codepage to allow UTF-8. We need to # wrap the commands we send to the remote host to get it working. echo_commands: - win_ssh_powershell: '[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; echo {{ echo_string }}' - win_ssh_cmd: 'powershell.exe -Command "[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; echo {{ echo_string }}"' + win_aoh_cmd: 'powershell.exe -Command "[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; echo {{ echo_string }}"' default: echo {{ echo_string }} register: command - name: check output of raw with unicode arg and output