From 4b67afc2b0ce5e4e96fe20ea51e56360a5f699cc Mon Sep 17 00:00:00 2001 From: fizmat Date: Sat, 17 Jan 2026 00:07:11 +0400 Subject: [PATCH] Add option for wsl_shell_type, protect wsl.exe arguments if SSH shell is Powershell (#11308) * feat(wsl): add option for wsl_shell_type, protect wsl arguments if SSH shell is Powershell * docs(wsl): add changelog fragment * docs(wsl): fix changelog fragment syntax, add issue link Co-authored-by: Felix Fontein * feat(wsl): improve new option documentation Co-authored-by: Felix Fontein * refactor(wsl): put integrasion test flag into a variable for convenience * feat(wsl): rename option to wsl_remote_ssh_shell_type * feat(wsl): escape "%" if shell is cmd, raise AnsibleError if powershell * test(wsl): fix unit tests for wsl - remove redundant check - moved to a separate function - fix check for cmd escaping of "%" - fix formatting / whitespace * test(wsl): fix expected error message * test(wsl): fix test - position of stop-parsing token changed Co-authored-by: Felix Fontein --------- Co-authored-by: Felix Fontein --- changelogs/fragments/11308-wsl-shell-type.yml | 3 ++ plugins/connection/wsl.py | 32 +++++++++++++++++-- tests/unit/plugins/connection/test_wsl.py | 13 ++++++++ 3 files changed, 45 insertions(+), 3 deletions(-) create mode 100644 changelogs/fragments/11308-wsl-shell-type.yml diff --git a/changelogs/fragments/11308-wsl-shell-type.yml b/changelogs/fragments/11308-wsl-shell-type.yml new file mode 100644 index 0000000000..958c37dd94 --- /dev/null +++ b/changelogs/fragments/11308-wsl-shell-type.yml @@ -0,0 +1,3 @@ +minor_changes: + - wsl connection plugin - add option ``wsl_remote_ssh_shell_type``. Support PowerShell in addition to cmd as the Windows + shell (https://github.com/ansible-collections/community.general/issues/11307, https://github.com/ansible-collections/community.general/pull/11308). diff --git a/plugins/connection/wsl.py b/plugins/connection/wsl.py index 0b33f12cb4..1bdf6bb6f1 100644 --- a/plugins/connection/wsl.py +++ b/plugins/connection/wsl.py @@ -231,6 +231,18 @@ options: required: true vars: - name: wsl_distribution + wsl_remote_ssh_shell_type: + description: + - The shell type expected in the SSH session (not inside the WSL session). + - See also C(ansible_shell_type). + type: string + choices: + - cmd + - powershell + default: cmd + vars: + - name: wsl_remote_ssh_shell_type + version_added: 12.2.0 wsl_user: description: - WSL distribution user. @@ -578,18 +590,32 @@ class Connection(ConnectionBase): wsl_distribution = self.get_option("wsl_distribution") become = self.get_option("become") become_user = self.get_option("become_user") + wsl_remote_ssh_shell_type = self.get_option("wsl_remote_ssh_shell_type") + is_integration_test = os.getenv("_ANSIBLE_TEST_WSL_CONNECTION_PLUGIN_WAERI5TEPHEESHA2FAE8") + if "%" in cmd: + if wsl_remote_ssh_shell_type == "powershell": + # there is no universal way to escape '%' here + # if this is raised, add a workaround to allow the specific situation (if possible) + raise AnsibleError("The command contains '%', cannot safely escape it for Powershell") + else: + cmd = cmd.replace("%", "^%") if become and become_user: wsl_user = become_user else: wsl_user = self.get_option("wsl_user") - args = ["wsl.exe", "--distribution", wsl_distribution] + args = ["wsl.exe"] + if wsl_remote_ssh_shell_type == "powershell" and not is_integration_test: + # Powershell stop-parsing token, treat the rest as arguments to the native command wsl.exe + args.append("--%") + args.extend(["--distribution", wsl_distribution]) if wsl_user: args.extend(["--user", wsl_user]) args.extend(["--"]) args.extend(shlex.split(cmd)) - if os.getenv("_ANSIBLE_TEST_WSL_CONNECTION_PLUGIN_WAERI5TEPHEESHA2FAE8"): + if is_integration_test: return shlex.join(args) - return list2cmdline(args) # see https://github.com/python/cpython/blob/3.11/Lib/subprocess.py#L576 + else: + return list2cmdline(args) # see https://github.com/python/cpython/blob/3.11/Lib/subprocess.py#L576 def exec_command(self, cmd: str, in_data: bytes | None = None, sudoable: bool = True) -> tuple[int, bytes, bytes]: """run a command on inside a WSL distribution""" diff --git a/tests/unit/plugins/connection/test_wsl.py b/tests/unit/plugins/connection/test_wsl.py index 5445bfde83..5121a16c9b 100644 --- a/tests/unit/plugins/connection/test_wsl.py +++ b/tests/unit/plugins/connection/test_wsl.py @@ -210,6 +210,19 @@ def test_build_wsl_command(connection): assert cmd == 'wsl.exe --distribution test --user test-become-user -- /bin/sh -c "ls -la"' +def test_build_wsl_command_powershell(connection): + """Test wsl command building for powershell and cmd remote ssh shell""" + cmd = connection._build_wsl_command('/bin/sh -c "ls -la %PATH%"') + assert cmd == 'wsl.exe --distribution test -- /bin/sh -c "ls -la ^%PATH^%"' + + connection.set_option("wsl_remote_ssh_shell_type", "powershell") + cmd = connection._build_wsl_command('/bin/sh -c "ls -la"') + assert cmd == 'wsl.exe --% --distribution test -- /bin/sh -c "ls -la"' + + with pytest.raises(AnsibleError, match="The command contains '%', cannot safely escape it for Powershell"): + connection._build_wsl_command('/bin/sh -c "ls -la %PATH%"') + + @patch("paramiko.SSHClient") def test_exec_command_success(mock_ssh, connection): """Test successful command execution"""