Repository: ansible-collections/community.general
Pull request #9795: add the wsl connection plugin
Author: login: rgl
Labels: name: connection
description: connection plugin
name: tests
description: tests
name: unit
description: tests/unit
name: integration
description: tests/integration
name: new_contributor
description: Help guide this first time contributor
name: new_plugin
description: New plugin
name: plugins
description: plugin (any type)
name: backport-10
description: Automatically create a backport for the stable-10 branch
Created: 2025-02-22T16:13:52Z | Merged: 2025-04-19T07:01:37Z

##### SUMMARY

This adds the `community.general.wsl` connection plugin.

This allows Ansible to remotely manage a [WSL Distribution](https://learn.microsoft.com/en-us/windows/wsl/install) by using SSH to connect to a Windows machine, then use `wsl.exe` to execute commands inside a WSL Distribution.

This is derived from [the existing `community.general.proxmox_pct_remote` connection plugin](https://github.com/ansible-collections/community.general/blob/main/plugins/connection/proxmox_pct_remote.py).

##### ISSUE TYPE

- New Plugin Pull Request

##### COMPONENT NAME

community.general.wsl

##### ADDITIONAL INFORMATION

Here's an simple example how to use this. The full example is at the branch at https://github.com/rgl/terraform-libvirt-ansible-windows-example/tree/wsl.

Example inventory:

```yaml
all:
  children:
    wsl:
      hosts:
        example-wsl-ubuntu:
          ansible_host: 10.17.3.2 # the windows host machine.
          wsl_distribution: Ubuntu-24.04
          wsl_user: ubuntu # the user inside the wsl distribution which will run the command.
      vars:
        ansible_connection: community.general.wsl
        ansible_user: vagrant # the user on the windows host machine.
        ansible_password: vagrant # the password for the user on the windows host machine.
```

Example playbook:

```yaml
- name: WSL Example
  hosts: wsl
  gather_facts: true
  become: true
  tasks:
    - name: Ping
      ansible.builtin.ping:
    - name: Id (with become false)
      become: false
      changed_when: false
      args:
        executable: /bin/bash
      ansible.builtin.shell: |
        exec 2>&1
        set -x
        echo "$0"
        pwd
        id
    - name: Id (with become true)
      changed_when: false
      args:
        executable: /bin/bash
      ansible.builtin.shell: |
        exec 2>&1
        set -x
        echo "$0"
        pwd
        id
    - name: Install vim
      ansible.builtin.apt:
        name: vim
        install_recommends: false
    - name: Reboot
      ansible.builtin.reboot:
        boot_time_command: systemctl show -p ActiveEnterTimestamp init.scope
```



Diff:
diff --git a/.github/BOTMETA.yml b/.github/BOTMETA.yml
index c92ce760344..501c1370d4e 100644
--- a/.github/BOTMETA.yml
+++ b/.github/BOTMETA.yml
@@ -119,6 +119,8 @@ files:
   $connections/saltstack.py:
     labels: saltstack
     maintainers: mscherer
+  $connections/wsl.py:
+    maintainers: rgl
   $connections/zone.py:
     maintainers: $team_ansible_core
   $doc_fragments/:
diff --git a/plugins/connection/wsl.py b/plugins/connection/wsl.py
new file mode 100644
index 00000000000..886ac5c60f0
--- /dev/null
+++ b/plugins/connection/wsl.py
@@ -0,0 +1,786 @@
+# -*- coding: utf-8 -*-
+# Derived from ansible/plugins/connection/proxmox_pct_remote.py (c) 2024 Nils Stein (@mietzen) <github.nstein@mailbox.org>
+# Derived from ansible/plugins/connection/paramiko_ssh.py (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
+# Copyright (c) 2025 Rui Lopes (@rgl) <ruilopes.com>
+# Copyright (c) 2025 Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import annotations
+
+DOCUMENTATION = r"""
+author: Rui Lopes (@rgl) <ruilopes.com>
+name: wsl
+short_description: Run tasks in WSL distribution using wsl.exe CLI via SSH
+requirements:
+  - paramiko
+description:
+  - Run commands or put/fetch files to an existing WSL distribution using wsl.exe CLI via SSH.
+  - Uses the Python SSH implementation (Paramiko) to connect to the WSL host.
+version_added: "10.6.0"
+options:
+  remote_addr:
+    description:
+      - Address of the remote target.
+    default: inventory_hostname
+    type: string
+    vars:
+      - name: inventory_hostname
+      - name: ansible_host
+      - name: ansible_ssh_host
+      - name: ansible_paramiko_host
+  port:
+    description: Remote port to connect to.
+    type: int
+    default: 22
+    ini:
+      - section: defaults
+        key: remote_port
+      - section: paramiko_connection
+        key: remote_port
+    env:
+      - name: ANSIBLE_REMOTE_PORT
+      - name: ANSIBLE_REMOTE_PARAMIKO_PORT
+    vars:
+      - name: ansible_port
+      - name: ansible_ssh_port
+      - name: ansible_paramiko_port
+    keyword:
+      - name: port
+  remote_user:
+    description:
+      - User to login/authenticate as.
+      - Can be set from the CLI via the C(--user) or C(-u) options.
+    type: string
+    vars:
+      - name: ansible_user
+      - name: ansible_ssh_user
+      - name: ansible_paramiko_user
+    env:
+      - name: ANSIBLE_REMOTE_USER
+      - name: ANSIBLE_PARAMIKO_REMOTE_USER
+    ini:
+      - section: defaults
+        key: remote_user
+      - section: paramiko_connection
+        key: remote_user
+    keyword:
+      - name: remote_user
+  password:
+    description:
+      - Secret used to either login the SSH server or as a passphrase for SSH keys that require it.
+      - Can be set from the CLI via the C(--ask-pass) option.
+    type: string
+    vars:
+      - name: ansible_password
+      - name: ansible_ssh_pass
+      - name: ansible_ssh_password
+      - name: ansible_paramiko_pass
+      - name: ansible_paramiko_password
+  use_rsa_sha2_algorithms:
+    description:
+      - Whether or not to enable RSA SHA2 algorithms for pubkeys and hostkeys.
+      - On paramiko versions older than 2.9, this only affects hostkeys.
+      - For behavior matching paramiko<2.9 set this to V(false).
+    vars:
+      - name: ansible_paramiko_use_rsa_sha2_algorithms
+    ini:
+      - {key: use_rsa_sha2_algorithms, section: paramiko_connection}
+    env:
+      - {name: ANSIBLE_PARAMIKO_USE_RSA_SHA2_ALGORITHMS}
+    default: true
+    type: boolean
+  host_key_auto_add:
+    description: "Automatically add host keys to C(~/.ssh/known_hosts)."
+    env:
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_AUTO_ADD
+    ini:
+      - key: host_key_auto_add
+        section: paramiko_connection
+    type: boolean
+  look_for_keys:
+    default: True
+    description: "Set to V(false) to disable searching for private key files in C(~/.ssh/)."
+    env:
+      - name: ANSIBLE_PARAMIKO_LOOK_FOR_KEYS
+    ini:
+      - {key: look_for_keys, section: paramiko_connection}
+    type: boolean
+  proxy_command:
+    default: ""
+    description:
+      - Proxy information for running the connection via a jumphost.
+      - This option is supported by paramiko version 1.9.0 or newer.
+    type: string
+    env:
+      - name: ANSIBLE_PARAMIKO_PROXY_COMMAND
+    ini:
+      - {key: proxy_command, section: paramiko_connection}
+    vars:
+      - name: ansible_paramiko_proxy_command
+  record_host_keys:
+    default: True
+    description: "Save the host keys to a file."
+    env:
+      - name: ANSIBLE_PARAMIKO_RECORD_HOST_KEYS
+    ini:
+      - section: paramiko_connection
+        key: record_host_keys
+    type: boolean
+  host_key_checking:
+    description: "Set this to V(false) if you want to avoid host key checking by the underlying tools Ansible uses to connect to the host."
+    type: boolean
+    default: true
+    env:
+      - name: ANSIBLE_HOST_KEY_CHECKING
+      - name: ANSIBLE_SSH_HOST_KEY_CHECKING
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_CHECKING
+    ini:
+      - section: defaults
+        key: host_key_checking
+      - section: paramiko_connection
+        key: host_key_checking
+    vars:
+      - name: ansible_host_key_checking
+      - name: ansible_ssh_host_key_checking
+      - name: ansible_paramiko_host_key_checking
+  use_persistent_connections:
+    description: "Toggles the use of persistence for connections."
+    type: boolean
+    default: False
+    env:
+      - name: ANSIBLE_USE_PERSISTENT_CONNECTIONS
+    ini:
+      - section: defaults
+        key: use_persistent_connections
+  banner_timeout:
+    type: float
+    default: 30
+    description:
+      - Configures, in seconds, the amount of time to wait for the SSH
+        banner to be presented.
+      - This option is supported by paramiko version 1.15.0 or newer.
+    ini:
+      - section: paramiko_connection
+        key: banner_timeout
+    env:
+      - name: ANSIBLE_PARAMIKO_BANNER_TIMEOUT
+  timeout:
+    type: int
+    default: 10
+    description:
+      - Number of seconds until the plugin gives up on failing to establish a TCP connection.
+      - This option is supported by paramiko version 2.2.0 or newer.
+    ini:
+      - section: defaults
+        key: timeout
+      - section: ssh_connection
+        key: timeout
+      - section: paramiko_connection
+        key: timeout
+    env:
+      - name: ANSIBLE_TIMEOUT
+      - name: ANSIBLE_SSH_TIMEOUT
+      - name: ANSIBLE_PARAMIKO_TIMEOUT
+    vars:
+      - name: ansible_ssh_timeout
+      - name: ansible_paramiko_timeout
+    cli:
+      - name: timeout
+  lock_file_timeout:
+    type: int
+    default: 60
+    description: Number of seconds until the plugin gives up on trying to write a lock file when writing SSH known host keys.
+    vars:
+      - name: ansible_lock_file_timeout
+    env:
+      - name: ANSIBLE_LOCK_FILE_TIMEOUT
+  private_key_file:
+    description:
+      - Path to private key file to use for authentication.
+    type: path
+    ini:
+      - section: defaults
+        key: private_key_file
+      - section: paramiko_connection
+        key: private_key_file
+    env:
+      - name: ANSIBLE_PRIVATE_KEY_FILE
+      - name: ANSIBLE_PARAMIKO_PRIVATE_KEY_FILE
+    vars:
+      - name: ansible_private_key_file
+      - name: ansible_ssh_private_key_file
+      - name: ansible_paramiko_private_key_file
+    cli:
+      - name: private_key_file
+        option: "--private-key"
+  user_known_hosts_file:
+    description:
+      - Path to the user known hosts file.
+      - Used to verify the ssh hosts keys.
+    type: path
+    default: ~/.ssh/known_hosts
+    ini:
+      - section: paramiko_connection
+        key: user_known_hosts_file
+    vars:
+      - name: ansible_paramiko_user_known_hosts_file
+  wsl_distribution:
+    description:
+      - WSL distribution name
+    type: string
+    required: true
+    vars:
+      - name: wsl_distribution
+  wsl_user:
+    description:
+      - WSL distribution user
+    type: string
+    vars:
+      - name: wsl_user
+  become_user:
+    description:
+      - WSL distribution user
+    type: string
+    default: root
+    vars:
+      - name: become_user
+      - name: ansible_become_user
+  become:
+    description:
+      - whether to use the user defined by ansible_become_user.
+    type: bool
+    default: false
+    vars:
+      - name: become
+      - name: ansible_become
+"""
+
+EXAMPLES = r"""
+# ------------------------
+# Inventory: inventory.yml
+# ------------------------
+---
+all:
+  children:
+    wsl:
+      hosts:
+        example-wsl-ubuntu:
+          ansible_host: 10.0.0.10
+          wsl_distribution: ubuntu
+          wsl_user: ubuntu
+      vars:
+        ansible_connection: community.general.wsl
+        ansible_user: vagrant
+# ----------------------
+# Playbook: playbook.yml
+# ----------------------
+---
+- name: WSL Example
+  hosts: wsl
+  gather_facts: true
+  become: true
+  tasks:
+    - name: Ping
+      ansible.builtin.ping:
+    - name: Id (with become false)
+      become: false
+      changed_when: false
+      args:
+        executable: /bin/bash
+      ansible.builtin.shell: |
+        exec 2>&1
+        set -x
+        echo "$0"
+        pwd
+        id
+    - name: Id (with become true)
+      changed_when: false
+      args:
+        executable: /bin/bash
+      ansible.builtin.shell: |
+        exec 2>&1
+        set -x
+        echo "$0"
+        pwd
+        id
+    - name: Reboot
+      ansible.builtin.reboot:
+        boot_time_command: systemctl show -p ActiveEnterTimestamp init.scope
+"""
+
+import io
+import os
+import pathlib
+import shlex
+import socket
+import tempfile
+import typing as t
+
+from ansible.errors import (
+    AnsibleAuthenticationFailure,
+    AnsibleConnectionFailure,
+    AnsibleError,
+)
+from ansible_collections.community.general.plugins.module_utils._filelock import FileLock, LockTimeout
+from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
+from ansible.module_utils.compat.paramiko import PARAMIKO_IMPORT_ERR, paramiko
+from ansible.module_utils.compat.version import LooseVersion
+from ansible.playbook.play_context import PlayContext
+from ansible.plugins.connection import ConnectionBase
+from ansible.utils.display import Display
+from ansible.utils.path import makedirs_safe
+from binascii import hexlify
+from subprocess import list2cmdline
+
+
+if t.TYPE_CHECKING and paramiko:
+    from paramiko import MissingHostKeyPolicy
+    from paramiko.client import SSHClient
+    from paramiko.pkey import PKey
+else:
+    MissingHostKeyPolicy: type = object
+    SSHClient: type = object
+    PKey: type = object
+
+
+display = Display()
+
+
+def authenticity_msg(hostname: str, ktype: str, fingerprint: str) -> str:
+    msg = f"""
+    paramiko: The authenticity of host '{hostname}' can't be established.
+    The {ktype} key fingerprint is {fingerprint}.
+    Are you sure you want to continue connecting (yes/no)?
+    """
+    return msg
+
+
+class MyAddPolicy(MissingHostKeyPolicy):
+    """
+    Based on AutoAddPolicy in paramiko so we can determine when keys are added
+
+    and also prompt for input.
+
+    Policy for automatically adding the hostname and new host key to the
+    local L{HostKeys} object, and saving it. This is used by L{SSHClient}.
+    """
+
+    def __init__(self, connection: Connection) -> None:
+        self.connection = connection
+        self._options = connection._options
+
+    def missing_host_key(self, client: SSHClient, hostname: str, key: PKey) -> None:
+
+        if all((self.connection.get_option('host_key_checking'), not self.connection.get_option('host_key_auto_add'))):
+
+            fingerprint = hexlify(key.get_fingerprint())
+            ktype = key.get_name()
+
+            if self.connection.get_option('use_persistent_connections') or self.connection.force_persistence:
+                # don't print the prompt string since the user cannot respond
+                # to the question anyway
+                raise AnsibleError(authenticity_msg(hostname, ktype, fingerprint)[1:92])
+
+            inp = to_text(
+                display.prompt_until(authenticity_msg(hostname, ktype, fingerprint), private=False),
+                errors='surrogate_or_strict'
+            )
+
+            if inp.lower() not in ['yes', 'y', '']:
+                raise AnsibleError('host connection rejected by user')
+
+        key._added_by_ansible_this_time = True
+
+        # existing implementation below:
+        client._host_keys.add(hostname, key.get_name(), key)
+
+        # host keys are actually saved in close() function below
+        # in order to control ordering.
+
+
+class Connection(ConnectionBase):
+    """ SSH based connections (paramiko) to WSL """
+
+    transport = 'community.general.wsl'
+    _log_channel: str | None = None
+
+    def __init__(self, play_context: PlayContext, new_stdin: io.TextIOWrapper | None = None, *args: t.Any, **kwargs: t.Any):
+        super(Connection, self).__init__(play_context, new_stdin, *args, **kwargs)
+
+    def _set_log_channel(self, name: str) -> None:
+        """ Mimic paramiko.SSHClient.set_log_channel """
+        self._log_channel = name
+
+    def _parse_proxy_command(self, port: int = 22) -> dict[str, t.Any]:
+        proxy_command = self.get_option('proxy_command') or None
+
+        sock_kwarg = {}
+        if proxy_command:
+            replacers: t.Dict[str, str] = {
+                '%h': self.get_option('remote_addr'),
+                '%p': str(port),
+                '%r': self.get_option('remote_user')
+            }
+            for find, replace in replacers.items():
+                proxy_command = proxy_command.replace(find, replace)
+            try:
+                sock_kwarg = {'sock': paramiko.ProxyCommand(proxy_command)}
+                display.vvv(f'CONFIGURE PROXY COMMAND FOR CONNECTION: {proxy_command}', host=self.get_option('remote_addr'))
+            except AttributeError:
+                display.warning('Paramiko ProxyCommand support unavailable. '
+                                'Please upgrade to Paramiko 1.9.0 or newer. '
+                                'Not using configured ProxyCommand')
+
+        return sock_kwarg
+
+    def _connect(self) -> Connection:
+        """ activates the connection object """
+
+        if paramiko is None:
+            raise AnsibleError(f'paramiko is not installed: {to_native(PARAMIKO_IMPORT_ERR)}')
+
+        port = self.get_option('port')
+        display.vvv(f'ESTABLISH PARAMIKO SSH CONNECTION FOR USER: {self.get_option("remote_user")} on PORT {to_text(port)} TO {self.get_option("remote_addr")}',
+                    host=self.get_option('remote_addr'))
+
+        ssh = paramiko.SSHClient()
+
+        # Set pubkey and hostkey algorithms to disable, the only manipulation allowed currently
+        # is keeping or omitting rsa-sha2 algorithms
+        # default_keys: t.Tuple[str] = ()
+        paramiko_preferred_pubkeys = getattr(paramiko.Transport, '_preferred_pubkeys', ())
+        paramiko_preferred_hostkeys = getattr(paramiko.Transport, '_preferred_keys', ())
+        use_rsa_sha2_algorithms = self.get_option('use_rsa_sha2_algorithms')
+        disabled_algorithms: t.Dict[str, t.Iterable[str]] = {}
+        if not use_rsa_sha2_algorithms:
+            if paramiko_preferred_pubkeys:
+                disabled_algorithms['pubkeys'] = tuple(a for a in paramiko_preferred_pubkeys if 'rsa-sha2' in a)
+            if paramiko_preferred_hostkeys:
+                disabled_algorithms['keys'] = tuple(a for a in paramiko_preferred_hostkeys if 'rsa-sha2' in a)
+
+        # override paramiko's default logger name
+        if self._log_channel is not None:
+            ssh.set_log_channel(self._log_channel)
+
+        self.keyfile = os.path.expanduser(self.get_option('user_known_hosts_file'))
+
+        if self.get_option('host_key_checking'):
+            for ssh_known_hosts in ('/etc/ssh/ssh_known_hosts', '/etc/openssh/ssh_known_hosts', self.keyfile):
+                try:
+                    ssh.load_system_host_keys(ssh_known_hosts)
+                    break
+                except IOError:
+                    pass  # file was not found, but not required to function
+                except paramiko.hostkeys.InvalidHostKey as e:
+                    raise AnsibleConnectionFailure(f'Invalid host key: {to_text(e.line)}')
+            try:
+                ssh.load_system_host_keys()
+            except paramiko.hostkeys.InvalidHostKey as e:
+                raise AnsibleConnectionFailure(f'Invalid host key: {to_text(e.line)}')
+
+        ssh_connect_kwargs = self._parse_proxy_command(port)
+        ssh.set_missing_host_key_policy(MyAddPolicy(self))
+        conn_password = self.get_option('password')
+        allow_agent = True
+
+        if conn_password is not None:
+            allow_agent = False
+
+        try:
+            key_filename = None
+            if self.get_option('private_key_file'):
+                key_filename = os.path.expanduser(self.get_option('private_key_file'))
+
+            # paramiko 2.2 introduced auth_timeout parameter
+            if LooseVersion(paramiko.__version__) >= LooseVersion('2.2.0'):
+                ssh_connect_kwargs['auth_timeout'] = self.get_option('timeout')
+
+            # paramiko 1.15 introduced banner timeout parameter
+            if LooseVersion(paramiko.__version__) >= LooseVersion('1.15.0'):
+                ssh_connect_kwargs['banner_timeout'] = self.get_option('banner_timeout')
+
+            ssh.connect(
+                self.get_option('remote_addr').lower(),
+                username=self.get_option('remote_user'),
+                allow_agent=allow_agent,
+                look_for_keys=self.get_option('look_for_keys'),
+                key_filename=key_filename,
+                password=conn_password,
+                timeout=self.get_option('timeout'),
+                port=port,
+                disabled_algorithms=disabled_algorithms,
+                **ssh_connect_kwargs,
+            )
+        except paramiko.ssh_exception.BadHostKeyException as e:
+            raise AnsibleConnectionFailure(f'host key mismatch for {to_text(e.hostname)}')
+        except paramiko.ssh_exception.AuthenticationException as e:
+            msg = f'Failed to authenticate: {e}'
+            raise AnsibleAuthenticationFailure(msg)
+        except Exception as e:
+            msg = to_text(e)
+            if u'PID check failed' in msg:
+                raise AnsibleError('paramiko version issue, please upgrade paramiko on the machine running ansible')
+            elif u'Private key file is encrypted' in msg:
+                msg = f'ssh {self.get_option("remote_user")}@{self.get_options("remote_addr")}:{port} : ' + \
+                    f'{msg}\nTo connect as a different user, use -u <username>.'
+                raise AnsibleConnectionFailure(msg)
+            else:
+                raise AnsibleConnectionFailure(msg)
+        self.ssh = ssh
+        self._connected = True
+        return self
+
+    def _any_keys_added(self) -> bool:
+        for hostname, keys in self.ssh._host_keys.items():
+            for keytype, key in keys.items():
+                added_this_time = getattr(key, '_added_by_ansible_this_time', False)
+                if added_this_time:
+                    return True
+        return False
+
+    def _save_ssh_host_keys(self, filename: str) -> None:
+        """
+        not using the paramiko save_ssh_host_keys function as we want to add new SSH keys at the bottom so folks
+        don't complain about it :)
+        """
+
+        if not self._any_keys_added():
+            return
+
+        path = os.path.expanduser('~/.ssh')
+        makedirs_safe(path)
+
+        with open(filename, 'w') as f:
+            for hostname, keys in self.ssh._host_keys.items():
+                for keytype, key in keys.items():
+                    # was f.write
+                    added_this_time = getattr(key, '_added_by_ansible_this_time', False)
+                    if not added_this_time:
+                        f.write(f'{hostname} {keytype} {key.get_base64()}\n')
+
+            for hostname, keys in self.ssh._host_keys.items():
+                for keytype, key in keys.items():
+                    added_this_time = getattr(key, '_added_by_ansible_this_time', False)
+                    if added_this_time:
+                        f.write(f'{hostname} {keytype} {key.get_base64()}\n')
+
+    def _build_wsl_command(self, cmd: str) -> str:
+        wsl_distribution = self.get_option('wsl_distribution')
+        become = self.get_option('become')
+        become_user = self.get_option('become_user')
+        if become and become_user:
+            wsl_user = become_user
+        else:
+            wsl_user = self.get_option('wsl_user')
+        args = ['wsl.exe', '--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'):
+            return shlex.join(args)
+        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 """
+
+        cmd = self._build_wsl_command(cmd)
+
+        super(Connection, self).exec_command(cmd, in_data=in_data, sudoable=sudoable)
+
+        bufsize = 4096
+
+        try:
+            self.ssh.get_transport().set_keepalive(5)
+            chan = self.ssh.get_transport().open_session()
+        except Exception as e:
+            text_e = to_text(e)
+            msg = 'Failed to open session'
+            if text_e:
+                msg += f': {text_e}'
+            raise AnsibleConnectionFailure(to_native(msg))
+
+        display.vvv(f'EXEC {cmd}', host=self.get_option('remote_addr'))
+
+        cmd = to_bytes(cmd, errors='surrogate_or_strict')
+
+        no_prompt_out = b''
+        no_prompt_err = b''
+        become_output = b''
+
+        try:
+            chan.exec_command(cmd)
+            if self.become and self.become.expect_prompt():
+                password_prompt = False
+                become_success = False
+                while not (become_success or password_prompt):
+                    display.debug('Waiting for Privilege Escalation input')
+
+                    chunk = chan.recv(bufsize)
+                    display.debug(f'chunk is: {to_text(chunk)}')
+                    if not chunk:
+                        if b'unknown user' in become_output:
+                            n_become_user = to_native(self.become.get_option('become_user'))
+                            raise AnsibleError(f'user {n_become_user} does not exist')
+                        else:
+                            break
+                            # raise AnsibleError('ssh connection closed waiting for password prompt')
+                    become_output += chunk
+
+                    # need to check every line because we might get lectured
+                    # and we might get the middle of a line in a chunk
+                    for line in become_output.splitlines(True):
+                        if self.become.check_success(line):
+                            become_success = True
+                            break
+                        elif self.become.check_password_prompt(line):
+                            password_prompt = True
+                            break
+
+                if password_prompt:
+                    if self.become:
+                        become_pass = self.become.get_option('become_pass')
+                        chan.sendall(to_bytes(become_pass + '\n', errors='surrogate_or_strict'))
+                    else:
+                        raise AnsibleError('A password is required but none was supplied')
+                else:
+                    no_prompt_out += become_output
+                    no_prompt_err += become_output
+
+            if in_data:
+                for i in range(0, len(in_data), bufsize):
+                    chan.send(in_data[i:i + bufsize])
+                chan.shutdown_write()
+            elif in_data == b'':
+                chan.shutdown_write()
+
+        except socket.timeout:
+            raise AnsibleError('ssh timed out waiting for privilege escalation.\n' + to_text(become_output))
+
+        stdout = b''.join(chan.makefile('rb', bufsize))
+        stderr = b''.join(chan.makefile_stderr('rb', bufsize))
+        returncode = chan.recv_exit_status()
+
+        # NB the full english error message is:
+        #     'wsl.exe' is not recognized as an internal or external command,
+        #     operable program or batch file.
+        if "'wsl.exe' is not recognized" in stderr.decode('utf-8'):
+            raise AnsibleError(
+                f'wsl.exe not found in path of host: {to_text(self.get_option("remote_addr"))}')
+
+        return (returncode, no_prompt_out + stdout, no_prompt_out + stderr)
+
+    def put_file(self, in_path: str, out_path: str) -> None:
+        """ transfer a file from local to remote """
+
+        display.vvv(f'PUT {in_path} TO {out_path}', host=self.get_option('remote_addr'))
+        try:
+            with open(in_path, 'rb') as f:
+                data = f.read()
+                returncode, stdout, stderr = self.exec_command(
+                    ' '.join([
+                        self._shell.executable, '-c',
+                        self._shell.quote(f'cat > {out_path}')]),
+                    in_data=data,
+                    sudoable=False)
+            if returncode != 0:
+                if 'cat: not found' in stderr.decode('utf-8'):
+                    raise AnsibleError(
+                        f'cat not found in path of WSL distribution: {to_text(self.get_option("wsl_distribution"))}')
+                raise AnsibleError(
+                    f'{to_text(stdout)}\n{to_text(stderr)}')
+        except Exception as e:
+            raise AnsibleError(
+                f'error occurred while putting file from {in_path} to {out_path}!\n{to_text(e)}')
+
+    def fetch_file(self, in_path: str, out_path: str) -> None:
+        """ save a remote file to the specified path """
+
+        display.vvv(f'FETCH {in_path} TO {out_path}', host=self.get_option('remote_addr'))
+        try:
+            returncode, stdout, stderr = self.exec_command(
+                ' '.join([
+                    self._shell.executable, '-c',
+                    self._shell.quote(f'cat {in_path}')]),
+                sudoable=False)
+            if returncode != 0:
+                if 'cat: not found' in stderr.decode('utf-8'):
+                    raise AnsibleError(
+                        f'cat not found in path of WSL distribution: {to_text(self.get_option("wsl_distribution"))}')
+                raise AnsibleError(
+                    f'{to_text(stdout)}\n{to_text(stderr)}')
+            with open(out_path, 'wb') as f:
+                f.write(stdout)
+        except Exception as e:
+            raise AnsibleError(
+                f'error occurred while fetching file from {in_path} to {out_path}!\n{to_text(e)}')
+
+    def reset(self) -> None:
+        """ reset the connection """
+
+        if not self._connected:
+            return
+        self.close()
+        self._connect()
+
+    def close(self) -> None:
+        """ terminate the connection """
+
+        if self.get_option('host_key_checking') and self.get_option('record_host_keys') and self._any_keys_added():
+            # add any new SSH host keys -- warning -- this could be slow
+            # (This doesn't acquire the connection lock because it needs
+            # to exclude only other known_hosts writers, not connections
+            # that are starting up.)
+            lockfile = os.path.basename(self.keyfile)
+            dirname = os.path.dirname(self.keyfile)
+            makedirs_safe(dirname)
+            tmp_keyfile_name = None
+            try:
+                with FileLock().lock_file(lockfile, dirname, self.get_option('lock_file_timeout')):
+                    # just in case any were added recently
+
+                    self.ssh.load_system_host_keys()
+                    self.ssh._host_keys.update(self.ssh._system_host_keys)
+
+                    # gather information about the current key file, so
+                    # we can ensure the new file has the correct mode/owner
+
+                    key_dir = os.path.dirname(self.keyfile)
+                    if os.path.exists(self.keyfile):
+                        key_stat = os.stat(self.keyfile)
+                        mode = key_stat.st_mode & 0o777
+                        uid = key_stat.st_uid
+                        gid = key_stat.st_gid
+                    else:
+                        mode = 0o644
+                        uid = os.getuid()
+                        gid = os.getgid()
+
+                    # Save the new keys to a temporary file and move it into place
+                    # rather than rewriting the file. We set delete=False because
+                    # the file will be moved into place rather than cleaned up.
+
+                    with tempfile.NamedTemporaryFile(dir=key_dir, delete=False) as tmp_keyfile:
+                        tmp_keyfile_name = tmp_keyfile.name
+                        os.chmod(tmp_keyfile_name, mode)
+                        os.chown(tmp_keyfile_name, uid, gid)
+                        self._save_ssh_host_keys(tmp_keyfile_name)
+
+                    os.rename(tmp_keyfile_name, self.keyfile)
+            except LockTimeout:
+                raise AnsibleError(
+                    f'writing lock file for {self.keyfile} ran in to the timeout of {self.get_option("lock_file_timeout")}s')
+            except paramiko.hostkeys.InvalidHostKey as e:
+                raise AnsibleConnectionFailure(f'Invalid host key: {e.line}')
+            except Exception as e:
+                # unable to save keys, including scenario when key was invalid
+                # and caught earlier
+                raise AnsibleError(
+                    f'error occurred while writing SSH host keys!\n{to_text(e)}')
+            finally:
+                if tmp_keyfile_name is not None:
+                    pathlib.Path(tmp_keyfile_name).unlink(missing_ok=True)
+
+        self.ssh.close()
+        self._connected = False
diff --git a/tests/integration/targets/connection_wsl/aliases b/tests/integration/targets/connection_wsl/aliases
new file mode 100644
index 00000000000..aeb39c141a7
--- /dev/null
+++ b/tests/integration/targets/connection_wsl/aliases
@@ -0,0 +1,11 @@
+# Derived from ../connection_proxmox_pct_remote/aliases Copyright (c) 2025 Nils Stein (@mietzen) <github.nstein@mailbox.org>
+# Copyright (c) 2025 Rui Lopes (@rgl) <ruilopes.com>
+# Copyright (c) 2025 Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+azp/posix/3
+destructive
+needs/root
+needs/target/connection
+skip/macos
diff --git a/tests/integration/targets/connection_wsl/dependencies.yml b/tests/integration/targets/connection_wsl/dependencies.yml
new file mode 100644
index 00000000000..fdf36f5e95a
--- /dev/null
+++ b/tests/integration/targets/connection_wsl/dependencies.yml
@@ -0,0 +1,19 @@
+---
+# Derived from ../connection_proxmox_pct_remote/dependencies.yml Copyright (c) 2025 Nils Stein (@mietzen) <github.nstein@mailbox.org>
+# Copyright (c) 2025 Rui Lopes (@rgl) <ruilopes.com>
+# Copyright (c) 2025 Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+- hosts: localhost
+  gather_facts: true
+  serial: 1
+  tasks:
+    - name: Copy wsl.exe mock
+      copy:
+        src: files/wsl.exe
+        dest: /usr/local/bin/wsl.exe
+        mode: '0755'
+    - name: Install paramiko
+      pip:
+        name: "paramiko>=3.0.0"
diff --git a/tests/integration/targets/connection_wsl/files/wsl.exe b/tests/integration/targets/connection_wsl/files/wsl.exe
new file mode 100755
index 00000000000..0c6aafaf0fb
--- /dev/null
+++ b/tests/integration/targets/connection_wsl/files/wsl.exe
@@ -0,0 +1,72 @@
+#!/usr/bin/env bash
+# Derived from ../../connection_proxmox_pct_remote/files/pct Copyright (c) 2025 Nils Stein (@mietzen) <github.nstein@mailbox.org>
+# Copyright (c) 2025 Rui Lopes (@rgl) <ruilopes.com>
+# Copyright (c) 2025 Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+# Shell script to mock wsl.exe behavior
+
+set -euo pipefail
+
+function quote_args {
+  local quoted_args=()
+  for arg in "$@"; do
+    if [[ -z "$arg" || "$arg" =~ [^a-zA-Z0-9@%+=:,./-] ]]; then
+      local escaped_arg=${arg//\'/\'\\\'\'}
+      quoted_args+=("'$escaped_arg'")
+    else
+      quoted_args+=("$arg")
+    fi
+  done
+  echo -n "${quoted_args[@]}"
+}
+
+declare -a mock_args=()
+declare -a cmd_args=()
+wsl_distribution=""
+wsl_user=""
+
+while [[ $# -gt 0 ]]; do
+  case $1 in
+    --distribution|-d)
+      wsl_distribution="$2"
+      mock_args+=("$1" "$2")
+      shift 2
+      ;;
+    --user|-u)
+      wsl_user="$2"
+      mock_args+=("$1" "$2")
+      shift 2
+      ;;
+    --)
+      mock_args+=("$1")
+      shift
+      while [[ $# -gt 0 ]]; do
+        mock_args+=("$1")
+        cmd_args+=("$1")
+        shift
+      done
+      ;;
+    *)
+      >&2 echo "unexpected args: $@"
+      exit 1
+      ;;
+  esac
+done
+
+mock_cmd="wsl.exe $(quote_args "${mock_args[@]}")"
+cmd="$(quote_args "${cmd_args[@]}")"
+
+>&2 echo "[INFO] MOCKING: $mock_cmd"
+>&2 echo "[INFO] CMD: $cmd"
+
+tmp_dir="/tmp/ansible-remote/wsl/integration_test/wsl_distribution_${wsl_distribution}"
+
+mkdir -p "$tmp_dir"
+
+pushd "$tmp_dir" >/dev/null
+
+eval "$cmd"
+
+popd >/dev/null
diff --git a/tests/integration/targets/connection_wsl/plugin-specific-tests.yml b/tests/integration/targets/connection_wsl/plugin-specific-tests.yml
new file mode 100644
index 00000000000..228c34dc51c
--- /dev/null
+++ b/tests/integration/targets/connection_wsl/plugin-specific-tests.yml
@@ -0,0 +1,31 @@
+---
+# Copyright (c) Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+- hosts: "{{ target_hosts }}"
+  gather_facts: false
+  tasks:
+    - name: create file without content
+      copy:
+        content: ""
+        dest: "{{ remote_tmp }}/test_empty.txt"
+        force: no
+        mode: '0644'
+
+    - name: assert file without content exists
+      stat:
+        path: "{{ remote_tmp }}/test_empty.txt"
+      register: empty_file_stat
+
+    - name: verify file without content exists
+      assert:
+        that:
+          - empty_file_stat.stat.exists
+        fail_msg: "The file {{ remote_tmp }}/test_empty.txt does not exist."
+
+    - name: verify file without content is empty
+      assert:
+        that:
+          - empty_file_stat.stat.size == 0
+        fail_msg: "The file {{ remote_tmp }}/test_empty.txt is not empty."
diff --git a/tests/integration/targets/connection_wsl/runme.sh b/tests/integration/targets/connection_wsl/runme.sh
new file mode 100755
index 00000000000..cb1e5b9cd07
--- /dev/null
+++ b/tests/integration/targets/connection_wsl/runme.sh
@@ -0,0 +1,26 @@
+#!/usr/bin/env bash
+# Derived from ../connection_proxmox_pct_remote/runme.sh Copyright (c) 2025 Nils Stein (@mietzen) <github.nstein@mailbox.org>
+# Copyright (c) 2025 Rui Lopes (@rgl) <ruilopes.com>
+# Copyright (c) 2025 Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+set -eux
+
+# signal the wsl connection plugin that its running under the integration testing mode.
+# NB while running integration tests, the mock wsl.exe implementation is actually
+#    running on unix, instead of on running windows, so the wsl.exe command line
+#    construction must use unix rules instead of windows rules.
+export _ANSIBLE_TEST_WSL_CONNECTION_PLUGIN_Waeri5tepheeSha2fae8=1
+
+ANSIBLE_ROLES_PATH=../ \
+    ansible-playbook dependencies.yml -v "$@"
+
+./test.sh "$@"
+
+ansible-playbook plugin-specific-tests.yml -i "./test_connection.inventory" \
+    -e target_hosts=wsl \
+    -e action_prefix= \
+    -e local_tmp=/tmp/ansible-local \
+    -e remote_tmp=/tmp/ansible-remote \
+    "$@"
diff --git a/tests/integration/targets/connection_wsl/test.sh b/tests/integration/targets/connection_wsl/test.sh
new file mode 120000
index 00000000000..70aa5dbdba4
--- /dev/null
+++ b/tests/integration/targets/connection_wsl/test.sh
@@ -0,0 +1 @@
+../connection_posix/test.sh
\ No newline at end of file
diff --git a/tests/integration/targets/connection_wsl/test_connection.inventory b/tests/integration/targets/connection_wsl/test_connection.inventory
new file mode 100644
index 00000000000..53b9a3a0313
--- /dev/null
+++ b/tests/integration/targets/connection_wsl/test_connection.inventory
@@ -0,0 +1,15 @@
+# Derived from ../connection_proxmox_pct_remote/test_connection.inventory Copyright (c) 2025 Nils Stein (@mietzen) <github.nstein@mailbox.org>
+# Copyright (c) 2025 Rui Lopes (@rgl) <ruilopes.com>
+# Copyright (c) 2025 Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+[wsl]
+wsl-pipelining    ansible_ssh_pipelining=true
+wsl-no-pipelining ansible_ssh_pipelining=false
+[wsl:vars]
+ansible_host=localhost
+ansible_user=root
+ansible_python_interpreter="{{ ansible_playbook_python }}"
+ansible_connection=community.general.wsl
+wsl_distribution=test
diff --git a/tests/sanity/ignore-2.15.txt b/tests/sanity/ignore-2.15.txt
index f042b888e81..6115954d00d 100644
--- a/tests/sanity/ignore-2.15.txt
+++ b/tests/sanity/ignore-2.15.txt
@@ -1,4 +1,5 @@
 .azure-pipelines/scripts/publish-codecov.py replace-urlopen
+plugins/connection/wsl.py yamllint:unparsable-with-libyaml
 plugins/inventory/gitlab_runners.py yamllint:unparsable-with-libyaml
 plugins/inventory/iocage.py yamllint:unparsable-with-libyaml
 plugins/inventory/linode.py yamllint:unparsable-with-libyaml
diff --git a/tests/sanity/ignore-2.16.txt b/tests/sanity/ignore-2.16.txt
index 6f3a7f038e7..8ac70d76d7f 100644
--- a/tests/sanity/ignore-2.16.txt
+++ b/tests/sanity/ignore-2.16.txt
@@ -1,3 +1,4 @@
+plugins/connection/wsl.py yamllint:unparsable-with-libyaml
 plugins/inventory/gitlab_runners.py yamllint:unparsable-with-libyaml
 plugins/inventory/iocage.py yamllint:unparsable-with-libyaml
 plugins/inventory/linode.py yamllint:unparsable-with-libyaml
diff --git a/tests/unit/plugins/connection/test_wsl.py b/tests/unit/plugins/connection/test_wsl.py
new file mode 100644
index 00000000000..768f9a8d276
--- /dev/null
+++ b/tests/unit/plugins/connection/test_wsl.py
@@ -0,0 +1,586 @@
+# -*- coding: utf-8 -*-
+# Derived from test_proxmox_pct_remote.py (c) 2024 Nils Stein (@mietzen) <github.nstein@mailbox.org>
+# Copyright (c) 2025 Rui Lopes (@rgl) <ruilopes.com>
+# Copyright (c) 2025 Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+import os
+import pytest
+
+from ansible_collections.community.general.plugins.connection.wsl import authenticity_msg, MyAddPolicy
+from ansible_collections.community.general.plugins.module_utils._filelock import FileLock, LockTimeout
+from ansible.errors import AnsibleError, AnsibleAuthenticationFailure, AnsibleConnectionFailure
+from ansible.module_utils.common.text.converters import to_bytes
+from ansible.module_utils.compat.paramiko import paramiko
+from ansible.playbook.play_context import PlayContext
+from ansible.plugins.loader import connection_loader
+from io import StringIO
+from pathlib import Path
+from unittest.mock import patch, MagicMock, mock_open
+
+
+@pytest.fixture
+def connection():
+    play_context = PlayContext()
+    in_stream = StringIO()
+    conn = connection_loader.get('community.general.wsl', play_context, in_stream)
+    conn.set_option('remote_addr', '192.168.1.100')
+    conn.set_option('remote_user', 'root')
+    conn.set_option('password', 'password')
+    conn.set_option('wsl_distribution', 'test')
+    return conn
+
+
+def test_connection_options(connection):
+    """ Test that connection options are properly set """
+    assert connection.get_option('remote_addr') == '192.168.1.100'
+    assert connection.get_option('remote_user') == 'root'
+    assert connection.get_option('password') == 'password'
+    assert connection.get_option('wsl_distribution') == 'test'
+
+
+def test_authenticity_msg():
+    """ Test authenticity message formatting """
+    msg = authenticity_msg('test.host', 'ssh-rsa', 'AA:BB:CC:DD')
+    assert 'test.host' in msg
+    assert 'ssh-rsa' in msg
+    assert 'AA:BB:CC:DD' in msg
+
+
+def test_missing_host_key(connection):
+    """ Test MyAddPolicy missing_host_key method """
+
+    client = MagicMock()
+    key = MagicMock()
+    key.get_fingerprint.return_value = b'fingerprint'
+    key.get_name.return_value = 'ssh-rsa'
+
+    policy = MyAddPolicy(connection)
+
+    connection.set_option('host_key_auto_add', True)
+    policy.missing_host_key(client, 'test.host', key)
+    assert hasattr(key, '_added_by_ansible_this_time')
+
+    connection.set_option('host_key_auto_add', False)
+    connection.set_option('host_key_checking', False)
+    policy.missing_host_key(client, 'test.host', key)
+
+    connection.set_option('host_key_checking', True)
+    connection.set_option('host_key_auto_add', False)
+    connection.set_option('use_persistent_connections', False)
+
+    with patch('ansible.utils.display.Display.prompt_until', return_value='yes'):
+        policy.missing_host_key(client, 'test.host', key)
+
+    with patch('ansible.utils.display.Display.prompt_until', return_value='no'):
+        with pytest.raises(AnsibleError, match='host connection rejected by user'):
+            policy.missing_host_key(client, 'test.host', key)
+
+
+def test_set_log_channel(connection):
+    """ Test setting log channel """
+    connection._set_log_channel('test_channel')
+    assert connection._log_channel == 'test_channel'
+
+
+def test_parse_proxy_command(connection):
+    """ Test proxy command parsing """
+    connection.set_option('proxy_command', 'ssh -W %h:%p proxy.example.com')
+    connection.set_option('remote_addr', 'target.example.com')
+    connection.set_option('remote_user', 'testuser')
+
+    result = connection._parse_proxy_command(port=2222)
+    assert 'sock' in result
+    assert isinstance(result['sock'], paramiko.ProxyCommand)
+
+
+@patch('paramiko.SSHClient')
+def test_connect_with_rsa_sha2_disabled(mock_ssh, connection):
+    """ Test connection with RSA SHA2 algorithms disabled """
+    connection.set_option('use_rsa_sha2_algorithms', False)
+    mock_client = MagicMock()
+    mock_ssh.return_value = mock_client
+
+    connection._connect()
+
+    call_kwargs = mock_client.connect.call_args[1]
+    assert 'disabled_algorithms' in call_kwargs
+    assert 'pubkeys' in call_kwargs['disabled_algorithms']
+
+
+@patch('paramiko.SSHClient')
+def test_connect_with_bad_host_key(mock_ssh, connection):
+    """ Test connection with bad host key """
+    mock_client = MagicMock()
+    mock_ssh.return_value = mock_client
+    mock_client.connect.side_effect = paramiko.ssh_exception.BadHostKeyException(
+        'hostname', MagicMock(), MagicMock())
+
+    with pytest.raises(AnsibleConnectionFailure, match='host key mismatch'):
+        connection._connect()
+
+
+@patch('paramiko.SSHClient')
+def test_connect_with_invalid_host_key(mock_ssh, connection):
+    """ Test connection with bad host key """
+    connection.set_option('host_key_checking', True)
+    mock_client = MagicMock()
+    mock_ssh.return_value = mock_client
+    mock_client.load_system_host_keys.side_effect = paramiko.hostkeys.InvalidHostKey(
+        "Bad Line!", Exception('Something crashed!'))
+
+    with pytest.raises(AnsibleConnectionFailure, match="Invalid host key: Bad Line!"):
+        connection._connect()
+
+
+@patch('paramiko.SSHClient')
+def test_connect_success(mock_ssh, connection):
+    """ Test successful SSH connection establishment """
+    mock_client = MagicMock()
+    mock_ssh.return_value = mock_client
+
+    connection._connect()
+
+    assert mock_client.connect.called
+    assert connection._connected
+
+
+@patch('paramiko.SSHClient')
+def test_connect_authentication_failure(mock_ssh, connection):
+    """ Test SSH connection with authentication failure """
+    mock_client = MagicMock()
+    mock_ssh.return_value = mock_client
+    mock_client.connect.side_effect = paramiko.ssh_exception.AuthenticationException('Auth failed')
+
+    with pytest.raises(AnsibleAuthenticationFailure):
+        connection._connect()
+
+
+def test_any_keys_added(connection):
+    """ Test checking for added host keys """
+    connection.ssh = MagicMock()
+    connection.ssh._host_keys = {
+        'host1': {
+            'ssh-rsa': MagicMock(_added_by_ansible_this_time=True),
+            'ssh-ed25519': MagicMock(_added_by_ansible_this_time=False)
+        }
+    }
+
+    assert connection._any_keys_added() is True
+
+    connection.ssh._host_keys = {
+        'host1': {
+            'ssh-rsa': MagicMock(_added_by_ansible_this_time=False)
+        }
+    }
+    assert connection._any_keys_added() is False
+
+
+@patch('os.path.exists')
+@patch('os.stat')
+@patch('tempfile.NamedTemporaryFile')
+def test_save_ssh_host_keys(mock_tempfile, mock_stat, mock_exists, connection):
+    """ Test saving SSH host keys """
+    mock_exists.return_value = True
+    mock_stat.return_value = MagicMock(st_mode=0o644, st_uid=1000, st_gid=1000)
+    mock_tempfile.return_value.__enter__.return_value.name = '/tmp/test_keys'
+
+    connection.ssh = MagicMock()
+    connection.ssh._host_keys = {
+        'host1': {
+            'ssh-rsa': MagicMock(
+                get_base64=lambda: 'KEY1',
+                _added_by_ansible_this_time=True
+            )
+        }
+    }
+
+    mock_open_obj = mock_open()
+    with patch('builtins.open', mock_open_obj):
+        connection._save_ssh_host_keys('/tmp/test_keys')
+
+    mock_open_obj().write.assert_called_with('host1 ssh-rsa KEY1\n')
+
+
+def test_build_wsl_command(connection):
+    """ Test wsl command building with different users """
+    cmd = connection._build_wsl_command('/bin/sh -c "ls -la"')
+    assert cmd == 'wsl.exe --distribution test -- /bin/sh -c "ls -la"'
+
+    connection.set_option('wsl_user', 'test-user')
+    cmd = connection._build_wsl_command('/bin/sh -c "ls -la"')
+    assert cmd == 'wsl.exe --distribution test --user test-user -- /bin/sh -c "ls -la"'
+
+    connection.set_option('become', True)
+    connection.set_option('become_user', 'test-become-user')
+    cmd = connection._build_wsl_command('/bin/sh -c "ls -la"')
+    assert cmd == 'wsl.exe --distribution test --user test-become-user -- /bin/sh -c "ls -la"'
+
+
+@patch('paramiko.SSHClient')
+def test_exec_command_success(mock_ssh, connection):
+    """ Test successful command execution """
+    mock_client = MagicMock()
+    mock_ssh.return_value = mock_client
+    mock_channel = MagicMock()
+    mock_transport = MagicMock()
+
+    mock_client.get_transport.return_value = mock_transport
+    mock_transport.open_session.return_value = mock_channel
+    mock_channel.recv_exit_status.return_value = 0
+    mock_channel.makefile.return_value = [to_bytes('stdout')]
+    mock_channel.makefile_stderr.return_value = [to_bytes("")]
+
+    connection._connected = True
+    connection.ssh = mock_client
+
+    returncode, stdout, stderr = connection.exec_command('ls -la')
+
+    mock_transport.open_session.assert_called_once()
+    mock_transport.set_keepalive.assert_called_once_with(5)
+
+
+@patch('paramiko.SSHClient')
+def test_exec_command_wsl_not_found(mock_ssh, connection):
+    """ Test command execution when wsl.exe is not found """
+    mock_client = MagicMock()
+    mock_ssh.return_value = mock_client
+    mock_channel = MagicMock()
+    mock_transport = MagicMock()
+
+    mock_client.get_transport.return_value = mock_transport
+    mock_transport.open_session.return_value = mock_channel
+    mock_channel.recv_exit_status.return_value = 1
+    mock_channel.makefile.return_value = [to_bytes("")]
+    mock_channel.makefile_stderr.return_value = [to_bytes("'wsl.exe' is not recognized")]
+
+    connection._connected = True
+    connection.ssh = mock_client
+
+    with pytest.raises(AnsibleError, match='wsl.exe not found in path of host'):
+        connection.exec_command('ls -la')
+
+
+@patch('paramiko.SSHClient')
+def test_exec_command_session_open_failure(mock_ssh, connection):
+    """ Test exec_command when session opening fails """
+    mock_client = MagicMock()
+    mock_transport = MagicMock()
+    mock_transport.open_session.side_effect = Exception('Failed to open session')
+    mock_client.get_transport.return_value = mock_transport
+
+    connection._connected = True
+    connection.ssh = mock_client
+
+    with pytest.raises(AnsibleConnectionFailure, match='Failed to open session'):
+        connection.exec_command('test command')
+
+
+@patch('paramiko.SSHClient')
+def test_exec_command_with_privilege_escalation(mock_ssh, connection):
+    """ Test exec_command with privilege escalation """
+    mock_client = MagicMock()
+    mock_channel = MagicMock()
+    mock_transport = MagicMock()
+
+    mock_client.get_transport.return_value = mock_transport
+    mock_transport.open_session.return_value = mock_channel
+    connection._connected = True
+    connection.ssh = mock_client
+
+    connection.become = MagicMock()
+    connection.become.expect_prompt.return_value = True
+    connection.become.check_success.return_value = False
+    connection.become.check_password_prompt.return_value = True
+    connection.become.get_option.return_value = 'sudo_password'
+
+    mock_channel.recv.return_value = b'[sudo] password:'
+    mock_channel.recv_exit_status.return_value = 0
+    mock_channel.makefile.return_value = [b""]
+    mock_channel.makefile_stderr.return_value = [b""]
+
+    returncode, stdout, stderr = connection.exec_command('sudo test command')
+
+    mock_channel.sendall.assert_called_once_with(b'sudo_password\n')
+
+
+def test_put_file(connection):
+    """ Test putting a file to the remote system """
+    connection.exec_command = MagicMock()
+    connection.exec_command.return_value = (0, b"", b"")
+
+    with patch('builtins.open', create=True) as mock_open:
+        mock_open.return_value.__enter__.return_value.read.return_value = b'test content'
+        connection.put_file('/local/path', '/remote/path')
+
+    connection.exec_command.assert_called_once_with("/bin/sh -c 'cat > /remote/path'", in_data=b'test content', sudoable=False)
+
+
+@patch('paramiko.SSHClient')
+def test_put_file_general_error(mock_ssh, connection):
+    """ Test put_file with general error """
+    mock_client = MagicMock()
+    mock_ssh.return_value = mock_client
+    mock_channel = MagicMock()
+    mock_transport = MagicMock()
+
+    mock_client.get_transport.return_value = mock_transport
+    mock_transport.open_session.return_value = mock_channel
+    mock_channel.recv_exit_status.return_value = 1
+    mock_channel.makefile.return_value = [to_bytes("")]
+    mock_channel.makefile_stderr.return_value = [to_bytes('Some error')]
+
+    connection._connected = True
+    connection.ssh = mock_client
+
+    with pytest.raises(AnsibleError, match='error occurred while putting file from /remote/path to /local/path'):
+        connection.put_file('/remote/path', '/local/path')
+
+
+@patch('paramiko.SSHClient')
+def test_put_file_cat_not_found(mock_ssh, connection):
+    """ Test command execution when cat is not found """
+    mock_client = MagicMock()
+    mock_ssh.return_value = mock_client
+    mock_channel = MagicMock()
+    mock_transport = MagicMock()
+
+    mock_client.get_transport.return_value = mock_transport
+    mock_transport.open_session.return_value = mock_channel
+    mock_channel.recv_exit_status.return_value = 1
+    mock_channel.makefile.return_value = [to_bytes("")]
+    mock_channel.makefile_stderr.return_value = [to_bytes('cat: not found')]
+
+    connection._connected = True
+    connection.ssh = mock_client
+
+    with pytest.raises(AnsibleError, match='cat not found in path of WSL distribution'):
+        connection.fetch_file('/remote/path', '/local/path')
+
+
+def test_fetch_file(connection):
+    """ Test fetching a file from the remote system """
+    connection.exec_command = MagicMock()
+    connection.exec_command.return_value = (0, b'test content', b"")
+
+    with patch('builtins.open', create=True) as mock_open:
+        connection.fetch_file('/remote/path', '/local/path')
+
+    connection.exec_command.assert_called_once_with("/bin/sh -c 'cat /remote/path'", sudoable=False)
+    mock_open.assert_called_with('/local/path', 'wb')
+
+
+@patch('paramiko.SSHClient')
+def test_fetch_file_general_error(mock_ssh, connection):
+    """ Test fetch_file with general error """
+    mock_client = MagicMock()
+    mock_ssh.return_value = mock_client
+    mock_channel = MagicMock()
+    mock_transport = MagicMock()
+
+    mock_client.get_transport.return_value = mock_transport
+    mock_transport.open_session.return_value = mock_channel
+    mock_channel.recv_exit_status.return_value = 1
+    mock_channel.makefile.return_value = [to_bytes("")]
+    mock_channel.makefile_stderr.return_value = [to_bytes('Some error')]
+
+    connection._connected = True
+    connection.ssh = mock_client
+
+    with pytest.raises(AnsibleError, match='error occurred while fetching file from /remote/path to /local/path'):
+        connection.fetch_file('/remote/path', '/local/path')
+
+
+@patch('paramiko.SSHClient')
+def test_fetch_file_cat_not_found(mock_ssh, connection):
+    """ Test command execution when cat is not found """
+    mock_client = MagicMock()
+    mock_ssh.return_value = mock_client
+    mock_channel = MagicMock()
+    mock_transport = MagicMock()
+
+    mock_client.get_transport.return_value = mock_transport
+    mock_transport.open_session.return_value = mock_channel
+    mock_channel.recv_exit_status.return_value = 1
+    mock_channel.makefile.return_value = [to_bytes("")]
+    mock_channel.makefile_stderr.return_value = [to_bytes('cat: not found')]
+
+    connection._connected = True
+    connection.ssh = mock_client
+
+    with pytest.raises(AnsibleError, match='cat not found in path of WSL distribution'):
+        connection.fetch_file('/remote/path', '/local/path')
+
+
+def test_close(connection):
+    """ Test connection close """
+    mock_ssh = MagicMock()
+    connection.ssh = mock_ssh
+    connection._connected = True
+
+    connection.close()
+
+    assert mock_ssh.close.called, 'ssh.close was not called'
+    assert not connection._connected, 'self._connected is still True'
+
+
+def test_close_with_lock_file(connection):
+    """ Test close method with lock file creation """
+    connection._any_keys_added = MagicMock(return_value=True)
+    connection._connected = True
+    connection.keyfile = '/tmp/wsl-known_hosts-test'
+    connection.set_option('host_key_checking', True)
+    connection.set_option('lock_file_timeout', 5)
+    connection.set_option('record_host_keys', True)
+    connection.ssh = MagicMock()
+
+    lock_file_path = os.path.join(os.path.dirname(connection.keyfile),
+                                  f'ansible-{os.path.basename(connection.keyfile)}.lock')
+
+    try:
+        connection.close()
+        assert os.path.exists(lock_file_path), 'Lock file was not created'
+
+        lock_stat = os.stat(lock_file_path)
+        assert lock_stat.st_mode & 0o777 == 0o600, 'Incorrect lock file permissions'
+    finally:
+        Path(lock_file_path).unlink(missing_ok=True)
+
+
+@patch('pathlib.Path.unlink')
+@patch('os.path.exists')
+def test_close_lock_file_time_out_error_handling(mock_exists, mock_unlink, connection):
+    """ Test close method with lock file timeout error """
+    connection._any_keys_added = MagicMock(return_value=True)
+    connection._connected = True
+    connection._save_ssh_host_keys = MagicMock()
+    connection.keyfile = '/tmp/wsl-known_hosts-test'
+    connection.set_option('host_key_checking', True)
+    connection.set_option('lock_file_timeout', 5)
+    connection.set_option('record_host_keys', True)
+    connection.ssh = MagicMock()
+
+    mock_exists.return_value = False
+    matcher = f'writing lock file for {connection.keyfile} ran in to the timeout of {connection.get_option("lock_file_timeout")}s'
+    with pytest.raises(AnsibleError, match=matcher):
+        with patch('os.getuid', return_value=1000), \
+             patch('os.getgid', return_value=1000), \
+             patch('os.chmod'), patch('os.chown'), \
+             patch('os.rename'), \
+             patch.object(FileLock, 'lock_file', side_effect=LockTimeout()):
+            connection.close()
+
+
+@patch('ansible_collections.community.general.plugins.module_utils._filelock.FileLock.lock_file')
+@patch('tempfile.NamedTemporaryFile')
+@patch('os.chmod')
+@patch('os.chown')
+@patch('os.rename')
+@patch('os.path.exists')
+def test_tempfile_creation_and_move(mock_exists, mock_rename, mock_chown, mock_chmod, mock_tempfile, mock_lock_file, connection):
+    """ Test tempfile creation and move during close """
+    connection._any_keys_added = MagicMock(return_value=True)
+    connection._connected = True
+    connection._save_ssh_host_keys = MagicMock()
+    connection.keyfile = '/tmp/wsl-known_hosts-test'
+    connection.set_option('host_key_checking', True)
+    connection.set_option('lock_file_timeout', 5)
+    connection.set_option('record_host_keys', True)
+    connection.ssh = MagicMock()
+
+    mock_exists.return_value = False
+
+    mock_lock_file_instance = MagicMock()
+    mock_lock_file.return_value = mock_lock_file_instance
+    mock_lock_file_instance.__enter__.return_value = None
+
+    mock_tempfile_instance = MagicMock()
+    mock_tempfile_instance.name = '/tmp/mock_tempfile'
+    mock_tempfile.return_value.__enter__.return_value = mock_tempfile_instance
+
+    mode = 0o644
+    uid = 1000
+    gid = 1000
+    key_dir = os.path.dirname(connection.keyfile)
+
+    with patch('os.getuid', return_value=uid), patch('os.getgid', return_value=gid):
+        connection.close()
+
+    connection._save_ssh_host_keys.assert_called_once_with('/tmp/mock_tempfile')
+    mock_chmod.assert_called_once_with('/tmp/mock_tempfile', mode)
+    mock_chown.assert_called_once_with('/tmp/mock_tempfile', uid, gid)
+    mock_rename.assert_called_once_with('/tmp/mock_tempfile', connection.keyfile)
+    mock_tempfile.assert_called_once_with(dir=key_dir, delete=False)
+
+
+@patch('pathlib.Path.unlink')
+@patch('tempfile.NamedTemporaryFile')
+@patch('ansible_collections.community.general.plugins.module_utils._filelock.FileLock.lock_file')
+@patch('os.path.exists')
+def test_close_tempfile_error_handling(mock_exists, mock_lock_file, mock_tempfile, mock_unlink, connection):
+    """ Test tempfile creation error """
+    connection._any_keys_added = MagicMock(return_value=True)
+    connection._connected = True
+    connection._save_ssh_host_keys = MagicMock()
+    connection.keyfile = '/tmp/wsl-known_hosts-test'
+    connection.set_option('host_key_checking', True)
+    connection.set_option('lock_file_timeout', 5)
+    connection.set_option('record_host_keys', True)
+    connection.ssh = MagicMock()
+
+    mock_exists.return_value = False
+
+    mock_lock_file_instance = MagicMock()
+    mock_lock_file.return_value = mock_lock_file_instance
+    mock_lock_file_instance.__enter__.return_value = None
+
+    mock_tempfile_instance = MagicMock()
+    mock_tempfile_instance.name = '/tmp/mock_tempfile'
+    mock_tempfile.return_value.__enter__.return_value = mock_tempfile_instance
+
+    with pytest.raises(AnsibleError, match='error occurred while writing SSH host keys!'):
+        with patch.object(os, 'chmod', side_effect=Exception()):
+            connection.close()
+    mock_unlink.assert_called_with(missing_ok=True)
+
+
+@patch('ansible_collections.community.general.plugins.module_utils._filelock.FileLock.lock_file')
+@patch('os.path.exists')
+def test_close_with_invalid_host_key(mock_exists, mock_lock_file, connection):
+    """ Test load_system_host_keys on close with InvalidHostKey error """
+    connection._any_keys_added = MagicMock(return_value=True)
+    connection._connected = True
+    connection._save_ssh_host_keys = MagicMock()
+    connection.keyfile = '/tmp/wsl-known_hosts-test'
+    connection.set_option('host_key_checking', True)
+    connection.set_option('lock_file_timeout', 5)
+    connection.set_option('record_host_keys', True)
+    connection.ssh = MagicMock()
+    connection.ssh.load_system_host_keys.side_effect = paramiko.hostkeys.InvalidHostKey(
+        "Bad Line!", Exception('Something crashed!'))
+
+    mock_exists.return_value = False
+
+    mock_lock_file_instance = MagicMock()
+    mock_lock_file.return_value = mock_lock_file_instance
+    mock_lock_file_instance.__enter__.return_value = None
+
+    with pytest.raises(AnsibleConnectionFailure, match="Invalid host key: Bad Line!"):
+        connection.close()
+
+
+def test_reset(connection):
+    """ Test connection reset """
+    connection._connected = True
+    connection.close = MagicMock()
+    connection._connect = MagicMock()
+
+    connection.reset()
+
+    connection.close.assert_called_once()
+    connection._connect.assert_called_once()
+
+    connection._connected = False
+    connection.reset()
+    assert connection.close.call_count == 1


Reviews:
user: login: russoz
body: hi @rgl thanks for your contribution!!

I have a couple of comments below. :-)
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-02-23T02:40:22Z
user: login: rgl
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-23T12:06:20Z
user: login: rgl
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-23T12:06:39Z
user: login: rgl
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-23T12:16:09Z
user: login: felixfontein
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-02-23T12:25:43Z
user: login: felixfontein
body: Thanks for your contribution! I'm wondering a bit whether this plugin would be more appropriate to be added to community.windows, mostly because community.windows *does* have test infrastructure with Windows remotes (community.general does not and does not plan to add any). (I don't know whether their Windows remotes do have WSL installed, though.)

CC @jborean93 @briantist since they know community.windows a lot better.
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-02-23T12:32:04Z
user: login: rgl
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-23T12:49:05Z
user: login: rgl
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-25T08:31:05Z
user: login: felixfontein
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-02-26T20:12:24Z
user: login: rgl
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-26T21:54:35Z
user: login: felixfontein
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-01T14:29:54Z
user: login: rgl
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-01T14:55:33Z
user: login: rgl
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T18:53:03Z
user: login: rgl
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T18:53:08Z
user: login: rgl
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T18:53:23Z
user: login: rgl
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T18:54:51Z
user: login: rgl
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T18:55:43Z
user: login: rgl
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T18:55:52Z
user: login: rgl
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T18:56:02Z
user: login: rgl
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T18:58:01Z
user: login: rgl
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T18:58:20Z
user: login: rgl
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T18:59:13Z
user: login: rgl
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T19:01:25Z
user: login: rgl
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T19:01:36Z
user: login: felixfontein
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-05T20:02:21Z
user: login: mietzen
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T07:19:13Z
user: login: mietzen
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T07:25:08Z
user: login: mietzen
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T07:40:34Z
user: login: rgl
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T09:04:04Z
user: login: felixfontein
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-15T07:20:33Z
user: login: rgl
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-13T11:52:03Z
user: login: russoz
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-13T21:34:56Z
user: login: russoz
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-13T21:35:22Z
user: login: russoz
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-13T21:36:01Z
user: login: russoz
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-13T21:37:22Z
user: login: russoz
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-13T21:44:10Z
user: login: rgl
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T18:38:33Z
user: login: rgl
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T18:40:08Z
user: login: rgl
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T18:40:32Z
user: login: rgl
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T18:42:52Z
user: login: rgl
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T18:47:52Z
user: login: rgl
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T19:14:45Z
user: login: felixfontein
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-14T19:58:43Z
user: login: felixfontein
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-14T20:00:06Z
user: login: rgl
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T20:27:52Z
user: login: rgl
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T20:30:21Z
user: login: felixfontein
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-15T18:54:09Z
user: login: russoz
body: hi @rgl 

A couple of things to tend to, but we're getting there. :-)
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-16T21:30:19Z
user: login: rgl
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T18:43:24Z
user: login: rgl
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T18:43:46Z
user: login: russoz
body: LGTM
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-18T07:32:35Z

Review comments:
diff_hunk: @@ -0,0 +1,758 @@
+# -*- coding: utf-8 -*-
+# Derived from ansible/plugins/connection/proxmox_pct_remote.py (c) 2024 Nils Stein (@mietzen) <github.nstein@mailbox.org>
+# Derived from ansible/plugins/connection/paramiko_ssh.py (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
+# Copyright (c) 2025 Rui Lopes (@rgl) <ruilopes.com>
+# Copyright (c) 2025 Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import annotations
+
+DOCUMENTATION = r"""
+author: Rui Lopes (@rgl) <ruilopes.com>
+name: wsl
+short_description: Run tasks in WSL distribution using wsl.exe CLI via SSH
+requirements:
+  - paramiko
+description:
+  - Run commands or put/fetch files to an existing WSL distribution using wsl.exe CLI via SSH.
+  - Uses the Python SSH implementation (Paramiko) to connect to the WSL host.
+version_added: "10.3.0"
path: plugins/connection/wsl.py
user: login: russoz
body: ```suggestion
version_added: "10.4.0"
```
created_at: 2025-02-23T00:20:03Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,758 @@
+# -*- coding: utf-8 -*-
+# Derived from ansible/plugins/connection/proxmox_pct_remote.py (c) 2024 Nils Stein (@mietzen) <github.nstein@mailbox.org>
+# Derived from ansible/plugins/connection/paramiko_ssh.py (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
+# Copyright (c) 2025 Rui Lopes (@rgl) <ruilopes.com>
+# Copyright (c) 2025 Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import annotations
+
+DOCUMENTATION = r"""
+author: Rui Lopes (@rgl) <ruilopes.com>
+name: wsl
+short_description: Run tasks in WSL distribution using wsl.exe CLI via SSH
+requirements:
+  - paramiko
+description:
+  - Run commands or put/fetch files to an existing WSL distribution using wsl.exe CLI via SSH.
+  - Uses the Python SSH implementation (Paramiko) to connect to the WSL host.
+version_added: "10.3.0"
+options:
+  remote_addr:
+    description:
+      - Address of the remote target.
+    default: inventory_hostname
+    type: string
+    vars:
+      - name: inventory_hostname
+      - name: ansible_host
+      - name: ansible_ssh_host
+      - name: ansible_paramiko_host
+  port:
+    description: Remote port to connect to.
+    type: int
+    default: 22
+    ini:
+      - section: defaults
+        key: remote_port
+      - section: paramiko_connection
+        key: remote_port
+    env:
+      - name: ANSIBLE_REMOTE_PORT
+      - name: ANSIBLE_REMOTE_PARAMIKO_PORT
+    vars:
+      - name: ansible_port
+      - name: ansible_ssh_port
+      - name: ansible_paramiko_port
+    keyword:
+      - name: port
+  remote_user:
+    description:
+      - User to login/authenticate as.
+      - Can be set from the CLI via the C(--user) or C(-u) options.
+    type: string
+    vars:
+      - name: ansible_user
+      - name: ansible_ssh_user
+      - name: ansible_paramiko_user
+    env:
+      - name: ANSIBLE_REMOTE_USER
+      - name: ANSIBLE_PARAMIKO_REMOTE_USER
+    ini:
+      - section: defaults
+        key: remote_user
+      - section: paramiko_connection
+        key: remote_user
+    keyword:
+      - name: remote_user
+  password:
+    description:
+      - Secret used to either login the SSH server or as a passphrase for SSH keys that require it.
+      - Can be set from the CLI via the C(--ask-pass) option.
+    type: string
+    vars:
+      - name: ansible_password
+      - name: ansible_ssh_pass
+      - name: ansible_ssh_password
+      - name: ansible_paramiko_pass
+      - name: ansible_paramiko_password
+  use_rsa_sha2_algorithms:
+    description:
+      - Whether or not to enable RSA SHA2 algorithms for pubkeys and hostkeys.
+      - On paramiko versions older than 2.9, this only affects hostkeys.
+      - For behavior matching paramiko<2.9 set this to V(false).
+    vars:
+      - name: ansible_paramiko_use_rsa_sha2_algorithms
+    ini:
+      - {key: use_rsa_sha2_algorithms, section: paramiko_connection}
+    env:
+      - {name: ANSIBLE_PARAMIKO_USE_RSA_SHA2_ALGORITHMS}
+    default: true
+    type: boolean
+  host_key_auto_add:
+    description: "Automatically add host keys to C(~/.ssh/known_hosts)."
+    env:
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_AUTO_ADD
+    ini:
+      - key: host_key_auto_add
+        section: paramiko_connection
+    type: boolean
+  look_for_keys:
+    default: True
+    description: "Set to V(false) to disable searching for private key files in C(~/.ssh/)."
+    env:
+      - name: ANSIBLE_PARAMIKO_LOOK_FOR_KEYS
+    ini:
+      - {key: look_for_keys, section: paramiko_connection}
+    type: boolean
+  proxy_command:
+    default: ""
+    description:
+      - Proxy information for running the connection via a jumphost.
+    type: string
+    env:
+      - name: ANSIBLE_PARAMIKO_PROXY_COMMAND
+    ini:
+      - {key: proxy_command, section: paramiko_connection}
+    vars:
+      - name: ansible_paramiko_proxy_command
+  record_host_keys:
+    default: True
+    description: "Save the host keys to a file."
+    env:
+      - name: ANSIBLE_PARAMIKO_RECORD_HOST_KEYS
+    ini:
+      - section: paramiko_connection
+        key: record_host_keys
+    type: boolean
+  host_key_checking:
+    description: "Set this to V(false) if you want to avoid host key checking by the underlying tools Ansible uses to connect to the host."
+    type: boolean
+    default: true
+    env:
+      - name: ANSIBLE_HOST_KEY_CHECKING
+      - name: ANSIBLE_SSH_HOST_KEY_CHECKING
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_CHECKING
+    ini:
+      - section: defaults
+        key: host_key_checking
+      - section: paramiko_connection
+        key: host_key_checking
+    vars:
+      - name: ansible_host_key_checking
+      - name: ansible_ssh_host_key_checking
+      - name: ansible_paramiko_host_key_checking
+  use_persistent_connections:
+    description: "Toggles the use of persistence for connections."
+    type: boolean
+    default: False
+    env:
+      - name: ANSIBLE_USE_PERSISTENT_CONNECTIONS
+    ini:
+      - section: defaults
+        key: use_persistent_connections
+  banner_timeout:
+    type: float
+    default: 30
+    description:
+      - Configures, in seconds, the amount of time to wait for the SSH
+        banner to be presented. This option is supported by paramiko
+        version 1.15.0 or newer.
+    ini:
+      - section: paramiko_connection
+        key: banner_timeout
+    env:
+      - name: ANSIBLE_PARAMIKO_BANNER_TIMEOUT
+  timeout:
+    type: int
+    default: 10
+    description: Number of seconds until the plugin gives up on failing to establish a TCP connection.
+    ini:
+      - section: defaults
+        key: timeout
+      - section: ssh_connection
+        key: timeout
+      - section: paramiko_connection
+        key: timeout
+    env:
+      - name: ANSIBLE_TIMEOUT
+      - name: ANSIBLE_SSH_TIMEOUT
+      - name: ANSIBLE_PARAMIKO_TIMEOUT
+    vars:
+      - name: ansible_ssh_timeout
+      - name: ansible_paramiko_timeout
+    cli:
+      - name: timeout
+  lock_file_timeout:
+    type: int
+    default: 60
+    description: Number of seconds until the plugin gives up on trying to write a lock file when writing SSH known host keys.
+    vars:
+      - name: ansible_lock_file_timeout
+    env:
+      - name: ANSIBLE_LOCK_FILE_TIMEOUT
+  private_key_file:
+    description:
+      - Path to private key file to use for authentication.
+    type: string
+    ini:
+      - section: defaults
+        key: private_key_file
+      - section: paramiko_connection
+        key: private_key_file
+    env:
+      - name: ANSIBLE_PRIVATE_KEY_FILE
+      - name: ANSIBLE_PARAMIKO_PRIVATE_KEY_FILE
+    vars:
+      - name: ansible_private_key_file
+      - name: ansible_ssh_private_key_file
+      - name: ansible_paramiko_private_key_file
+    cli:
+      - name: private_key_file
+        option: "--private-key"
+  wsl_distribution:
+    description:
+      - WSL distribution name
+    type: string
+    vars:
+      - name: wsl_distribution
+  wsl_user:
+    description:
+      - WSL distribution user
+    type: string
+    vars:
+      - name: wsl_user
+  become_user:
+    description:
+      - WSL distribution user
+    type: string
+    default: root
+    vars:
+      - name: become_user
+      - name: ansible_become_user
+  become:
+    description:
+      - whether to use the user defined by ansible_become_user.
+    type: bool
+    default: false
+    vars:
+      - name: become
+      - name: ansible_become
+"""
+
+EXAMPLES = r"""
+# ------------------------
+# Inventory: inventory.yml
+# ------------------------
+# all:
+#   children:
+#     wsl:
+#       hosts:
+#         ubuntu:
+#           ansible_host: 10.0.0.10
+#           wsl_distribution: ubuntu
+#         debian:
+#           ansible_host: 10.0.0.10
+#           wsl_distribution: debian
+#       vars:
+#         ansible_connection: community.general.wsl
+#         ansible_user: vagrant
+#         wsl_user: wsl
+# ----------------------
+# Playbook: playbook.yml
+# ----------------------
+---
+- hosts: wsl
+  tasks:
+    - name: ping
+      ansible.builtin.ping:
+    - name: id
+      args:
+        executable: /bin/bash
+      ansible.builtin.shell: |
+        exec 2>&1
+        set -x
+        echo "$0"
+        pwd
+        id
+    - name: become id
+      become: true
+      args:
+        executable: /bin/bash
+      ansible.builtin.shell: |
+        exec 2>&1
+        set -x
+        echo "$0"
+        pwd
+        id
+"""
+
+import os
+import pathlib
+import shlex
+import socket
+import tempfile
+import typing as t
+
+from ansible.errors import (
+    AnsibleAuthenticationFailure,
+    AnsibleConnectionFailure,
+    AnsibleError,
+)
+from ansible_collections.community.general.plugins.module_utils._filelock import FileLock, LockTimeout
+from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
+from ansible.module_utils.compat.paramiko import PARAMIKO_IMPORT_ERR, paramiko
+from ansible.module_utils.compat.version import LooseVersion
+from ansible.plugins.connection import ConnectionBase
+from ansible.utils.display import Display
+from ansible.utils.path import makedirs_safe
+from binascii import hexlify
+from subprocess import list2cmdline
+
+
+display = Display()
+
+
+def authenticity_msg(hostname: str, ktype: str, fingerprint: str) -> str:
+    msg = f"""
+    paramiko: The authenticity of host '{hostname}' can't be established.
+    The {ktype} key fingerprint is {fingerprint}.
+    Are you sure you want to continue connecting (yes/no)?
+    """
+    return msg
+
+
+MissingHostKeyPolicy: type = object
+if paramiko:
+    MissingHostKeyPolicy = paramiko.MissingHostKeyPolicy
+
+
+class MyAddPolicy(MissingHostKeyPolicy):
+    """
+    Based on AutoAddPolicy in paramiko so we can determine when keys are added
+
+    and also prompt for input.
+
+    Policy for automatically adding the hostname and new host key to the
+    local L{HostKeys} object, and saving it. This is used by L{SSHClient}.
+    """
+
+    def __init__(self, connection: Connection) -> None:
+        self.connection = connection
+        self._options = connection._options
+
+    def missing_host_key(self, client, hostname, key) -> None:
+
+        if all((self.connection.get_option('host_key_checking'), not self.connection.get_option('host_key_auto_add'))):
+
+            fingerprint = hexlify(key.get_fingerprint())
+            ktype = key.get_name()
+
+            if self.connection.get_option('use_persistent_connections') or self.connection.force_persistence:
+                # don't print the prompt string since the user cannot respond
+                # to the question anyway
+                raise AnsibleError(authenticity_msg(hostname, ktype, fingerprint)[1:92])
+
+            inp = to_text(
+                display.prompt_until(authenticity_msg(hostname, ktype, fingerprint), private=False),
+                errors='surrogate_or_strict'
+            )
+
+            if inp.lower() not in ['yes', 'y', '']:
+                raise AnsibleError('host connection rejected by user')
+
+        key._added_by_ansible_this_time = True
+
+        # existing implementation below:
+        client._host_keys.add(hostname, key.get_name(), key)
+
+        # host keys are actually saved in close() function below
+        # in order to control ordering.
+
+
+class Connection(ConnectionBase):
+    """ SSH based connections (paramiko) to WSL """
+
+    transport = 'community.general.wsl'
+    _log_channel: str | None = None
+
+    def __init__(self, play_context, new_stdin, *args, **kwargs):
+        super(Connection, self).__init__(play_context, new_stdin, *args, **kwargs)
+
+    def _set_log_channel(self, name: str) -> None:
+        """ Mimic paramiko.SSHClient.set_log_channel """
+        self._log_channel = name
+
+    def _parse_proxy_command(self, port: int = 22) -> dict[str, t.Any]:
+        proxy_command = self.get_option('proxy_command') or None
+
+        sock_kwarg = {}
+        if proxy_command:
+            replacers = {
+                '%h': self.get_option('remote_addr'),
+                '%p': port,
+                '%r': self.get_option('remote_user')
+            }
+            for find, replace in replacers.items():
+                proxy_command = proxy_command.replace(find, str(replace))
+            try:
+                sock_kwarg = {'sock': paramiko.ProxyCommand(proxy_command)}
+                display.vvv(f'CONFIGURE PROXY COMMAND FOR CONNECTION: {proxy_command}', host=self.get_option('remote_addr'))
+            except AttributeError:
+                display.warning('Paramiko ProxyCommand support unavailable. '
+                                'Please upgrade to Paramiko 1.9.0 or newer. '
+                                'Not using configured ProxyCommand')
+
+        return sock_kwarg
+
+    def _connect(self) -> Connection:
+        """ activates the connection object """
+
+        if paramiko is None:
+            raise AnsibleError(f'paramiko is not installed: {to_native(PARAMIKO_IMPORT_ERR)}')
+
+        port = self.get_option('port')
+        display.vvv(f'ESTABLISH PARAMIKO SSH CONNECTION FOR USER: {self.get_option("remote_user")} on PORT {to_text(port)} TO {self.get_option("remote_addr")}',
+                    host=self.get_option('remote_addr'))
+
+        ssh = paramiko.SSHClient()
+
+        # Set pubkey and hostkey algorithms to disable, the only manipulation allowed currently
+        # is keeping or omitting rsa-sha2 algorithms
+        # default_keys: t.Tuple[str] = ()
+        paramiko_preferred_pubkeys = getattr(paramiko.Transport, '_preferred_pubkeys', ())
+        paramiko_preferred_hostkeys = getattr(paramiko.Transport, '_preferred_keys', ())
+        use_rsa_sha2_algorithms = self.get_option('use_rsa_sha2_algorithms')
+        disabled_algorithms: t.Dict[str, t.Iterable[str]] = {}
+        if not use_rsa_sha2_algorithms:
+            if paramiko_preferred_pubkeys:
+                disabled_algorithms['pubkeys'] = tuple(a for a in paramiko_preferred_pubkeys if 'rsa-sha2' in a)
+            if paramiko_preferred_hostkeys:
+                disabled_algorithms['keys'] = tuple(a for a in paramiko_preferred_hostkeys if 'rsa-sha2' in a)
+
+        # override paramiko's default logger name
+        if self._log_channel is not None:
+            ssh.set_log_channel(self._log_channel)
+
+        self.keyfile = os.path.expanduser('~/.ssh/known_hosts')
+
+        if self.get_option('host_key_checking'):
+            for ssh_known_hosts in ('/etc/ssh/ssh_known_hosts', '/etc/openssh/ssh_known_hosts'):
+                try:
+                    ssh.load_system_host_keys(ssh_known_hosts)
+                    break
+                except IOError:
+                    pass  # file was not found, but not required to function
+                except paramiko.hostkeys.InvalidHostKey as e:
+                    raise AnsibleConnectionFailure(f'Invalid host key: {to_text(e.line)}')
+            try:
+                ssh.load_system_host_keys()
+            except paramiko.hostkeys.InvalidHostKey as e:
+                raise AnsibleConnectionFailure(f'Invalid host key: {to_text(e.line)}')
+
+        ssh_connect_kwargs = self._parse_proxy_command(port)
+        ssh.set_missing_host_key_policy(MyAddPolicy(self))
+        conn_password = self.get_option('password')
+        allow_agent = True
+
+        if conn_password is not None:
+            allow_agent = False
+
+        try:
+            key_filename = None
+            if self.get_option('private_key_file'):
+                key_filename = os.path.expanduser(self.get_option('private_key_file'))
+
+            # paramiko 2.2 introduced auth_timeout parameter
+            if LooseVersion(paramiko.__version__) >= LooseVersion('2.2.0'):
+                ssh_connect_kwargs['auth_timeout'] = self.get_option('timeout')
+
+            # paramiko 1.15 introduced banner timeout parameter
+            if LooseVersion(paramiko.__version__) >= LooseVersion('1.15.0'):
+                ssh_connect_kwargs['banner_timeout'] = self.get_option('banner_timeout')
+
+            ssh.connect(
+                self.get_option('remote_addr').lower(),
+                username=self.get_option('remote_user'),
+                allow_agent=allow_agent,
+                look_for_keys=self.get_option('look_for_keys'),
+                key_filename=key_filename,
+                password=conn_password,
+                timeout=self.get_option('timeout'),
+                port=port,
+                disabled_algorithms=disabled_algorithms,
+                **ssh_connect_kwargs,
+            )
+        except paramiko.ssh_exception.BadHostKeyException as e:
+            raise AnsibleConnectionFailure(f'host key mismatch for {to_text(e.hostname)}')
+        except paramiko.ssh_exception.AuthenticationException as e:
+            msg = f'Failed to authenticate: {e}'
+            raise AnsibleAuthenticationFailure(msg)
+        except Exception as e:
+            msg = to_text(e)
+            if u'PID check failed' in msg:
+                raise AnsibleError('paramiko version issue, please upgrade paramiko on the machine running ansible')
+            elif u'Private key file is encrypted' in msg:
+                msg = f'ssh {self.get_option("remote_user")}@{self.get_options("remote_addr")}:{port} : ' + \
+                    f'{msg}\nTo connect as a different user, use -u <username>.'
+                raise AnsibleConnectionFailure(msg)
+            else:
+                raise AnsibleConnectionFailure(msg)
+        self.ssh = ssh
+        self._connected = True
+        return self
+
+    def _any_keys_added(self) -> bool:
+        for hostname, keys in self.ssh._host_keys.items():
+            for keytype, key in keys.items():
+                added_this_time = getattr(key, '_added_by_ansible_this_time', False)
+                if added_this_time:
+                    return True
+        return False
+
+    def _save_ssh_host_keys(self, filename: str) -> None:
+        """
+        not using the paramiko save_ssh_host_keys function as we want to add new SSH keys at the bottom so folks
+        don't complain about it :)
+        """
+
+        if not self._any_keys_added():
+            return
+
+        path = os.path.expanduser('~/.ssh')
+        makedirs_safe(path)
+
+        with open(filename, 'w') as f:
+            for hostname, keys in self.ssh._host_keys.items():
+                for keytype, key in keys.items():
+                    # was f.write
+                    added_this_time = getattr(key, '_added_by_ansible_this_time', False)
+                    if not added_this_time:
+                        f.write(f'{hostname} {keytype} {key.get_base64()}\n')
+
+            for hostname, keys in self.ssh._host_keys.items():
+                for keytype, key in keys.items():
+                    added_this_time = getattr(key, '_added_by_ansible_this_time', False)
+                    if added_this_time:
+                        f.write(f'{hostname} {keytype} {key.get_base64()}\n')
+
+    def _build_wsl_command(self, cmd: str) -> str:
+        wsl_distribution = self.get_option('wsl_distribution')
+        if not wsl_distribution:
+            raise AnsibleError('A wsl_distribution is required but none was supplied')
+        become = self.get_option('become')
+        become_user = self.get_option('become_user')
+        if become and become_user:
+            wsl_user = become_user
+        else:
+            wsl_user = self.get_option('wsl_user')
+        args = ['wsl.exe', '--distribution', wsl_distribution]
+        if wsl_user:
+            args.extend(['--user', wsl_user])
+        args.extend(['--'])
+        args.extend(shlex.split(cmd))
+        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 """
+
+        cmd = self._build_wsl_command(cmd)
+
+        super(Connection, self).exec_command(cmd, in_data=in_data, sudoable=sudoable)
+
+        bufsize = 4096
+
+        try:
+            self.ssh.get_transport().set_keepalive(5)
+            chan = self.ssh.get_transport().open_session()
+        except Exception as e:
+            text_e = to_text(e)
+            msg = 'Failed to open session'
+            if text_e:
+                msg += f': {text_e}'
+            raise AnsibleConnectionFailure(to_native(msg))
+
+        display.vvv(f'EXEC {cmd}', host=self.get_option('remote_addr'))
+
+        cmd = to_bytes(cmd, errors='surrogate_or_strict')
+
+        no_prompt_out = b''
+        no_prompt_err = b''
+        become_output = b''
+
+        try:
+            chan.exec_command(cmd)
+            if self.become and self.become.expect_prompt():
+                password_prompt = False
+                become_success = False
+                while not (become_success or password_prompt):
+                    display.debug('Waiting for Privilege Escalation input')
+
+                    chunk = chan.recv(bufsize)
+                    display.debug(f'chunk is: {to_text(chunk)}')
+                    if not chunk:
+                        if b'unknown user' in become_output:
+                            n_become_user = to_native(self.become.get_option('become_user'))
+                            raise AnsibleError(f'user {n_become_user} does not exist')
+                        else:
+                            break
+                            # raise AnsibleError('ssh connection closed waiting for password prompt')
+                    become_output += chunk
+
+                    # need to check every line because we might get lectured
+                    # and we might get the middle of a line in a chunk
+                    for line in become_output.splitlines(True):
+                        if self.become.check_success(line):
+                            become_success = True
+                            break
+                        elif self.become.check_password_prompt(line):
+                            password_prompt = True
+                            break
+
+                if password_prompt:
+                    if self.become:
+                        become_pass = self.become.get_option('become_pass')
+                        chan.sendall(to_bytes(become_pass, errors='surrogate_or_strict') + b'\n')
+                    else:
+                        raise AnsibleError('A password is required but none was supplied')
+                else:
+                    no_prompt_out += become_output
+                    no_prompt_err += become_output
+
+            if in_data:
+                for i in range(0, len(in_data), bufsize):
+                    chan.send(in_data[i:i + bufsize])
+                chan.shutdown_write()
+            elif in_data == b'':
+                chan.shutdown_write()
+
+        except socket.timeout:
+            raise AnsibleError('ssh timed out waiting for privilege escalation.\n' + to_text(become_output))
+
+        stdout = b''.join(chan.makefile('rb', bufsize))
+        stderr = b''.join(chan.makefile_stderr('rb', bufsize))
+        returncode = chan.recv_exit_status()
+
+        # NB the full english error message is:
+        #     'wsl.exe' is not recognized as an internal or external command,
+        #     operable program or batch file.
+        if "'wsl.exe' is not recognized" in stderr.decode('utf-8'):
+            raise AnsibleError(
+                f'wsl.exe not found in path of host: {to_text(self.get_option("remote_addr"))}')
+
+        return (returncode, no_prompt_out + stdout, no_prompt_out + stderr)
+
+    def put_file(self, in_path: str, out_path: str) -> None:
+        """ transfer a file from local to remote """
+
+        display.vvv(f'PUT {in_path} TO {out_path}', host=self.get_option('remote_addr'))
+        try:
+            with open(in_path, 'rb') as f:
+                data = f.read()
+                returncode, stdout, stderr = self.exec_command(
+                    ' '.join([
+                        self._shell.executable, '-c',
+                        self._shell.quote(f'cat > {out_path}')]),
+                    in_data=data,
+                    sudoable=False)
+            if returncode != 0:
+                if 'cat: not found' in stderr.decode('utf-8'):
+                    raise AnsibleError(
+                        f'cat not found in path of WSL distribution: {to_text(self.get_option("wsl_distribution"))}')
+                raise AnsibleError(
+                    f'{to_text(stdout)}\n{to_text(stderr)}')
+        except Exception as e:
+            raise AnsibleError(
+                f'error occurred while putting file from {in_path} to {out_path}!\n{to_text(e)}')
+
+    def fetch_file(self, in_path: str, out_path: str) -> None:
+        """ save a remote file to the specified path """
+
+        display.vvv(f'FETCH {in_path} TO {out_path}', host=self.get_option('remote_addr'))
+        try:
+            returncode, stdout, stderr = self.exec_command(
+                ' '.join([
+                    self._shell.executable, '-c',
+                    self._shell.quote(f'cat {in_path}')]),
+                sudoable=False)
+            if returncode != 0:
+                if 'cat: not found' in stderr.decode('utf-8'):
+                    raise AnsibleError(
+                        f'cat not found in path of WSL distribution: {to_text(self.get_option("wsl_distribution"))}')
+                raise AnsibleError(
+                    f'{to_text(stdout)}\n{to_text(stderr)}')
+            with open(out_path, 'wb') as f:
+                f.write(stdout)
+        except Exception as e:
+            raise AnsibleError(
+                f'error occurred while fetching file from {in_path} to {out_path}!\n{to_text(e)}')
+
+    def reset(self) -> None:
+        """ reset the connection """
+
+        if not self._connected:
+            return
+        self.close()
+        self._connect()
+
+    def close(self) -> None:
+        """ terminate the connection """
+
+        if self.get_option('host_key_checking') and self.get_option('record_host_keys') and self._any_keys_added():
+            # add any new SSH host keys -- warning -- this could be slow
+            # (This doesn't acquire the connection lock because it needs
+            # to exclude only other known_hosts writers, not connections
+            # that are starting up.)
+            lockfile = os.path.basename(self.keyfile)
+            dirname = os.path.dirname(self.keyfile)
+            makedirs_safe(dirname)
+            tmp_keyfile_name = None
+            try:
+                with FileLock().lock_file(lockfile, dirname, self.get_option('lock_file_timeout')):
+                    # just in case any were added recently
+
+                    self.ssh.load_system_host_keys()
+                    self.ssh._host_keys.update(self.ssh._system_host_keys)
+
+                    # gather information about the current key file, so
+                    # we can ensure the new file has the correct mode/owner
+
+                    key_dir = os.path.dirname(self.keyfile)
+                    if os.path.exists(self.keyfile):
+                        key_stat = os.stat(self.keyfile)
+                        mode = key_stat.st_mode & 0o777
+                        uid = key_stat.st_uid
+                        gid = key_stat.st_gid
+                    else:
+                        mode = 0o644
+                        uid = os.getuid()
+                        gid = os.getgid()
+
+                    # Save the new keys to a temporary file and move it into place
+                    # rather than rewriting the file. We set delete=False because
+                    # the file will be moved into place rather than cleaned up.
+
+                    with tempfile.NamedTemporaryFile(dir=key_dir, delete=False) as tmp_keyfile:
+                        tmp_keyfile_name = tmp_keyfile.name
+                        os.chmod(tmp_keyfile_name, mode)
+                        os.chown(tmp_keyfile_name, uid, gid)
+                        self._save_ssh_host_keys(tmp_keyfile_name)
+
+                    os.rename(tmp_keyfile_name, self.keyfile)
+            except LockTimeout:
+                raise AnsibleError(
+                    f'writing lock file for {self.keyfile} ran in to the timeout of {self.get_option("lock_file_timeout")}s')
+            except paramiko.hostkeys.InvalidHostKey as e:
+                raise AnsibleConnectionFailure(f'Invalid host key: {e.line}')
+            except Exception as e:
+                # unable to save keys, including scenario when key was invalid
+                # and caught earlier
+                raise AnsibleError(
+                    f'error occurred while writing SSH host keys!\n{to_text(e)}')
path: plugins/connection/wsl.py
user: login: russoz
body: The collection has a convention of 160 chars long lines, so there is no need to break the line here, and this might/will eventually be concatenated in the future.
```suggestion
                raise AnsibleError(f'error occurred while writing SSH host keys!\n{to_text(e)}')
```
created_at: 2025-02-23T00:44:21Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,758 @@
+# -*- coding: utf-8 -*-
+# Derived from ansible/plugins/connection/proxmox_pct_remote.py (c) 2024 Nils Stein (@mietzen) <github.nstein@mailbox.org>
+# Derived from ansible/plugins/connection/paramiko_ssh.py (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
+# Copyright (c) 2025 Rui Lopes (@rgl) <ruilopes.com>
+# Copyright (c) 2025 Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import annotations
+
+DOCUMENTATION = r"""
+author: Rui Lopes (@rgl) <ruilopes.com>
+name: wsl
+short_description: Run tasks in WSL distribution using wsl.exe CLI via SSH
+requirements:
+  - paramiko
+description:
+  - Run commands or put/fetch files to an existing WSL distribution using wsl.exe CLI via SSH.
+  - Uses the Python SSH implementation (Paramiko) to connect to the WSL host.
+version_added: "10.3.0"
+options:
+  remote_addr:
+    description:
+      - Address of the remote target.
+    default: inventory_hostname
+    type: string
+    vars:
+      - name: inventory_hostname
+      - name: ansible_host
+      - name: ansible_ssh_host
+      - name: ansible_paramiko_host
+  port:
+    description: Remote port to connect to.
+    type: int
+    default: 22
+    ini:
+      - section: defaults
+        key: remote_port
+      - section: paramiko_connection
+        key: remote_port
+    env:
+      - name: ANSIBLE_REMOTE_PORT
+      - name: ANSIBLE_REMOTE_PARAMIKO_PORT
+    vars:
+      - name: ansible_port
+      - name: ansible_ssh_port
+      - name: ansible_paramiko_port
+    keyword:
+      - name: port
+  remote_user:
+    description:
+      - User to login/authenticate as.
+      - Can be set from the CLI via the C(--user) or C(-u) options.
+    type: string
+    vars:
+      - name: ansible_user
+      - name: ansible_ssh_user
+      - name: ansible_paramiko_user
+    env:
+      - name: ANSIBLE_REMOTE_USER
+      - name: ANSIBLE_PARAMIKO_REMOTE_USER
+    ini:
+      - section: defaults
+        key: remote_user
+      - section: paramiko_connection
+        key: remote_user
+    keyword:
+      - name: remote_user
+  password:
+    description:
+      - Secret used to either login the SSH server or as a passphrase for SSH keys that require it.
+      - Can be set from the CLI via the C(--ask-pass) option.
+    type: string
+    vars:
+      - name: ansible_password
+      - name: ansible_ssh_pass
+      - name: ansible_ssh_password
+      - name: ansible_paramiko_pass
+      - name: ansible_paramiko_password
+  use_rsa_sha2_algorithms:
+    description:
+      - Whether or not to enable RSA SHA2 algorithms for pubkeys and hostkeys.
+      - On paramiko versions older than 2.9, this only affects hostkeys.
+      - For behavior matching paramiko<2.9 set this to V(false).
+    vars:
+      - name: ansible_paramiko_use_rsa_sha2_algorithms
+    ini:
+      - {key: use_rsa_sha2_algorithms, section: paramiko_connection}
+    env:
+      - {name: ANSIBLE_PARAMIKO_USE_RSA_SHA2_ALGORITHMS}
+    default: true
+    type: boolean
+  host_key_auto_add:
+    description: "Automatically add host keys to C(~/.ssh/known_hosts)."
+    env:
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_AUTO_ADD
+    ini:
+      - key: host_key_auto_add
+        section: paramiko_connection
+    type: boolean
+  look_for_keys:
+    default: True
+    description: "Set to V(false) to disable searching for private key files in C(~/.ssh/)."
+    env:
+      - name: ANSIBLE_PARAMIKO_LOOK_FOR_KEYS
+    ini:
+      - {key: look_for_keys, section: paramiko_connection}
+    type: boolean
+  proxy_command:
+    default: ""
+    description:
+      - Proxy information for running the connection via a jumphost.
+    type: string
+    env:
+      - name: ANSIBLE_PARAMIKO_PROXY_COMMAND
+    ini:
+      - {key: proxy_command, section: paramiko_connection}
+    vars:
+      - name: ansible_paramiko_proxy_command
+  record_host_keys:
+    default: True
+    description: "Save the host keys to a file."
+    env:
+      - name: ANSIBLE_PARAMIKO_RECORD_HOST_KEYS
+    ini:
+      - section: paramiko_connection
+        key: record_host_keys
+    type: boolean
+  host_key_checking:
+    description: "Set this to V(false) if you want to avoid host key checking by the underlying tools Ansible uses to connect to the host."
+    type: boolean
+    default: true
+    env:
+      - name: ANSIBLE_HOST_KEY_CHECKING
+      - name: ANSIBLE_SSH_HOST_KEY_CHECKING
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_CHECKING
+    ini:
+      - section: defaults
+        key: host_key_checking
+      - section: paramiko_connection
+        key: host_key_checking
+    vars:
+      - name: ansible_host_key_checking
+      - name: ansible_ssh_host_key_checking
+      - name: ansible_paramiko_host_key_checking
+  use_persistent_connections:
+    description: "Toggles the use of persistence for connections."
+    type: boolean
+    default: False
+    env:
+      - name: ANSIBLE_USE_PERSISTENT_CONNECTIONS
+    ini:
+      - section: defaults
+        key: use_persistent_connections
+  banner_timeout:
+    type: float
+    default: 30
+    description:
+      - Configures, in seconds, the amount of time to wait for the SSH
+        banner to be presented. This option is supported by paramiko
+        version 1.15.0 or newer.
+    ini:
+      - section: paramiko_connection
+        key: banner_timeout
+    env:
+      - name: ANSIBLE_PARAMIKO_BANNER_TIMEOUT
+  timeout:
+    type: int
+    default: 10
+    description: Number of seconds until the plugin gives up on failing to establish a TCP connection.
+    ini:
+      - section: defaults
+        key: timeout
+      - section: ssh_connection
+        key: timeout
+      - section: paramiko_connection
+        key: timeout
+    env:
+      - name: ANSIBLE_TIMEOUT
+      - name: ANSIBLE_SSH_TIMEOUT
+      - name: ANSIBLE_PARAMIKO_TIMEOUT
+    vars:
+      - name: ansible_ssh_timeout
+      - name: ansible_paramiko_timeout
+    cli:
+      - name: timeout
+  lock_file_timeout:
+    type: int
+    default: 60
+    description: Number of seconds until the plugin gives up on trying to write a lock file when writing SSH known host keys.
+    vars:
+      - name: ansible_lock_file_timeout
+    env:
+      - name: ANSIBLE_LOCK_FILE_TIMEOUT
+  private_key_file:
+    description:
+      - Path to private key file to use for authentication.
+    type: string
+    ini:
+      - section: defaults
+        key: private_key_file
+      - section: paramiko_connection
+        key: private_key_file
+    env:
+      - name: ANSIBLE_PRIVATE_KEY_FILE
+      - name: ANSIBLE_PARAMIKO_PRIVATE_KEY_FILE
+    vars:
+      - name: ansible_private_key_file
+      - name: ansible_ssh_private_key_file
+      - name: ansible_paramiko_private_key_file
+    cli:
+      - name: private_key_file
+        option: "--private-key"
+  wsl_distribution:
+    description:
+      - WSL distribution name
+    type: string
+    vars:
+      - name: wsl_distribution
+  wsl_user:
+    description:
+      - WSL distribution user
+    type: string
+    vars:
+      - name: wsl_user
+  become_user:
+    description:
+      - WSL distribution user
+    type: string
+    default: root
+    vars:
+      - name: become_user
+      - name: ansible_become_user
+  become:
+    description:
+      - whether to use the user defined by ansible_become_user.
+    type: bool
+    default: false
+    vars:
+      - name: become
+      - name: ansible_become
+"""
+
+EXAMPLES = r"""
+# ------------------------
+# Inventory: inventory.yml
+# ------------------------
+# all:
+#   children:
+#     wsl:
+#       hosts:
+#         ubuntu:
+#           ansible_host: 10.0.0.10
+#           wsl_distribution: ubuntu
+#         debian:
+#           ansible_host: 10.0.0.10
+#           wsl_distribution: debian
+#       vars:
+#         ansible_connection: community.general.wsl
+#         ansible_user: vagrant
+#         wsl_user: wsl
+# ----------------------
+# Playbook: playbook.yml
+# ----------------------
+---
+- hosts: wsl
+  tasks:
+    - name: ping
+      ansible.builtin.ping:
+    - name: id
+      args:
+        executable: /bin/bash
+      ansible.builtin.shell: |
+        exec 2>&1
+        set -x
+        echo "$0"
+        pwd
+        id
+    - name: become id
+      become: true
+      args:
+        executable: /bin/bash
+      ansible.builtin.shell: |
+        exec 2>&1
+        set -x
+        echo "$0"
+        pwd
+        id
+"""
+
+import os
+import pathlib
+import shlex
+import socket
+import tempfile
+import typing as t
+
+from ansible.errors import (
+    AnsibleAuthenticationFailure,
+    AnsibleConnectionFailure,
+    AnsibleError,
+)
+from ansible_collections.community.general.plugins.module_utils._filelock import FileLock, LockTimeout
+from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
+from ansible.module_utils.compat.paramiko import PARAMIKO_IMPORT_ERR, paramiko
+from ansible.module_utils.compat.version import LooseVersion
+from ansible.plugins.connection import ConnectionBase
+from ansible.utils.display import Display
+from ansible.utils.path import makedirs_safe
+from binascii import hexlify
+from subprocess import list2cmdline
+
+
+display = Display()
+
+
+def authenticity_msg(hostname: str, ktype: str, fingerprint: str) -> str:
+    msg = f"""
+    paramiko: The authenticity of host '{hostname}' can't be established.
+    The {ktype} key fingerprint is {fingerprint}.
+    Are you sure you want to continue connecting (yes/no)?
+    """
+    return msg
+
+
+MissingHostKeyPolicy: type = object
+if paramiko:
+    MissingHostKeyPolicy = paramiko.MissingHostKeyPolicy
+
+
+class MyAddPolicy(MissingHostKeyPolicy):
+    """
+    Based on AutoAddPolicy in paramiko so we can determine when keys are added
+
+    and also prompt for input.
+
+    Policy for automatically adding the hostname and new host key to the
+    local L{HostKeys} object, and saving it. This is used by L{SSHClient}.
+    """
+
+    def __init__(self, connection: Connection) -> None:
+        self.connection = connection
+        self._options = connection._options
+
+    def missing_host_key(self, client, hostname, key) -> None:
+
+        if all((self.connection.get_option('host_key_checking'), not self.connection.get_option('host_key_auto_add'))):
+
+            fingerprint = hexlify(key.get_fingerprint())
+            ktype = key.get_name()
+
+            if self.connection.get_option('use_persistent_connections') or self.connection.force_persistence:
+                # don't print the prompt string since the user cannot respond
+                # to the question anyway
+                raise AnsibleError(authenticity_msg(hostname, ktype, fingerprint)[1:92])
+
+            inp = to_text(
+                display.prompt_until(authenticity_msg(hostname, ktype, fingerprint), private=False),
+                errors='surrogate_or_strict'
+            )
+
+            if inp.lower() not in ['yes', 'y', '']:
+                raise AnsibleError('host connection rejected by user')
+
+        key._added_by_ansible_this_time = True
+
+        # existing implementation below:
+        client._host_keys.add(hostname, key.get_name(), key)
+
+        # host keys are actually saved in close() function below
+        # in order to control ordering.
+
+
+class Connection(ConnectionBase):
+    """ SSH based connections (paramiko) to WSL """
+
+    transport = 'community.general.wsl'
+    _log_channel: str | None = None
+
+    def __init__(self, play_context, new_stdin, *args, **kwargs):
+        super(Connection, self).__init__(play_context, new_stdin, *args, **kwargs)
+
+    def _set_log_channel(self, name: str) -> None:
+        """ Mimic paramiko.SSHClient.set_log_channel """
+        self._log_channel = name
+
+    def _parse_proxy_command(self, port: int = 22) -> dict[str, t.Any]:
+        proxy_command = self.get_option('proxy_command') or None
+
+        sock_kwarg = {}
+        if proxy_command:
+            replacers = {
+                '%h': self.get_option('remote_addr'),
+                '%p': port,
+                '%r': self.get_option('remote_user')
+            }
+            for find, replace in replacers.items():
+                proxy_command = proxy_command.replace(find, str(replace))
+            try:
+                sock_kwarg = {'sock': paramiko.ProxyCommand(proxy_command)}
+                display.vvv(f'CONFIGURE PROXY COMMAND FOR CONNECTION: {proxy_command}', host=self.get_option('remote_addr'))
+            except AttributeError:
+                display.warning('Paramiko ProxyCommand support unavailable. '
+                                'Please upgrade to Paramiko 1.9.0 or newer. '
+                                'Not using configured ProxyCommand')
+
+        return sock_kwarg
+
+    def _connect(self) -> Connection:
+        """ activates the connection object """
+
+        if paramiko is None:
+            raise AnsibleError(f'paramiko is not installed: {to_native(PARAMIKO_IMPORT_ERR)}')
+
+        port = self.get_option('port')
+        display.vvv(f'ESTABLISH PARAMIKO SSH CONNECTION FOR USER: {self.get_option("remote_user")} on PORT {to_text(port)} TO {self.get_option("remote_addr")}',
+                    host=self.get_option('remote_addr'))
+
+        ssh = paramiko.SSHClient()
+
+        # Set pubkey and hostkey algorithms to disable, the only manipulation allowed currently
+        # is keeping or omitting rsa-sha2 algorithms
+        # default_keys: t.Tuple[str] = ()
+        paramiko_preferred_pubkeys = getattr(paramiko.Transport, '_preferred_pubkeys', ())
+        paramiko_preferred_hostkeys = getattr(paramiko.Transport, '_preferred_keys', ())
+        use_rsa_sha2_algorithms = self.get_option('use_rsa_sha2_algorithms')
+        disabled_algorithms: t.Dict[str, t.Iterable[str]] = {}
+        if not use_rsa_sha2_algorithms:
+            if paramiko_preferred_pubkeys:
+                disabled_algorithms['pubkeys'] = tuple(a for a in paramiko_preferred_pubkeys if 'rsa-sha2' in a)
+            if paramiko_preferred_hostkeys:
+                disabled_algorithms['keys'] = tuple(a for a in paramiko_preferred_hostkeys if 'rsa-sha2' in a)
+
+        # override paramiko's default logger name
+        if self._log_channel is not None:
+            ssh.set_log_channel(self._log_channel)
+
+        self.keyfile = os.path.expanduser('~/.ssh/known_hosts')
+
+        if self.get_option('host_key_checking'):
+            for ssh_known_hosts in ('/etc/ssh/ssh_known_hosts', '/etc/openssh/ssh_known_hosts'):
+                try:
+                    ssh.load_system_host_keys(ssh_known_hosts)
+                    break
+                except IOError:
+                    pass  # file was not found, but not required to function
+                except paramiko.hostkeys.InvalidHostKey as e:
+                    raise AnsibleConnectionFailure(f'Invalid host key: {to_text(e.line)}')
+            try:
+                ssh.load_system_host_keys()
+            except paramiko.hostkeys.InvalidHostKey as e:
+                raise AnsibleConnectionFailure(f'Invalid host key: {to_text(e.line)}')
+
+        ssh_connect_kwargs = self._parse_proxy_command(port)
+        ssh.set_missing_host_key_policy(MyAddPolicy(self))
+        conn_password = self.get_option('password')
+        allow_agent = True
+
+        if conn_password is not None:
+            allow_agent = False
+
+        try:
+            key_filename = None
+            if self.get_option('private_key_file'):
+                key_filename = os.path.expanduser(self.get_option('private_key_file'))
+
+            # paramiko 2.2 introduced auth_timeout parameter
+            if LooseVersion(paramiko.__version__) >= LooseVersion('2.2.0'):
+                ssh_connect_kwargs['auth_timeout'] = self.get_option('timeout')
path: plugins/connection/wsl.py
user: login: russoz
body: This is not mentioned in the docs for the user. It should have some note about it, like the "banner timeout" below has.
created_at: 2025-02-23T00:47:02Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,758 @@
+# -*- coding: utf-8 -*-
+# Derived from ansible/plugins/connection/proxmox_pct_remote.py (c) 2024 Nils Stein (@mietzen) <github.nstein@mailbox.org>
+# Derived from ansible/plugins/connection/paramiko_ssh.py (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
+# Copyright (c) 2025 Rui Lopes (@rgl) <ruilopes.com>
+# Copyright (c) 2025 Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import annotations
+
+DOCUMENTATION = r"""
+author: Rui Lopes (@rgl) <ruilopes.com>
+name: wsl
+short_description: Run tasks in WSL distribution using wsl.exe CLI via SSH
+requirements:
+  - paramiko
+description:
+  - Run commands or put/fetch files to an existing WSL distribution using wsl.exe CLI via SSH.
+  - Uses the Python SSH implementation (Paramiko) to connect to the WSL host.
+version_added: "10.3.0"
+options:
+  remote_addr:
+    description:
+      - Address of the remote target.
+    default: inventory_hostname
+    type: string
+    vars:
+      - name: inventory_hostname
+      - name: ansible_host
+      - name: ansible_ssh_host
+      - name: ansible_paramiko_host
+  port:
+    description: Remote port to connect to.
+    type: int
+    default: 22
+    ini:
+      - section: defaults
+        key: remote_port
+      - section: paramiko_connection
+        key: remote_port
+    env:
+      - name: ANSIBLE_REMOTE_PORT
+      - name: ANSIBLE_REMOTE_PARAMIKO_PORT
+    vars:
+      - name: ansible_port
+      - name: ansible_ssh_port
+      - name: ansible_paramiko_port
+    keyword:
+      - name: port
+  remote_user:
+    description:
+      - User to login/authenticate as.
+      - Can be set from the CLI via the C(--user) or C(-u) options.
+    type: string
+    vars:
+      - name: ansible_user
+      - name: ansible_ssh_user
+      - name: ansible_paramiko_user
+    env:
+      - name: ANSIBLE_REMOTE_USER
+      - name: ANSIBLE_PARAMIKO_REMOTE_USER
+    ini:
+      - section: defaults
+        key: remote_user
+      - section: paramiko_connection
+        key: remote_user
+    keyword:
+      - name: remote_user
+  password:
+    description:
+      - Secret used to either login the SSH server or as a passphrase for SSH keys that require it.
+      - Can be set from the CLI via the C(--ask-pass) option.
+    type: string
+    vars:
+      - name: ansible_password
+      - name: ansible_ssh_pass
+      - name: ansible_ssh_password
+      - name: ansible_paramiko_pass
+      - name: ansible_paramiko_password
+  use_rsa_sha2_algorithms:
+    description:
+      - Whether or not to enable RSA SHA2 algorithms for pubkeys and hostkeys.
+      - On paramiko versions older than 2.9, this only affects hostkeys.
+      - For behavior matching paramiko<2.9 set this to V(false).
+    vars:
+      - name: ansible_paramiko_use_rsa_sha2_algorithms
+    ini:
+      - {key: use_rsa_sha2_algorithms, section: paramiko_connection}
+    env:
+      - {name: ANSIBLE_PARAMIKO_USE_RSA_SHA2_ALGORITHMS}
+    default: true
+    type: boolean
+  host_key_auto_add:
+    description: "Automatically add host keys to C(~/.ssh/known_hosts)."
+    env:
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_AUTO_ADD
+    ini:
+      - key: host_key_auto_add
+        section: paramiko_connection
+    type: boolean
+  look_for_keys:
+    default: True
+    description: "Set to V(false) to disable searching for private key files in C(~/.ssh/)."
+    env:
+      - name: ANSIBLE_PARAMIKO_LOOK_FOR_KEYS
+    ini:
+      - {key: look_for_keys, section: paramiko_connection}
+    type: boolean
+  proxy_command:
+    default: ""
+    description:
+      - Proxy information for running the connection via a jumphost.
+    type: string
+    env:
+      - name: ANSIBLE_PARAMIKO_PROXY_COMMAND
+    ini:
+      - {key: proxy_command, section: paramiko_connection}
+    vars:
+      - name: ansible_paramiko_proxy_command
+  record_host_keys:
+    default: True
+    description: "Save the host keys to a file."
+    env:
+      - name: ANSIBLE_PARAMIKO_RECORD_HOST_KEYS
+    ini:
+      - section: paramiko_connection
+        key: record_host_keys
+    type: boolean
+  host_key_checking:
+    description: "Set this to V(false) if you want to avoid host key checking by the underlying tools Ansible uses to connect to the host."
+    type: boolean
+    default: true
+    env:
+      - name: ANSIBLE_HOST_KEY_CHECKING
+      - name: ANSIBLE_SSH_HOST_KEY_CHECKING
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_CHECKING
+    ini:
+      - section: defaults
+        key: host_key_checking
+      - section: paramiko_connection
+        key: host_key_checking
+    vars:
+      - name: ansible_host_key_checking
+      - name: ansible_ssh_host_key_checking
+      - name: ansible_paramiko_host_key_checking
+  use_persistent_connections:
+    description: "Toggles the use of persistence for connections."
+    type: boolean
+    default: False
+    env:
+      - name: ANSIBLE_USE_PERSISTENT_CONNECTIONS
+    ini:
+      - section: defaults
+        key: use_persistent_connections
+  banner_timeout:
+    type: float
+    default: 30
+    description:
+      - Configures, in seconds, the amount of time to wait for the SSH
+        banner to be presented. This option is supported by paramiko
+        version 1.15.0 or newer.
+    ini:
+      - section: paramiko_connection
+        key: banner_timeout
+    env:
+      - name: ANSIBLE_PARAMIKO_BANNER_TIMEOUT
+  timeout:
+    type: int
+    default: 10
+    description: Number of seconds until the plugin gives up on failing to establish a TCP connection.
+    ini:
+      - section: defaults
+        key: timeout
+      - section: ssh_connection
+        key: timeout
+      - section: paramiko_connection
+        key: timeout
+    env:
+      - name: ANSIBLE_TIMEOUT
+      - name: ANSIBLE_SSH_TIMEOUT
+      - name: ANSIBLE_PARAMIKO_TIMEOUT
+    vars:
+      - name: ansible_ssh_timeout
+      - name: ansible_paramiko_timeout
+    cli:
+      - name: timeout
+  lock_file_timeout:
+    type: int
+    default: 60
+    description: Number of seconds until the plugin gives up on trying to write a lock file when writing SSH known host keys.
+    vars:
+      - name: ansible_lock_file_timeout
+    env:
+      - name: ANSIBLE_LOCK_FILE_TIMEOUT
+  private_key_file:
+    description:
+      - Path to private key file to use for authentication.
+    type: string
+    ini:
+      - section: defaults
+        key: private_key_file
+      - section: paramiko_connection
+        key: private_key_file
+    env:
+      - name: ANSIBLE_PRIVATE_KEY_FILE
+      - name: ANSIBLE_PARAMIKO_PRIVATE_KEY_FILE
+    vars:
+      - name: ansible_private_key_file
+      - name: ansible_ssh_private_key_file
+      - name: ansible_paramiko_private_key_file
+    cli:
+      - name: private_key_file
+        option: "--private-key"
+  wsl_distribution:
+    description:
+      - WSL distribution name
+    type: string
+    vars:
+      - name: wsl_distribution
+  wsl_user:
+    description:
+      - WSL distribution user
+    type: string
+    vars:
+      - name: wsl_user
+  become_user:
+    description:
+      - WSL distribution user
+    type: string
+    default: root
+    vars:
+      - name: become_user
+      - name: ansible_become_user
+  become:
+    description:
+      - whether to use the user defined by ansible_become_user.
+    type: bool
+    default: false
+    vars:
+      - name: become
+      - name: ansible_become
+"""
+
+EXAMPLES = r"""
+# ------------------------
+# Inventory: inventory.yml
+# ------------------------
+# all:
+#   children:
+#     wsl:
+#       hosts:
+#         ubuntu:
+#           ansible_host: 10.0.0.10
+#           wsl_distribution: ubuntu
+#         debian:
+#           ansible_host: 10.0.0.10
+#           wsl_distribution: debian
+#       vars:
+#         ansible_connection: community.general.wsl
+#         ansible_user: vagrant
+#         wsl_user: wsl
+# ----------------------
+# Playbook: playbook.yml
+# ----------------------
+---
+- hosts: wsl
+  tasks:
+    - name: ping
+      ansible.builtin.ping:
+    - name: id
+      args:
+        executable: /bin/bash
+      ansible.builtin.shell: |
+        exec 2>&1
+        set -x
+        echo "$0"
+        pwd
+        id
+    - name: become id
+      become: true
+      args:
+        executable: /bin/bash
+      ansible.builtin.shell: |
+        exec 2>&1
+        set -x
+        echo "$0"
+        pwd
+        id
+"""
+
+import os
+import pathlib
+import shlex
+import socket
+import tempfile
+import typing as t
+
+from ansible.errors import (
+    AnsibleAuthenticationFailure,
+    AnsibleConnectionFailure,
+    AnsibleError,
+)
+from ansible_collections.community.general.plugins.module_utils._filelock import FileLock, LockTimeout
+from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
+from ansible.module_utils.compat.paramiko import PARAMIKO_IMPORT_ERR, paramiko
+from ansible.module_utils.compat.version import LooseVersion
+from ansible.plugins.connection import ConnectionBase
+from ansible.utils.display import Display
+from ansible.utils.path import makedirs_safe
+from binascii import hexlify
+from subprocess import list2cmdline
+
+
+display = Display()
+
+
+def authenticity_msg(hostname: str, ktype: str, fingerprint: str) -> str:
+    msg = f"""
+    paramiko: The authenticity of host '{hostname}' can't be established.
+    The {ktype} key fingerprint is {fingerprint}.
+    Are you sure you want to continue connecting (yes/no)?
+    """
+    return msg
+
+
+MissingHostKeyPolicy: type = object
+if paramiko:
+    MissingHostKeyPolicy = paramiko.MissingHostKeyPolicy
+
+
+class MyAddPolicy(MissingHostKeyPolicy):
+    """
+    Based on AutoAddPolicy in paramiko so we can determine when keys are added
+
+    and also prompt for input.
+
+    Policy for automatically adding the hostname and new host key to the
+    local L{HostKeys} object, and saving it. This is used by L{SSHClient}.
+    """
+
+    def __init__(self, connection: Connection) -> None:
+        self.connection = connection
+        self._options = connection._options
+
+    def missing_host_key(self, client, hostname, key) -> None:
+
+        if all((self.connection.get_option('host_key_checking'), not self.connection.get_option('host_key_auto_add'))):
+
+            fingerprint = hexlify(key.get_fingerprint())
+            ktype = key.get_name()
+
+            if self.connection.get_option('use_persistent_connections') or self.connection.force_persistence:
+                # don't print the prompt string since the user cannot respond
+                # to the question anyway
+                raise AnsibleError(authenticity_msg(hostname, ktype, fingerprint)[1:92])
+
+            inp = to_text(
+                display.prompt_until(authenticity_msg(hostname, ktype, fingerprint), private=False),
+                errors='surrogate_or_strict'
+            )
+
+            if inp.lower() not in ['yes', 'y', '']:
+                raise AnsibleError('host connection rejected by user')
+
+        key._added_by_ansible_this_time = True
+
+        # existing implementation below:
+        client._host_keys.add(hostname, key.get_name(), key)
+
+        # host keys are actually saved in close() function below
+        # in order to control ordering.
+
+
+class Connection(ConnectionBase):
+    """ SSH based connections (paramiko) to WSL """
+
+    transport = 'community.general.wsl'
+    _log_channel: str | None = None
+
+    def __init__(self, play_context, new_stdin, *args, **kwargs):
+        super(Connection, self).__init__(play_context, new_stdin, *args, **kwargs)
+
+    def _set_log_channel(self, name: str) -> None:
+        """ Mimic paramiko.SSHClient.set_log_channel """
+        self._log_channel = name
+
+    def _parse_proxy_command(self, port: int = 22) -> dict[str, t.Any]:
+        proxy_command = self.get_option('proxy_command') or None
+
+        sock_kwarg = {}
+        if proxy_command:
+            replacers = {
+                '%h': self.get_option('remote_addr'),
+                '%p': port,
+                '%r': self.get_option('remote_user')
+            }
+            for find, replace in replacers.items():
+                proxy_command = proxy_command.replace(find, str(replace))
+            try:
+                sock_kwarg = {'sock': paramiko.ProxyCommand(proxy_command)}
+                display.vvv(f'CONFIGURE PROXY COMMAND FOR CONNECTION: {proxy_command}', host=self.get_option('remote_addr'))
+            except AttributeError:
+                display.warning('Paramiko ProxyCommand support unavailable. '
+                                'Please upgrade to Paramiko 1.9.0 or newer. '
+                                'Not using configured ProxyCommand')
path: plugins/connection/wsl.py
user: login: russoz
body: This should be mentioned in the docs for `proxy_command`.
created_at: 2025-02-23T00:48:36Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,758 @@
+# -*- coding: utf-8 -*-
+# Derived from ansible/plugins/connection/proxmox_pct_remote.py (c) 2024 Nils Stein (@mietzen) <github.nstein@mailbox.org>
+# Derived from ansible/plugins/connection/paramiko_ssh.py (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
+# Copyright (c) 2025 Rui Lopes (@rgl) <ruilopes.com>
+# Copyright (c) 2025 Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import annotations
+
+DOCUMENTATION = r"""
+author: Rui Lopes (@rgl) <ruilopes.com>
+name: wsl
+short_description: Run tasks in WSL distribution using wsl.exe CLI via SSH
+requirements:
+  - paramiko
+description:
+  - Run commands or put/fetch files to an existing WSL distribution using wsl.exe CLI via SSH.
+  - Uses the Python SSH implementation (Paramiko) to connect to the WSL host.
+version_added: "10.3.0"
+options:
+  remote_addr:
+    description:
+      - Address of the remote target.
+    default: inventory_hostname
+    type: string
+    vars:
+      - name: inventory_hostname
+      - name: ansible_host
+      - name: ansible_ssh_host
+      - name: ansible_paramiko_host
+  port:
+    description: Remote port to connect to.
+    type: int
+    default: 22
+    ini:
+      - section: defaults
+        key: remote_port
+      - section: paramiko_connection
+        key: remote_port
+    env:
+      - name: ANSIBLE_REMOTE_PORT
+      - name: ANSIBLE_REMOTE_PARAMIKO_PORT
+    vars:
+      - name: ansible_port
+      - name: ansible_ssh_port
+      - name: ansible_paramiko_port
+    keyword:
+      - name: port
+  remote_user:
+    description:
+      - User to login/authenticate as.
+      - Can be set from the CLI via the C(--user) or C(-u) options.
+    type: string
+    vars:
+      - name: ansible_user
+      - name: ansible_ssh_user
+      - name: ansible_paramiko_user
+    env:
+      - name: ANSIBLE_REMOTE_USER
+      - name: ANSIBLE_PARAMIKO_REMOTE_USER
+    ini:
+      - section: defaults
+        key: remote_user
+      - section: paramiko_connection
+        key: remote_user
+    keyword:
+      - name: remote_user
+  password:
+    description:
+      - Secret used to either login the SSH server or as a passphrase for SSH keys that require it.
+      - Can be set from the CLI via the C(--ask-pass) option.
+    type: string
+    vars:
+      - name: ansible_password
+      - name: ansible_ssh_pass
+      - name: ansible_ssh_password
+      - name: ansible_paramiko_pass
+      - name: ansible_paramiko_password
+  use_rsa_sha2_algorithms:
+    description:
+      - Whether or not to enable RSA SHA2 algorithms for pubkeys and hostkeys.
+      - On paramiko versions older than 2.9, this only affects hostkeys.
+      - For behavior matching paramiko<2.9 set this to V(false).
+    vars:
+      - name: ansible_paramiko_use_rsa_sha2_algorithms
+    ini:
+      - {key: use_rsa_sha2_algorithms, section: paramiko_connection}
+    env:
+      - {name: ANSIBLE_PARAMIKO_USE_RSA_SHA2_ALGORITHMS}
+    default: true
+    type: boolean
+  host_key_auto_add:
+    description: "Automatically add host keys to C(~/.ssh/known_hosts)."
+    env:
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_AUTO_ADD
+    ini:
+      - key: host_key_auto_add
+        section: paramiko_connection
+    type: boolean
+  look_for_keys:
+    default: True
+    description: "Set to V(false) to disable searching for private key files in C(~/.ssh/)."
+    env:
+      - name: ANSIBLE_PARAMIKO_LOOK_FOR_KEYS
+    ini:
+      - {key: look_for_keys, section: paramiko_connection}
+    type: boolean
+  proxy_command:
+    default: ""
+    description:
+      - Proxy information for running the connection via a jumphost.
+    type: string
+    env:
+      - name: ANSIBLE_PARAMIKO_PROXY_COMMAND
+    ini:
+      - {key: proxy_command, section: paramiko_connection}
+    vars:
+      - name: ansible_paramiko_proxy_command
+  record_host_keys:
+    default: True
+    description: "Save the host keys to a file."
+    env:
+      - name: ANSIBLE_PARAMIKO_RECORD_HOST_KEYS
+    ini:
+      - section: paramiko_connection
+        key: record_host_keys
+    type: boolean
+  host_key_checking:
+    description: "Set this to V(false) if you want to avoid host key checking by the underlying tools Ansible uses to connect to the host."
+    type: boolean
+    default: true
+    env:
+      - name: ANSIBLE_HOST_KEY_CHECKING
+      - name: ANSIBLE_SSH_HOST_KEY_CHECKING
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_CHECKING
+    ini:
+      - section: defaults
+        key: host_key_checking
+      - section: paramiko_connection
+        key: host_key_checking
+    vars:
+      - name: ansible_host_key_checking
+      - name: ansible_ssh_host_key_checking
+      - name: ansible_paramiko_host_key_checking
+  use_persistent_connections:
+    description: "Toggles the use of persistence for connections."
+    type: boolean
+    default: False
+    env:
+      - name: ANSIBLE_USE_PERSISTENT_CONNECTIONS
+    ini:
+      - section: defaults
+        key: use_persistent_connections
+  banner_timeout:
+    type: float
+    default: 30
+    description:
+      - Configures, in seconds, the amount of time to wait for the SSH
+        banner to be presented. This option is supported by paramiko
+        version 1.15.0 or newer.
+    ini:
+      - section: paramiko_connection
+        key: banner_timeout
+    env:
+      - name: ANSIBLE_PARAMIKO_BANNER_TIMEOUT
+  timeout:
+    type: int
+    default: 10
+    description: Number of seconds until the plugin gives up on failing to establish a TCP connection.
+    ini:
+      - section: defaults
+        key: timeout
+      - section: ssh_connection
+        key: timeout
+      - section: paramiko_connection
+        key: timeout
+    env:
+      - name: ANSIBLE_TIMEOUT
+      - name: ANSIBLE_SSH_TIMEOUT
+      - name: ANSIBLE_PARAMIKO_TIMEOUT
+    vars:
+      - name: ansible_ssh_timeout
+      - name: ansible_paramiko_timeout
+    cli:
+      - name: timeout
+  lock_file_timeout:
+    type: int
+    default: 60
+    description: Number of seconds until the plugin gives up on trying to write a lock file when writing SSH known host keys.
+    vars:
+      - name: ansible_lock_file_timeout
+    env:
+      - name: ANSIBLE_LOCK_FILE_TIMEOUT
+  private_key_file:
+    description:
+      - Path to private key file to use for authentication.
+    type: string
+    ini:
+      - section: defaults
+        key: private_key_file
+      - section: paramiko_connection
+        key: private_key_file
+    env:
+      - name: ANSIBLE_PRIVATE_KEY_FILE
+      - name: ANSIBLE_PARAMIKO_PRIVATE_KEY_FILE
+    vars:
+      - name: ansible_private_key_file
+      - name: ansible_ssh_private_key_file
+      - name: ansible_paramiko_private_key_file
+    cli:
+      - name: private_key_file
+        option: "--private-key"
+  wsl_distribution:
+    description:
+      - WSL distribution name
+    type: string
+    vars:
+      - name: wsl_distribution
+  wsl_user:
+    description:
+      - WSL distribution user
+    type: string
+    vars:
+      - name: wsl_user
+  become_user:
+    description:
+      - WSL distribution user
+    type: string
+    default: root
+    vars:
+      - name: become_user
+      - name: ansible_become_user
+  become:
+    description:
+      - whether to use the user defined by ansible_become_user.
+    type: bool
+    default: false
+    vars:
+      - name: become
+      - name: ansible_become
+"""
+
+EXAMPLES = r"""
+# ------------------------
+# Inventory: inventory.yml
+# ------------------------
+# all:
+#   children:
+#     wsl:
+#       hosts:
+#         ubuntu:
+#           ansible_host: 10.0.0.10
+#           wsl_distribution: ubuntu
+#         debian:
+#           ansible_host: 10.0.0.10
+#           wsl_distribution: debian
+#       vars:
+#         ansible_connection: community.general.wsl
+#         ansible_user: vagrant
+#         wsl_user: wsl
path: plugins/connection/wsl.py
user: login: russoz
body: No need to comment it out, EXAMPLES is kinda free form. The only thing we need is that you add the marker for another YAML document within the same file, `---` on the top of each section/example, as in before line 248.
created_at: 2025-02-23T00:55:55Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,758 @@
+# -*- coding: utf-8 -*-
+# Derived from ansible/plugins/connection/proxmox_pct_remote.py (c) 2024 Nils Stein (@mietzen) <github.nstein@mailbox.org>
+# Derived from ansible/plugins/connection/paramiko_ssh.py (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
+# Copyright (c) 2025 Rui Lopes (@rgl) <ruilopes.com>
+# Copyright (c) 2025 Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import annotations
+
+DOCUMENTATION = r"""
+author: Rui Lopes (@rgl) <ruilopes.com>
+name: wsl
+short_description: Run tasks in WSL distribution using wsl.exe CLI via SSH
+requirements:
+  - paramiko
+description:
+  - Run commands or put/fetch files to an existing WSL distribution using wsl.exe CLI via SSH.
+  - Uses the Python SSH implementation (Paramiko) to connect to the WSL host.
+version_added: "10.3.0"
+options:
+  remote_addr:
+    description:
+      - Address of the remote target.
+    default: inventory_hostname
+    type: string
+    vars:
+      - name: inventory_hostname
+      - name: ansible_host
+      - name: ansible_ssh_host
+      - name: ansible_paramiko_host
+  port:
+    description: Remote port to connect to.
+    type: int
+    default: 22
+    ini:
+      - section: defaults
+        key: remote_port
+      - section: paramiko_connection
+        key: remote_port
+    env:
+      - name: ANSIBLE_REMOTE_PORT
+      - name: ANSIBLE_REMOTE_PARAMIKO_PORT
+    vars:
+      - name: ansible_port
+      - name: ansible_ssh_port
+      - name: ansible_paramiko_port
+    keyword:
+      - name: port
+  remote_user:
+    description:
+      - User to login/authenticate as.
+      - Can be set from the CLI via the C(--user) or C(-u) options.
+    type: string
+    vars:
+      - name: ansible_user
+      - name: ansible_ssh_user
+      - name: ansible_paramiko_user
+    env:
+      - name: ANSIBLE_REMOTE_USER
+      - name: ANSIBLE_PARAMIKO_REMOTE_USER
+    ini:
+      - section: defaults
+        key: remote_user
+      - section: paramiko_connection
+        key: remote_user
+    keyword:
+      - name: remote_user
+  password:
+    description:
+      - Secret used to either login the SSH server or as a passphrase for SSH keys that require it.
+      - Can be set from the CLI via the C(--ask-pass) option.
+    type: string
+    vars:
+      - name: ansible_password
+      - name: ansible_ssh_pass
+      - name: ansible_ssh_password
+      - name: ansible_paramiko_pass
+      - name: ansible_paramiko_password
+  use_rsa_sha2_algorithms:
+    description:
+      - Whether or not to enable RSA SHA2 algorithms for pubkeys and hostkeys.
+      - On paramiko versions older than 2.9, this only affects hostkeys.
+      - For behavior matching paramiko<2.9 set this to V(false).
+    vars:
+      - name: ansible_paramiko_use_rsa_sha2_algorithms
+    ini:
+      - {key: use_rsa_sha2_algorithms, section: paramiko_connection}
+    env:
+      - {name: ANSIBLE_PARAMIKO_USE_RSA_SHA2_ALGORITHMS}
+    default: true
+    type: boolean
+  host_key_auto_add:
+    description: "Automatically add host keys to C(~/.ssh/known_hosts)."
+    env:
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_AUTO_ADD
+    ini:
+      - key: host_key_auto_add
+        section: paramiko_connection
+    type: boolean
+  look_for_keys:
+    default: True
+    description: "Set to V(false) to disable searching for private key files in C(~/.ssh/)."
+    env:
+      - name: ANSIBLE_PARAMIKO_LOOK_FOR_KEYS
+    ini:
+      - {key: look_for_keys, section: paramiko_connection}
+    type: boolean
+  proxy_command:
+    default: ""
+    description:
+      - Proxy information for running the connection via a jumphost.
+    type: string
+    env:
+      - name: ANSIBLE_PARAMIKO_PROXY_COMMAND
+    ini:
+      - {key: proxy_command, section: paramiko_connection}
+    vars:
+      - name: ansible_paramiko_proxy_command
+  record_host_keys:
+    default: True
+    description: "Save the host keys to a file."
+    env:
+      - name: ANSIBLE_PARAMIKO_RECORD_HOST_KEYS
+    ini:
+      - section: paramiko_connection
+        key: record_host_keys
+    type: boolean
+  host_key_checking:
+    description: "Set this to V(false) if you want to avoid host key checking by the underlying tools Ansible uses to connect to the host."
+    type: boolean
+    default: true
+    env:
+      - name: ANSIBLE_HOST_KEY_CHECKING
+      - name: ANSIBLE_SSH_HOST_KEY_CHECKING
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_CHECKING
+    ini:
+      - section: defaults
+        key: host_key_checking
+      - section: paramiko_connection
+        key: host_key_checking
+    vars:
+      - name: ansible_host_key_checking
+      - name: ansible_ssh_host_key_checking
+      - name: ansible_paramiko_host_key_checking
+  use_persistent_connections:
+    description: "Toggles the use of persistence for connections."
+    type: boolean
+    default: False
+    env:
+      - name: ANSIBLE_USE_PERSISTENT_CONNECTIONS
+    ini:
+      - section: defaults
+        key: use_persistent_connections
+  banner_timeout:
+    type: float
+    default: 30
+    description:
+      - Configures, in seconds, the amount of time to wait for the SSH
+        banner to be presented. This option is supported by paramiko
+        version 1.15.0 or newer.
+    ini:
+      - section: paramiko_connection
+        key: banner_timeout
+    env:
+      - name: ANSIBLE_PARAMIKO_BANNER_TIMEOUT
+  timeout:
+    type: int
+    default: 10
+    description: Number of seconds until the plugin gives up on failing to establish a TCP connection.
+    ini:
+      - section: defaults
+        key: timeout
+      - section: ssh_connection
+        key: timeout
+      - section: paramiko_connection
+        key: timeout
+    env:
+      - name: ANSIBLE_TIMEOUT
+      - name: ANSIBLE_SSH_TIMEOUT
+      - name: ANSIBLE_PARAMIKO_TIMEOUT
+    vars:
+      - name: ansible_ssh_timeout
+      - name: ansible_paramiko_timeout
+    cli:
+      - name: timeout
+  lock_file_timeout:
+    type: int
+    default: 60
+    description: Number of seconds until the plugin gives up on trying to write a lock file when writing SSH known host keys.
+    vars:
+      - name: ansible_lock_file_timeout
+    env:
+      - name: ANSIBLE_LOCK_FILE_TIMEOUT
+  private_key_file:
+    description:
+      - Path to private key file to use for authentication.
+    type: string
+    ini:
+      - section: defaults
+        key: private_key_file
+      - section: paramiko_connection
+        key: private_key_file
+    env:
+      - name: ANSIBLE_PRIVATE_KEY_FILE
+      - name: ANSIBLE_PARAMIKO_PRIVATE_KEY_FILE
+    vars:
+      - name: ansible_private_key_file
+      - name: ansible_ssh_private_key_file
+      - name: ansible_paramiko_private_key_file
+    cli:
+      - name: private_key_file
+        option: "--private-key"
+  wsl_distribution:
+    description:
+      - WSL distribution name
+    type: string
+    vars:
+      - name: wsl_distribution
+  wsl_user:
+    description:
+      - WSL distribution user
+    type: string
+    vars:
+      - name: wsl_user
+  become_user:
+    description:
+      - WSL distribution user
+    type: string
+    default: root
+    vars:
+      - name: become_user
+      - name: ansible_become_user
+  become:
+    description:
+      - whether to use the user defined by ansible_become_user.
+    type: bool
+    default: false
+    vars:
+      - name: become
+      - name: ansible_become
+"""
+
+EXAMPLES = r"""
+# ------------------------
+# Inventory: inventory.yml
+# ------------------------
+# all:
+#   children:
+#     wsl:
+#       hosts:
+#         ubuntu:
+#           ansible_host: 10.0.0.10
+#           wsl_distribution: ubuntu
+#         debian:
+#           ansible_host: 10.0.0.10
+#           wsl_distribution: debian
+#       vars:
+#         ansible_connection: community.general.wsl
+#         ansible_user: vagrant
+#         wsl_user: wsl
+# ----------------------
+# Playbook: playbook.yml
+# ----------------------
+---
+- hosts: wsl
+  tasks:
+    - name: ping
+      ansible.builtin.ping:
+    - name: id
+      args:
+        executable: /bin/bash
+      ansible.builtin.shell: |
+        exec 2>&1
+        set -x
+        echo "$0"
+        pwd
+        id
+    - name: become id
+      become: true
+      args:
+        executable: /bin/bash
+      ansible.builtin.shell: |
+        exec 2>&1
+        set -x
+        echo "$0"
+        pwd
+        id
+"""
+
+import os
+import pathlib
+import shlex
+import socket
+import tempfile
+import typing as t
path: plugins/connection/wsl.py
user: login: russoz
body: This is only used 3 times in the entire file. I reckon it might better to use the form:
```suggestion
from typing import Any, Dict, Iterable
```
created_at: 2025-02-23T00:58:20Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,758 @@
+# -*- coding: utf-8 -*-
+# Derived from ansible/plugins/connection/proxmox_pct_remote.py (c) 2024 Nils Stein (@mietzen) <github.nstein@mailbox.org>
+# Derived from ansible/plugins/connection/paramiko_ssh.py (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
+# Copyright (c) 2025 Rui Lopes (@rgl) <ruilopes.com>
+# Copyright (c) 2025 Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import annotations
+
+DOCUMENTATION = r"""
+author: Rui Lopes (@rgl) <ruilopes.com>
+name: wsl
+short_description: Run tasks in WSL distribution using wsl.exe CLI via SSH
+requirements:
+  - paramiko
+description:
+  - Run commands or put/fetch files to an existing WSL distribution using wsl.exe CLI via SSH.
+  - Uses the Python SSH implementation (Paramiko) to connect to the WSL host.
+version_added: "10.3.0"
+options:
+  remote_addr:
+    description:
+      - Address of the remote target.
+    default: inventory_hostname
+    type: string
+    vars:
+      - name: inventory_hostname
+      - name: ansible_host
+      - name: ansible_ssh_host
+      - name: ansible_paramiko_host
+  port:
+    description: Remote port to connect to.
+    type: int
+    default: 22
+    ini:
+      - section: defaults
+        key: remote_port
+      - section: paramiko_connection
+        key: remote_port
+    env:
+      - name: ANSIBLE_REMOTE_PORT
+      - name: ANSIBLE_REMOTE_PARAMIKO_PORT
+    vars:
+      - name: ansible_port
+      - name: ansible_ssh_port
+      - name: ansible_paramiko_port
+    keyword:
+      - name: port
+  remote_user:
+    description:
+      - User to login/authenticate as.
+      - Can be set from the CLI via the C(--user) or C(-u) options.
+    type: string
+    vars:
+      - name: ansible_user
+      - name: ansible_ssh_user
+      - name: ansible_paramiko_user
+    env:
+      - name: ANSIBLE_REMOTE_USER
+      - name: ANSIBLE_PARAMIKO_REMOTE_USER
+    ini:
+      - section: defaults
+        key: remote_user
+      - section: paramiko_connection
+        key: remote_user
+    keyword:
+      - name: remote_user
+  password:
+    description:
+      - Secret used to either login the SSH server or as a passphrase for SSH keys that require it.
+      - Can be set from the CLI via the C(--ask-pass) option.
+    type: string
+    vars:
+      - name: ansible_password
+      - name: ansible_ssh_pass
+      - name: ansible_ssh_password
+      - name: ansible_paramiko_pass
+      - name: ansible_paramiko_password
+  use_rsa_sha2_algorithms:
+    description:
+      - Whether or not to enable RSA SHA2 algorithms for pubkeys and hostkeys.
+      - On paramiko versions older than 2.9, this only affects hostkeys.
+      - For behavior matching paramiko<2.9 set this to V(false).
+    vars:
+      - name: ansible_paramiko_use_rsa_sha2_algorithms
+    ini:
+      - {key: use_rsa_sha2_algorithms, section: paramiko_connection}
+    env:
+      - {name: ANSIBLE_PARAMIKO_USE_RSA_SHA2_ALGORITHMS}
+    default: true
+    type: boolean
+  host_key_auto_add:
+    description: "Automatically add host keys to C(~/.ssh/known_hosts)."
+    env:
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_AUTO_ADD
+    ini:
+      - key: host_key_auto_add
+        section: paramiko_connection
+    type: boolean
+  look_for_keys:
+    default: True
+    description: "Set to V(false) to disable searching for private key files in C(~/.ssh/)."
+    env:
+      - name: ANSIBLE_PARAMIKO_LOOK_FOR_KEYS
+    ini:
+      - {key: look_for_keys, section: paramiko_connection}
+    type: boolean
+  proxy_command:
+    default: ""
+    description:
+      - Proxy information for running the connection via a jumphost.
+    type: string
+    env:
+      - name: ANSIBLE_PARAMIKO_PROXY_COMMAND
+    ini:
+      - {key: proxy_command, section: paramiko_connection}
+    vars:
+      - name: ansible_paramiko_proxy_command
+  record_host_keys:
+    default: True
+    description: "Save the host keys to a file."
+    env:
+      - name: ANSIBLE_PARAMIKO_RECORD_HOST_KEYS
+    ini:
+      - section: paramiko_connection
+        key: record_host_keys
+    type: boolean
+  host_key_checking:
+    description: "Set this to V(false) if you want to avoid host key checking by the underlying tools Ansible uses to connect to the host."
+    type: boolean
+    default: true
+    env:
+      - name: ANSIBLE_HOST_KEY_CHECKING
+      - name: ANSIBLE_SSH_HOST_KEY_CHECKING
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_CHECKING
+    ini:
+      - section: defaults
+        key: host_key_checking
+      - section: paramiko_connection
+        key: host_key_checking
+    vars:
+      - name: ansible_host_key_checking
+      - name: ansible_ssh_host_key_checking
+      - name: ansible_paramiko_host_key_checking
+  use_persistent_connections:
+    description: "Toggles the use of persistence for connections."
+    type: boolean
+    default: False
+    env:
+      - name: ANSIBLE_USE_PERSISTENT_CONNECTIONS
+    ini:
+      - section: defaults
+        key: use_persistent_connections
+  banner_timeout:
+    type: float
+    default: 30
+    description:
+      - Configures, in seconds, the amount of time to wait for the SSH
+        banner to be presented. This option is supported by paramiko
+        version 1.15.0 or newer.
+    ini:
+      - section: paramiko_connection
+        key: banner_timeout
+    env:
+      - name: ANSIBLE_PARAMIKO_BANNER_TIMEOUT
+  timeout:
+    type: int
+    default: 10
+    description: Number of seconds until the plugin gives up on failing to establish a TCP connection.
+    ini:
+      - section: defaults
+        key: timeout
+      - section: ssh_connection
+        key: timeout
+      - section: paramiko_connection
+        key: timeout
+    env:
+      - name: ANSIBLE_TIMEOUT
+      - name: ANSIBLE_SSH_TIMEOUT
+      - name: ANSIBLE_PARAMIKO_TIMEOUT
+    vars:
+      - name: ansible_ssh_timeout
+      - name: ansible_paramiko_timeout
+    cli:
+      - name: timeout
+  lock_file_timeout:
+    type: int
+    default: 60
+    description: Number of seconds until the plugin gives up on trying to write a lock file when writing SSH known host keys.
+    vars:
+      - name: ansible_lock_file_timeout
+    env:
+      - name: ANSIBLE_LOCK_FILE_TIMEOUT
+  private_key_file:
+    description:
+      - Path to private key file to use for authentication.
+    type: string
+    ini:
+      - section: defaults
+        key: private_key_file
+      - section: paramiko_connection
+        key: private_key_file
+    env:
+      - name: ANSIBLE_PRIVATE_KEY_FILE
+      - name: ANSIBLE_PARAMIKO_PRIVATE_KEY_FILE
+    vars:
+      - name: ansible_private_key_file
+      - name: ansible_ssh_private_key_file
+      - name: ansible_paramiko_private_key_file
+    cli:
+      - name: private_key_file
+        option: "--private-key"
+  wsl_distribution:
+    description:
+      - WSL distribution name
+    type: string
+    vars:
+      - name: wsl_distribution
+  wsl_user:
+    description:
+      - WSL distribution user
+    type: string
+    vars:
+      - name: wsl_user
+  become_user:
+    description:
+      - WSL distribution user
+    type: string
+    default: root
+    vars:
+      - name: become_user
+      - name: ansible_become_user
+  become:
+    description:
+      - whether to use the user defined by ansible_become_user.
+    type: bool
+    default: false
+    vars:
+      - name: become
+      - name: ansible_become
+"""
+
+EXAMPLES = r"""
+# ------------------------
+# Inventory: inventory.yml
+# ------------------------
+# all:
+#   children:
+#     wsl:
+#       hosts:
+#         ubuntu:
+#           ansible_host: 10.0.0.10
+#           wsl_distribution: ubuntu
+#         debian:
+#           ansible_host: 10.0.0.10
+#           wsl_distribution: debian
+#       vars:
+#         ansible_connection: community.general.wsl
+#         ansible_user: vagrant
+#         wsl_user: wsl
+# ----------------------
+# Playbook: playbook.yml
+# ----------------------
+---
+- hosts: wsl
+  tasks:
+    - name: ping
+      ansible.builtin.ping:
+    - name: id
+      args:
+        executable: /bin/bash
+      ansible.builtin.shell: |
+        exec 2>&1
+        set -x
+        echo "$0"
+        pwd
+        id
+    - name: become id
+      become: true
+      args:
+        executable: /bin/bash
+      ansible.builtin.shell: |
+        exec 2>&1
+        set -x
+        echo "$0"
+        pwd
+        id
+"""
+
+import os
+import pathlib
+import shlex
+import socket
+import tempfile
+import typing as t
+
+from ansible.errors import (
+    AnsibleAuthenticationFailure,
+    AnsibleConnectionFailure,
+    AnsibleError,
+)
+from ansible_collections.community.general.plugins.module_utils._filelock import FileLock, LockTimeout
+from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
+from ansible.module_utils.compat.paramiko import PARAMIKO_IMPORT_ERR, paramiko
+from ansible.module_utils.compat.version import LooseVersion
+from ansible.plugins.connection import ConnectionBase
+from ansible.utils.display import Display
+from ansible.utils.path import makedirs_safe
+from binascii import hexlify
+from subprocess import list2cmdline
+
+
+display = Display()
+
+
+def authenticity_msg(hostname: str, ktype: str, fingerprint: str) -> str:
+    msg = f"""
+    paramiko: The authenticity of host '{hostname}' can't be established.
+    The {ktype} key fingerprint is {fingerprint}.
+    Are you sure you want to continue connecting (yes/no)?
+    """
+    return msg
+
+
+MissingHostKeyPolicy: type = object
+if paramiko:
+    MissingHostKeyPolicy = paramiko.MissingHostKeyPolicy
+
+
+class MyAddPolicy(MissingHostKeyPolicy):
+    """
+    Based on AutoAddPolicy in paramiko so we can determine when keys are added
+
+    and also prompt for input.
+
+    Policy for automatically adding the hostname and new host key to the
+    local L{HostKeys} object, and saving it. This is used by L{SSHClient}.
+    """
+
+    def __init__(self, connection: Connection) -> None:
+        self.connection = connection
+        self._options = connection._options
+
+    def missing_host_key(self, client, hostname, key) -> None:
+
+        if all((self.connection.get_option('host_key_checking'), not self.connection.get_option('host_key_auto_add'))):
+
+            fingerprint = hexlify(key.get_fingerprint())
+            ktype = key.get_name()
+
+            if self.connection.get_option('use_persistent_connections') or self.connection.force_persistence:
+                # don't print the prompt string since the user cannot respond
+                # to the question anyway
+                raise AnsibleError(authenticity_msg(hostname, ktype, fingerprint)[1:92])
+
+            inp = to_text(
+                display.prompt_until(authenticity_msg(hostname, ktype, fingerprint), private=False),
+                errors='surrogate_or_strict'
+            )
+
+            if inp.lower() not in ['yes', 'y', '']:
+                raise AnsibleError('host connection rejected by user')
+
+        key._added_by_ansible_this_time = True
+
+        # existing implementation below:
+        client._host_keys.add(hostname, key.get_name(), key)
+
+        # host keys are actually saved in close() function below
+        # in order to control ordering.
+
+
+class Connection(ConnectionBase):
+    """ SSH based connections (paramiko) to WSL """
+
+    transport = 'community.general.wsl'
+    _log_channel: str | None = None
+
+    def __init__(self, play_context, new_stdin, *args, **kwargs):
+        super(Connection, self).__init__(play_context, new_stdin, *args, **kwargs)
+
+    def _set_log_channel(self, name: str) -> None:
+        """ Mimic paramiko.SSHClient.set_log_channel """
+        self._log_channel = name
+
+    def _parse_proxy_command(self, port: int = 22) -> dict[str, t.Any]:
+        proxy_command = self.get_option('proxy_command') or None
+
+        sock_kwarg = {}
+        if proxy_command:
+            replacers = {
+                '%h': self.get_option('remote_addr'),
+                '%p': port,
+                '%r': self.get_option('remote_user')
+            }
+            for find, replace in replacers.items():
+                proxy_command = proxy_command.replace(find, str(replace))
+            try:
+                sock_kwarg = {'sock': paramiko.ProxyCommand(proxy_command)}
+                display.vvv(f'CONFIGURE PROXY COMMAND FOR CONNECTION: {proxy_command}', host=self.get_option('remote_addr'))
+            except AttributeError:
+                display.warning('Paramiko ProxyCommand support unavailable. '
+                                'Please upgrade to Paramiko 1.9.0 or newer. '
+                                'Not using configured ProxyCommand')
+
+        return sock_kwarg
+
+    def _connect(self) -> Connection:
+        """ activates the connection object """
+
+        if paramiko is None:
+            raise AnsibleError(f'paramiko is not installed: {to_native(PARAMIKO_IMPORT_ERR)}')
+
+        port = self.get_option('port')
+        display.vvv(f'ESTABLISH PARAMIKO SSH CONNECTION FOR USER: {self.get_option("remote_user")} on PORT {to_text(port)} TO {self.get_option("remote_addr")}',
+                    host=self.get_option('remote_addr'))
+
+        ssh = paramiko.SSHClient()
+
+        # Set pubkey and hostkey algorithms to disable, the only manipulation allowed currently
+        # is keeping or omitting rsa-sha2 algorithms
+        # default_keys: t.Tuple[str] = ()
+        paramiko_preferred_pubkeys = getattr(paramiko.Transport, '_preferred_pubkeys', ())
+        paramiko_preferred_hostkeys = getattr(paramiko.Transport, '_preferred_keys', ())
+        use_rsa_sha2_algorithms = self.get_option('use_rsa_sha2_algorithms')
+        disabled_algorithms: t.Dict[str, t.Iterable[str]] = {}
+        if not use_rsa_sha2_algorithms:
+            if paramiko_preferred_pubkeys:
+                disabled_algorithms['pubkeys'] = tuple(a for a in paramiko_preferred_pubkeys if 'rsa-sha2' in a)
+            if paramiko_preferred_hostkeys:
+                disabled_algorithms['keys'] = tuple(a for a in paramiko_preferred_hostkeys if 'rsa-sha2' in a)
+
+        # override paramiko's default logger name
+        if self._log_channel is not None:
+            ssh.set_log_channel(self._log_channel)
+
+        self.keyfile = os.path.expanduser('~/.ssh/known_hosts')
+
+        if self.get_option('host_key_checking'):
+            for ssh_known_hosts in ('/etc/ssh/ssh_known_hosts', '/etc/openssh/ssh_known_hosts'):
+                try:
+                    ssh.load_system_host_keys(ssh_known_hosts)
+                    break
+                except IOError:
+                    pass  # file was not found, but not required to function
+                except paramiko.hostkeys.InvalidHostKey as e:
+                    raise AnsibleConnectionFailure(f'Invalid host key: {to_text(e.line)}')
+            try:
+                ssh.load_system_host_keys()
+            except paramiko.hostkeys.InvalidHostKey as e:
+                raise AnsibleConnectionFailure(f'Invalid host key: {to_text(e.line)}')
+
+        ssh_connect_kwargs = self._parse_proxy_command(port)
+        ssh.set_missing_host_key_policy(MyAddPolicy(self))
+        conn_password = self.get_option('password')
+        allow_agent = True
+
+        if conn_password is not None:
+            allow_agent = False
+
+        try:
+            key_filename = None
+            if self.get_option('private_key_file'):
+                key_filename = os.path.expanduser(self.get_option('private_key_file'))
+
+            # paramiko 2.2 introduced auth_timeout parameter
+            if LooseVersion(paramiko.__version__) >= LooseVersion('2.2.0'):
+                ssh_connect_kwargs['auth_timeout'] = self.get_option('timeout')
+
+            # paramiko 1.15 introduced banner timeout parameter
+            if LooseVersion(paramiko.__version__) >= LooseVersion('1.15.0'):
+                ssh_connect_kwargs['banner_timeout'] = self.get_option('banner_timeout')
+
+            ssh.connect(
+                self.get_option('remote_addr').lower(),
+                username=self.get_option('remote_user'),
+                allow_agent=allow_agent,
+                look_for_keys=self.get_option('look_for_keys'),
+                key_filename=key_filename,
+                password=conn_password,
+                timeout=self.get_option('timeout'),
+                port=port,
+                disabled_algorithms=disabled_algorithms,
+                **ssh_connect_kwargs,
+            )
+        except paramiko.ssh_exception.BadHostKeyException as e:
+            raise AnsibleConnectionFailure(f'host key mismatch for {to_text(e.hostname)}')
+        except paramiko.ssh_exception.AuthenticationException as e:
+            msg = f'Failed to authenticate: {e}'
+            raise AnsibleAuthenticationFailure(msg)
+        except Exception as e:
+            msg = to_text(e)
+            if u'PID check failed' in msg:
+                raise AnsibleError('paramiko version issue, please upgrade paramiko on the machine running ansible')
+            elif u'Private key file is encrypted' in msg:
+                msg = f'ssh {self.get_option("remote_user")}@{self.get_options("remote_addr")}:{port} : ' + \
+                    f'{msg}\nTo connect as a different user, use -u <username>.'
+                raise AnsibleConnectionFailure(msg)
+            else:
+                raise AnsibleConnectionFailure(msg)
+        self.ssh = ssh
+        self._connected = True
+        return self
+
+    def _any_keys_added(self) -> bool:
+        for hostname, keys in self.ssh._host_keys.items():
+            for keytype, key in keys.items():
+                added_this_time = getattr(key, '_added_by_ansible_this_time', False)
+                if added_this_time:
+                    return True
+        return False
+
+    def _save_ssh_host_keys(self, filename: str) -> None:
+        """
+        not using the paramiko save_ssh_host_keys function as we want to add new SSH keys at the bottom so folks
+        don't complain about it :)
+        """
+
+        if not self._any_keys_added():
+            return
+
+        path = os.path.expanduser('~/.ssh')
+        makedirs_safe(path)
+
+        with open(filename, 'w') as f:
+            for hostname, keys in self.ssh._host_keys.items():
+                for keytype, key in keys.items():
+                    # was f.write
+                    added_this_time = getattr(key, '_added_by_ansible_this_time', False)
+                    if not added_this_time:
+                        f.write(f'{hostname} {keytype} {key.get_base64()}\n')
+
+            for hostname, keys in self.ssh._host_keys.items():
+                for keytype, key in keys.items():
+                    added_this_time = getattr(key, '_added_by_ansible_this_time', False)
+                    if added_this_time:
+                        f.write(f'{hostname} {keytype} {key.get_base64()}\n')
+
+    def _build_wsl_command(self, cmd: str) -> str:
+        wsl_distribution = self.get_option('wsl_distribution')
+        if not wsl_distribution:
+            raise AnsibleError('A wsl_distribution is required but none was supplied')
+        become = self.get_option('become')
+        become_user = self.get_option('become_user')
+        if become and become_user:
+            wsl_user = become_user
+        else:
+            wsl_user = self.get_option('wsl_user')
+        args = ['wsl.exe', '--distribution', wsl_distribution]
+        if wsl_user:
+            args.extend(['--user', wsl_user])
+        args.extend(['--'])
+        args.extend(shlex.split(cmd))
+        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]:
path: plugins/connection/wsl.py
user: login: russoz
body: ... but then again, I _think_ the recommended way for this would be:
```suggestion
    def exec_command(self, cmd: str, in_data: Optional[bytes] = None, sudoable: bool = True) -> Tuple[int, bytes, bytes]:
```
and, naturally, you would have to import those types as well.

Take it with a grain of sat - I am not positive about it.
created_at: 2025-02-23T01:02:26Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,758 @@
+# -*- coding: utf-8 -*-
+# Derived from ansible/plugins/connection/proxmox_pct_remote.py (c) 2024 Nils Stein (@mietzen) <github.nstein@mailbox.org>
+# Derived from ansible/plugins/connection/paramiko_ssh.py (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
+# Copyright (c) 2025 Rui Lopes (@rgl) <ruilopes.com>
+# Copyright (c) 2025 Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import annotations
+
+DOCUMENTATION = r"""
+author: Rui Lopes (@rgl) <ruilopes.com>
+name: wsl
+short_description: Run tasks in WSL distribution using wsl.exe CLI via SSH
+requirements:
+  - paramiko
+description:
+  - Run commands or put/fetch files to an existing WSL distribution using wsl.exe CLI via SSH.
+  - Uses the Python SSH implementation (Paramiko) to connect to the WSL host.
+version_added: "10.3.0"
+options:
+  remote_addr:
+    description:
+      - Address of the remote target.
+    default: inventory_hostname
+    type: string
+    vars:
+      - name: inventory_hostname
+      - name: ansible_host
+      - name: ansible_ssh_host
+      - name: ansible_paramiko_host
+  port:
+    description: Remote port to connect to.
+    type: int
+    default: 22
+    ini:
+      - section: defaults
+        key: remote_port
+      - section: paramiko_connection
+        key: remote_port
+    env:
+      - name: ANSIBLE_REMOTE_PORT
+      - name: ANSIBLE_REMOTE_PARAMIKO_PORT
+    vars:
+      - name: ansible_port
+      - name: ansible_ssh_port
+      - name: ansible_paramiko_port
+    keyword:
+      - name: port
+  remote_user:
+    description:
+      - User to login/authenticate as.
+      - Can be set from the CLI via the C(--user) or C(-u) options.
+    type: string
+    vars:
+      - name: ansible_user
+      - name: ansible_ssh_user
+      - name: ansible_paramiko_user
+    env:
+      - name: ANSIBLE_REMOTE_USER
+      - name: ANSIBLE_PARAMIKO_REMOTE_USER
+    ini:
+      - section: defaults
+        key: remote_user
+      - section: paramiko_connection
+        key: remote_user
+    keyword:
+      - name: remote_user
+  password:
+    description:
+      - Secret used to either login the SSH server or as a passphrase for SSH keys that require it.
+      - Can be set from the CLI via the C(--ask-pass) option.
+    type: string
+    vars:
+      - name: ansible_password
+      - name: ansible_ssh_pass
+      - name: ansible_ssh_password
+      - name: ansible_paramiko_pass
+      - name: ansible_paramiko_password
+  use_rsa_sha2_algorithms:
+    description:
+      - Whether or not to enable RSA SHA2 algorithms for pubkeys and hostkeys.
+      - On paramiko versions older than 2.9, this only affects hostkeys.
+      - For behavior matching paramiko<2.9 set this to V(false).
+    vars:
+      - name: ansible_paramiko_use_rsa_sha2_algorithms
+    ini:
+      - {key: use_rsa_sha2_algorithms, section: paramiko_connection}
+    env:
+      - {name: ANSIBLE_PARAMIKO_USE_RSA_SHA2_ALGORITHMS}
+    default: true
+    type: boolean
+  host_key_auto_add:
+    description: "Automatically add host keys to C(~/.ssh/known_hosts)."
+    env:
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_AUTO_ADD
+    ini:
+      - key: host_key_auto_add
+        section: paramiko_connection
+    type: boolean
+  look_for_keys:
+    default: True
+    description: "Set to V(false) to disable searching for private key files in C(~/.ssh/)."
+    env:
+      - name: ANSIBLE_PARAMIKO_LOOK_FOR_KEYS
+    ini:
+      - {key: look_for_keys, section: paramiko_connection}
+    type: boolean
+  proxy_command:
+    default: ""
+    description:
+      - Proxy information for running the connection via a jumphost.
+    type: string
+    env:
+      - name: ANSIBLE_PARAMIKO_PROXY_COMMAND
+    ini:
+      - {key: proxy_command, section: paramiko_connection}
+    vars:
+      - name: ansible_paramiko_proxy_command
+  record_host_keys:
+    default: True
+    description: "Save the host keys to a file."
+    env:
+      - name: ANSIBLE_PARAMIKO_RECORD_HOST_KEYS
+    ini:
+      - section: paramiko_connection
+        key: record_host_keys
+    type: boolean
+  host_key_checking:
+    description: "Set this to V(false) if you want to avoid host key checking by the underlying tools Ansible uses to connect to the host."
+    type: boolean
+    default: true
+    env:
+      - name: ANSIBLE_HOST_KEY_CHECKING
+      - name: ANSIBLE_SSH_HOST_KEY_CHECKING
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_CHECKING
+    ini:
+      - section: defaults
+        key: host_key_checking
+      - section: paramiko_connection
+        key: host_key_checking
+    vars:
+      - name: ansible_host_key_checking
+      - name: ansible_ssh_host_key_checking
+      - name: ansible_paramiko_host_key_checking
+  use_persistent_connections:
+    description: "Toggles the use of persistence for connections."
+    type: boolean
+    default: False
+    env:
+      - name: ANSIBLE_USE_PERSISTENT_CONNECTIONS
+    ini:
+      - section: defaults
+        key: use_persistent_connections
+  banner_timeout:
+    type: float
+    default: 30
+    description:
+      - Configures, in seconds, the amount of time to wait for the SSH
+        banner to be presented. This option is supported by paramiko
+        version 1.15.0 or newer.
+    ini:
+      - section: paramiko_connection
+        key: banner_timeout
+    env:
+      - name: ANSIBLE_PARAMIKO_BANNER_TIMEOUT
+  timeout:
+    type: int
+    default: 10
+    description: Number of seconds until the plugin gives up on failing to establish a TCP connection.
+    ini:
+      - section: defaults
+        key: timeout
+      - section: ssh_connection
+        key: timeout
+      - section: paramiko_connection
+        key: timeout
+    env:
+      - name: ANSIBLE_TIMEOUT
+      - name: ANSIBLE_SSH_TIMEOUT
+      - name: ANSIBLE_PARAMIKO_TIMEOUT
+    vars:
+      - name: ansible_ssh_timeout
+      - name: ansible_paramiko_timeout
+    cli:
+      - name: timeout
+  lock_file_timeout:
+    type: int
+    default: 60
+    description: Number of seconds until the plugin gives up on trying to write a lock file when writing SSH known host keys.
+    vars:
+      - name: ansible_lock_file_timeout
+    env:
+      - name: ANSIBLE_LOCK_FILE_TIMEOUT
+  private_key_file:
+    description:
+      - Path to private key file to use for authentication.
+    type: string
+    ini:
+      - section: defaults
+        key: private_key_file
+      - section: paramiko_connection
+        key: private_key_file
+    env:
+      - name: ANSIBLE_PRIVATE_KEY_FILE
+      - name: ANSIBLE_PARAMIKO_PRIVATE_KEY_FILE
+    vars:
+      - name: ansible_private_key_file
+      - name: ansible_ssh_private_key_file
+      - name: ansible_paramiko_private_key_file
+    cli:
+      - name: private_key_file
+        option: "--private-key"
+  wsl_distribution:
+    description:
+      - WSL distribution name
+    type: string
+    vars:
+      - name: wsl_distribution
+  wsl_user:
+    description:
+      - WSL distribution user
+    type: string
+    vars:
+      - name: wsl_user
+  become_user:
+    description:
+      - WSL distribution user
+    type: string
+    default: root
+    vars:
+      - name: become_user
+      - name: ansible_become_user
+  become:
+    description:
+      - whether to use the user defined by ansible_become_user.
+    type: bool
+    default: false
+    vars:
+      - name: become
+      - name: ansible_become
+"""
+
+EXAMPLES = r"""
+# ------------------------
+# Inventory: inventory.yml
+# ------------------------
+# all:
+#   children:
+#     wsl:
+#       hosts:
+#         ubuntu:
+#           ansible_host: 10.0.0.10
+#           wsl_distribution: ubuntu
+#         debian:
+#           ansible_host: 10.0.0.10
+#           wsl_distribution: debian
+#       vars:
+#         ansible_connection: community.general.wsl
+#         ansible_user: vagrant
+#         wsl_user: wsl
+# ----------------------
+# Playbook: playbook.yml
+# ----------------------
+---
+- hosts: wsl
+  tasks:
+    - name: ping
+      ansible.builtin.ping:
+    - name: id
+      args:
+        executable: /bin/bash
+      ansible.builtin.shell: |
+        exec 2>&1
+        set -x
+        echo "$0"
+        pwd
+        id
+    - name: become id
+      become: true
+      args:
+        executable: /bin/bash
+      ansible.builtin.shell: |
+        exec 2>&1
+        set -x
+        echo "$0"
+        pwd
+        id
+"""
+
+import os
+import pathlib
+import shlex
+import socket
+import tempfile
+import typing as t
+
+from ansible.errors import (
+    AnsibleAuthenticationFailure,
+    AnsibleConnectionFailure,
+    AnsibleError,
+)
+from ansible_collections.community.general.plugins.module_utils._filelock import FileLock, LockTimeout
+from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
+from ansible.module_utils.compat.paramiko import PARAMIKO_IMPORT_ERR, paramiko
+from ansible.module_utils.compat.version import LooseVersion
+from ansible.plugins.connection import ConnectionBase
+from ansible.utils.display import Display
+from ansible.utils.path import makedirs_safe
+from binascii import hexlify
+from subprocess import list2cmdline
+
+
+display = Display()
+
+
+def authenticity_msg(hostname: str, ktype: str, fingerprint: str) -> str:
+    msg = f"""
+    paramiko: The authenticity of host '{hostname}' can't be established.
+    The {ktype} key fingerprint is {fingerprint}.
+    Are you sure you want to continue connecting (yes/no)?
+    """
+    return msg
+
+
+MissingHostKeyPolicy: type = object
+if paramiko:
+    MissingHostKeyPolicy = paramiko.MissingHostKeyPolicy
+
+
+class MyAddPolicy(MissingHostKeyPolicy):
+    """
+    Based on AutoAddPolicy in paramiko so we can determine when keys are added
+
+    and also prompt for input.
+
+    Policy for automatically adding the hostname and new host key to the
+    local L{HostKeys} object, and saving it. This is used by L{SSHClient}.
+    """
+
+    def __init__(self, connection: Connection) -> None:
+        self.connection = connection
+        self._options = connection._options
+
+    def missing_host_key(self, client, hostname, key) -> None:
path: plugins/connection/wsl.py
user: login: russoz
body: please add the type hints for the parameters here, for consistency 
created_at: 2025-02-23T01:03:54Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,758 @@
+# -*- coding: utf-8 -*-
+# Derived from ansible/plugins/connection/proxmox_pct_remote.py (c) 2024 Nils Stein (@mietzen) <github.nstein@mailbox.org>
+# Derived from ansible/plugins/connection/paramiko_ssh.py (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
+# Copyright (c) 2025 Rui Lopes (@rgl) <ruilopes.com>
+# Copyright (c) 2025 Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import annotations
+
+DOCUMENTATION = r"""
+author: Rui Lopes (@rgl) <ruilopes.com>
+name: wsl
+short_description: Run tasks in WSL distribution using wsl.exe CLI via SSH
+requirements:
+  - paramiko
+description:
+  - Run commands or put/fetch files to an existing WSL distribution using wsl.exe CLI via SSH.
+  - Uses the Python SSH implementation (Paramiko) to connect to the WSL host.
+version_added: "10.3.0"
+options:
+  remote_addr:
+    description:
+      - Address of the remote target.
+    default: inventory_hostname
+    type: string
+    vars:
+      - name: inventory_hostname
+      - name: ansible_host
+      - name: ansible_ssh_host
+      - name: ansible_paramiko_host
+  port:
+    description: Remote port to connect to.
+    type: int
+    default: 22
+    ini:
+      - section: defaults
+        key: remote_port
+      - section: paramiko_connection
+        key: remote_port
+    env:
+      - name: ANSIBLE_REMOTE_PORT
+      - name: ANSIBLE_REMOTE_PARAMIKO_PORT
+    vars:
+      - name: ansible_port
+      - name: ansible_ssh_port
+      - name: ansible_paramiko_port
+    keyword:
+      - name: port
+  remote_user:
+    description:
+      - User to login/authenticate as.
+      - Can be set from the CLI via the C(--user) or C(-u) options.
+    type: string
+    vars:
+      - name: ansible_user
+      - name: ansible_ssh_user
+      - name: ansible_paramiko_user
+    env:
+      - name: ANSIBLE_REMOTE_USER
+      - name: ANSIBLE_PARAMIKO_REMOTE_USER
+    ini:
+      - section: defaults
+        key: remote_user
+      - section: paramiko_connection
+        key: remote_user
+    keyword:
+      - name: remote_user
+  password:
+    description:
+      - Secret used to either login the SSH server or as a passphrase for SSH keys that require it.
+      - Can be set from the CLI via the C(--ask-pass) option.
+    type: string
+    vars:
+      - name: ansible_password
+      - name: ansible_ssh_pass
+      - name: ansible_ssh_password
+      - name: ansible_paramiko_pass
+      - name: ansible_paramiko_password
+  use_rsa_sha2_algorithms:
+    description:
+      - Whether or not to enable RSA SHA2 algorithms for pubkeys and hostkeys.
+      - On paramiko versions older than 2.9, this only affects hostkeys.
+      - For behavior matching paramiko<2.9 set this to V(false).
+    vars:
+      - name: ansible_paramiko_use_rsa_sha2_algorithms
+    ini:
+      - {key: use_rsa_sha2_algorithms, section: paramiko_connection}
+    env:
+      - {name: ANSIBLE_PARAMIKO_USE_RSA_SHA2_ALGORITHMS}
+    default: true
+    type: boolean
+  host_key_auto_add:
+    description: "Automatically add host keys to C(~/.ssh/known_hosts)."
+    env:
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_AUTO_ADD
+    ini:
+      - key: host_key_auto_add
+        section: paramiko_connection
+    type: boolean
+  look_for_keys:
+    default: True
+    description: "Set to V(false) to disable searching for private key files in C(~/.ssh/)."
+    env:
+      - name: ANSIBLE_PARAMIKO_LOOK_FOR_KEYS
+    ini:
+      - {key: look_for_keys, section: paramiko_connection}
+    type: boolean
+  proxy_command:
+    default: ""
+    description:
+      - Proxy information for running the connection via a jumphost.
+    type: string
+    env:
+      - name: ANSIBLE_PARAMIKO_PROXY_COMMAND
+    ini:
+      - {key: proxy_command, section: paramiko_connection}
+    vars:
+      - name: ansible_paramiko_proxy_command
+  record_host_keys:
+    default: True
+    description: "Save the host keys to a file."
+    env:
+      - name: ANSIBLE_PARAMIKO_RECORD_HOST_KEYS
+    ini:
+      - section: paramiko_connection
+        key: record_host_keys
+    type: boolean
+  host_key_checking:
+    description: "Set this to V(false) if you want to avoid host key checking by the underlying tools Ansible uses to connect to the host."
+    type: boolean
+    default: true
+    env:
+      - name: ANSIBLE_HOST_KEY_CHECKING
+      - name: ANSIBLE_SSH_HOST_KEY_CHECKING
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_CHECKING
+    ini:
+      - section: defaults
+        key: host_key_checking
+      - section: paramiko_connection
+        key: host_key_checking
+    vars:
+      - name: ansible_host_key_checking
+      - name: ansible_ssh_host_key_checking
+      - name: ansible_paramiko_host_key_checking
+  use_persistent_connections:
+    description: "Toggles the use of persistence for connections."
+    type: boolean
+    default: False
+    env:
+      - name: ANSIBLE_USE_PERSISTENT_CONNECTIONS
+    ini:
+      - section: defaults
+        key: use_persistent_connections
+  banner_timeout:
+    type: float
+    default: 30
+    description:
+      - Configures, in seconds, the amount of time to wait for the SSH
+        banner to be presented. This option is supported by paramiko
+        version 1.15.0 or newer.
+    ini:
+      - section: paramiko_connection
+        key: banner_timeout
+    env:
+      - name: ANSIBLE_PARAMIKO_BANNER_TIMEOUT
+  timeout:
+    type: int
+    default: 10
+    description: Number of seconds until the plugin gives up on failing to establish a TCP connection.
+    ini:
+      - section: defaults
+        key: timeout
+      - section: ssh_connection
+        key: timeout
+      - section: paramiko_connection
+        key: timeout
+    env:
+      - name: ANSIBLE_TIMEOUT
+      - name: ANSIBLE_SSH_TIMEOUT
+      - name: ANSIBLE_PARAMIKO_TIMEOUT
+    vars:
+      - name: ansible_ssh_timeout
+      - name: ansible_paramiko_timeout
+    cli:
+      - name: timeout
+  lock_file_timeout:
+    type: int
+    default: 60
+    description: Number of seconds until the plugin gives up on trying to write a lock file when writing SSH known host keys.
+    vars:
+      - name: ansible_lock_file_timeout
+    env:
+      - name: ANSIBLE_LOCK_FILE_TIMEOUT
+  private_key_file:
+    description:
+      - Path to private key file to use for authentication.
+    type: string
+    ini:
+      - section: defaults
+        key: private_key_file
+      - section: paramiko_connection
+        key: private_key_file
+    env:
+      - name: ANSIBLE_PRIVATE_KEY_FILE
+      - name: ANSIBLE_PARAMIKO_PRIVATE_KEY_FILE
+    vars:
+      - name: ansible_private_key_file
+      - name: ansible_ssh_private_key_file
+      - name: ansible_paramiko_private_key_file
+    cli:
+      - name: private_key_file
+        option: "--private-key"
+  wsl_distribution:
+    description:
+      - WSL distribution name
+    type: string
+    vars:
+      - name: wsl_distribution
+  wsl_user:
+    description:
+      - WSL distribution user
+    type: string
+    vars:
+      - name: wsl_user
+  become_user:
+    description:
+      - WSL distribution user
+    type: string
+    default: root
+    vars:
+      - name: become_user
+      - name: ansible_become_user
+  become:
+    description:
+      - whether to use the user defined by ansible_become_user.
+    type: bool
+    default: false
+    vars:
+      - name: become
+      - name: ansible_become
+"""
+
+EXAMPLES = r"""
+# ------------------------
+# Inventory: inventory.yml
+# ------------------------
+# all:
+#   children:
+#     wsl:
+#       hosts:
+#         ubuntu:
+#           ansible_host: 10.0.0.10
+#           wsl_distribution: ubuntu
+#         debian:
+#           ansible_host: 10.0.0.10
+#           wsl_distribution: debian
+#       vars:
+#         ansible_connection: community.general.wsl
+#         ansible_user: vagrant
+#         wsl_user: wsl
+# ----------------------
+# Playbook: playbook.yml
+# ----------------------
+---
+- hosts: wsl
+  tasks:
+    - name: ping
+      ansible.builtin.ping:
+    - name: id
+      args:
+        executable: /bin/bash
+      ansible.builtin.shell: |
+        exec 2>&1
+        set -x
+        echo "$0"
+        pwd
+        id
+    - name: become id
+      become: true
+      args:
+        executable: /bin/bash
+      ansible.builtin.shell: |
+        exec 2>&1
+        set -x
+        echo "$0"
+        pwd
+        id
+"""
+
+import os
+import pathlib
+import shlex
+import socket
+import tempfile
+import typing as t
+
+from ansible.errors import (
+    AnsibleAuthenticationFailure,
+    AnsibleConnectionFailure,
+    AnsibleError,
+)
+from ansible_collections.community.general.plugins.module_utils._filelock import FileLock, LockTimeout
+from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
+from ansible.module_utils.compat.paramiko import PARAMIKO_IMPORT_ERR, paramiko
+from ansible.module_utils.compat.version import LooseVersion
+from ansible.plugins.connection import ConnectionBase
+from ansible.utils.display import Display
+from ansible.utils.path import makedirs_safe
+from binascii import hexlify
+from subprocess import list2cmdline
+
+
+display = Display()
+
+
+def authenticity_msg(hostname: str, ktype: str, fingerprint: str) -> str:
+    msg = f"""
+    paramiko: The authenticity of host '{hostname}' can't be established.
+    The {ktype} key fingerprint is {fingerprint}.
+    Are you sure you want to continue connecting (yes/no)?
+    """
+    return msg
+
+
+MissingHostKeyPolicy: type = object
+if paramiko:
+    MissingHostKeyPolicy = paramiko.MissingHostKeyPolicy
+
+
+class MyAddPolicy(MissingHostKeyPolicy):
+    """
+    Based on AutoAddPolicy in paramiko so we can determine when keys are added
+
+    and also prompt for input.
+
+    Policy for automatically adding the hostname and new host key to the
+    local L{HostKeys} object, and saving it. This is used by L{SSHClient}.
+    """
+
+    def __init__(self, connection: Connection) -> None:
+        self.connection = connection
+        self._options = connection._options
+
+    def missing_host_key(self, client, hostname, key) -> None:
+
+        if all((self.connection.get_option('host_key_checking'), not self.connection.get_option('host_key_auto_add'))):
+
+            fingerprint = hexlify(key.get_fingerprint())
+            ktype = key.get_name()
+
+            if self.connection.get_option('use_persistent_connections') or self.connection.force_persistence:
+                # don't print the prompt string since the user cannot respond
+                # to the question anyway
+                raise AnsibleError(authenticity_msg(hostname, ktype, fingerprint)[1:92])
+
+            inp = to_text(
+                display.prompt_until(authenticity_msg(hostname, ktype, fingerprint), private=False),
+                errors='surrogate_or_strict'
+            )
+
+            if inp.lower() not in ['yes', 'y', '']:
+                raise AnsibleError('host connection rejected by user')
+
+        key._added_by_ansible_this_time = True
+
+        # existing implementation below:
+        client._host_keys.add(hostname, key.get_name(), key)
+
+        # host keys are actually saved in close() function below
+        # in order to control ordering.
+
+
+class Connection(ConnectionBase):
+    """ SSH based connections (paramiko) to WSL """
+
+    transport = 'community.general.wsl'
+    _log_channel: str | None = None
+
+    def __init__(self, play_context, new_stdin, *args, **kwargs):
path: plugins/connection/wsl.py
user: login: russoz
body: please add the type hints for the parameters here, for consistency 
created_at: 2025-02-23T01:04:03Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,758 @@
+# -*- coding: utf-8 -*-
+# Derived from ansible/plugins/connection/proxmox_pct_remote.py (c) 2024 Nils Stein (@mietzen) <github.nstein@mailbox.org>
+# Derived from ansible/plugins/connection/paramiko_ssh.py (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
+# Copyright (c) 2025 Rui Lopes (@rgl) <ruilopes.com>
+# Copyright (c) 2025 Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import annotations
+
+DOCUMENTATION = r"""
+author: Rui Lopes (@rgl) <ruilopes.com>
+name: wsl
+short_description: Run tasks in WSL distribution using wsl.exe CLI via SSH
+requirements:
+  - paramiko
+description:
+  - Run commands or put/fetch files to an existing WSL distribution using wsl.exe CLI via SSH.
+  - Uses the Python SSH implementation (Paramiko) to connect to the WSL host.
+version_added: "10.3.0"
+options:
+  remote_addr:
+    description:
+      - Address of the remote target.
+    default: inventory_hostname
+    type: string
+    vars:
+      - name: inventory_hostname
+      - name: ansible_host
+      - name: ansible_ssh_host
+      - name: ansible_paramiko_host
+  port:
+    description: Remote port to connect to.
+    type: int
+    default: 22
+    ini:
+      - section: defaults
+        key: remote_port
+      - section: paramiko_connection
+        key: remote_port
+    env:
+      - name: ANSIBLE_REMOTE_PORT
+      - name: ANSIBLE_REMOTE_PARAMIKO_PORT
+    vars:
+      - name: ansible_port
+      - name: ansible_ssh_port
+      - name: ansible_paramiko_port
+    keyword:
+      - name: port
+  remote_user:
+    description:
+      - User to login/authenticate as.
+      - Can be set from the CLI via the C(--user) or C(-u) options.
+    type: string
+    vars:
+      - name: ansible_user
+      - name: ansible_ssh_user
+      - name: ansible_paramiko_user
+    env:
+      - name: ANSIBLE_REMOTE_USER
+      - name: ANSIBLE_PARAMIKO_REMOTE_USER
+    ini:
+      - section: defaults
+        key: remote_user
+      - section: paramiko_connection
+        key: remote_user
+    keyword:
+      - name: remote_user
+  password:
+    description:
+      - Secret used to either login the SSH server or as a passphrase for SSH keys that require it.
+      - Can be set from the CLI via the C(--ask-pass) option.
+    type: string
+    vars:
+      - name: ansible_password
+      - name: ansible_ssh_pass
+      - name: ansible_ssh_password
+      - name: ansible_paramiko_pass
+      - name: ansible_paramiko_password
+  use_rsa_sha2_algorithms:
+    description:
+      - Whether or not to enable RSA SHA2 algorithms for pubkeys and hostkeys.
+      - On paramiko versions older than 2.9, this only affects hostkeys.
+      - For behavior matching paramiko<2.9 set this to V(false).
+    vars:
+      - name: ansible_paramiko_use_rsa_sha2_algorithms
+    ini:
+      - {key: use_rsa_sha2_algorithms, section: paramiko_connection}
+    env:
+      - {name: ANSIBLE_PARAMIKO_USE_RSA_SHA2_ALGORITHMS}
+    default: true
+    type: boolean
+  host_key_auto_add:
+    description: "Automatically add host keys to C(~/.ssh/known_hosts)."
+    env:
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_AUTO_ADD
+    ini:
+      - key: host_key_auto_add
+        section: paramiko_connection
+    type: boolean
+  look_for_keys:
+    default: True
+    description: "Set to V(false) to disable searching for private key files in C(~/.ssh/)."
+    env:
+      - name: ANSIBLE_PARAMIKO_LOOK_FOR_KEYS
+    ini:
+      - {key: look_for_keys, section: paramiko_connection}
+    type: boolean
+  proxy_command:
+    default: ""
+    description:
+      - Proxy information for running the connection via a jumphost.
+    type: string
+    env:
+      - name: ANSIBLE_PARAMIKO_PROXY_COMMAND
+    ini:
+      - {key: proxy_command, section: paramiko_connection}
+    vars:
+      - name: ansible_paramiko_proxy_command
+  record_host_keys:
+    default: True
+    description: "Save the host keys to a file."
+    env:
+      - name: ANSIBLE_PARAMIKO_RECORD_HOST_KEYS
+    ini:
+      - section: paramiko_connection
+        key: record_host_keys
+    type: boolean
+  host_key_checking:
+    description: "Set this to V(false) if you want to avoid host key checking by the underlying tools Ansible uses to connect to the host."
+    type: boolean
+    default: true
+    env:
+      - name: ANSIBLE_HOST_KEY_CHECKING
+      - name: ANSIBLE_SSH_HOST_KEY_CHECKING
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_CHECKING
+    ini:
+      - section: defaults
+        key: host_key_checking
+      - section: paramiko_connection
+        key: host_key_checking
+    vars:
+      - name: ansible_host_key_checking
+      - name: ansible_ssh_host_key_checking
+      - name: ansible_paramiko_host_key_checking
+  use_persistent_connections:
+    description: "Toggles the use of persistence for connections."
+    type: boolean
+    default: False
+    env:
+      - name: ANSIBLE_USE_PERSISTENT_CONNECTIONS
+    ini:
+      - section: defaults
+        key: use_persistent_connections
+  banner_timeout:
+    type: float
+    default: 30
+    description:
+      - Configures, in seconds, the amount of time to wait for the SSH
+        banner to be presented. This option is supported by paramiko
+        version 1.15.0 or newer.
+    ini:
+      - section: paramiko_connection
+        key: banner_timeout
+    env:
+      - name: ANSIBLE_PARAMIKO_BANNER_TIMEOUT
+  timeout:
+    type: int
+    default: 10
+    description: Number of seconds until the plugin gives up on failing to establish a TCP connection.
+    ini:
+      - section: defaults
+        key: timeout
+      - section: ssh_connection
+        key: timeout
+      - section: paramiko_connection
+        key: timeout
+    env:
+      - name: ANSIBLE_TIMEOUT
+      - name: ANSIBLE_SSH_TIMEOUT
+      - name: ANSIBLE_PARAMIKO_TIMEOUT
+    vars:
+      - name: ansible_ssh_timeout
+      - name: ansible_paramiko_timeout
+    cli:
+      - name: timeout
+  lock_file_timeout:
+    type: int
+    default: 60
+    description: Number of seconds until the plugin gives up on trying to write a lock file when writing SSH known host keys.
+    vars:
+      - name: ansible_lock_file_timeout
+    env:
+      - name: ANSIBLE_LOCK_FILE_TIMEOUT
+  private_key_file:
+    description:
+      - Path to private key file to use for authentication.
+    type: string
+    ini:
+      - section: defaults
+        key: private_key_file
+      - section: paramiko_connection
+        key: private_key_file
+    env:
+      - name: ANSIBLE_PRIVATE_KEY_FILE
+      - name: ANSIBLE_PARAMIKO_PRIVATE_KEY_FILE
+    vars:
+      - name: ansible_private_key_file
+      - name: ansible_ssh_private_key_file
+      - name: ansible_paramiko_private_key_file
+    cli:
+      - name: private_key_file
+        option: "--private-key"
+  wsl_distribution:
+    description:
+      - WSL distribution name
+    type: string
+    vars:
+      - name: wsl_distribution
+  wsl_user:
+    description:
+      - WSL distribution user
+    type: string
+    vars:
+      - name: wsl_user
+  become_user:
+    description:
+      - WSL distribution user
+    type: string
+    default: root
+    vars:
+      - name: become_user
+      - name: ansible_become_user
+  become:
+    description:
+      - whether to use the user defined by ansible_become_user.
+    type: bool
+    default: false
+    vars:
+      - name: become
+      - name: ansible_become
+"""
+
+EXAMPLES = r"""
+# ------------------------
+# Inventory: inventory.yml
+# ------------------------
+# all:
+#   children:
+#     wsl:
+#       hosts:
+#         ubuntu:
+#           ansible_host: 10.0.0.10
+#           wsl_distribution: ubuntu
+#         debian:
+#           ansible_host: 10.0.0.10
+#           wsl_distribution: debian
+#       vars:
+#         ansible_connection: community.general.wsl
+#         ansible_user: vagrant
+#         wsl_user: wsl
+# ----------------------
+# Playbook: playbook.yml
+# ----------------------
+---
+- hosts: wsl
+  tasks:
+    - name: ping
+      ansible.builtin.ping:
+    - name: id
+      args:
+        executable: /bin/bash
+      ansible.builtin.shell: |
+        exec 2>&1
+        set -x
+        echo "$0"
+        pwd
+        id
+    - name: become id
+      become: true
+      args:
+        executable: /bin/bash
+      ansible.builtin.shell: |
+        exec 2>&1
+        set -x
+        echo "$0"
+        pwd
+        id
+"""
+
+import os
+import pathlib
+import shlex
+import socket
+import tempfile
+import typing as t
+
+from ansible.errors import (
+    AnsibleAuthenticationFailure,
+    AnsibleConnectionFailure,
+    AnsibleError,
+)
+from ansible_collections.community.general.plugins.module_utils._filelock import FileLock, LockTimeout
+from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
+from ansible.module_utils.compat.paramiko import PARAMIKO_IMPORT_ERR, paramiko
+from ansible.module_utils.compat.version import LooseVersion
+from ansible.plugins.connection import ConnectionBase
+from ansible.utils.display import Display
+from ansible.utils.path import makedirs_safe
+from binascii import hexlify
+from subprocess import list2cmdline
+
+
+display = Display()
+
+
+def authenticity_msg(hostname: str, ktype: str, fingerprint: str) -> str:
+    msg = f"""
+    paramiko: The authenticity of host '{hostname}' can't be established.
+    The {ktype} key fingerprint is {fingerprint}.
+    Are you sure you want to continue connecting (yes/no)?
+    """
+    return msg
+
+
+MissingHostKeyPolicy: type = object
+if paramiko:
+    MissingHostKeyPolicy = paramiko.MissingHostKeyPolicy
+
+
+class MyAddPolicy(MissingHostKeyPolicy):
+    """
+    Based on AutoAddPolicy in paramiko so we can determine when keys are added
+
+    and also prompt for input.
+
+    Policy for automatically adding the hostname and new host key to the
+    local L{HostKeys} object, and saving it. This is used by L{SSHClient}.
+    """
+
+    def __init__(self, connection: Connection) -> None:
+        self.connection = connection
+        self._options = connection._options
+
+    def missing_host_key(self, client, hostname, key) -> None:
+
+        if all((self.connection.get_option('host_key_checking'), not self.connection.get_option('host_key_auto_add'))):
+
+            fingerprint = hexlify(key.get_fingerprint())
+            ktype = key.get_name()
+
+            if self.connection.get_option('use_persistent_connections') or self.connection.force_persistence:
+                # don't print the prompt string since the user cannot respond
+                # to the question anyway
+                raise AnsibleError(authenticity_msg(hostname, ktype, fingerprint)[1:92])
+
+            inp = to_text(
+                display.prompt_until(authenticity_msg(hostname, ktype, fingerprint), private=False),
+                errors='surrogate_or_strict'
+            )
+
+            if inp.lower() not in ['yes', 'y', '']:
+                raise AnsibleError('host connection rejected by user')
+
+        key._added_by_ansible_this_time = True
+
+        # existing implementation below:
+        client._host_keys.add(hostname, key.get_name(), key)
+
+        # host keys are actually saved in close() function below
+        # in order to control ordering.
+
+
+class Connection(ConnectionBase):
+    """ SSH based connections (paramiko) to WSL """
+
+    transport = 'community.general.wsl'
+    _log_channel: str | None = None
+
+    def __init__(self, play_context, new_stdin, *args, **kwargs):
+        super(Connection, self).__init__(play_context, new_stdin, *args, **kwargs)
+
+    def _set_log_channel(self, name: str) -> None:
+        """ Mimic paramiko.SSHClient.set_log_channel """
+        self._log_channel = name
+
+    def _parse_proxy_command(self, port: int = 22) -> dict[str, t.Any]:
+        proxy_command = self.get_option('proxy_command') or None
+
+        sock_kwarg = {}
+        if proxy_command:
+            replacers = {
+                '%h': self.get_option('remote_addr'),
+                '%p': port,
+                '%r': self.get_option('remote_user')
+            }
+            for find, replace in replacers.items():
+                proxy_command = proxy_command.replace(find, str(replace))
path: plugins/connection/wsl.py
user: login: russoz
body: If I got it right, `port` is the only replacer that will not be a string, so I would suggest:
```suggestion
                '%p': str(port),
                '%r': self.get_option('remote_user')
            }
            for find, replace in replacers.items():
                proxy_command = proxy_command.replace(find, replace)
```
created_at: 2025-02-23T01:05:23Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,758 @@
+# -*- coding: utf-8 -*-
+# Derived from ansible/plugins/connection/proxmox_pct_remote.py (c) 2024 Nils Stein (@mietzen) <github.nstein@mailbox.org>
+# Derived from ansible/plugins/connection/paramiko_ssh.py (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
+# Copyright (c) 2025 Rui Lopes (@rgl) <ruilopes.com>
+# Copyright (c) 2025 Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import annotations
+
+DOCUMENTATION = r"""
+author: Rui Lopes (@rgl) <ruilopes.com>
+name: wsl
+short_description: Run tasks in WSL distribution using wsl.exe CLI via SSH
+requirements:
+  - paramiko
+description:
+  - Run commands or put/fetch files to an existing WSL distribution using wsl.exe CLI via SSH.
+  - Uses the Python SSH implementation (Paramiko) to connect to the WSL host.
+version_added: "10.3.0"
+options:
+  remote_addr:
+    description:
+      - Address of the remote target.
+    default: inventory_hostname
+    type: string
+    vars:
+      - name: inventory_hostname
+      - name: ansible_host
+      - name: ansible_ssh_host
+      - name: ansible_paramiko_host
+  port:
+    description: Remote port to connect to.
+    type: int
+    default: 22
+    ini:
+      - section: defaults
+        key: remote_port
+      - section: paramiko_connection
+        key: remote_port
+    env:
+      - name: ANSIBLE_REMOTE_PORT
+      - name: ANSIBLE_REMOTE_PARAMIKO_PORT
+    vars:
+      - name: ansible_port
+      - name: ansible_ssh_port
+      - name: ansible_paramiko_port
+    keyword:
+      - name: port
+  remote_user:
+    description:
+      - User to login/authenticate as.
+      - Can be set from the CLI via the C(--user) or C(-u) options.
+    type: string
+    vars:
+      - name: ansible_user
+      - name: ansible_ssh_user
+      - name: ansible_paramiko_user
+    env:
+      - name: ANSIBLE_REMOTE_USER
+      - name: ANSIBLE_PARAMIKO_REMOTE_USER
+    ini:
+      - section: defaults
+        key: remote_user
+      - section: paramiko_connection
+        key: remote_user
+    keyword:
+      - name: remote_user
+  password:
+    description:
+      - Secret used to either login the SSH server or as a passphrase for SSH keys that require it.
+      - Can be set from the CLI via the C(--ask-pass) option.
+    type: string
+    vars:
+      - name: ansible_password
+      - name: ansible_ssh_pass
+      - name: ansible_ssh_password
+      - name: ansible_paramiko_pass
+      - name: ansible_paramiko_password
+  use_rsa_sha2_algorithms:
+    description:
+      - Whether or not to enable RSA SHA2 algorithms for pubkeys and hostkeys.
+      - On paramiko versions older than 2.9, this only affects hostkeys.
+      - For behavior matching paramiko<2.9 set this to V(false).
+    vars:
+      - name: ansible_paramiko_use_rsa_sha2_algorithms
+    ini:
+      - {key: use_rsa_sha2_algorithms, section: paramiko_connection}
+    env:
+      - {name: ANSIBLE_PARAMIKO_USE_RSA_SHA2_ALGORITHMS}
+    default: true
+    type: boolean
+  host_key_auto_add:
+    description: "Automatically add host keys to C(~/.ssh/known_hosts)."
+    env:
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_AUTO_ADD
+    ini:
+      - key: host_key_auto_add
+        section: paramiko_connection
+    type: boolean
+  look_for_keys:
+    default: True
+    description: "Set to V(false) to disable searching for private key files in C(~/.ssh/)."
+    env:
+      - name: ANSIBLE_PARAMIKO_LOOK_FOR_KEYS
+    ini:
+      - {key: look_for_keys, section: paramiko_connection}
+    type: boolean
+  proxy_command:
+    default: ""
+    description:
+      - Proxy information for running the connection via a jumphost.
+    type: string
+    env:
+      - name: ANSIBLE_PARAMIKO_PROXY_COMMAND
+    ini:
+      - {key: proxy_command, section: paramiko_connection}
+    vars:
+      - name: ansible_paramiko_proxy_command
+  record_host_keys:
+    default: True
+    description: "Save the host keys to a file."
+    env:
+      - name: ANSIBLE_PARAMIKO_RECORD_HOST_KEYS
+    ini:
+      - section: paramiko_connection
+        key: record_host_keys
+    type: boolean
+  host_key_checking:
+    description: "Set this to V(false) if you want to avoid host key checking by the underlying tools Ansible uses to connect to the host."
+    type: boolean
+    default: true
+    env:
+      - name: ANSIBLE_HOST_KEY_CHECKING
+      - name: ANSIBLE_SSH_HOST_KEY_CHECKING
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_CHECKING
+    ini:
+      - section: defaults
+        key: host_key_checking
+      - section: paramiko_connection
+        key: host_key_checking
+    vars:
+      - name: ansible_host_key_checking
+      - name: ansible_ssh_host_key_checking
+      - name: ansible_paramiko_host_key_checking
+  use_persistent_connections:
+    description: "Toggles the use of persistence for connections."
+    type: boolean
+    default: False
+    env:
+      - name: ANSIBLE_USE_PERSISTENT_CONNECTIONS
+    ini:
+      - section: defaults
+        key: use_persistent_connections
+  banner_timeout:
+    type: float
+    default: 30
+    description:
+      - Configures, in seconds, the amount of time to wait for the SSH
+        banner to be presented. This option is supported by paramiko
+        version 1.15.0 or newer.
+    ini:
+      - section: paramiko_connection
+        key: banner_timeout
+    env:
+      - name: ANSIBLE_PARAMIKO_BANNER_TIMEOUT
+  timeout:
+    type: int
+    default: 10
+    description: Number of seconds until the plugin gives up on failing to establish a TCP connection.
+    ini:
+      - section: defaults
+        key: timeout
+      - section: ssh_connection
+        key: timeout
+      - section: paramiko_connection
+        key: timeout
+    env:
+      - name: ANSIBLE_TIMEOUT
+      - name: ANSIBLE_SSH_TIMEOUT
+      - name: ANSIBLE_PARAMIKO_TIMEOUT
+    vars:
+      - name: ansible_ssh_timeout
+      - name: ansible_paramiko_timeout
+    cli:
+      - name: timeout
+  lock_file_timeout:
+    type: int
+    default: 60
+    description: Number of seconds until the plugin gives up on trying to write a lock file when writing SSH known host keys.
+    vars:
+      - name: ansible_lock_file_timeout
+    env:
+      - name: ANSIBLE_LOCK_FILE_TIMEOUT
+  private_key_file:
+    description:
+      - Path to private key file to use for authentication.
+    type: string
+    ini:
+      - section: defaults
+        key: private_key_file
+      - section: paramiko_connection
+        key: private_key_file
+    env:
+      - name: ANSIBLE_PRIVATE_KEY_FILE
+      - name: ANSIBLE_PARAMIKO_PRIVATE_KEY_FILE
+    vars:
+      - name: ansible_private_key_file
+      - name: ansible_ssh_private_key_file
+      - name: ansible_paramiko_private_key_file
+    cli:
+      - name: private_key_file
+        option: "--private-key"
+  wsl_distribution:
+    description:
+      - WSL distribution name
+    type: string
+    vars:
+      - name: wsl_distribution
+  wsl_user:
+    description:
+      - WSL distribution user
+    type: string
+    vars:
+      - name: wsl_user
+  become_user:
+    description:
+      - WSL distribution user
+    type: string
+    default: root
+    vars:
+      - name: become_user
+      - name: ansible_become_user
+  become:
+    description:
+      - whether to use the user defined by ansible_become_user.
+    type: bool
+    default: false
+    vars:
+      - name: become
+      - name: ansible_become
+"""
+
+EXAMPLES = r"""
+# ------------------------
+# Inventory: inventory.yml
+# ------------------------
+# all:
+#   children:
+#     wsl:
+#       hosts:
+#         ubuntu:
+#           ansible_host: 10.0.0.10
+#           wsl_distribution: ubuntu
+#         debian:
+#           ansible_host: 10.0.0.10
+#           wsl_distribution: debian
+#       vars:
+#         ansible_connection: community.general.wsl
+#         ansible_user: vagrant
+#         wsl_user: wsl
+# ----------------------
+# Playbook: playbook.yml
+# ----------------------
+---
+- hosts: wsl
+  tasks:
+    - name: ping
+      ansible.builtin.ping:
+    - name: id
+      args:
+        executable: /bin/bash
+      ansible.builtin.shell: |
+        exec 2>&1
+        set -x
+        echo "$0"
+        pwd
+        id
+    - name: become id
+      become: true
+      args:
+        executable: /bin/bash
+      ansible.builtin.shell: |
+        exec 2>&1
+        set -x
+        echo "$0"
+        pwd
+        id
+"""
+
+import os
+import pathlib
+import shlex
+import socket
+import tempfile
+import typing as t
+
+from ansible.errors import (
+    AnsibleAuthenticationFailure,
+    AnsibleConnectionFailure,
+    AnsibleError,
+)
+from ansible_collections.community.general.plugins.module_utils._filelock import FileLock, LockTimeout
+from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
+from ansible.module_utils.compat.paramiko import PARAMIKO_IMPORT_ERR, paramiko
+from ansible.module_utils.compat.version import LooseVersion
+from ansible.plugins.connection import ConnectionBase
+from ansible.utils.display import Display
+from ansible.utils.path import makedirs_safe
+from binascii import hexlify
+from subprocess import list2cmdline
+
+
+display = Display()
+
+
+def authenticity_msg(hostname: str, ktype: str, fingerprint: str) -> str:
+    msg = f"""
+    paramiko: The authenticity of host '{hostname}' can't be established.
+    The {ktype} key fingerprint is {fingerprint}.
+    Are you sure you want to continue connecting (yes/no)?
+    """
+    return msg
+
+
+MissingHostKeyPolicy: type = object
+if paramiko:
+    MissingHostKeyPolicy = paramiko.MissingHostKeyPolicy
+
+
+class MyAddPolicy(MissingHostKeyPolicy):
+    """
+    Based on AutoAddPolicy in paramiko so we can determine when keys are added
+
+    and also prompt for input.
+
+    Policy for automatically adding the hostname and new host key to the
+    local L{HostKeys} object, and saving it. This is used by L{SSHClient}.
+    """
+
+    def __init__(self, connection: Connection) -> None:
+        self.connection = connection
+        self._options = connection._options
+
+    def missing_host_key(self, client, hostname, key) -> None:
+
+        if all((self.connection.get_option('host_key_checking'), not self.connection.get_option('host_key_auto_add'))):
+
+            fingerprint = hexlify(key.get_fingerprint())
+            ktype = key.get_name()
+
+            if self.connection.get_option('use_persistent_connections') or self.connection.force_persistence:
+                # don't print the prompt string since the user cannot respond
+                # to the question anyway
+                raise AnsibleError(authenticity_msg(hostname, ktype, fingerprint)[1:92])
+
+            inp = to_text(
+                display.prompt_until(authenticity_msg(hostname, ktype, fingerprint), private=False),
+                errors='surrogate_or_strict'
+            )
+
+            if inp.lower() not in ['yes', 'y', '']:
+                raise AnsibleError('host connection rejected by user')
+
+        key._added_by_ansible_this_time = True
+
+        # existing implementation below:
+        client._host_keys.add(hostname, key.get_name(), key)
+
+        # host keys are actually saved in close() function below
+        # in order to control ordering.
+
+
+class Connection(ConnectionBase):
+    """ SSH based connections (paramiko) to WSL """
+
+    transport = 'community.general.wsl'
+    _log_channel: str | None = None
path: plugins/connection/wsl.py
user: login: russoz
body: again, I _think_ this should be:
```suggestion
    _log_channel: Optional[str] = None
```
created_at: 2025-02-23T01:06:56Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,758 @@
+# -*- coding: utf-8 -*-
+# Derived from ansible/plugins/connection/proxmox_pct_remote.py (c) 2024 Nils Stein (@mietzen) <github.nstein@mailbox.org>
+# Derived from ansible/plugins/connection/paramiko_ssh.py (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
+# Copyright (c) 2025 Rui Lopes (@rgl) <ruilopes.com>
+# Copyright (c) 2025 Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import annotations
+
+DOCUMENTATION = r"""
+author: Rui Lopes (@rgl) <ruilopes.com>
+name: wsl
+short_description: Run tasks in WSL distribution using wsl.exe CLI via SSH
+requirements:
+  - paramiko
+description:
+  - Run commands or put/fetch files to an existing WSL distribution using wsl.exe CLI via SSH.
+  - Uses the Python SSH implementation (Paramiko) to connect to the WSL host.
+version_added: "10.3.0"
+options:
+  remote_addr:
+    description:
+      - Address of the remote target.
+    default: inventory_hostname
+    type: string
+    vars:
+      - name: inventory_hostname
+      - name: ansible_host
+      - name: ansible_ssh_host
+      - name: ansible_paramiko_host
+  port:
+    description: Remote port to connect to.
+    type: int
+    default: 22
+    ini:
+      - section: defaults
+        key: remote_port
+      - section: paramiko_connection
+        key: remote_port
+    env:
+      - name: ANSIBLE_REMOTE_PORT
+      - name: ANSIBLE_REMOTE_PARAMIKO_PORT
+    vars:
+      - name: ansible_port
+      - name: ansible_ssh_port
+      - name: ansible_paramiko_port
+    keyword:
+      - name: port
+  remote_user:
+    description:
+      - User to login/authenticate as.
+      - Can be set from the CLI via the C(--user) or C(-u) options.
+    type: string
+    vars:
+      - name: ansible_user
+      - name: ansible_ssh_user
+      - name: ansible_paramiko_user
+    env:
+      - name: ANSIBLE_REMOTE_USER
+      - name: ANSIBLE_PARAMIKO_REMOTE_USER
+    ini:
+      - section: defaults
+        key: remote_user
+      - section: paramiko_connection
+        key: remote_user
+    keyword:
+      - name: remote_user
+  password:
+    description:
+      - Secret used to either login the SSH server or as a passphrase for SSH keys that require it.
+      - Can be set from the CLI via the C(--ask-pass) option.
+    type: string
+    vars:
+      - name: ansible_password
+      - name: ansible_ssh_pass
+      - name: ansible_ssh_password
+      - name: ansible_paramiko_pass
+      - name: ansible_paramiko_password
+  use_rsa_sha2_algorithms:
+    description:
+      - Whether or not to enable RSA SHA2 algorithms for pubkeys and hostkeys.
+      - On paramiko versions older than 2.9, this only affects hostkeys.
+      - For behavior matching paramiko<2.9 set this to V(false).
+    vars:
+      - name: ansible_paramiko_use_rsa_sha2_algorithms
+    ini:
+      - {key: use_rsa_sha2_algorithms, section: paramiko_connection}
+    env:
+      - {name: ANSIBLE_PARAMIKO_USE_RSA_SHA2_ALGORITHMS}
+    default: true
+    type: boolean
+  host_key_auto_add:
+    description: "Automatically add host keys to C(~/.ssh/known_hosts)."
+    env:
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_AUTO_ADD
+    ini:
+      - key: host_key_auto_add
+        section: paramiko_connection
+    type: boolean
+  look_for_keys:
+    default: True
+    description: "Set to V(false) to disable searching for private key files in C(~/.ssh/)."
+    env:
+      - name: ANSIBLE_PARAMIKO_LOOK_FOR_KEYS
+    ini:
+      - {key: look_for_keys, section: paramiko_connection}
+    type: boolean
+  proxy_command:
+    default: ""
+    description:
+      - Proxy information for running the connection via a jumphost.
+    type: string
+    env:
+      - name: ANSIBLE_PARAMIKO_PROXY_COMMAND
+    ini:
+      - {key: proxy_command, section: paramiko_connection}
+    vars:
+      - name: ansible_paramiko_proxy_command
+  record_host_keys:
+    default: True
+    description: "Save the host keys to a file."
+    env:
+      - name: ANSIBLE_PARAMIKO_RECORD_HOST_KEYS
+    ini:
+      - section: paramiko_connection
+        key: record_host_keys
+    type: boolean
+  host_key_checking:
+    description: "Set this to V(false) if you want to avoid host key checking by the underlying tools Ansible uses to connect to the host."
+    type: boolean
+    default: true
+    env:
+      - name: ANSIBLE_HOST_KEY_CHECKING
+      - name: ANSIBLE_SSH_HOST_KEY_CHECKING
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_CHECKING
+    ini:
+      - section: defaults
+        key: host_key_checking
+      - section: paramiko_connection
+        key: host_key_checking
+    vars:
+      - name: ansible_host_key_checking
+      - name: ansible_ssh_host_key_checking
+      - name: ansible_paramiko_host_key_checking
+  use_persistent_connections:
+    description: "Toggles the use of persistence for connections."
+    type: boolean
+    default: False
+    env:
+      - name: ANSIBLE_USE_PERSISTENT_CONNECTIONS
+    ini:
+      - section: defaults
+        key: use_persistent_connections
+  banner_timeout:
+    type: float
+    default: 30
+    description:
+      - Configures, in seconds, the amount of time to wait for the SSH
+        banner to be presented. This option is supported by paramiko
+        version 1.15.0 or newer.
+    ini:
+      - section: paramiko_connection
+        key: banner_timeout
+    env:
+      - name: ANSIBLE_PARAMIKO_BANNER_TIMEOUT
+  timeout:
+    type: int
+    default: 10
+    description: Number of seconds until the plugin gives up on failing to establish a TCP connection.
+    ini:
+      - section: defaults
+        key: timeout
+      - section: ssh_connection
+        key: timeout
+      - section: paramiko_connection
+        key: timeout
+    env:
+      - name: ANSIBLE_TIMEOUT
+      - name: ANSIBLE_SSH_TIMEOUT
+      - name: ANSIBLE_PARAMIKO_TIMEOUT
+    vars:
+      - name: ansible_ssh_timeout
+      - name: ansible_paramiko_timeout
+    cli:
+      - name: timeout
+  lock_file_timeout:
+    type: int
+    default: 60
+    description: Number of seconds until the plugin gives up on trying to write a lock file when writing SSH known host keys.
+    vars:
+      - name: ansible_lock_file_timeout
+    env:
+      - name: ANSIBLE_LOCK_FILE_TIMEOUT
+  private_key_file:
+    description:
+      - Path to private key file to use for authentication.
+    type: string
+    ini:
+      - section: defaults
+        key: private_key_file
+      - section: paramiko_connection
+        key: private_key_file
+    env:
+      - name: ANSIBLE_PRIVATE_KEY_FILE
+      - name: ANSIBLE_PARAMIKO_PRIVATE_KEY_FILE
+    vars:
+      - name: ansible_private_key_file
+      - name: ansible_ssh_private_key_file
+      - name: ansible_paramiko_private_key_file
+    cli:
+      - name: private_key_file
+        option: "--private-key"
+  wsl_distribution:
+    description:
+      - WSL distribution name
+    type: string
+    vars:
+      - name: wsl_distribution
+  wsl_user:
+    description:
+      - WSL distribution user
+    type: string
+    vars:
+      - name: wsl_user
+  become_user:
+    description:
+      - WSL distribution user
+    type: string
+    default: root
+    vars:
+      - name: become_user
+      - name: ansible_become_user
+  become:
+    description:
+      - whether to use the user defined by ansible_become_user.
+    type: bool
+    default: false
+    vars:
+      - name: become
+      - name: ansible_become
+"""
+
+EXAMPLES = r"""
+# ------------------------
+# Inventory: inventory.yml
+# ------------------------
+# all:
+#   children:
+#     wsl:
+#       hosts:
+#         ubuntu:
+#           ansible_host: 10.0.0.10
+#           wsl_distribution: ubuntu
+#         debian:
+#           ansible_host: 10.0.0.10
+#           wsl_distribution: debian
+#       vars:
+#         ansible_connection: community.general.wsl
+#         ansible_user: vagrant
+#         wsl_user: wsl
+# ----------------------
+# Playbook: playbook.yml
+# ----------------------
+---
+- hosts: wsl
+  tasks:
+    - name: ping
+      ansible.builtin.ping:
+    - name: id
+      args:
+        executable: /bin/bash
+      ansible.builtin.shell: |
+        exec 2>&1
+        set -x
+        echo "$0"
+        pwd
+        id
+    - name: become id
+      become: true
+      args:
+        executable: /bin/bash
+      ansible.builtin.shell: |
+        exec 2>&1
+        set -x
+        echo "$0"
+        pwd
+        id
+"""
+
+import os
+import pathlib
+import shlex
+import socket
+import tempfile
+import typing as t
+
+from ansible.errors import (
+    AnsibleAuthenticationFailure,
+    AnsibleConnectionFailure,
+    AnsibleError,
+)
+from ansible_collections.community.general.plugins.module_utils._filelock import FileLock, LockTimeout
+from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
+from ansible.module_utils.compat.paramiko import PARAMIKO_IMPORT_ERR, paramiko
+from ansible.module_utils.compat.version import LooseVersion
+from ansible.plugins.connection import ConnectionBase
+from ansible.utils.display import Display
+from ansible.utils.path import makedirs_safe
+from binascii import hexlify
+from subprocess import list2cmdline
+
+
+display = Display()
+
+
+def authenticity_msg(hostname: str, ktype: str, fingerprint: str) -> str:
+    msg = f"""
+    paramiko: The authenticity of host '{hostname}' can't be established.
+    The {ktype} key fingerprint is {fingerprint}.
+    Are you sure you want to continue connecting (yes/no)?
+    """
+    return msg
+
+
+MissingHostKeyPolicy: type = object
+if paramiko:
+    MissingHostKeyPolicy = paramiko.MissingHostKeyPolicy
+
+
+class MyAddPolicy(MissingHostKeyPolicy):
+    """
+    Based on AutoAddPolicy in paramiko so we can determine when keys are added
+
+    and also prompt for input.
+
+    Policy for automatically adding the hostname and new host key to the
+    local L{HostKeys} object, and saving it. This is used by L{SSHClient}.
+    """
+
+    def __init__(self, connection: Connection) -> None:
+        self.connection = connection
+        self._options = connection._options
+
+    def missing_host_key(self, client, hostname, key) -> None:
+
+        if all((self.connection.get_option('host_key_checking'), not self.connection.get_option('host_key_auto_add'))):
+
+            fingerprint = hexlify(key.get_fingerprint())
+            ktype = key.get_name()
+
+            if self.connection.get_option('use_persistent_connections') or self.connection.force_persistence:
+                # don't print the prompt string since the user cannot respond
+                # to the question anyway
+                raise AnsibleError(authenticity_msg(hostname, ktype, fingerprint)[1:92])
+
+            inp = to_text(
+                display.prompt_until(authenticity_msg(hostname, ktype, fingerprint), private=False),
+                errors='surrogate_or_strict'
+            )
+
+            if inp.lower() not in ['yes', 'y', '']:
+                raise AnsibleError('host connection rejected by user')
+
+        key._added_by_ansible_this_time = True
+
+        # existing implementation below:
+        client._host_keys.add(hostname, key.get_name(), key)
+
+        # host keys are actually saved in close() function below
+        # in order to control ordering.
+
+
+class Connection(ConnectionBase):
+    """ SSH based connections (paramiko) to WSL """
+
+    transport = 'community.general.wsl'
+    _log_channel: str | None = None
+
+    def __init__(self, play_context, new_stdin, *args, **kwargs):
+        super(Connection, self).__init__(play_context, new_stdin, *args, **kwargs)
+
+    def _set_log_channel(self, name: str) -> None:
+        """ Mimic paramiko.SSHClient.set_log_channel """
+        self._log_channel = name
+
+    def _parse_proxy_command(self, port: int = 22) -> dict[str, t.Any]:
+        proxy_command = self.get_option('proxy_command') or None
+
+        sock_kwarg = {}
+        if proxy_command:
+            replacers = {
+                '%h': self.get_option('remote_addr'),
+                '%p': port,
+                '%r': self.get_option('remote_user')
+            }
+            for find, replace in replacers.items():
+                proxy_command = proxy_command.replace(find, str(replace))
+            try:
+                sock_kwarg = {'sock': paramiko.ProxyCommand(proxy_command)}
+                display.vvv(f'CONFIGURE PROXY COMMAND FOR CONNECTION: {proxy_command}', host=self.get_option('remote_addr'))
+            except AttributeError:
+                display.warning('Paramiko ProxyCommand support unavailable. '
+                                'Please upgrade to Paramiko 1.9.0 or newer. '
+                                'Not using configured ProxyCommand')
+
+        return sock_kwarg
+
+    def _connect(self) -> Connection:
+        """ activates the connection object """
+
+        if paramiko is None:
+            raise AnsibleError(f'paramiko is not installed: {to_native(PARAMIKO_IMPORT_ERR)}')
+
+        port = self.get_option('port')
+        display.vvv(f'ESTABLISH PARAMIKO SSH CONNECTION FOR USER: {self.get_option("remote_user")} on PORT {to_text(port)} TO {self.get_option("remote_addr")}',
+                    host=self.get_option('remote_addr'))
+
+        ssh = paramiko.SSHClient()
+
+        # Set pubkey and hostkey algorithms to disable, the only manipulation allowed currently
+        # is keeping or omitting rsa-sha2 algorithms
+        # default_keys: t.Tuple[str] = ()
+        paramiko_preferred_pubkeys = getattr(paramiko.Transport, '_preferred_pubkeys', ())
+        paramiko_preferred_hostkeys = getattr(paramiko.Transport, '_preferred_keys', ())
+        use_rsa_sha2_algorithms = self.get_option('use_rsa_sha2_algorithms')
+        disabled_algorithms: t.Dict[str, t.Iterable[str]] = {}
+        if not use_rsa_sha2_algorithms:
+            if paramiko_preferred_pubkeys:
+                disabled_algorithms['pubkeys'] = tuple(a for a in paramiko_preferred_pubkeys if 'rsa-sha2' in a)
+            if paramiko_preferred_hostkeys:
+                disabled_algorithms['keys'] = tuple(a for a in paramiko_preferred_hostkeys if 'rsa-sha2' in a)
+
+        # override paramiko's default logger name
+        if self._log_channel is not None:
+            ssh.set_log_channel(self._log_channel)
+
+        self.keyfile = os.path.expanduser('~/.ssh/known_hosts')
+
+        if self.get_option('host_key_checking'):
+            for ssh_known_hosts in ('/etc/ssh/ssh_known_hosts', '/etc/openssh/ssh_known_hosts'):
+                try:
+                    ssh.load_system_host_keys(ssh_known_hosts)
+                    break
+                except IOError:
+                    pass  # file was not found, but not required to function
path: plugins/connection/wsl.py
user: login: russoz
body: I have not investigated that in paramiko, but I believe that there might be reasons for an `IOError` other than the file not found. The usual exception for that case, in Python, is `FileNotFoundError` but you should verify if paramiko makes the distinction.
created_at: 2025-02-23T01:10:32Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,758 @@
+# -*- coding: utf-8 -*-
+# Derived from ansible/plugins/connection/proxmox_pct_remote.py (c) 2024 Nils Stein (@mietzen) <github.nstein@mailbox.org>
+# Derived from ansible/plugins/connection/paramiko_ssh.py (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
+# Copyright (c) 2025 Rui Lopes (@rgl) <ruilopes.com>
+# Copyright (c) 2025 Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import annotations
+
+DOCUMENTATION = r"""
+author: Rui Lopes (@rgl) <ruilopes.com>
+name: wsl
+short_description: Run tasks in WSL distribution using wsl.exe CLI via SSH
+requirements:
+  - paramiko
+description:
+  - Run commands or put/fetch files to an existing WSL distribution using wsl.exe CLI via SSH.
+  - Uses the Python SSH implementation (Paramiko) to connect to the WSL host.
+version_added: "10.3.0"
+options:
+  remote_addr:
+    description:
+      - Address of the remote target.
+    default: inventory_hostname
+    type: string
+    vars:
+      - name: inventory_hostname
+      - name: ansible_host
+      - name: ansible_ssh_host
+      - name: ansible_paramiko_host
+  port:
+    description: Remote port to connect to.
+    type: int
+    default: 22
+    ini:
+      - section: defaults
+        key: remote_port
+      - section: paramiko_connection
+        key: remote_port
+    env:
+      - name: ANSIBLE_REMOTE_PORT
+      - name: ANSIBLE_REMOTE_PARAMIKO_PORT
+    vars:
+      - name: ansible_port
+      - name: ansible_ssh_port
+      - name: ansible_paramiko_port
+    keyword:
+      - name: port
+  remote_user:
+    description:
+      - User to login/authenticate as.
+      - Can be set from the CLI via the C(--user) or C(-u) options.
+    type: string
+    vars:
+      - name: ansible_user
+      - name: ansible_ssh_user
+      - name: ansible_paramiko_user
+    env:
+      - name: ANSIBLE_REMOTE_USER
+      - name: ANSIBLE_PARAMIKO_REMOTE_USER
+    ini:
+      - section: defaults
+        key: remote_user
+      - section: paramiko_connection
+        key: remote_user
+    keyword:
+      - name: remote_user
+  password:
+    description:
+      - Secret used to either login the SSH server or as a passphrase for SSH keys that require it.
+      - Can be set from the CLI via the C(--ask-pass) option.
+    type: string
+    vars:
+      - name: ansible_password
+      - name: ansible_ssh_pass
+      - name: ansible_ssh_password
+      - name: ansible_paramiko_pass
+      - name: ansible_paramiko_password
+  use_rsa_sha2_algorithms:
+    description:
+      - Whether or not to enable RSA SHA2 algorithms for pubkeys and hostkeys.
+      - On paramiko versions older than 2.9, this only affects hostkeys.
+      - For behavior matching paramiko<2.9 set this to V(false).
+    vars:
+      - name: ansible_paramiko_use_rsa_sha2_algorithms
+    ini:
+      - {key: use_rsa_sha2_algorithms, section: paramiko_connection}
+    env:
+      - {name: ANSIBLE_PARAMIKO_USE_RSA_SHA2_ALGORITHMS}
+    default: true
+    type: boolean
+  host_key_auto_add:
+    description: "Automatically add host keys to C(~/.ssh/known_hosts)."
+    env:
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_AUTO_ADD
+    ini:
+      - key: host_key_auto_add
+        section: paramiko_connection
+    type: boolean
+  look_for_keys:
+    default: True
+    description: "Set to V(false) to disable searching for private key files in C(~/.ssh/)."
+    env:
+      - name: ANSIBLE_PARAMIKO_LOOK_FOR_KEYS
+    ini:
+      - {key: look_for_keys, section: paramiko_connection}
+    type: boolean
+  proxy_command:
+    default: ""
+    description:
+      - Proxy information for running the connection via a jumphost.
+    type: string
+    env:
+      - name: ANSIBLE_PARAMIKO_PROXY_COMMAND
+    ini:
+      - {key: proxy_command, section: paramiko_connection}
+    vars:
+      - name: ansible_paramiko_proxy_command
+  record_host_keys:
+    default: True
+    description: "Save the host keys to a file."
+    env:
+      - name: ANSIBLE_PARAMIKO_RECORD_HOST_KEYS
+    ini:
+      - section: paramiko_connection
+        key: record_host_keys
+    type: boolean
+  host_key_checking:
+    description: "Set this to V(false) if you want to avoid host key checking by the underlying tools Ansible uses to connect to the host."
+    type: boolean
+    default: true
+    env:
+      - name: ANSIBLE_HOST_KEY_CHECKING
+      - name: ANSIBLE_SSH_HOST_KEY_CHECKING
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_CHECKING
+    ini:
+      - section: defaults
+        key: host_key_checking
+      - section: paramiko_connection
+        key: host_key_checking
+    vars:
+      - name: ansible_host_key_checking
+      - name: ansible_ssh_host_key_checking
+      - name: ansible_paramiko_host_key_checking
+  use_persistent_connections:
+    description: "Toggles the use of persistence for connections."
+    type: boolean
+    default: False
+    env:
+      - name: ANSIBLE_USE_PERSISTENT_CONNECTIONS
+    ini:
+      - section: defaults
+        key: use_persistent_connections
+  banner_timeout:
+    type: float
+    default: 30
+    description:
+      - Configures, in seconds, the amount of time to wait for the SSH
+        banner to be presented. This option is supported by paramiko
+        version 1.15.0 or newer.
+    ini:
+      - section: paramiko_connection
+        key: banner_timeout
+    env:
+      - name: ANSIBLE_PARAMIKO_BANNER_TIMEOUT
+  timeout:
+    type: int
+    default: 10
+    description: Number of seconds until the plugin gives up on failing to establish a TCP connection.
+    ini:
+      - section: defaults
+        key: timeout
+      - section: ssh_connection
+        key: timeout
+      - section: paramiko_connection
+        key: timeout
+    env:
+      - name: ANSIBLE_TIMEOUT
+      - name: ANSIBLE_SSH_TIMEOUT
+      - name: ANSIBLE_PARAMIKO_TIMEOUT
+    vars:
+      - name: ansible_ssh_timeout
+      - name: ansible_paramiko_timeout
+    cli:
+      - name: timeout
+  lock_file_timeout:
+    type: int
+    default: 60
+    description: Number of seconds until the plugin gives up on trying to write a lock file when writing SSH known host keys.
+    vars:
+      - name: ansible_lock_file_timeout
+    env:
+      - name: ANSIBLE_LOCK_FILE_TIMEOUT
+  private_key_file:
+    description:
+      - Path to private key file to use for authentication.
+    type: string
+    ini:
+      - section: defaults
+        key: private_key_file
+      - section: paramiko_connection
+        key: private_key_file
+    env:
+      - name: ANSIBLE_PRIVATE_KEY_FILE
+      - name: ANSIBLE_PARAMIKO_PRIVATE_KEY_FILE
+    vars:
+      - name: ansible_private_key_file
+      - name: ansible_ssh_private_key_file
+      - name: ansible_paramiko_private_key_file
+    cli:
+      - name: private_key_file
+        option: "--private-key"
+  wsl_distribution:
+    description:
+      - WSL distribution name
+    type: string
+    vars:
+      - name: wsl_distribution
+  wsl_user:
+    description:
+      - WSL distribution user
+    type: string
+    vars:
+      - name: wsl_user
+  become_user:
+    description:
+      - WSL distribution user
+    type: string
+    default: root
+    vars:
+      - name: become_user
+      - name: ansible_become_user
+  become:
+    description:
+      - whether to use the user defined by ansible_become_user.
+    type: bool
+    default: false
+    vars:
+      - name: become
+      - name: ansible_become
+"""
+
+EXAMPLES = r"""
+# ------------------------
+# Inventory: inventory.yml
+# ------------------------
+# all:
+#   children:
+#     wsl:
+#       hosts:
+#         ubuntu:
+#           ansible_host: 10.0.0.10
+#           wsl_distribution: ubuntu
+#         debian:
+#           ansible_host: 10.0.0.10
+#           wsl_distribution: debian
+#       vars:
+#         ansible_connection: community.general.wsl
+#         ansible_user: vagrant
+#         wsl_user: wsl
+# ----------------------
+# Playbook: playbook.yml
+# ----------------------
+---
+- hosts: wsl
+  tasks:
+    - name: ping
+      ansible.builtin.ping:
+    - name: id
+      args:
+        executable: /bin/bash
+      ansible.builtin.shell: |
+        exec 2>&1
+        set -x
+        echo "$0"
+        pwd
+        id
+    - name: become id
+      become: true
+      args:
+        executable: /bin/bash
+      ansible.builtin.shell: |
+        exec 2>&1
+        set -x
+        echo "$0"
+        pwd
+        id
+"""
+
+import os
+import pathlib
+import shlex
+import socket
+import tempfile
+import typing as t
+
+from ansible.errors import (
+    AnsibleAuthenticationFailure,
+    AnsibleConnectionFailure,
+    AnsibleError,
+)
+from ansible_collections.community.general.plugins.module_utils._filelock import FileLock, LockTimeout
+from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
+from ansible.module_utils.compat.paramiko import PARAMIKO_IMPORT_ERR, paramiko
+from ansible.module_utils.compat.version import LooseVersion
+from ansible.plugins.connection import ConnectionBase
+from ansible.utils.display import Display
+from ansible.utils.path import makedirs_safe
+from binascii import hexlify
+from subprocess import list2cmdline
+
+
+display = Display()
+
+
+def authenticity_msg(hostname: str, ktype: str, fingerprint: str) -> str:
+    msg = f"""
+    paramiko: The authenticity of host '{hostname}' can't be established.
+    The {ktype} key fingerprint is {fingerprint}.
+    Are you sure you want to continue connecting (yes/no)?
+    """
+    return msg
+
+
+MissingHostKeyPolicy: type = object
+if paramiko:
+    MissingHostKeyPolicy = paramiko.MissingHostKeyPolicy
+
+
+class MyAddPolicy(MissingHostKeyPolicy):
+    """
+    Based on AutoAddPolicy in paramiko so we can determine when keys are added
+
+    and also prompt for input.
+
+    Policy for automatically adding the hostname and new host key to the
+    local L{HostKeys} object, and saving it. This is used by L{SSHClient}.
+    """
+
+    def __init__(self, connection: Connection) -> None:
+        self.connection = connection
+        self._options = connection._options
+
+    def missing_host_key(self, client, hostname, key) -> None:
+
+        if all((self.connection.get_option('host_key_checking'), not self.connection.get_option('host_key_auto_add'))):
+
+            fingerprint = hexlify(key.get_fingerprint())
+            ktype = key.get_name()
+
+            if self.connection.get_option('use_persistent_connections') or self.connection.force_persistence:
+                # don't print the prompt string since the user cannot respond
+                # to the question anyway
+                raise AnsibleError(authenticity_msg(hostname, ktype, fingerprint)[1:92])
+
+            inp = to_text(
+                display.prompt_until(authenticity_msg(hostname, ktype, fingerprint), private=False),
+                errors='surrogate_or_strict'
+            )
+
+            if inp.lower() not in ['yes', 'y', '']:
+                raise AnsibleError('host connection rejected by user')
+
+        key._added_by_ansible_this_time = True
+
+        # existing implementation below:
+        client._host_keys.add(hostname, key.get_name(), key)
+
+        # host keys are actually saved in close() function below
+        # in order to control ordering.
+
+
+class Connection(ConnectionBase):
+    """ SSH based connections (paramiko) to WSL """
+
+    transport = 'community.general.wsl'
+    _log_channel: str | None = None
+
+    def __init__(self, play_context, new_stdin, *args, **kwargs):
+        super(Connection, self).__init__(play_context, new_stdin, *args, **kwargs)
+
+    def _set_log_channel(self, name: str) -> None:
+        """ Mimic paramiko.SSHClient.set_log_channel """
+        self._log_channel = name
+
+    def _parse_proxy_command(self, port: int = 22) -> dict[str, t.Any]:
+        proxy_command = self.get_option('proxy_command') or None
+
+        sock_kwarg = {}
+        if proxy_command:
+            replacers = {
+                '%h': self.get_option('remote_addr'),
+                '%p': port,
+                '%r': self.get_option('remote_user')
+            }
+            for find, replace in replacers.items():
+                proxy_command = proxy_command.replace(find, str(replace))
+            try:
+                sock_kwarg = {'sock': paramiko.ProxyCommand(proxy_command)}
+                display.vvv(f'CONFIGURE PROXY COMMAND FOR CONNECTION: {proxy_command}', host=self.get_option('remote_addr'))
+            except AttributeError:
+                display.warning('Paramiko ProxyCommand support unavailable. '
+                                'Please upgrade to Paramiko 1.9.0 or newer. '
+                                'Not using configured ProxyCommand')
+
+        return sock_kwarg
+
+    def _connect(self) -> Connection:
+        """ activates the connection object """
+
+        if paramiko is None:
+            raise AnsibleError(f'paramiko is not installed: {to_native(PARAMIKO_IMPORT_ERR)}')
+
+        port = self.get_option('port')
+        display.vvv(f'ESTABLISH PARAMIKO SSH CONNECTION FOR USER: {self.get_option("remote_user")} on PORT {to_text(port)} TO {self.get_option("remote_addr")}',
+                    host=self.get_option('remote_addr'))
+
+        ssh = paramiko.SSHClient()
+
+        # Set pubkey and hostkey algorithms to disable, the only manipulation allowed currently
+        # is keeping or omitting rsa-sha2 algorithms
+        # default_keys: t.Tuple[str] = ()
+        paramiko_preferred_pubkeys = getattr(paramiko.Transport, '_preferred_pubkeys', ())
+        paramiko_preferred_hostkeys = getattr(paramiko.Transport, '_preferred_keys', ())
+        use_rsa_sha2_algorithms = self.get_option('use_rsa_sha2_algorithms')
+        disabled_algorithms: t.Dict[str, t.Iterable[str]] = {}
+        if not use_rsa_sha2_algorithms:
+            if paramiko_preferred_pubkeys:
+                disabled_algorithms['pubkeys'] = tuple(a for a in paramiko_preferred_pubkeys if 'rsa-sha2' in a)
+            if paramiko_preferred_hostkeys:
+                disabled_algorithms['keys'] = tuple(a for a in paramiko_preferred_hostkeys if 'rsa-sha2' in a)
+
+        # override paramiko's default logger name
+        if self._log_channel is not None:
+            ssh.set_log_channel(self._log_channel)
+
+        self.keyfile = os.path.expanduser('~/.ssh/known_hosts')
+
+        if self.get_option('host_key_checking'):
+            for ssh_known_hosts in ('/etc/ssh/ssh_known_hosts', '/etc/openssh/ssh_known_hosts'):
+                try:
+                    ssh.load_system_host_keys(ssh_known_hosts)
+                    break
+                except IOError:
+                    pass  # file was not found, but not required to function
+                except paramiko.hostkeys.InvalidHostKey as e:
+                    raise AnsibleConnectionFailure(f'Invalid host key: {to_text(e.line)}')
+            try:
+                ssh.load_system_host_keys()
+            except paramiko.hostkeys.InvalidHostKey as e:
+                raise AnsibleConnectionFailure(f'Invalid host key: {to_text(e.line)}')
+
+        ssh_connect_kwargs = self._parse_proxy_command(port)
+        ssh.set_missing_host_key_policy(MyAddPolicy(self))
+        conn_password = self.get_option('password')
+        allow_agent = True
+
+        if conn_password is not None:
+            allow_agent = False
+
+        try:
+            key_filename = None
+            if self.get_option('private_key_file'):
+                key_filename = os.path.expanduser(self.get_option('private_key_file'))
+
+            # paramiko 2.2 introduced auth_timeout parameter
+            if LooseVersion(paramiko.__version__) >= LooseVersion('2.2.0'):
+                ssh_connect_kwargs['auth_timeout'] = self.get_option('timeout')
+
+            # paramiko 1.15 introduced banner timeout parameter
+            if LooseVersion(paramiko.__version__) >= LooseVersion('1.15.0'):
+                ssh_connect_kwargs['banner_timeout'] = self.get_option('banner_timeout')
+
+            ssh.connect(
+                self.get_option('remote_addr').lower(),
+                username=self.get_option('remote_user'),
+                allow_agent=allow_agent,
+                look_for_keys=self.get_option('look_for_keys'),
+                key_filename=key_filename,
+                password=conn_password,
+                timeout=self.get_option('timeout'),
+                port=port,
+                disabled_algorithms=disabled_algorithms,
+                **ssh_connect_kwargs,
+            )
+        except paramiko.ssh_exception.BadHostKeyException as e:
+            raise AnsibleConnectionFailure(f'host key mismatch for {to_text(e.hostname)}')
+        except paramiko.ssh_exception.AuthenticationException as e:
+            msg = f'Failed to authenticate: {e}'
+            raise AnsibleAuthenticationFailure(msg)
+        except Exception as e:
+            msg = to_text(e)
+            if u'PID check failed' in msg:
+                raise AnsibleError('paramiko version issue, please upgrade paramiko on the machine running ansible')
path: plugins/connection/wsl.py
user: login: russoz
body: This should be more explicit - the code should tell the user the minimal version required for that to work, and that should be mentioned in the documentation as well.
created_at: 2025-02-23T01:11:59Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,758 @@
+# -*- coding: utf-8 -*-
+# Derived from ansible/plugins/connection/proxmox_pct_remote.py (c) 2024 Nils Stein (@mietzen) <github.nstein@mailbox.org>
+# Derived from ansible/plugins/connection/paramiko_ssh.py (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
+# Copyright (c) 2025 Rui Lopes (@rgl) <ruilopes.com>
+# Copyright (c) 2025 Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import annotations
+
+DOCUMENTATION = r"""
+author: Rui Lopes (@rgl) <ruilopes.com>
+name: wsl
+short_description: Run tasks in WSL distribution using wsl.exe CLI via SSH
+requirements:
+  - paramiko
+description:
+  - Run commands or put/fetch files to an existing WSL distribution using wsl.exe CLI via SSH.
+  - Uses the Python SSH implementation (Paramiko) to connect to the WSL host.
+version_added: "10.3.0"
+options:
+  remote_addr:
+    description:
+      - Address of the remote target.
+    default: inventory_hostname
+    type: string
+    vars:
+      - name: inventory_hostname
+      - name: ansible_host
+      - name: ansible_ssh_host
+      - name: ansible_paramiko_host
+  port:
+    description: Remote port to connect to.
+    type: int
+    default: 22
+    ini:
+      - section: defaults
+        key: remote_port
+      - section: paramiko_connection
+        key: remote_port
+    env:
+      - name: ANSIBLE_REMOTE_PORT
+      - name: ANSIBLE_REMOTE_PARAMIKO_PORT
+    vars:
+      - name: ansible_port
+      - name: ansible_ssh_port
+      - name: ansible_paramiko_port
+    keyword:
+      - name: port
+  remote_user:
+    description:
+      - User to login/authenticate as.
+      - Can be set from the CLI via the C(--user) or C(-u) options.
+    type: string
+    vars:
+      - name: ansible_user
+      - name: ansible_ssh_user
+      - name: ansible_paramiko_user
+    env:
+      - name: ANSIBLE_REMOTE_USER
+      - name: ANSIBLE_PARAMIKO_REMOTE_USER
+    ini:
+      - section: defaults
+        key: remote_user
+      - section: paramiko_connection
+        key: remote_user
+    keyword:
+      - name: remote_user
+  password:
+    description:
+      - Secret used to either login the SSH server or as a passphrase for SSH keys that require it.
+      - Can be set from the CLI via the C(--ask-pass) option.
+    type: string
+    vars:
+      - name: ansible_password
+      - name: ansible_ssh_pass
+      - name: ansible_ssh_password
+      - name: ansible_paramiko_pass
+      - name: ansible_paramiko_password
+  use_rsa_sha2_algorithms:
+    description:
+      - Whether or not to enable RSA SHA2 algorithms for pubkeys and hostkeys.
+      - On paramiko versions older than 2.9, this only affects hostkeys.
+      - For behavior matching paramiko<2.9 set this to V(false).
+    vars:
+      - name: ansible_paramiko_use_rsa_sha2_algorithms
+    ini:
+      - {key: use_rsa_sha2_algorithms, section: paramiko_connection}
+    env:
+      - {name: ANSIBLE_PARAMIKO_USE_RSA_SHA2_ALGORITHMS}
+    default: true
+    type: boolean
+  host_key_auto_add:
+    description: "Automatically add host keys to C(~/.ssh/known_hosts)."
+    env:
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_AUTO_ADD
+    ini:
+      - key: host_key_auto_add
+        section: paramiko_connection
+    type: boolean
+  look_for_keys:
+    default: True
+    description: "Set to V(false) to disable searching for private key files in C(~/.ssh/)."
+    env:
+      - name: ANSIBLE_PARAMIKO_LOOK_FOR_KEYS
+    ini:
+      - {key: look_for_keys, section: paramiko_connection}
+    type: boolean
+  proxy_command:
+    default: ""
+    description:
+      - Proxy information for running the connection via a jumphost.
+    type: string
+    env:
+      - name: ANSIBLE_PARAMIKO_PROXY_COMMAND
+    ini:
+      - {key: proxy_command, section: paramiko_connection}
+    vars:
+      - name: ansible_paramiko_proxy_command
+  record_host_keys:
+    default: True
+    description: "Save the host keys to a file."
+    env:
+      - name: ANSIBLE_PARAMIKO_RECORD_HOST_KEYS
+    ini:
+      - section: paramiko_connection
+        key: record_host_keys
+    type: boolean
+  host_key_checking:
+    description: "Set this to V(false) if you want to avoid host key checking by the underlying tools Ansible uses to connect to the host."
+    type: boolean
+    default: true
+    env:
+      - name: ANSIBLE_HOST_KEY_CHECKING
+      - name: ANSIBLE_SSH_HOST_KEY_CHECKING
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_CHECKING
+    ini:
+      - section: defaults
+        key: host_key_checking
+      - section: paramiko_connection
+        key: host_key_checking
+    vars:
+      - name: ansible_host_key_checking
+      - name: ansible_ssh_host_key_checking
+      - name: ansible_paramiko_host_key_checking
+  use_persistent_connections:
+    description: "Toggles the use of persistence for connections."
+    type: boolean
+    default: False
+    env:
+      - name: ANSIBLE_USE_PERSISTENT_CONNECTIONS
+    ini:
+      - section: defaults
+        key: use_persistent_connections
+  banner_timeout:
+    type: float
+    default: 30
+    description:
+      - Configures, in seconds, the amount of time to wait for the SSH
+        banner to be presented. This option is supported by paramiko
+        version 1.15.0 or newer.
+    ini:
+      - section: paramiko_connection
+        key: banner_timeout
+    env:
+      - name: ANSIBLE_PARAMIKO_BANNER_TIMEOUT
+  timeout:
+    type: int
+    default: 10
+    description: Number of seconds until the plugin gives up on failing to establish a TCP connection.
+    ini:
+      - section: defaults
+        key: timeout
+      - section: ssh_connection
+        key: timeout
+      - section: paramiko_connection
+        key: timeout
+    env:
+      - name: ANSIBLE_TIMEOUT
+      - name: ANSIBLE_SSH_TIMEOUT
+      - name: ANSIBLE_PARAMIKO_TIMEOUT
+    vars:
+      - name: ansible_ssh_timeout
+      - name: ansible_paramiko_timeout
+    cli:
+      - name: timeout
+  lock_file_timeout:
+    type: int
+    default: 60
+    description: Number of seconds until the plugin gives up on trying to write a lock file when writing SSH known host keys.
+    vars:
+      - name: ansible_lock_file_timeout
+    env:
+      - name: ANSIBLE_LOCK_FILE_TIMEOUT
+  private_key_file:
+    description:
+      - Path to private key file to use for authentication.
+    type: string
+    ini:
+      - section: defaults
+        key: private_key_file
+      - section: paramiko_connection
+        key: private_key_file
+    env:
+      - name: ANSIBLE_PRIVATE_KEY_FILE
+      - name: ANSIBLE_PARAMIKO_PRIVATE_KEY_FILE
+    vars:
+      - name: ansible_private_key_file
+      - name: ansible_ssh_private_key_file
+      - name: ansible_paramiko_private_key_file
+    cli:
+      - name: private_key_file
+        option: "--private-key"
+  wsl_distribution:
+    description:
+      - WSL distribution name
+    type: string
+    vars:
+      - name: wsl_distribution
+  wsl_user:
+    description:
+      - WSL distribution user
+    type: string
+    vars:
+      - name: wsl_user
+  become_user:
+    description:
+      - WSL distribution user
+    type: string
+    default: root
+    vars:
+      - name: become_user
+      - name: ansible_become_user
+  become:
+    description:
+      - whether to use the user defined by ansible_become_user.
+    type: bool
+    default: false
+    vars:
+      - name: become
+      - name: ansible_become
+"""
+
+EXAMPLES = r"""
+# ------------------------
+# Inventory: inventory.yml
+# ------------------------
+# all:
+#   children:
+#     wsl:
+#       hosts:
+#         ubuntu:
+#           ansible_host: 10.0.0.10
+#           wsl_distribution: ubuntu
+#         debian:
+#           ansible_host: 10.0.0.10
+#           wsl_distribution: debian
+#       vars:
+#         ansible_connection: community.general.wsl
+#         ansible_user: vagrant
+#         wsl_user: wsl
+# ----------------------
+# Playbook: playbook.yml
+# ----------------------
+---
+- hosts: wsl
+  tasks:
+    - name: ping
+      ansible.builtin.ping:
+    - name: id
+      args:
+        executable: /bin/bash
+      ansible.builtin.shell: |
+        exec 2>&1
+        set -x
+        echo "$0"
+        pwd
+        id
+    - name: become id
+      become: true
+      args:
+        executable: /bin/bash
+      ansible.builtin.shell: |
+        exec 2>&1
+        set -x
+        echo "$0"
+        pwd
+        id
+"""
+
+import os
+import pathlib
+import shlex
+import socket
+import tempfile
+import typing as t
+
+from ansible.errors import (
+    AnsibleAuthenticationFailure,
+    AnsibleConnectionFailure,
+    AnsibleError,
+)
+from ansible_collections.community.general.plugins.module_utils._filelock import FileLock, LockTimeout
+from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
+from ansible.module_utils.compat.paramiko import PARAMIKO_IMPORT_ERR, paramiko
+from ansible.module_utils.compat.version import LooseVersion
+from ansible.plugins.connection import ConnectionBase
+from ansible.utils.display import Display
+from ansible.utils.path import makedirs_safe
+from binascii import hexlify
+from subprocess import list2cmdline
+
+
+display = Display()
+
+
+def authenticity_msg(hostname: str, ktype: str, fingerprint: str) -> str:
+    msg = f"""
+    paramiko: The authenticity of host '{hostname}' can't be established.
+    The {ktype} key fingerprint is {fingerprint}.
+    Are you sure you want to continue connecting (yes/no)?
+    """
+    return msg
+
+
+MissingHostKeyPolicy: type = object
+if paramiko:
+    MissingHostKeyPolicy = paramiko.MissingHostKeyPolicy
+
+
+class MyAddPolicy(MissingHostKeyPolicy):
+    """
+    Based on AutoAddPolicy in paramiko so we can determine when keys are added
+
+    and also prompt for input.
+
+    Policy for automatically adding the hostname and new host key to the
+    local L{HostKeys} object, and saving it. This is used by L{SSHClient}.
+    """
+
+    def __init__(self, connection: Connection) -> None:
+        self.connection = connection
+        self._options = connection._options
+
+    def missing_host_key(self, client, hostname, key) -> None:
+
+        if all((self.connection.get_option('host_key_checking'), not self.connection.get_option('host_key_auto_add'))):
+
+            fingerprint = hexlify(key.get_fingerprint())
+            ktype = key.get_name()
+
+            if self.connection.get_option('use_persistent_connections') or self.connection.force_persistence:
+                # don't print the prompt string since the user cannot respond
+                # to the question anyway
+                raise AnsibleError(authenticity_msg(hostname, ktype, fingerprint)[1:92])
+
+            inp = to_text(
+                display.prompt_until(authenticity_msg(hostname, ktype, fingerprint), private=False),
+                errors='surrogate_or_strict'
+            )
+
+            if inp.lower() not in ['yes', 'y', '']:
+                raise AnsibleError('host connection rejected by user')
+
+        key._added_by_ansible_this_time = True
+
+        # existing implementation below:
+        client._host_keys.add(hostname, key.get_name(), key)
+
+        # host keys are actually saved in close() function below
+        # in order to control ordering.
+
+
+class Connection(ConnectionBase):
+    """ SSH based connections (paramiko) to WSL """
+
+    transport = 'community.general.wsl'
+    _log_channel: str | None = None
+
+    def __init__(self, play_context, new_stdin, *args, **kwargs):
+        super(Connection, self).__init__(play_context, new_stdin, *args, **kwargs)
+
+    def _set_log_channel(self, name: str) -> None:
+        """ Mimic paramiko.SSHClient.set_log_channel """
+        self._log_channel = name
+
+    def _parse_proxy_command(self, port: int = 22) -> dict[str, t.Any]:
+        proxy_command = self.get_option('proxy_command') or None
+
+        sock_kwarg = {}
+        if proxy_command:
+            replacers = {
+                '%h': self.get_option('remote_addr'),
+                '%p': port,
+                '%r': self.get_option('remote_user')
+            }
+            for find, replace in replacers.items():
+                proxy_command = proxy_command.replace(find, str(replace))
+            try:
+                sock_kwarg = {'sock': paramiko.ProxyCommand(proxy_command)}
+                display.vvv(f'CONFIGURE PROXY COMMAND FOR CONNECTION: {proxy_command}', host=self.get_option('remote_addr'))
+            except AttributeError:
+                display.warning('Paramiko ProxyCommand support unavailable. '
+                                'Please upgrade to Paramiko 1.9.0 or newer. '
+                                'Not using configured ProxyCommand')
+
+        return sock_kwarg
+
+    def _connect(self) -> Connection:
+        """ activates the connection object """
+
+        if paramiko is None:
+            raise AnsibleError(f'paramiko is not installed: {to_native(PARAMIKO_IMPORT_ERR)}')
+
+        port = self.get_option('port')
+        display.vvv(f'ESTABLISH PARAMIKO SSH CONNECTION FOR USER: {self.get_option("remote_user")} on PORT {to_text(port)} TO {self.get_option("remote_addr")}',
+                    host=self.get_option('remote_addr'))
+
+        ssh = paramiko.SSHClient()
+
+        # Set pubkey and hostkey algorithms to disable, the only manipulation allowed currently
+        # is keeping or omitting rsa-sha2 algorithms
+        # default_keys: t.Tuple[str] = ()
+        paramiko_preferred_pubkeys = getattr(paramiko.Transport, '_preferred_pubkeys', ())
+        paramiko_preferred_hostkeys = getattr(paramiko.Transport, '_preferred_keys', ())
+        use_rsa_sha2_algorithms = self.get_option('use_rsa_sha2_algorithms')
+        disabled_algorithms: t.Dict[str, t.Iterable[str]] = {}
+        if not use_rsa_sha2_algorithms:
+            if paramiko_preferred_pubkeys:
+                disabled_algorithms['pubkeys'] = tuple(a for a in paramiko_preferred_pubkeys if 'rsa-sha2' in a)
+            if paramiko_preferred_hostkeys:
+                disabled_algorithms['keys'] = tuple(a for a in paramiko_preferred_hostkeys if 'rsa-sha2' in a)
+
+        # override paramiko's default logger name
+        if self._log_channel is not None:
+            ssh.set_log_channel(self._log_channel)
+
+        self.keyfile = os.path.expanduser('~/.ssh/known_hosts')
+
+        if self.get_option('host_key_checking'):
+            for ssh_known_hosts in ('/etc/ssh/ssh_known_hosts', '/etc/openssh/ssh_known_hosts'):
+                try:
+                    ssh.load_system_host_keys(ssh_known_hosts)
+                    break
+                except IOError:
+                    pass  # file was not found, but not required to function
+                except paramiko.hostkeys.InvalidHostKey as e:
+                    raise AnsibleConnectionFailure(f'Invalid host key: {to_text(e.line)}')
+            try:
+                ssh.load_system_host_keys()
+            except paramiko.hostkeys.InvalidHostKey as e:
+                raise AnsibleConnectionFailure(f'Invalid host key: {to_text(e.line)}')
+
+        ssh_connect_kwargs = self._parse_proxy_command(port)
+        ssh.set_missing_host_key_policy(MyAddPolicy(self))
+        conn_password = self.get_option('password')
+        allow_agent = True
+
+        if conn_password is not None:
+            allow_agent = False
+
+        try:
+            key_filename = None
+            if self.get_option('private_key_file'):
+                key_filename = os.path.expanduser(self.get_option('private_key_file'))
+
+            # paramiko 2.2 introduced auth_timeout parameter
+            if LooseVersion(paramiko.__version__) >= LooseVersion('2.2.0'):
+                ssh_connect_kwargs['auth_timeout'] = self.get_option('timeout')
+
+            # paramiko 1.15 introduced banner timeout parameter
+            if LooseVersion(paramiko.__version__) >= LooseVersion('1.15.0'):
+                ssh_connect_kwargs['banner_timeout'] = self.get_option('banner_timeout')
+
+            ssh.connect(
+                self.get_option('remote_addr').lower(),
+                username=self.get_option('remote_user'),
+                allow_agent=allow_agent,
+                look_for_keys=self.get_option('look_for_keys'),
+                key_filename=key_filename,
+                password=conn_password,
+                timeout=self.get_option('timeout'),
+                port=port,
+                disabled_algorithms=disabled_algorithms,
+                **ssh_connect_kwargs,
+            )
+        except paramiko.ssh_exception.BadHostKeyException as e:
+            raise AnsibleConnectionFailure(f'host key mismatch for {to_text(e.hostname)}')
+        except paramiko.ssh_exception.AuthenticationException as e:
+            msg = f'Failed to authenticate: {e}'
+            raise AnsibleAuthenticationFailure(msg)
+        except Exception as e:
+            msg = to_text(e)
+            if u'PID check failed' in msg:
+                raise AnsibleError('paramiko version issue, please upgrade paramiko on the machine running ansible')
+            elif u'Private key file is encrypted' in msg:
+                msg = f'ssh {self.get_option("remote_user")}@{self.get_options("remote_addr")}:{port} : ' + \
+                    f'{msg}\nTo connect as a different user, use -u <username>.'
+                raise AnsibleConnectionFailure(msg)
+            else:
+                raise AnsibleConnectionFailure(msg)
+        self.ssh = ssh
+        self._connected = True
+        return self
+
+    def _any_keys_added(self) -> bool:
+        for hostname, keys in self.ssh._host_keys.items():
+            for keytype, key in keys.items():
+                added_this_time = getattr(key, '_added_by_ansible_this_time', False)
+                if added_this_time:
+                    return True
+        return False
+
+    def _save_ssh_host_keys(self, filename: str) -> None:
+        """
+        not using the paramiko save_ssh_host_keys function as we want to add new SSH keys at the bottom so folks
+        don't complain about it :)
+        """
+
+        if not self._any_keys_added():
+            return
+
+        path = os.path.expanduser('~/.ssh')
+        makedirs_safe(path)
+
+        with open(filename, 'w') as f:
+            for hostname, keys in self.ssh._host_keys.items():
+                for keytype, key in keys.items():
+                    # was f.write
+                    added_this_time = getattr(key, '_added_by_ansible_this_time', False)
+                    if not added_this_time:
+                        f.write(f'{hostname} {keytype} {key.get_base64()}\n')
+
+            for hostname, keys in self.ssh._host_keys.items():
+                for keytype, key in keys.items():
+                    added_this_time = getattr(key, '_added_by_ansible_this_time', False)
+                    if added_this_time:
+                        f.write(f'{hostname} {keytype} {key.get_base64()}\n')
+
+    def _build_wsl_command(self, cmd: str) -> str:
+        wsl_distribution = self.get_option('wsl_distribution')
+        if not wsl_distribution:
+            raise AnsibleError('A wsl_distribution is required but none was supplied')
+        become = self.get_option('become')
+        become_user = self.get_option('become_user')
+        if become and become_user:
+            wsl_user = become_user
+        else:
+            wsl_user = self.get_option('wsl_user')
+        args = ['wsl.exe', '--distribution', wsl_distribution]
+        if wsl_user:
+            args.extend(['--user', wsl_user])
+        args.extend(['--'])
+        args.extend(shlex.split(cmd))
+        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 """
+
+        cmd = self._build_wsl_command(cmd)
+
+        super(Connection, self).exec_command(cmd, in_data=in_data, sudoable=sudoable)
+
+        bufsize = 4096
+
+        try:
+            self.ssh.get_transport().set_keepalive(5)
+            chan = self.ssh.get_transport().open_session()
+        except Exception as e:
+            text_e = to_text(e)
+            msg = 'Failed to open session'
+            if text_e:
+                msg += f': {text_e}'
+            raise AnsibleConnectionFailure(to_native(msg))
+
+        display.vvv(f'EXEC {cmd}', host=self.get_option('remote_addr'))
+
+        cmd = to_bytes(cmd, errors='surrogate_or_strict')
+
+        no_prompt_out = b''
+        no_prompt_err = b''
+        become_output = b''
+
+        try:
+            chan.exec_command(cmd)
+            if self.become and self.become.expect_prompt():
+                password_prompt = False
+                become_success = False
+                while not (become_success or password_prompt):
+                    display.debug('Waiting for Privilege Escalation input')
+
+                    chunk = chan.recv(bufsize)
+                    display.debug(f'chunk is: {to_text(chunk)}')
+                    if not chunk:
+                        if b'unknown user' in become_output:
+                            n_become_user = to_native(self.become.get_option('become_user'))
+                            raise AnsibleError(f'user {n_become_user} does not exist')
+                        else:
+                            break
+                            # raise AnsibleError('ssh connection closed waiting for password prompt')
+                    become_output += chunk
+
+                    # need to check every line because we might get lectured
+                    # and we might get the middle of a line in a chunk
+                    for line in become_output.splitlines(True):
+                        if self.become.check_success(line):
+                            become_success = True
+                            break
+                        elif self.become.check_password_prompt(line):
+                            password_prompt = True
+                            break
+
+                if password_prompt:
+                    if self.become:
+                        become_pass = self.become.get_option('become_pass')
+                        chan.sendall(to_bytes(become_pass, errors='surrogate_or_strict') + b'\n')
+                    else:
+                        raise AnsibleError('A password is required but none was supplied')
+                else:
+                    no_prompt_out += become_output
+                    no_prompt_err += become_output
+
+            if in_data:
+                for i in range(0, len(in_data), bufsize):
+                    chan.send(in_data[i:i + bufsize])
+                chan.shutdown_write()
+            elif in_data == b'':
+                chan.shutdown_write()
+
+        except socket.timeout:
+            raise AnsibleError('ssh timed out waiting for privilege escalation.\n' + to_text(become_output))
+
+        stdout = b''.join(chan.makefile('rb', bufsize))
+        stderr = b''.join(chan.makefile_stderr('rb', bufsize))
+        returncode = chan.recv_exit_status()
+
+        # NB the full english error message is:
path: plugins/connection/wsl.py
user: login: russoz
body: I wonder if that is not subject to localization settings. If the remote user, or the remote system, sets the language to be something else, like `fr_FR` or `pt_BR`, would the error message still be in English? I know this is something we usually need to consider in Linux, and given that WSL runs an Ubuntu (or other distro) on top of itself, it might be something to consider as well.
created_at: 2025-02-23T01:15:14Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,758 @@
+# -*- coding: utf-8 -*-
+# Derived from ansible/plugins/connection/proxmox_pct_remote.py (c) 2024 Nils Stein (@mietzen) <github.nstein@mailbox.org>
+# Derived from ansible/plugins/connection/paramiko_ssh.py (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
+# Copyright (c) 2025 Rui Lopes (@rgl) <ruilopes.com>
+# Copyright (c) 2025 Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import annotations
+
+DOCUMENTATION = r"""
+author: Rui Lopes (@rgl) <ruilopes.com>
+name: wsl
+short_description: Run tasks in WSL distribution using wsl.exe CLI via SSH
+requirements:
+  - paramiko
+description:
+  - Run commands or put/fetch files to an existing WSL distribution using wsl.exe CLI via SSH.
+  - Uses the Python SSH implementation (Paramiko) to connect to the WSL host.
+version_added: "10.3.0"
+options:
+  remote_addr:
+    description:
+      - Address of the remote target.
+    default: inventory_hostname
+    type: string
+    vars:
+      - name: inventory_hostname
+      - name: ansible_host
+      - name: ansible_ssh_host
+      - name: ansible_paramiko_host
+  port:
+    description: Remote port to connect to.
+    type: int
+    default: 22
+    ini:
+      - section: defaults
+        key: remote_port
+      - section: paramiko_connection
+        key: remote_port
+    env:
+      - name: ANSIBLE_REMOTE_PORT
+      - name: ANSIBLE_REMOTE_PARAMIKO_PORT
+    vars:
+      - name: ansible_port
+      - name: ansible_ssh_port
+      - name: ansible_paramiko_port
+    keyword:
+      - name: port
+  remote_user:
+    description:
+      - User to login/authenticate as.
+      - Can be set from the CLI via the C(--user) or C(-u) options.
+    type: string
+    vars:
+      - name: ansible_user
+      - name: ansible_ssh_user
+      - name: ansible_paramiko_user
+    env:
+      - name: ANSIBLE_REMOTE_USER
+      - name: ANSIBLE_PARAMIKO_REMOTE_USER
+    ini:
+      - section: defaults
+        key: remote_user
+      - section: paramiko_connection
+        key: remote_user
+    keyword:
+      - name: remote_user
+  password:
+    description:
+      - Secret used to either login the SSH server or as a passphrase for SSH keys that require it.
+      - Can be set from the CLI via the C(--ask-pass) option.
+    type: string
+    vars:
+      - name: ansible_password
+      - name: ansible_ssh_pass
+      - name: ansible_ssh_password
+      - name: ansible_paramiko_pass
+      - name: ansible_paramiko_password
+  use_rsa_sha2_algorithms:
+    description:
+      - Whether or not to enable RSA SHA2 algorithms for pubkeys and hostkeys.
+      - On paramiko versions older than 2.9, this only affects hostkeys.
+      - For behavior matching paramiko<2.9 set this to V(false).
+    vars:
+      - name: ansible_paramiko_use_rsa_sha2_algorithms
+    ini:
+      - {key: use_rsa_sha2_algorithms, section: paramiko_connection}
+    env:
+      - {name: ANSIBLE_PARAMIKO_USE_RSA_SHA2_ALGORITHMS}
+    default: true
+    type: boolean
+  host_key_auto_add:
+    description: "Automatically add host keys to C(~/.ssh/known_hosts)."
+    env:
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_AUTO_ADD
+    ini:
+      - key: host_key_auto_add
+        section: paramiko_connection
+    type: boolean
+  look_for_keys:
+    default: True
+    description: "Set to V(false) to disable searching for private key files in C(~/.ssh/)."
+    env:
+      - name: ANSIBLE_PARAMIKO_LOOK_FOR_KEYS
+    ini:
+      - {key: look_for_keys, section: paramiko_connection}
+    type: boolean
+  proxy_command:
+    default: ""
+    description:
+      - Proxy information for running the connection via a jumphost.
+    type: string
+    env:
+      - name: ANSIBLE_PARAMIKO_PROXY_COMMAND
+    ini:
+      - {key: proxy_command, section: paramiko_connection}
+    vars:
+      - name: ansible_paramiko_proxy_command
+  record_host_keys:
+    default: True
+    description: "Save the host keys to a file."
+    env:
+      - name: ANSIBLE_PARAMIKO_RECORD_HOST_KEYS
+    ini:
+      - section: paramiko_connection
+        key: record_host_keys
+    type: boolean
+  host_key_checking:
+    description: "Set this to V(false) if you want to avoid host key checking by the underlying tools Ansible uses to connect to the host."
+    type: boolean
+    default: true
+    env:
+      - name: ANSIBLE_HOST_KEY_CHECKING
+      - name: ANSIBLE_SSH_HOST_KEY_CHECKING
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_CHECKING
+    ini:
+      - section: defaults
+        key: host_key_checking
+      - section: paramiko_connection
+        key: host_key_checking
+    vars:
+      - name: ansible_host_key_checking
+      - name: ansible_ssh_host_key_checking
+      - name: ansible_paramiko_host_key_checking
+  use_persistent_connections:
+    description: "Toggles the use of persistence for connections."
+    type: boolean
+    default: False
+    env:
+      - name: ANSIBLE_USE_PERSISTENT_CONNECTIONS
+    ini:
+      - section: defaults
+        key: use_persistent_connections
+  banner_timeout:
+    type: float
+    default: 30
+    description:
+      - Configures, in seconds, the amount of time to wait for the SSH
+        banner to be presented. This option is supported by paramiko
+        version 1.15.0 or newer.
+    ini:
+      - section: paramiko_connection
+        key: banner_timeout
+    env:
+      - name: ANSIBLE_PARAMIKO_BANNER_TIMEOUT
+  timeout:
+    type: int
+    default: 10
+    description: Number of seconds until the plugin gives up on failing to establish a TCP connection.
+    ini:
+      - section: defaults
+        key: timeout
+      - section: ssh_connection
+        key: timeout
+      - section: paramiko_connection
+        key: timeout
+    env:
+      - name: ANSIBLE_TIMEOUT
+      - name: ANSIBLE_SSH_TIMEOUT
+      - name: ANSIBLE_PARAMIKO_TIMEOUT
+    vars:
+      - name: ansible_ssh_timeout
+      - name: ansible_paramiko_timeout
+    cli:
+      - name: timeout
+  lock_file_timeout:
+    type: int
+    default: 60
+    description: Number of seconds until the plugin gives up on trying to write a lock file when writing SSH known host keys.
+    vars:
+      - name: ansible_lock_file_timeout
+    env:
+      - name: ANSIBLE_LOCK_FILE_TIMEOUT
+  private_key_file:
+    description:
+      - Path to private key file to use for authentication.
+    type: string
+    ini:
+      - section: defaults
+        key: private_key_file
+      - section: paramiko_connection
+        key: private_key_file
+    env:
+      - name: ANSIBLE_PRIVATE_KEY_FILE
+      - name: ANSIBLE_PARAMIKO_PRIVATE_KEY_FILE
+    vars:
+      - name: ansible_private_key_file
+      - name: ansible_ssh_private_key_file
+      - name: ansible_paramiko_private_key_file
+    cli:
+      - name: private_key_file
+        option: "--private-key"
+  wsl_distribution:
+    description:
+      - WSL distribution name
+    type: string
+    vars:
+      - name: wsl_distribution
+  wsl_user:
+    description:
+      - WSL distribution user
+    type: string
+    vars:
+      - name: wsl_user
+  become_user:
+    description:
+      - WSL distribution user
+    type: string
+    default: root
+    vars:
+      - name: become_user
+      - name: ansible_become_user
+  become:
+    description:
+      - whether to use the user defined by ansible_become_user.
+    type: bool
+    default: false
+    vars:
+      - name: become
+      - name: ansible_become
+"""
+
+EXAMPLES = r"""
+# ------------------------
+# Inventory: inventory.yml
+# ------------------------
+# all:
+#   children:
+#     wsl:
+#       hosts:
+#         ubuntu:
+#           ansible_host: 10.0.0.10
+#           wsl_distribution: ubuntu
+#         debian:
+#           ansible_host: 10.0.0.10
+#           wsl_distribution: debian
+#       vars:
+#         ansible_connection: community.general.wsl
+#         ansible_user: vagrant
+#         wsl_user: wsl
+# ----------------------
+# Playbook: playbook.yml
+# ----------------------
+---
+- hosts: wsl
+  tasks:
+    - name: ping
+      ansible.builtin.ping:
+    - name: id
+      args:
+        executable: /bin/bash
+      ansible.builtin.shell: |
+        exec 2>&1
+        set -x
+        echo "$0"
+        pwd
+        id
+    - name: become id
+      become: true
+      args:
+        executable: /bin/bash
+      ansible.builtin.shell: |
+        exec 2>&1
+        set -x
+        echo "$0"
+        pwd
+        id
+"""
+
+import os
+import pathlib
+import shlex
+import socket
+import tempfile
+import typing as t
+
+from ansible.errors import (
+    AnsibleAuthenticationFailure,
+    AnsibleConnectionFailure,
+    AnsibleError,
+)
+from ansible_collections.community.general.plugins.module_utils._filelock import FileLock, LockTimeout
+from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
+from ansible.module_utils.compat.paramiko import PARAMIKO_IMPORT_ERR, paramiko
+from ansible.module_utils.compat.version import LooseVersion
+from ansible.plugins.connection import ConnectionBase
+from ansible.utils.display import Display
+from ansible.utils.path import makedirs_safe
+from binascii import hexlify
+from subprocess import list2cmdline
+
+
+display = Display()
+
+
+def authenticity_msg(hostname: str, ktype: str, fingerprint: str) -> str:
+    msg = f"""
+    paramiko: The authenticity of host '{hostname}' can't be established.
+    The {ktype} key fingerprint is {fingerprint}.
+    Are you sure you want to continue connecting (yes/no)?
+    """
+    return msg
+
+
+MissingHostKeyPolicy: type = object
+if paramiko:
+    MissingHostKeyPolicy = paramiko.MissingHostKeyPolicy
+
+
+class MyAddPolicy(MissingHostKeyPolicy):
+    """
+    Based on AutoAddPolicy in paramiko so we can determine when keys are added
+
+    and also prompt for input.
+
+    Policy for automatically adding the hostname and new host key to the
+    local L{HostKeys} object, and saving it. This is used by L{SSHClient}.
+    """
+
+    def __init__(self, connection: Connection) -> None:
+        self.connection = connection
+        self._options = connection._options
+
+    def missing_host_key(self, client, hostname, key) -> None:
+
+        if all((self.connection.get_option('host_key_checking'), not self.connection.get_option('host_key_auto_add'))):
+
+            fingerprint = hexlify(key.get_fingerprint())
+            ktype = key.get_name()
+
+            if self.connection.get_option('use_persistent_connections') or self.connection.force_persistence:
+                # don't print the prompt string since the user cannot respond
+                # to the question anyway
+                raise AnsibleError(authenticity_msg(hostname, ktype, fingerprint)[1:92])
+
+            inp = to_text(
+                display.prompt_until(authenticity_msg(hostname, ktype, fingerprint), private=False),
+                errors='surrogate_or_strict'
+            )
+
+            if inp.lower() not in ['yes', 'y', '']:
+                raise AnsibleError('host connection rejected by user')
+
+        key._added_by_ansible_this_time = True
+
+        # existing implementation below:
+        client._host_keys.add(hostname, key.get_name(), key)
+
+        # host keys are actually saved in close() function below
+        # in order to control ordering.
+
+
+class Connection(ConnectionBase):
+    """ SSH based connections (paramiko) to WSL """
+
+    transport = 'community.general.wsl'
+    _log_channel: str | None = None
+
+    def __init__(self, play_context, new_stdin, *args, **kwargs):
+        super(Connection, self).__init__(play_context, new_stdin, *args, **kwargs)
+
+    def _set_log_channel(self, name: str) -> None:
+        """ Mimic paramiko.SSHClient.set_log_channel """
+        self._log_channel = name
+
+    def _parse_proxy_command(self, port: int = 22) -> dict[str, t.Any]:
+        proxy_command = self.get_option('proxy_command') or None
+
+        sock_kwarg = {}
+        if proxy_command:
+            replacers = {
+                '%h': self.get_option('remote_addr'),
+                '%p': port,
+                '%r': self.get_option('remote_user')
+            }
+            for find, replace in replacers.items():
+                proxy_command = proxy_command.replace(find, str(replace))
+            try:
+                sock_kwarg = {'sock': paramiko.ProxyCommand(proxy_command)}
+                display.vvv(f'CONFIGURE PROXY COMMAND FOR CONNECTION: {proxy_command}', host=self.get_option('remote_addr'))
+            except AttributeError:
+                display.warning('Paramiko ProxyCommand support unavailable. '
+                                'Please upgrade to Paramiko 1.9.0 or newer. '
+                                'Not using configured ProxyCommand')
+
+        return sock_kwarg
+
+    def _connect(self) -> Connection:
+        """ activates the connection object """
+
+        if paramiko is None:
+            raise AnsibleError(f'paramiko is not installed: {to_native(PARAMIKO_IMPORT_ERR)}')
+
+        port = self.get_option('port')
+        display.vvv(f'ESTABLISH PARAMIKO SSH CONNECTION FOR USER: {self.get_option("remote_user")} on PORT {to_text(port)} TO {self.get_option("remote_addr")}',
+                    host=self.get_option('remote_addr'))
+
+        ssh = paramiko.SSHClient()
+
+        # Set pubkey and hostkey algorithms to disable, the only manipulation allowed currently
+        # is keeping or omitting rsa-sha2 algorithms
+        # default_keys: t.Tuple[str] = ()
+        paramiko_preferred_pubkeys = getattr(paramiko.Transport, '_preferred_pubkeys', ())
+        paramiko_preferred_hostkeys = getattr(paramiko.Transport, '_preferred_keys', ())
+        use_rsa_sha2_algorithms = self.get_option('use_rsa_sha2_algorithms')
+        disabled_algorithms: t.Dict[str, t.Iterable[str]] = {}
+        if not use_rsa_sha2_algorithms:
+            if paramiko_preferred_pubkeys:
+                disabled_algorithms['pubkeys'] = tuple(a for a in paramiko_preferred_pubkeys if 'rsa-sha2' in a)
+            if paramiko_preferred_hostkeys:
+                disabled_algorithms['keys'] = tuple(a for a in paramiko_preferred_hostkeys if 'rsa-sha2' in a)
+
+        # override paramiko's default logger name
+        if self._log_channel is not None:
+            ssh.set_log_channel(self._log_channel)
+
+        self.keyfile = os.path.expanduser('~/.ssh/known_hosts')
+
+        if self.get_option('host_key_checking'):
+            for ssh_known_hosts in ('/etc/ssh/ssh_known_hosts', '/etc/openssh/ssh_known_hosts'):
+                try:
+                    ssh.load_system_host_keys(ssh_known_hosts)
+                    break
+                except IOError:
+                    pass  # file was not found, but not required to function
+                except paramiko.hostkeys.InvalidHostKey as e:
+                    raise AnsibleConnectionFailure(f'Invalid host key: {to_text(e.line)}')
+            try:
+                ssh.load_system_host_keys()
+            except paramiko.hostkeys.InvalidHostKey as e:
+                raise AnsibleConnectionFailure(f'Invalid host key: {to_text(e.line)}')
+
+        ssh_connect_kwargs = self._parse_proxy_command(port)
+        ssh.set_missing_host_key_policy(MyAddPolicy(self))
+        conn_password = self.get_option('password')
+        allow_agent = True
+
+        if conn_password is not None:
+            allow_agent = False
+
+        try:
+            key_filename = None
+            if self.get_option('private_key_file'):
+                key_filename = os.path.expanduser(self.get_option('private_key_file'))
+
+            # paramiko 2.2 introduced auth_timeout parameter
+            if LooseVersion(paramiko.__version__) >= LooseVersion('2.2.0'):
+                ssh_connect_kwargs['auth_timeout'] = self.get_option('timeout')
+
+            # paramiko 1.15 introduced banner timeout parameter
+            if LooseVersion(paramiko.__version__) >= LooseVersion('1.15.0'):
+                ssh_connect_kwargs['banner_timeout'] = self.get_option('banner_timeout')
+
+            ssh.connect(
+                self.get_option('remote_addr').lower(),
+                username=self.get_option('remote_user'),
+                allow_agent=allow_agent,
+                look_for_keys=self.get_option('look_for_keys'),
+                key_filename=key_filename,
+                password=conn_password,
+                timeout=self.get_option('timeout'),
+                port=port,
+                disabled_algorithms=disabled_algorithms,
+                **ssh_connect_kwargs,
+            )
+        except paramiko.ssh_exception.BadHostKeyException as e:
+            raise AnsibleConnectionFailure(f'host key mismatch for {to_text(e.hostname)}')
+        except paramiko.ssh_exception.AuthenticationException as e:
+            msg = f'Failed to authenticate: {e}'
+            raise AnsibleAuthenticationFailure(msg)
+        except Exception as e:
+            msg = to_text(e)
+            if u'PID check failed' in msg:
+                raise AnsibleError('paramiko version issue, please upgrade paramiko on the machine running ansible')
+            elif u'Private key file is encrypted' in msg:
+                msg = f'ssh {self.get_option("remote_user")}@{self.get_options("remote_addr")}:{port} : ' + \
+                    f'{msg}\nTo connect as a different user, use -u <username>.'
+                raise AnsibleConnectionFailure(msg)
+            else:
+                raise AnsibleConnectionFailure(msg)
+        self.ssh = ssh
+        self._connected = True
+        return self
+
+    def _any_keys_added(self) -> bool:
+        for hostname, keys in self.ssh._host_keys.items():
+            for keytype, key in keys.items():
+                added_this_time = getattr(key, '_added_by_ansible_this_time', False)
+                if added_this_time:
+                    return True
+        return False
+
+    def _save_ssh_host_keys(self, filename: str) -> None:
+        """
+        not using the paramiko save_ssh_host_keys function as we want to add new SSH keys at the bottom so folks
+        don't complain about it :)
+        """
+
+        if not self._any_keys_added():
+            return
+
+        path = os.path.expanduser('~/.ssh')
+        makedirs_safe(path)
+
+        with open(filename, 'w') as f:
+            for hostname, keys in self.ssh._host_keys.items():
+                for keytype, key in keys.items():
+                    # was f.write
+                    added_this_time = getattr(key, '_added_by_ansible_this_time', False)
+                    if not added_this_time:
+                        f.write(f'{hostname} {keytype} {key.get_base64()}\n')
+
+            for hostname, keys in self.ssh._host_keys.items():
+                for keytype, key in keys.items():
+                    added_this_time = getattr(key, '_added_by_ansible_this_time', False)
+                    if added_this_time:
+                        f.write(f'{hostname} {keytype} {key.get_base64()}\n')
+
+    def _build_wsl_command(self, cmd: str) -> str:
+        wsl_distribution = self.get_option('wsl_distribution')
+        if not wsl_distribution:
+            raise AnsibleError('A wsl_distribution is required but none was supplied')
+        become = self.get_option('become')
+        become_user = self.get_option('become_user')
+        if become and become_user:
+            wsl_user = become_user
+        else:
+            wsl_user = self.get_option('wsl_user')
+        args = ['wsl.exe', '--distribution', wsl_distribution]
+        if wsl_user:
+            args.extend(['--user', wsl_user])
+        args.extend(['--'])
+        args.extend(shlex.split(cmd))
+        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 """
+
+        cmd = self._build_wsl_command(cmd)
+
+        super(Connection, self).exec_command(cmd, in_data=in_data, sudoable=sudoable)
+
+        bufsize = 4096
+
+        try:
+            self.ssh.get_transport().set_keepalive(5)
+            chan = self.ssh.get_transport().open_session()
+        except Exception as e:
+            text_e = to_text(e)
+            msg = 'Failed to open session'
+            if text_e:
+                msg += f': {text_e}'
+            raise AnsibleConnectionFailure(to_native(msg))
+
+        display.vvv(f'EXEC {cmd}', host=self.get_option('remote_addr'))
+
+        cmd = to_bytes(cmd, errors='surrogate_or_strict')
+
+        no_prompt_out = b''
+        no_prompt_err = b''
+        become_output = b''
+
+        try:
+            chan.exec_command(cmd)
+            if self.become and self.become.expect_prompt():
+                password_prompt = False
+                become_success = False
+                while not (become_success or password_prompt):
+                    display.debug('Waiting for Privilege Escalation input')
+
+                    chunk = chan.recv(bufsize)
+                    display.debug(f'chunk is: {to_text(chunk)}')
+                    if not chunk:
+                        if b'unknown user' in become_output:
+                            n_become_user = to_native(self.become.get_option('become_user'))
+                            raise AnsibleError(f'user {n_become_user} does not exist')
+                        else:
+                            break
+                            # raise AnsibleError('ssh connection closed waiting for password prompt')
+                    become_output += chunk
+
+                    # need to check every line because we might get lectured
+                    # and we might get the middle of a line in a chunk
+                    for line in become_output.splitlines(True):
+                        if self.become.check_success(line):
+                            become_success = True
+                            break
+                        elif self.become.check_password_prompt(line):
+                            password_prompt = True
+                            break
+
+                if password_prompt:
+                    if self.become:
+                        become_pass = self.become.get_option('become_pass')
+                        chan.sendall(to_bytes(become_pass, errors='surrogate_or_strict') + b'\n')
+                    else:
+                        raise AnsibleError('A password is required but none was supplied')
+                else:
+                    no_prompt_out += become_output
+                    no_prompt_err += become_output
+
+            if in_data:
+                for i in range(0, len(in_data), bufsize):
+                    chan.send(in_data[i:i + bufsize])
+                chan.shutdown_write()
+            elif in_data == b'':
+                chan.shutdown_write()
+
+        except socket.timeout:
+            raise AnsibleError('ssh timed out waiting for privilege escalation.\n' + to_text(become_output))
+
+        stdout = b''.join(chan.makefile('rb', bufsize))
+        stderr = b''.join(chan.makefile_stderr('rb', bufsize))
+        returncode = chan.recv_exit_status()
+
+        # NB the full english error message is:
+        #     'wsl.exe' is not recognized as an internal or external command,
+        #     operable program or batch file.
+        if "'wsl.exe' is not recognized" in stderr.decode('utf-8'):
+            raise AnsibleError(
+                f'wsl.exe not found in path of host: {to_text(self.get_option("remote_addr"))}')
+
+        return (returncode, no_prompt_out + stdout, no_prompt_out + stderr)
+
+    def put_file(self, in_path: str, out_path: str) -> None:
+        """ transfer a file from local to remote """
+
+        display.vvv(f'PUT {in_path} TO {out_path}', host=self.get_option('remote_addr'))
+        try:
+            with open(in_path, 'rb') as f:
+                data = f.read()
+                returncode, stdout, stderr = self.exec_command(
+                    ' '.join([
+                        self._shell.executable, '-c',
+                        self._shell.quote(f'cat > {out_path}')]),
+                    in_data=data,
+                    sudoable=False)
+            if returncode != 0:
+                if 'cat: not found' in stderr.decode('utf-8'):
+                    raise AnsibleError(
+                        f'cat not found in path of WSL distribution: {to_text(self.get_option("wsl_distribution"))}')
+                raise AnsibleError(
+                    f'{to_text(stdout)}\n{to_text(stderr)}')
+        except Exception as e:
+            raise AnsibleError(
+                f'error occurred while putting file from {in_path} to {out_path}!\n{to_text(e)}')
+
+    def fetch_file(self, in_path: str, out_path: str) -> None:
+        """ save a remote file to the specified path """
+
+        display.vvv(f'FETCH {in_path} TO {out_path}', host=self.get_option('remote_addr'))
+        try:
+            returncode, stdout, stderr = self.exec_command(
+                ' '.join([
+                    self._shell.executable, '-c',
+                    self._shell.quote(f'cat {in_path}')]),
+                sudoable=False)
+            if returncode != 0:
+                if 'cat: not found' in stderr.decode('utf-8'):
+                    raise AnsibleError(
+                        f'cat not found in path of WSL distribution: {to_text(self.get_option("wsl_distribution"))}')
+                raise AnsibleError(
+                    f'{to_text(stdout)}\n{to_text(stderr)}')
+            with open(out_path, 'wb') as f:
+                f.write(stdout)
+        except Exception as e:
+            raise AnsibleError(
+                f'error occurred while fetching file from {in_path} to {out_path}!\n{to_text(e)}')
+
+    def reset(self) -> None:
+        """ reset the connection """
+
+        if not self._connected:
+            return
+        self.close()
+        self._connect()
+
+    def close(self) -> None:
+        """ terminate the connection """
+
+        if self.get_option('host_key_checking') and self.get_option('record_host_keys') and self._any_keys_added():
+            # add any new SSH host keys -- warning -- this could be slow
+            # (This doesn't acquire the connection lock because it needs
+            # to exclude only other known_hosts writers, not connections
+            # that are starting up.)
+            lockfile = os.path.basename(self.keyfile)
+            dirname = os.path.dirname(self.keyfile)
+            makedirs_safe(dirname)
+            tmp_keyfile_name = None
+            try:
+                with FileLock().lock_file(lockfile, dirname, self.get_option('lock_file_timeout')):
+                    # just in case any were added recently
+
+                    self.ssh.load_system_host_keys()
+                    self.ssh._host_keys.update(self.ssh._system_host_keys)
+
+                    # gather information about the current key file, so
+                    # we can ensure the new file has the correct mode/owner
+
+                    key_dir = os.path.dirname(self.keyfile)
+                    if os.path.exists(self.keyfile):
+                        key_stat = os.stat(self.keyfile)
+                        mode = key_stat.st_mode & 0o777
+                        uid = key_stat.st_uid
+                        gid = key_stat.st_gid
+                    else:
+                        mode = 0o644
+                        uid = os.getuid()
+                        gid = os.getgid()
+
+                    # Save the new keys to a temporary file and move it into place
+                    # rather than rewriting the file. We set delete=False because
+                    # the file will be moved into place rather than cleaned up.
+
+                    with tempfile.NamedTemporaryFile(dir=key_dir, delete=False) as tmp_keyfile:
+                        tmp_keyfile_name = tmp_keyfile.name
+                        os.chmod(tmp_keyfile_name, mode)
+                        os.chown(tmp_keyfile_name, uid, gid)
+                        self._save_ssh_host_keys(tmp_keyfile_name)
+
+                    os.rename(tmp_keyfile_name, self.keyfile)
path: plugins/connection/wsl.py
user: login: russoz
body: My Concurrency-fu is a little bit rusty, but I _think_ you might benefit from a finer granularity in the lock here. 

I might be wrong, but it doesn't look like `load_system_host_keys()` touches the path referred by `keyfile`. In that case, you might perhaps lock the file just for the `rename` operation, instead of the entire process.

And now that I wrote that, I just remembered that there is an utility function in Ansible called... `atomic_move`, but it is not an utility function, it is a method from the `AnsibleModule` class :( anyways, it might be interesting taking a look and see if you can get some ideas out of it. :-)
created_at: 2025-02-23T02:11:08Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,758 @@
+# -*- coding: utf-8 -*-
+# Derived from ansible/plugins/connection/proxmox_pct_remote.py (c) 2024 Nils Stein (@mietzen) <github.nstein@mailbox.org>
+# Derived from ansible/plugins/connection/paramiko_ssh.py (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
+# Copyright (c) 2025 Rui Lopes (@rgl) <ruilopes.com>
+# Copyright (c) 2025 Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import annotations
+
+DOCUMENTATION = r"""
+author: Rui Lopes (@rgl) <ruilopes.com>
+name: wsl
+short_description: Run tasks in WSL distribution using wsl.exe CLI via SSH
+requirements:
+  - paramiko
+description:
+  - Run commands or put/fetch files to an existing WSL distribution using wsl.exe CLI via SSH.
+  - Uses the Python SSH implementation (Paramiko) to connect to the WSL host.
+version_added: "10.3.0"
+options:
+  remote_addr:
+    description:
+      - Address of the remote target.
+    default: inventory_hostname
+    type: string
+    vars:
+      - name: inventory_hostname
+      - name: ansible_host
+      - name: ansible_ssh_host
+      - name: ansible_paramiko_host
+  port:
+    description: Remote port to connect to.
+    type: int
+    default: 22
+    ini:
+      - section: defaults
+        key: remote_port
+      - section: paramiko_connection
+        key: remote_port
+    env:
+      - name: ANSIBLE_REMOTE_PORT
+      - name: ANSIBLE_REMOTE_PARAMIKO_PORT
+    vars:
+      - name: ansible_port
+      - name: ansible_ssh_port
+      - name: ansible_paramiko_port
+    keyword:
+      - name: port
+  remote_user:
+    description:
+      - User to login/authenticate as.
+      - Can be set from the CLI via the C(--user) or C(-u) options.
+    type: string
+    vars:
+      - name: ansible_user
+      - name: ansible_ssh_user
+      - name: ansible_paramiko_user
+    env:
+      - name: ANSIBLE_REMOTE_USER
+      - name: ANSIBLE_PARAMIKO_REMOTE_USER
+    ini:
+      - section: defaults
+        key: remote_user
+      - section: paramiko_connection
+        key: remote_user
+    keyword:
+      - name: remote_user
+  password:
+    description:
+      - Secret used to either login the SSH server or as a passphrase for SSH keys that require it.
+      - Can be set from the CLI via the C(--ask-pass) option.
+    type: string
+    vars:
+      - name: ansible_password
+      - name: ansible_ssh_pass
+      - name: ansible_ssh_password
+      - name: ansible_paramiko_pass
+      - name: ansible_paramiko_password
+  use_rsa_sha2_algorithms:
+    description:
+      - Whether or not to enable RSA SHA2 algorithms for pubkeys and hostkeys.
+      - On paramiko versions older than 2.9, this only affects hostkeys.
+      - For behavior matching paramiko<2.9 set this to V(false).
+    vars:
+      - name: ansible_paramiko_use_rsa_sha2_algorithms
+    ini:
+      - {key: use_rsa_sha2_algorithms, section: paramiko_connection}
+    env:
+      - {name: ANSIBLE_PARAMIKO_USE_RSA_SHA2_ALGORITHMS}
+    default: true
+    type: boolean
+  host_key_auto_add:
+    description: "Automatically add host keys to C(~/.ssh/known_hosts)."
+    env:
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_AUTO_ADD
+    ini:
+      - key: host_key_auto_add
+        section: paramiko_connection
+    type: boolean
+  look_for_keys:
+    default: True
+    description: "Set to V(false) to disable searching for private key files in C(~/.ssh/)."
+    env:
+      - name: ANSIBLE_PARAMIKO_LOOK_FOR_KEYS
+    ini:
+      - {key: look_for_keys, section: paramiko_connection}
+    type: boolean
+  proxy_command:
+    default: ""
+    description:
+      - Proxy information for running the connection via a jumphost.
+    type: string
+    env:
+      - name: ANSIBLE_PARAMIKO_PROXY_COMMAND
+    ini:
+      - {key: proxy_command, section: paramiko_connection}
+    vars:
+      - name: ansible_paramiko_proxy_command
+  record_host_keys:
+    default: True
+    description: "Save the host keys to a file."
+    env:
+      - name: ANSIBLE_PARAMIKO_RECORD_HOST_KEYS
+    ini:
+      - section: paramiko_connection
+        key: record_host_keys
+    type: boolean
+  host_key_checking:
+    description: "Set this to V(false) if you want to avoid host key checking by the underlying tools Ansible uses to connect to the host."
+    type: boolean
+    default: true
+    env:
+      - name: ANSIBLE_HOST_KEY_CHECKING
+      - name: ANSIBLE_SSH_HOST_KEY_CHECKING
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_CHECKING
+    ini:
+      - section: defaults
+        key: host_key_checking
+      - section: paramiko_connection
+        key: host_key_checking
+    vars:
+      - name: ansible_host_key_checking
+      - name: ansible_ssh_host_key_checking
+      - name: ansible_paramiko_host_key_checking
+  use_persistent_connections:
+    description: "Toggles the use of persistence for connections."
+    type: boolean
+    default: False
+    env:
+      - name: ANSIBLE_USE_PERSISTENT_CONNECTIONS
+    ini:
+      - section: defaults
+        key: use_persistent_connections
+  banner_timeout:
+    type: float
+    default: 30
+    description:
+      - Configures, in seconds, the amount of time to wait for the SSH
+        banner to be presented. This option is supported by paramiko
+        version 1.15.0 or newer.
+    ini:
+      - section: paramiko_connection
+        key: banner_timeout
+    env:
+      - name: ANSIBLE_PARAMIKO_BANNER_TIMEOUT
+  timeout:
+    type: int
+    default: 10
+    description: Number of seconds until the plugin gives up on failing to establish a TCP connection.
+    ini:
+      - section: defaults
+        key: timeout
+      - section: ssh_connection
+        key: timeout
+      - section: paramiko_connection
+        key: timeout
+    env:
+      - name: ANSIBLE_TIMEOUT
+      - name: ANSIBLE_SSH_TIMEOUT
+      - name: ANSIBLE_PARAMIKO_TIMEOUT
+    vars:
+      - name: ansible_ssh_timeout
+      - name: ansible_paramiko_timeout
+    cli:
+      - name: timeout
+  lock_file_timeout:
+    type: int
+    default: 60
+    description: Number of seconds until the plugin gives up on trying to write a lock file when writing SSH known host keys.
+    vars:
+      - name: ansible_lock_file_timeout
+    env:
+      - name: ANSIBLE_LOCK_FILE_TIMEOUT
+  private_key_file:
+    description:
+      - Path to private key file to use for authentication.
+    type: string
+    ini:
+      - section: defaults
+        key: private_key_file
+      - section: paramiko_connection
+        key: private_key_file
+    env:
+      - name: ANSIBLE_PRIVATE_KEY_FILE
+      - name: ANSIBLE_PARAMIKO_PRIVATE_KEY_FILE
+    vars:
+      - name: ansible_private_key_file
+      - name: ansible_ssh_private_key_file
+      - name: ansible_paramiko_private_key_file
+    cli:
+      - name: private_key_file
+        option: "--private-key"
+  wsl_distribution:
+    description:
+      - WSL distribution name
+    type: string
+    vars:
+      - name: wsl_distribution
+  wsl_user:
+    description:
+      - WSL distribution user
+    type: string
+    vars:
+      - name: wsl_user
+  become_user:
+    description:
+      - WSL distribution user
+    type: string
+    default: root
+    vars:
+      - name: become_user
+      - name: ansible_become_user
+  become:
+    description:
+      - whether to use the user defined by ansible_become_user.
+    type: bool
+    default: false
+    vars:
+      - name: become
+      - name: ansible_become
+"""
+
+EXAMPLES = r"""
+# ------------------------
+# Inventory: inventory.yml
+# ------------------------
+# all:
+#   children:
+#     wsl:
+#       hosts:
+#         ubuntu:
+#           ansible_host: 10.0.0.10
+#           wsl_distribution: ubuntu
+#         debian:
+#           ansible_host: 10.0.0.10
+#           wsl_distribution: debian
+#       vars:
+#         ansible_connection: community.general.wsl
+#         ansible_user: vagrant
+#         wsl_user: wsl
+# ----------------------
+# Playbook: playbook.yml
+# ----------------------
+---
+- hosts: wsl
+  tasks:
+    - name: ping
+      ansible.builtin.ping:
+    - name: id
+      args:
+        executable: /bin/bash
+      ansible.builtin.shell: |
+        exec 2>&1
+        set -x
+        echo "$0"
+        pwd
+        id
+    - name: become id
+      become: true
+      args:
+        executable: /bin/bash
+      ansible.builtin.shell: |
+        exec 2>&1
+        set -x
+        echo "$0"
+        pwd
+        id
+"""
+
+import os
+import pathlib
+import shlex
+import socket
+import tempfile
+import typing as t
+
+from ansible.errors import (
+    AnsibleAuthenticationFailure,
+    AnsibleConnectionFailure,
+    AnsibleError,
+)
+from ansible_collections.community.general.plugins.module_utils._filelock import FileLock, LockTimeout
+from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
+from ansible.module_utils.compat.paramiko import PARAMIKO_IMPORT_ERR, paramiko
+from ansible.module_utils.compat.version import LooseVersion
+from ansible.plugins.connection import ConnectionBase
+from ansible.utils.display import Display
+from ansible.utils.path import makedirs_safe
+from binascii import hexlify
+from subprocess import list2cmdline
+
+
+display = Display()
+
+
+def authenticity_msg(hostname: str, ktype: str, fingerprint: str) -> str:
+    msg = f"""
+    paramiko: The authenticity of host '{hostname}' can't be established.
+    The {ktype} key fingerprint is {fingerprint}.
+    Are you sure you want to continue connecting (yes/no)?
+    """
+    return msg
+
+
+MissingHostKeyPolicy: type = object
+if paramiko:
+    MissingHostKeyPolicy = paramiko.MissingHostKeyPolicy
+
+
+class MyAddPolicy(MissingHostKeyPolicy):
+    """
+    Based on AutoAddPolicy in paramiko so we can determine when keys are added
+
+    and also prompt for input.
+
+    Policy for automatically adding the hostname and new host key to the
+    local L{HostKeys} object, and saving it. This is used by L{SSHClient}.
+    """
+
+    def __init__(self, connection: Connection) -> None:
+        self.connection = connection
+        self._options = connection._options
+
+    def missing_host_key(self, client, hostname, key) -> None:
+
+        if all((self.connection.get_option('host_key_checking'), not self.connection.get_option('host_key_auto_add'))):
+
+            fingerprint = hexlify(key.get_fingerprint())
+            ktype = key.get_name()
+
+            if self.connection.get_option('use_persistent_connections') or self.connection.force_persistence:
+                # don't print the prompt string since the user cannot respond
+                # to the question anyway
+                raise AnsibleError(authenticity_msg(hostname, ktype, fingerprint)[1:92])
+
+            inp = to_text(
+                display.prompt_until(authenticity_msg(hostname, ktype, fingerprint), private=False),
+                errors='surrogate_or_strict'
+            )
+
+            if inp.lower() not in ['yes', 'y', '']:
+                raise AnsibleError('host connection rejected by user')
+
+        key._added_by_ansible_this_time = True
+
+        # existing implementation below:
+        client._host_keys.add(hostname, key.get_name(), key)
+
+        # host keys are actually saved in close() function below
+        # in order to control ordering.
+
+
+class Connection(ConnectionBase):
+    """ SSH based connections (paramiko) to WSL """
+
+    transport = 'community.general.wsl'
+    _log_channel: str | None = None
+
+    def __init__(self, play_context, new_stdin, *args, **kwargs):
+        super(Connection, self).__init__(play_context, new_stdin, *args, **kwargs)
+
+    def _set_log_channel(self, name: str) -> None:
+        """ Mimic paramiko.SSHClient.set_log_channel """
+        self._log_channel = name
+
+    def _parse_proxy_command(self, port: int = 22) -> dict[str, t.Any]:
+        proxy_command = self.get_option('proxy_command') or None
+
+        sock_kwarg = {}
+        if proxy_command:
+            replacers = {
+                '%h': self.get_option('remote_addr'),
+                '%p': port,
+                '%r': self.get_option('remote_user')
+            }
+            for find, replace in replacers.items():
+                proxy_command = proxy_command.replace(find, str(replace))
+            try:
+                sock_kwarg = {'sock': paramiko.ProxyCommand(proxy_command)}
+                display.vvv(f'CONFIGURE PROXY COMMAND FOR CONNECTION: {proxy_command}', host=self.get_option('remote_addr'))
+            except AttributeError:
+                display.warning('Paramiko ProxyCommand support unavailable. '
+                                'Please upgrade to Paramiko 1.9.0 or newer. '
+                                'Not using configured ProxyCommand')
+
+        return sock_kwarg
+
+    def _connect(self) -> Connection:
+        """ activates the connection object """
+
+        if paramiko is None:
+            raise AnsibleError(f'paramiko is not installed: {to_native(PARAMIKO_IMPORT_ERR)}')
+
+        port = self.get_option('port')
+        display.vvv(f'ESTABLISH PARAMIKO SSH CONNECTION FOR USER: {self.get_option("remote_user")} on PORT {to_text(port)} TO {self.get_option("remote_addr")}',
+                    host=self.get_option('remote_addr'))
+
+        ssh = paramiko.SSHClient()
+
+        # Set pubkey and hostkey algorithms to disable, the only manipulation allowed currently
+        # is keeping or omitting rsa-sha2 algorithms
+        # default_keys: t.Tuple[str] = ()
+        paramiko_preferred_pubkeys = getattr(paramiko.Transport, '_preferred_pubkeys', ())
+        paramiko_preferred_hostkeys = getattr(paramiko.Transport, '_preferred_keys', ())
+        use_rsa_sha2_algorithms = self.get_option('use_rsa_sha2_algorithms')
+        disabled_algorithms: t.Dict[str, t.Iterable[str]] = {}
+        if not use_rsa_sha2_algorithms:
+            if paramiko_preferred_pubkeys:
+                disabled_algorithms['pubkeys'] = tuple(a for a in paramiko_preferred_pubkeys if 'rsa-sha2' in a)
+            if paramiko_preferred_hostkeys:
+                disabled_algorithms['keys'] = tuple(a for a in paramiko_preferred_hostkeys if 'rsa-sha2' in a)
+
+        # override paramiko's default logger name
+        if self._log_channel is not None:
+            ssh.set_log_channel(self._log_channel)
+
+        self.keyfile = os.path.expanduser('~/.ssh/known_hosts')
+
+        if self.get_option('host_key_checking'):
+            for ssh_known_hosts in ('/etc/ssh/ssh_known_hosts', '/etc/openssh/ssh_known_hosts'):
+                try:
+                    ssh.load_system_host_keys(ssh_known_hosts)
+                    break
+                except IOError:
+                    pass  # file was not found, but not required to function
+                except paramiko.hostkeys.InvalidHostKey as e:
+                    raise AnsibleConnectionFailure(f'Invalid host key: {to_text(e.line)}')
+            try:
+                ssh.load_system_host_keys()
+            except paramiko.hostkeys.InvalidHostKey as e:
+                raise AnsibleConnectionFailure(f'Invalid host key: {to_text(e.line)}')
+
+        ssh_connect_kwargs = self._parse_proxy_command(port)
+        ssh.set_missing_host_key_policy(MyAddPolicy(self))
+        conn_password = self.get_option('password')
+        allow_agent = True
+
+        if conn_password is not None:
+            allow_agent = False
+
+        try:
+            key_filename = None
+            if self.get_option('private_key_file'):
+                key_filename = os.path.expanduser(self.get_option('private_key_file'))
+
+            # paramiko 2.2 introduced auth_timeout parameter
+            if LooseVersion(paramiko.__version__) >= LooseVersion('2.2.0'):
+                ssh_connect_kwargs['auth_timeout'] = self.get_option('timeout')
+
+            # paramiko 1.15 introduced banner timeout parameter
+            if LooseVersion(paramiko.__version__) >= LooseVersion('1.15.0'):
+                ssh_connect_kwargs['banner_timeout'] = self.get_option('banner_timeout')
+
+            ssh.connect(
+                self.get_option('remote_addr').lower(),
+                username=self.get_option('remote_user'),
+                allow_agent=allow_agent,
+                look_for_keys=self.get_option('look_for_keys'),
+                key_filename=key_filename,
+                password=conn_password,
+                timeout=self.get_option('timeout'),
+                port=port,
+                disabled_algorithms=disabled_algorithms,
+                **ssh_connect_kwargs,
+            )
+        except paramiko.ssh_exception.BadHostKeyException as e:
+            raise AnsibleConnectionFailure(f'host key mismatch for {to_text(e.hostname)}')
+        except paramiko.ssh_exception.AuthenticationException as e:
+            msg = f'Failed to authenticate: {e}'
+            raise AnsibleAuthenticationFailure(msg)
+        except Exception as e:
+            msg = to_text(e)
+            if u'PID check failed' in msg:
+                raise AnsibleError('paramiko version issue, please upgrade paramiko on the machine running ansible')
+            elif u'Private key file is encrypted' in msg:
+                msg = f'ssh {self.get_option("remote_user")}@{self.get_options("remote_addr")}:{port} : ' + \
+                    f'{msg}\nTo connect as a different user, use -u <username>.'
+                raise AnsibleConnectionFailure(msg)
+            else:
+                raise AnsibleConnectionFailure(msg)
+        self.ssh = ssh
+        self._connected = True
+        return self
+
+    def _any_keys_added(self) -> bool:
+        for hostname, keys in self.ssh._host_keys.items():
+            for keytype, key in keys.items():
+                added_this_time = getattr(key, '_added_by_ansible_this_time', False)
+                if added_this_time:
+                    return True
+        return False
+
+    def _save_ssh_host_keys(self, filename: str) -> None:
+        """
+        not using the paramiko save_ssh_host_keys function as we want to add new SSH keys at the bottom so folks
+        don't complain about it :)
+        """
+
+        if not self._any_keys_added():
+            return
+
+        path = os.path.expanduser('~/.ssh')
+        makedirs_safe(path)
+
+        with open(filename, 'w') as f:
+            for hostname, keys in self.ssh._host_keys.items():
+                for keytype, key in keys.items():
+                    # was f.write
+                    added_this_time = getattr(key, '_added_by_ansible_this_time', False)
+                    if not added_this_time:
+                        f.write(f'{hostname} {keytype} {key.get_base64()}\n')
+
+            for hostname, keys in self.ssh._host_keys.items():
+                for keytype, key in keys.items():
+                    added_this_time = getattr(key, '_added_by_ansible_this_time', False)
+                    if added_this_time:
+                        f.write(f'{hostname} {keytype} {key.get_base64()}\n')
+
+    def _build_wsl_command(self, cmd: str) -> str:
+        wsl_distribution = self.get_option('wsl_distribution')
+        if not wsl_distribution:
+            raise AnsibleError('A wsl_distribution is required but none was supplied')
+        become = self.get_option('become')
+        become_user = self.get_option('become_user')
+        if become and become_user:
+            wsl_user = become_user
+        else:
+            wsl_user = self.get_option('wsl_user')
+        args = ['wsl.exe', '--distribution', wsl_distribution]
+        if wsl_user:
+            args.extend(['--user', wsl_user])
+        args.extend(['--'])
+        args.extend(shlex.split(cmd))
+        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 """
+
+        cmd = self._build_wsl_command(cmd)
+
+        super(Connection, self).exec_command(cmd, in_data=in_data, sudoable=sudoable)
+
+        bufsize = 4096
+
+        try:
+            self.ssh.get_transport().set_keepalive(5)
+            chan = self.ssh.get_transport().open_session()
+        except Exception as e:
+            text_e = to_text(e)
+            msg = 'Failed to open session'
+            if text_e:
+                msg += f': {text_e}'
+            raise AnsibleConnectionFailure(to_native(msg))
+
+        display.vvv(f'EXEC {cmd}', host=self.get_option('remote_addr'))
+
+        cmd = to_bytes(cmd, errors='surrogate_or_strict')
+
+        no_prompt_out = b''
+        no_prompt_err = b''
+        become_output = b''
+
+        try:
+            chan.exec_command(cmd)
+            if self.become and self.become.expect_prompt():
+                password_prompt = False
+                become_success = False
+                while not (become_success or password_prompt):
+                    display.debug('Waiting for Privilege Escalation input')
+
+                    chunk = chan.recv(bufsize)
+                    display.debug(f'chunk is: {to_text(chunk)}')
+                    if not chunk:
+                        if b'unknown user' in become_output:
+                            n_become_user = to_native(self.become.get_option('become_user'))
+                            raise AnsibleError(f'user {n_become_user} does not exist')
+                        else:
+                            break
+                            # raise AnsibleError('ssh connection closed waiting for password prompt')
+                    become_output += chunk
+
+                    # need to check every line because we might get lectured
+                    # and we might get the middle of a line in a chunk
+                    for line in become_output.splitlines(True):
+                        if self.become.check_success(line):
+                            become_success = True
+                            break
+                        elif self.become.check_password_prompt(line):
+                            password_prompt = True
+                            break
+
+                if password_prompt:
+                    if self.become:
+                        become_pass = self.become.get_option('become_pass')
+                        chan.sendall(to_bytes(become_pass, errors='surrogate_or_strict') + b'\n')
path: plugins/connection/wsl.py
user: login: russoz
body: Minor style nit picking. It might be leaner as:
```suggestion
                        chan.sendall(to_bytes(become_pass + '\n', errors='surrogate_or_strict'))
```
created_at: 2025-02-23T02:16:48Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,758 @@
+# -*- coding: utf-8 -*-
+# Derived from ansible/plugins/connection/proxmox_pct_remote.py (c) 2024 Nils Stein (@mietzen) <github.nstein@mailbox.org>
+# Derived from ansible/plugins/connection/paramiko_ssh.py (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
+# Copyright (c) 2025 Rui Lopes (@rgl) <ruilopes.com>
+# Copyright (c) 2025 Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import annotations
+
+DOCUMENTATION = r"""
+author: Rui Lopes (@rgl) <ruilopes.com>
+name: wsl
+short_description: Run tasks in WSL distribution using wsl.exe CLI via SSH
+requirements:
+  - paramiko
+description:
+  - Run commands or put/fetch files to an existing WSL distribution using wsl.exe CLI via SSH.
+  - Uses the Python SSH implementation (Paramiko) to connect to the WSL host.
+version_added: "10.3.0"
path: plugins/connection/wsl.py
user: login: rgl
body: done. please check it now.
created_at: 2025-02-23T12:06:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,758 @@
+# -*- coding: utf-8 -*-
+# Derived from ansible/plugins/connection/proxmox_pct_remote.py (c) 2024 Nils Stein (@mietzen) <github.nstein@mailbox.org>
+# Derived from ansible/plugins/connection/paramiko_ssh.py (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
+# Copyright (c) 2025 Rui Lopes (@rgl) <ruilopes.com>
+# Copyright (c) 2025 Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import annotations
+
+DOCUMENTATION = r"""
+author: Rui Lopes (@rgl) <ruilopes.com>
+name: wsl
+short_description: Run tasks in WSL distribution using wsl.exe CLI via SSH
+requirements:
+  - paramiko
+description:
+  - Run commands or put/fetch files to an existing WSL distribution using wsl.exe CLI via SSH.
+  - Uses the Python SSH implementation (Paramiko) to connect to the WSL host.
+version_added: "10.3.0"
+options:
+  remote_addr:
+    description:
+      - Address of the remote target.
+    default: inventory_hostname
+    type: string
+    vars:
+      - name: inventory_hostname
+      - name: ansible_host
+      - name: ansible_ssh_host
+      - name: ansible_paramiko_host
+  port:
+    description: Remote port to connect to.
+    type: int
+    default: 22
+    ini:
+      - section: defaults
+        key: remote_port
+      - section: paramiko_connection
+        key: remote_port
+    env:
+      - name: ANSIBLE_REMOTE_PORT
+      - name: ANSIBLE_REMOTE_PARAMIKO_PORT
+    vars:
+      - name: ansible_port
+      - name: ansible_ssh_port
+      - name: ansible_paramiko_port
+    keyword:
+      - name: port
+  remote_user:
+    description:
+      - User to login/authenticate as.
+      - Can be set from the CLI via the C(--user) or C(-u) options.
+    type: string
+    vars:
+      - name: ansible_user
+      - name: ansible_ssh_user
+      - name: ansible_paramiko_user
+    env:
+      - name: ANSIBLE_REMOTE_USER
+      - name: ANSIBLE_PARAMIKO_REMOTE_USER
+    ini:
+      - section: defaults
+        key: remote_user
+      - section: paramiko_connection
+        key: remote_user
+    keyword:
+      - name: remote_user
+  password:
+    description:
+      - Secret used to either login the SSH server or as a passphrase for SSH keys that require it.
+      - Can be set from the CLI via the C(--ask-pass) option.
+    type: string
+    vars:
+      - name: ansible_password
+      - name: ansible_ssh_pass
+      - name: ansible_ssh_password
+      - name: ansible_paramiko_pass
+      - name: ansible_paramiko_password
+  use_rsa_sha2_algorithms:
+    description:
+      - Whether or not to enable RSA SHA2 algorithms for pubkeys and hostkeys.
+      - On paramiko versions older than 2.9, this only affects hostkeys.
+      - For behavior matching paramiko<2.9 set this to V(false).
+    vars:
+      - name: ansible_paramiko_use_rsa_sha2_algorithms
+    ini:
+      - {key: use_rsa_sha2_algorithms, section: paramiko_connection}
+    env:
+      - {name: ANSIBLE_PARAMIKO_USE_RSA_SHA2_ALGORITHMS}
+    default: true
+    type: boolean
+  host_key_auto_add:
+    description: "Automatically add host keys to C(~/.ssh/known_hosts)."
+    env:
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_AUTO_ADD
+    ini:
+      - key: host_key_auto_add
+        section: paramiko_connection
+    type: boolean
+  look_for_keys:
+    default: True
+    description: "Set to V(false) to disable searching for private key files in C(~/.ssh/)."
+    env:
+      - name: ANSIBLE_PARAMIKO_LOOK_FOR_KEYS
+    ini:
+      - {key: look_for_keys, section: paramiko_connection}
+    type: boolean
+  proxy_command:
+    default: ""
+    description:
+      - Proxy information for running the connection via a jumphost.
+    type: string
+    env:
+      - name: ANSIBLE_PARAMIKO_PROXY_COMMAND
+    ini:
+      - {key: proxy_command, section: paramiko_connection}
+    vars:
+      - name: ansible_paramiko_proxy_command
+  record_host_keys:
+    default: True
+    description: "Save the host keys to a file."
+    env:
+      - name: ANSIBLE_PARAMIKO_RECORD_HOST_KEYS
+    ini:
+      - section: paramiko_connection
+        key: record_host_keys
+    type: boolean
+  host_key_checking:
+    description: "Set this to V(false) if you want to avoid host key checking by the underlying tools Ansible uses to connect to the host."
+    type: boolean
+    default: true
+    env:
+      - name: ANSIBLE_HOST_KEY_CHECKING
+      - name: ANSIBLE_SSH_HOST_KEY_CHECKING
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_CHECKING
+    ini:
+      - section: defaults
+        key: host_key_checking
+      - section: paramiko_connection
+        key: host_key_checking
+    vars:
+      - name: ansible_host_key_checking
+      - name: ansible_ssh_host_key_checking
+      - name: ansible_paramiko_host_key_checking
+  use_persistent_connections:
+    description: "Toggles the use of persistence for connections."
+    type: boolean
+    default: False
+    env:
+      - name: ANSIBLE_USE_PERSISTENT_CONNECTIONS
+    ini:
+      - section: defaults
+        key: use_persistent_connections
+  banner_timeout:
+    type: float
+    default: 30
+    description:
+      - Configures, in seconds, the amount of time to wait for the SSH
+        banner to be presented. This option is supported by paramiko
+        version 1.15.0 or newer.
+    ini:
+      - section: paramiko_connection
+        key: banner_timeout
+    env:
+      - name: ANSIBLE_PARAMIKO_BANNER_TIMEOUT
+  timeout:
+    type: int
+    default: 10
+    description: Number of seconds until the plugin gives up on failing to establish a TCP connection.
+    ini:
+      - section: defaults
+        key: timeout
+      - section: ssh_connection
+        key: timeout
+      - section: paramiko_connection
+        key: timeout
+    env:
+      - name: ANSIBLE_TIMEOUT
+      - name: ANSIBLE_SSH_TIMEOUT
+      - name: ANSIBLE_PARAMIKO_TIMEOUT
+    vars:
+      - name: ansible_ssh_timeout
+      - name: ansible_paramiko_timeout
+    cli:
+      - name: timeout
+  lock_file_timeout:
+    type: int
+    default: 60
+    description: Number of seconds until the plugin gives up on trying to write a lock file when writing SSH known host keys.
+    vars:
+      - name: ansible_lock_file_timeout
+    env:
+      - name: ANSIBLE_LOCK_FILE_TIMEOUT
+  private_key_file:
+    description:
+      - Path to private key file to use for authentication.
+    type: string
+    ini:
+      - section: defaults
+        key: private_key_file
+      - section: paramiko_connection
+        key: private_key_file
+    env:
+      - name: ANSIBLE_PRIVATE_KEY_FILE
+      - name: ANSIBLE_PARAMIKO_PRIVATE_KEY_FILE
+    vars:
+      - name: ansible_private_key_file
+      - name: ansible_ssh_private_key_file
+      - name: ansible_paramiko_private_key_file
+    cli:
+      - name: private_key_file
+        option: "--private-key"
+  wsl_distribution:
+    description:
+      - WSL distribution name
+    type: string
+    vars:
+      - name: wsl_distribution
+  wsl_user:
+    description:
+      - WSL distribution user
+    type: string
+    vars:
+      - name: wsl_user
+  become_user:
+    description:
+      - WSL distribution user
+    type: string
+    default: root
+    vars:
+      - name: become_user
+      - name: ansible_become_user
+  become:
+    description:
+      - whether to use the user defined by ansible_become_user.
+    type: bool
+    default: false
+    vars:
+      - name: become
+      - name: ansible_become
+"""
+
+EXAMPLES = r"""
+# ------------------------
+# Inventory: inventory.yml
+# ------------------------
+# all:
+#   children:
+#     wsl:
+#       hosts:
+#         ubuntu:
+#           ansible_host: 10.0.0.10
+#           wsl_distribution: ubuntu
+#         debian:
+#           ansible_host: 10.0.0.10
+#           wsl_distribution: debian
+#       vars:
+#         ansible_connection: community.general.wsl
+#         ansible_user: vagrant
+#         wsl_user: wsl
path: plugins/connection/wsl.py
user: login: rgl
body: ah, much better! please check it now.
created_at: 2025-02-23T12:06:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,758 @@
+# -*- coding: utf-8 -*-
+# Derived from ansible/plugins/connection/proxmox_pct_remote.py (c) 2024 Nils Stein (@mietzen) <github.nstein@mailbox.org>
+# Derived from ansible/plugins/connection/paramiko_ssh.py (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
+# Copyright (c) 2025 Rui Lopes (@rgl) <ruilopes.com>
+# Copyright (c) 2025 Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import annotations
+
+DOCUMENTATION = r"""
+author: Rui Lopes (@rgl) <ruilopes.com>
+name: wsl
+short_description: Run tasks in WSL distribution using wsl.exe CLI via SSH
+requirements:
+  - paramiko
+description:
+  - Run commands or put/fetch files to an existing WSL distribution using wsl.exe CLI via SSH.
+  - Uses the Python SSH implementation (Paramiko) to connect to the WSL host.
+version_added: "10.3.0"
+options:
+  remote_addr:
+    description:
+      - Address of the remote target.
+    default: inventory_hostname
+    type: string
+    vars:
+      - name: inventory_hostname
+      - name: ansible_host
+      - name: ansible_ssh_host
+      - name: ansible_paramiko_host
+  port:
+    description: Remote port to connect to.
+    type: int
+    default: 22
+    ini:
+      - section: defaults
+        key: remote_port
+      - section: paramiko_connection
+        key: remote_port
+    env:
+      - name: ANSIBLE_REMOTE_PORT
+      - name: ANSIBLE_REMOTE_PARAMIKO_PORT
+    vars:
+      - name: ansible_port
+      - name: ansible_ssh_port
+      - name: ansible_paramiko_port
+    keyword:
+      - name: port
+  remote_user:
+    description:
+      - User to login/authenticate as.
+      - Can be set from the CLI via the C(--user) or C(-u) options.
+    type: string
+    vars:
+      - name: ansible_user
+      - name: ansible_ssh_user
+      - name: ansible_paramiko_user
+    env:
+      - name: ANSIBLE_REMOTE_USER
+      - name: ANSIBLE_PARAMIKO_REMOTE_USER
+    ini:
+      - section: defaults
+        key: remote_user
+      - section: paramiko_connection
+        key: remote_user
+    keyword:
+      - name: remote_user
+  password:
+    description:
+      - Secret used to either login the SSH server or as a passphrase for SSH keys that require it.
+      - Can be set from the CLI via the C(--ask-pass) option.
+    type: string
+    vars:
+      - name: ansible_password
+      - name: ansible_ssh_pass
+      - name: ansible_ssh_password
+      - name: ansible_paramiko_pass
+      - name: ansible_paramiko_password
+  use_rsa_sha2_algorithms:
+    description:
+      - Whether or not to enable RSA SHA2 algorithms for pubkeys and hostkeys.
+      - On paramiko versions older than 2.9, this only affects hostkeys.
+      - For behavior matching paramiko<2.9 set this to V(false).
+    vars:
+      - name: ansible_paramiko_use_rsa_sha2_algorithms
+    ini:
+      - {key: use_rsa_sha2_algorithms, section: paramiko_connection}
+    env:
+      - {name: ANSIBLE_PARAMIKO_USE_RSA_SHA2_ALGORITHMS}
+    default: true
+    type: boolean
+  host_key_auto_add:
+    description: "Automatically add host keys to C(~/.ssh/known_hosts)."
+    env:
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_AUTO_ADD
+    ini:
+      - key: host_key_auto_add
+        section: paramiko_connection
+    type: boolean
+  look_for_keys:
+    default: True
+    description: "Set to V(false) to disable searching for private key files in C(~/.ssh/)."
+    env:
+      - name: ANSIBLE_PARAMIKO_LOOK_FOR_KEYS
+    ini:
+      - {key: look_for_keys, section: paramiko_connection}
+    type: boolean
+  proxy_command:
+    default: ""
+    description:
+      - Proxy information for running the connection via a jumphost.
+    type: string
+    env:
+      - name: ANSIBLE_PARAMIKO_PROXY_COMMAND
+    ini:
+      - {key: proxy_command, section: paramiko_connection}
+    vars:
+      - name: ansible_paramiko_proxy_command
+  record_host_keys:
+    default: True
+    description: "Save the host keys to a file."
+    env:
+      - name: ANSIBLE_PARAMIKO_RECORD_HOST_KEYS
+    ini:
+      - section: paramiko_connection
+        key: record_host_keys
+    type: boolean
+  host_key_checking:
+    description: "Set this to V(false) if you want to avoid host key checking by the underlying tools Ansible uses to connect to the host."
+    type: boolean
+    default: true
+    env:
+      - name: ANSIBLE_HOST_KEY_CHECKING
+      - name: ANSIBLE_SSH_HOST_KEY_CHECKING
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_CHECKING
+    ini:
+      - section: defaults
+        key: host_key_checking
+      - section: paramiko_connection
+        key: host_key_checking
+    vars:
+      - name: ansible_host_key_checking
+      - name: ansible_ssh_host_key_checking
+      - name: ansible_paramiko_host_key_checking
+  use_persistent_connections:
+    description: "Toggles the use of persistence for connections."
+    type: boolean
+    default: False
+    env:
+      - name: ANSIBLE_USE_PERSISTENT_CONNECTIONS
+    ini:
+      - section: defaults
+        key: use_persistent_connections
+  banner_timeout:
+    type: float
+    default: 30
+    description:
+      - Configures, in seconds, the amount of time to wait for the SSH
+        banner to be presented. This option is supported by paramiko
+        version 1.15.0 or newer.
+    ini:
+      - section: paramiko_connection
+        key: banner_timeout
+    env:
+      - name: ANSIBLE_PARAMIKO_BANNER_TIMEOUT
+  timeout:
+    type: int
+    default: 10
+    description: Number of seconds until the plugin gives up on failing to establish a TCP connection.
+    ini:
+      - section: defaults
+        key: timeout
+      - section: ssh_connection
+        key: timeout
+      - section: paramiko_connection
+        key: timeout
+    env:
+      - name: ANSIBLE_TIMEOUT
+      - name: ANSIBLE_SSH_TIMEOUT
+      - name: ANSIBLE_PARAMIKO_TIMEOUT
+    vars:
+      - name: ansible_ssh_timeout
+      - name: ansible_paramiko_timeout
+    cli:
+      - name: timeout
+  lock_file_timeout:
+    type: int
+    default: 60
+    description: Number of seconds until the plugin gives up on trying to write a lock file when writing SSH known host keys.
+    vars:
+      - name: ansible_lock_file_timeout
+    env:
+      - name: ANSIBLE_LOCK_FILE_TIMEOUT
+  private_key_file:
+    description:
+      - Path to private key file to use for authentication.
+    type: string
+    ini:
+      - section: defaults
+        key: private_key_file
+      - section: paramiko_connection
+        key: private_key_file
+    env:
+      - name: ANSIBLE_PRIVATE_KEY_FILE
+      - name: ANSIBLE_PARAMIKO_PRIVATE_KEY_FILE
+    vars:
+      - name: ansible_private_key_file
+      - name: ansible_ssh_private_key_file
+      - name: ansible_paramiko_private_key_file
+    cli:
+      - name: private_key_file
+        option: "--private-key"
+  wsl_distribution:
+    description:
+      - WSL distribution name
+    type: string
+    vars:
+      - name: wsl_distribution
+  wsl_user:
+    description:
+      - WSL distribution user
+    type: string
+    vars:
+      - name: wsl_user
+  become_user:
+    description:
+      - WSL distribution user
+    type: string
+    default: root
+    vars:
+      - name: become_user
+      - name: ansible_become_user
+  become:
+    description:
+      - whether to use the user defined by ansible_become_user.
+    type: bool
+    default: false
+    vars:
+      - name: become
+      - name: ansible_become
+"""
+
+EXAMPLES = r"""
+# ------------------------
+# Inventory: inventory.yml
+# ------------------------
+# all:
+#   children:
+#     wsl:
+#       hosts:
+#         ubuntu:
+#           ansible_host: 10.0.0.10
+#           wsl_distribution: ubuntu
+#         debian:
+#           ansible_host: 10.0.0.10
+#           wsl_distribution: debian
+#       vars:
+#         ansible_connection: community.general.wsl
+#         ansible_user: vagrant
+#         wsl_user: wsl
path: plugins/connection/wsl.py
user: login: rgl
body: oh, it seems that comment can only have a single document due to https://github.com/ansible-collections/community.general/pull/9795#issuecomment-2676827673

should this change be reverted? or the CI be changed?
created_at: 2025-02-23T12:16:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,758 @@
+# -*- coding: utf-8 -*-
+# Derived from ansible/plugins/connection/proxmox_pct_remote.py (c) 2024 Nils Stein (@mietzen) <github.nstein@mailbox.org>
+# Derived from ansible/plugins/connection/paramiko_ssh.py (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
+# Copyright (c) 2025 Rui Lopes (@rgl) <ruilopes.com>
+# Copyright (c) 2025 Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import annotations
+
+DOCUMENTATION = r"""
+author: Rui Lopes (@rgl) <ruilopes.com>
+name: wsl
+short_description: Run tasks in WSL distribution using wsl.exe CLI via SSH
+requirements:
+  - paramiko
+description:
+  - Run commands or put/fetch files to an existing WSL distribution using wsl.exe CLI via SSH.
+  - Uses the Python SSH implementation (Paramiko) to connect to the WSL host.
+version_added: "10.3.0"
+options:
+  remote_addr:
+    description:
+      - Address of the remote target.
+    default: inventory_hostname
+    type: string
+    vars:
+      - name: inventory_hostname
+      - name: ansible_host
+      - name: ansible_ssh_host
+      - name: ansible_paramiko_host
+  port:
+    description: Remote port to connect to.
+    type: int
+    default: 22
+    ini:
+      - section: defaults
+        key: remote_port
+      - section: paramiko_connection
+        key: remote_port
+    env:
+      - name: ANSIBLE_REMOTE_PORT
+      - name: ANSIBLE_REMOTE_PARAMIKO_PORT
+    vars:
+      - name: ansible_port
+      - name: ansible_ssh_port
+      - name: ansible_paramiko_port
+    keyword:
+      - name: port
+  remote_user:
+    description:
+      - User to login/authenticate as.
+      - Can be set from the CLI via the C(--user) or C(-u) options.
+    type: string
+    vars:
+      - name: ansible_user
+      - name: ansible_ssh_user
+      - name: ansible_paramiko_user
+    env:
+      - name: ANSIBLE_REMOTE_USER
+      - name: ANSIBLE_PARAMIKO_REMOTE_USER
+    ini:
+      - section: defaults
+        key: remote_user
+      - section: paramiko_connection
+        key: remote_user
+    keyword:
+      - name: remote_user
+  password:
+    description:
+      - Secret used to either login the SSH server or as a passphrase for SSH keys that require it.
+      - Can be set from the CLI via the C(--ask-pass) option.
+    type: string
+    vars:
+      - name: ansible_password
+      - name: ansible_ssh_pass
+      - name: ansible_ssh_password
+      - name: ansible_paramiko_pass
+      - name: ansible_paramiko_password
+  use_rsa_sha2_algorithms:
+    description:
+      - Whether or not to enable RSA SHA2 algorithms for pubkeys and hostkeys.
+      - On paramiko versions older than 2.9, this only affects hostkeys.
+      - For behavior matching paramiko<2.9 set this to V(false).
+    vars:
+      - name: ansible_paramiko_use_rsa_sha2_algorithms
+    ini:
+      - {key: use_rsa_sha2_algorithms, section: paramiko_connection}
+    env:
+      - {name: ANSIBLE_PARAMIKO_USE_RSA_SHA2_ALGORITHMS}
+    default: true
+    type: boolean
+  host_key_auto_add:
+    description: "Automatically add host keys to C(~/.ssh/known_hosts)."
+    env:
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_AUTO_ADD
+    ini:
+      - key: host_key_auto_add
+        section: paramiko_connection
+    type: boolean
+  look_for_keys:
+    default: True
+    description: "Set to V(false) to disable searching for private key files in C(~/.ssh/)."
+    env:
+      - name: ANSIBLE_PARAMIKO_LOOK_FOR_KEYS
+    ini:
+      - {key: look_for_keys, section: paramiko_connection}
+    type: boolean
+  proxy_command:
+    default: ""
+    description:
+      - Proxy information for running the connection via a jumphost.
+    type: string
+    env:
+      - name: ANSIBLE_PARAMIKO_PROXY_COMMAND
+    ini:
+      - {key: proxy_command, section: paramiko_connection}
+    vars:
+      - name: ansible_paramiko_proxy_command
+  record_host_keys:
+    default: True
+    description: "Save the host keys to a file."
+    env:
+      - name: ANSIBLE_PARAMIKO_RECORD_HOST_KEYS
+    ini:
+      - section: paramiko_connection
+        key: record_host_keys
+    type: boolean
+  host_key_checking:
+    description: "Set this to V(false) if you want to avoid host key checking by the underlying tools Ansible uses to connect to the host."
+    type: boolean
+    default: true
+    env:
+      - name: ANSIBLE_HOST_KEY_CHECKING
+      - name: ANSIBLE_SSH_HOST_KEY_CHECKING
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_CHECKING
+    ini:
+      - section: defaults
+        key: host_key_checking
+      - section: paramiko_connection
+        key: host_key_checking
+    vars:
+      - name: ansible_host_key_checking
+      - name: ansible_ssh_host_key_checking
+      - name: ansible_paramiko_host_key_checking
+  use_persistent_connections:
+    description: "Toggles the use of persistence for connections."
+    type: boolean
+    default: False
+    env:
+      - name: ANSIBLE_USE_PERSISTENT_CONNECTIONS
+    ini:
+      - section: defaults
+        key: use_persistent_connections
+  banner_timeout:
+    type: float
+    default: 30
+    description:
+      - Configures, in seconds, the amount of time to wait for the SSH
+        banner to be presented. This option is supported by paramiko
+        version 1.15.0 or newer.
+    ini:
+      - section: paramiko_connection
+        key: banner_timeout
+    env:
+      - name: ANSIBLE_PARAMIKO_BANNER_TIMEOUT
+  timeout:
+    type: int
+    default: 10
+    description: Number of seconds until the plugin gives up on failing to establish a TCP connection.
+    ini:
+      - section: defaults
+        key: timeout
+      - section: ssh_connection
+        key: timeout
+      - section: paramiko_connection
+        key: timeout
+    env:
+      - name: ANSIBLE_TIMEOUT
+      - name: ANSIBLE_SSH_TIMEOUT
+      - name: ANSIBLE_PARAMIKO_TIMEOUT
+    vars:
+      - name: ansible_ssh_timeout
+      - name: ansible_paramiko_timeout
+    cli:
+      - name: timeout
+  lock_file_timeout:
+    type: int
+    default: 60
+    description: Number of seconds until the plugin gives up on trying to write a lock file when writing SSH known host keys.
+    vars:
+      - name: ansible_lock_file_timeout
+    env:
+      - name: ANSIBLE_LOCK_FILE_TIMEOUT
+  private_key_file:
+    description:
+      - Path to private key file to use for authentication.
+    type: string
+    ini:
+      - section: defaults
+        key: private_key_file
+      - section: paramiko_connection
+        key: private_key_file
+    env:
+      - name: ANSIBLE_PRIVATE_KEY_FILE
+      - name: ANSIBLE_PARAMIKO_PRIVATE_KEY_FILE
+    vars:
+      - name: ansible_private_key_file
+      - name: ansible_ssh_private_key_file
+      - name: ansible_paramiko_private_key_file
+    cli:
+      - name: private_key_file
+        option: "--private-key"
+  wsl_distribution:
+    description:
+      - WSL distribution name
+    type: string
+    vars:
+      - name: wsl_distribution
+  wsl_user:
+    description:
+      - WSL distribution user
+    type: string
+    vars:
+      - name: wsl_user
+  become_user:
+    description:
+      - WSL distribution user
+    type: string
+    default: root
+    vars:
+      - name: become_user
+      - name: ansible_become_user
+  become:
+    description:
+      - whether to use the user defined by ansible_become_user.
+    type: bool
+    default: false
+    vars:
+      - name: become
+      - name: ansible_become
+"""
+
+EXAMPLES = r"""
+# ------------------------
+# Inventory: inventory.yml
+# ------------------------
+# all:
+#   children:
+#     wsl:
+#       hosts:
+#         ubuntu:
+#           ansible_host: 10.0.0.10
+#           wsl_distribution: ubuntu
+#         debian:
+#           ansible_host: 10.0.0.10
+#           wsl_distribution: debian
+#       vars:
+#         ansible_connection: community.general.wsl
+#         ansible_user: vagrant
+#         wsl_user: wsl
+# ----------------------
+# Playbook: playbook.yml
+# ----------------------
+---
+- hosts: wsl
+  tasks:
+    - name: ping
+      ansible.builtin.ping:
+    - name: id
+      args:
+        executable: /bin/bash
+      ansible.builtin.shell: |
+        exec 2>&1
+        set -x
+        echo "$0"
+        pwd
+        id
+    - name: become id
+      become: true
+      args:
+        executable: /bin/bash
+      ansible.builtin.shell: |
+        exec 2>&1
+        set -x
+        echo "$0"
+        pwd
+        id
+"""
+
+import os
+import pathlib
+import shlex
+import socket
+import tempfile
+import typing as t
+
+from ansible.errors import (
+    AnsibleAuthenticationFailure,
+    AnsibleConnectionFailure,
+    AnsibleError,
+)
+from ansible_collections.community.general.plugins.module_utils._filelock import FileLock, LockTimeout
+from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
+from ansible.module_utils.compat.paramiko import PARAMIKO_IMPORT_ERR, paramiko
+from ansible.module_utils.compat.version import LooseVersion
+from ansible.plugins.connection import ConnectionBase
+from ansible.utils.display import Display
+from ansible.utils.path import makedirs_safe
+from binascii import hexlify
+from subprocess import list2cmdline
+
+
+display = Display()
+
+
+def authenticity_msg(hostname: str, ktype: str, fingerprint: str) -> str:
+    msg = f"""
+    paramiko: The authenticity of host '{hostname}' can't be established.
+    The {ktype} key fingerprint is {fingerprint}.
+    Are you sure you want to continue connecting (yes/no)?
+    """
+    return msg
+
+
+MissingHostKeyPolicy: type = object
+if paramiko:
+    MissingHostKeyPolicy = paramiko.MissingHostKeyPolicy
+
+
+class MyAddPolicy(MissingHostKeyPolicy):
+    """
+    Based on AutoAddPolicy in paramiko so we can determine when keys are added
+
+    and also prompt for input.
+
+    Policy for automatically adding the hostname and new host key to the
+    local L{HostKeys} object, and saving it. This is used by L{SSHClient}.
+    """
+
+    def __init__(self, connection: Connection) -> None:
+        self.connection = connection
+        self._options = connection._options
+
+    def missing_host_key(self, client, hostname, key) -> None:
+
+        if all((self.connection.get_option('host_key_checking'), not self.connection.get_option('host_key_auto_add'))):
+
+            fingerprint = hexlify(key.get_fingerprint())
+            ktype = key.get_name()
+
+            if self.connection.get_option('use_persistent_connections') or self.connection.force_persistence:
+                # don't print the prompt string since the user cannot respond
+                # to the question anyway
+                raise AnsibleError(authenticity_msg(hostname, ktype, fingerprint)[1:92])
+
+            inp = to_text(
+                display.prompt_until(authenticity_msg(hostname, ktype, fingerprint), private=False),
+                errors='surrogate_or_strict'
+            )
+
+            if inp.lower() not in ['yes', 'y', '']:
+                raise AnsibleError('host connection rejected by user')
+
+        key._added_by_ansible_this_time = True
+
+        # existing implementation below:
+        client._host_keys.add(hostname, key.get_name(), key)
+
+        # host keys are actually saved in close() function below
+        # in order to control ordering.
+
+
+class Connection(ConnectionBase):
+    """ SSH based connections (paramiko) to WSL """
+
+    transport = 'community.general.wsl'
+    _log_channel: str | None = None
path: plugins/connection/wsl.py
user: login: felixfontein
body: `str | None` is the more modern syntax. `Optional[str]` should only be needed if annotations have to be processed by code by older Python platforms. (For example when using pydantic.)
created_at: 2025-02-23T12:25:43Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,759 @@
+# -*- coding: utf-8 -*-
+# Derived from ansible/plugins/connection/proxmox_pct_remote.py (c) 2024 Nils Stein (@mietzen) <github.nstein@mailbox.org>
+# Derived from ansible/plugins/connection/paramiko_ssh.py (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
+# Copyright (c) 2025 Rui Lopes (@rgl) <ruilopes.com>
+# Copyright (c) 2025 Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import annotations
+
+DOCUMENTATION = r"""
+author: Rui Lopes (@rgl) <ruilopes.com>
+name: wsl
+short_description: Run tasks in WSL distribution using wsl.exe CLI via SSH
+requirements:
+  - paramiko
+description:
+  - Run commands or put/fetch files to an existing WSL distribution using wsl.exe CLI via SSH.
+  - Uses the Python SSH implementation (Paramiko) to connect to the WSL host.
+version_added: "10.4.0"
+options:
+  remote_addr:
+    description:
+      - Address of the remote target.
+    default: inventory_hostname
+    type: string
+    vars:
+      - name: inventory_hostname
+      - name: ansible_host
+      - name: ansible_ssh_host
+      - name: ansible_paramiko_host
+  port:
+    description: Remote port to connect to.
+    type: int
+    default: 22
+    ini:
+      - section: defaults
+        key: remote_port
+      - section: paramiko_connection
+        key: remote_port
+    env:
+      - name: ANSIBLE_REMOTE_PORT
+      - name: ANSIBLE_REMOTE_PARAMIKO_PORT
+    vars:
+      - name: ansible_port
+      - name: ansible_ssh_port
+      - name: ansible_paramiko_port
+    keyword:
+      - name: port
+  remote_user:
+    description:
+      - User to login/authenticate as.
+      - Can be set from the CLI via the C(--user) or C(-u) options.
+    type: string
+    vars:
+      - name: ansible_user
+      - name: ansible_ssh_user
+      - name: ansible_paramiko_user
+    env:
+      - name: ANSIBLE_REMOTE_USER
+      - name: ANSIBLE_PARAMIKO_REMOTE_USER
+    ini:
+      - section: defaults
+        key: remote_user
+      - section: paramiko_connection
+        key: remote_user
+    keyword:
+      - name: remote_user
+  password:
+    description:
+      - Secret used to either login the SSH server or as a passphrase for SSH keys that require it.
+      - Can be set from the CLI via the C(--ask-pass) option.
+    type: string
+    vars:
+      - name: ansible_password
+      - name: ansible_ssh_pass
+      - name: ansible_ssh_password
+      - name: ansible_paramiko_pass
+      - name: ansible_paramiko_password
+  use_rsa_sha2_algorithms:
+    description:
+      - Whether or not to enable RSA SHA2 algorithms for pubkeys and hostkeys.
+      - On paramiko versions older than 2.9, this only affects hostkeys.
+      - For behavior matching paramiko<2.9 set this to V(false).
+    vars:
+      - name: ansible_paramiko_use_rsa_sha2_algorithms
+    ini:
+      - {key: use_rsa_sha2_algorithms, section: paramiko_connection}
+    env:
+      - {name: ANSIBLE_PARAMIKO_USE_RSA_SHA2_ALGORITHMS}
+    default: true
+    type: boolean
+  host_key_auto_add:
+    description: "Automatically add host keys to C(~/.ssh/known_hosts)."
+    env:
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_AUTO_ADD
+    ini:
+      - key: host_key_auto_add
+        section: paramiko_connection
+    type: boolean
+  look_for_keys:
+    default: True
+    description: "Set to V(false) to disable searching for private key files in C(~/.ssh/)."
+    env:
+      - name: ANSIBLE_PARAMIKO_LOOK_FOR_KEYS
+    ini:
+      - {key: look_for_keys, section: paramiko_connection}
+    type: boolean
+  proxy_command:
+    default: ""
+    description:
+      - Proxy information for running the connection via a jumphost.
+    type: string
+    env:
+      - name: ANSIBLE_PARAMIKO_PROXY_COMMAND
+    ini:
+      - {key: proxy_command, section: paramiko_connection}
+    vars:
+      - name: ansible_paramiko_proxy_command
+  record_host_keys:
+    default: True
+    description: "Save the host keys to a file."
+    env:
+      - name: ANSIBLE_PARAMIKO_RECORD_HOST_KEYS
+    ini:
+      - section: paramiko_connection
+        key: record_host_keys
+    type: boolean
+  host_key_checking:
+    description: "Set this to V(false) if you want to avoid host key checking by the underlying tools Ansible uses to connect to the host."
+    type: boolean
+    default: true
+    env:
+      - name: ANSIBLE_HOST_KEY_CHECKING
+      - name: ANSIBLE_SSH_HOST_KEY_CHECKING
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_CHECKING
+    ini:
+      - section: defaults
+        key: host_key_checking
+      - section: paramiko_connection
+        key: host_key_checking
+    vars:
+      - name: ansible_host_key_checking
+      - name: ansible_ssh_host_key_checking
+      - name: ansible_paramiko_host_key_checking
+  use_persistent_connections:
+    description: "Toggles the use of persistence for connections."
+    type: boolean
+    default: False
+    env:
+      - name: ANSIBLE_USE_PERSISTENT_CONNECTIONS
+    ini:
+      - section: defaults
+        key: use_persistent_connections
+  banner_timeout:
+    type: float
+    default: 30
+    description:
+      - Configures, in seconds, the amount of time to wait for the SSH
+        banner to be presented. This option is supported by paramiko
+        version 1.15.0 or newer.
+    ini:
+      - section: paramiko_connection
+        key: banner_timeout
+    env:
+      - name: ANSIBLE_PARAMIKO_BANNER_TIMEOUT
+  timeout:
+    type: int
+    default: 10
+    description: Number of seconds until the plugin gives up on failing to establish a TCP connection.
+    ini:
+      - section: defaults
+        key: timeout
+      - section: ssh_connection
+        key: timeout
+      - section: paramiko_connection
+        key: timeout
+    env:
+      - name: ANSIBLE_TIMEOUT
+      - name: ANSIBLE_SSH_TIMEOUT
+      - name: ANSIBLE_PARAMIKO_TIMEOUT
+    vars:
+      - name: ansible_ssh_timeout
+      - name: ansible_paramiko_timeout
+    cli:
+      - name: timeout
+  lock_file_timeout:
+    type: int
+    default: 60
+    description: Number of seconds until the plugin gives up on trying to write a lock file when writing SSH known host keys.
+    vars:
+      - name: ansible_lock_file_timeout
+    env:
+      - name: ANSIBLE_LOCK_FILE_TIMEOUT
+  private_key_file:
+    description:
+      - Path to private key file to use for authentication.
+    type: string
+    ini:
+      - section: defaults
+        key: private_key_file
+      - section: paramiko_connection
+        key: private_key_file
+    env:
+      - name: ANSIBLE_PRIVATE_KEY_FILE
+      - name: ANSIBLE_PARAMIKO_PRIVATE_KEY_FILE
+    vars:
+      - name: ansible_private_key_file
+      - name: ansible_ssh_private_key_file
+      - name: ansible_paramiko_private_key_file
+    cli:
+      - name: private_key_file
+        option: "--private-key"
+  wsl_distribution:
+    description:
+      - WSL distribution name
+    type: string
+    vars:
+      - name: wsl_distribution
+  wsl_user:
+    description:
+      - WSL distribution user
+    type: string
+    vars:
+      - name: wsl_user
+  become_user:
+    description:
+      - WSL distribution user
+    type: string
+    default: root
+    vars:
+      - name: become_user
+      - name: ansible_become_user
+  become:
+    description:
+      - whether to use the user defined by ansible_become_user.
+    type: bool
+    default: false
+    vars:
+      - name: become
+      - name: ansible_become
+"""
+
+EXAMPLES = r"""
+# ------------------------
+# Inventory: inventory.yml
+# ------------------------
+---
+all:
+  children:
+    wsl:
+      hosts:
+        ubuntu:
+          ansible_host: 10.0.0.10
+          wsl_distribution: ubuntu
+        debian:
+          ansible_host: 10.0.0.10
+          wsl_distribution: debian
+      vars:
+        ansible_connection: community.general.wsl
+        ansible_user: vagrant
+        wsl_user: wsl
+# ----------------------
+# Playbook: playbook.yml
+# ----------------------
+---
+- hosts: wsl
+  tasks:
+    - name: ping
+      ansible.builtin.ping:
+    - name: id
+      args:
+        executable: /bin/bash
+      ansible.builtin.shell: |
+        exec 2>&1
+        set -x
+        echo "$0"
+        pwd
+        id
+    - name: become id
+      become: true
+      args:
+        executable: /bin/bash
+      ansible.builtin.shell: |
+        exec 2>&1
+        set -x
+        echo "$0"
+        pwd
+        id
+"""
+
+import os
+import pathlib
+import shlex
+import socket
+import tempfile
+import typing as t
+
+from ansible.errors import (
+    AnsibleAuthenticationFailure,
+    AnsibleConnectionFailure,
+    AnsibleError,
+)
+from ansible_collections.community.general.plugins.module_utils._filelock import FileLock, LockTimeout
+from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
+from ansible.module_utils.compat.paramiko import PARAMIKO_IMPORT_ERR, paramiko
+from ansible.module_utils.compat.version import LooseVersion
+from ansible.plugins.connection import ConnectionBase
+from ansible.utils.display import Display
+from ansible.utils.path import makedirs_safe
+from binascii import hexlify
+from subprocess import list2cmdline
+
+
+display = Display()
+
+
+def authenticity_msg(hostname: str, ktype: str, fingerprint: str) -> str:
+    msg = f"""
+    paramiko: The authenticity of host '{hostname}' can't be established.
+    The {ktype} key fingerprint is {fingerprint}.
+    Are you sure you want to continue connecting (yes/no)?
+    """
+    return msg
+
+
+MissingHostKeyPolicy: type = object
+if paramiko:
+    MissingHostKeyPolicy = paramiko.MissingHostKeyPolicy
+
+
+class MyAddPolicy(MissingHostKeyPolicy):
+    """
+    Based on AutoAddPolicy in paramiko so we can determine when keys are added
+
+    and also prompt for input.
+
+    Policy for automatically adding the hostname and new host key to the
+    local L{HostKeys} object, and saving it. This is used by L{SSHClient}.
+    """
+
+    def __init__(self, connection: Connection) -> None:
+        self.connection = connection
+        self._options = connection._options
+
+    def missing_host_key(self, client, hostname, key) -> None:
+
+        if all((self.connection.get_option('host_key_checking'), not self.connection.get_option('host_key_auto_add'))):
+
+            fingerprint = hexlify(key.get_fingerprint())
+            ktype = key.get_name()
+
+            if self.connection.get_option('use_persistent_connections') or self.connection.force_persistence:
+                # don't print the prompt string since the user cannot respond
+                # to the question anyway
+                raise AnsibleError(authenticity_msg(hostname, ktype, fingerprint)[1:92])
+
+            inp = to_text(
+                display.prompt_until(authenticity_msg(hostname, ktype, fingerprint), private=False),
+                errors='surrogate_or_strict'
+            )
+
+            if inp.lower() not in ['yes', 'y', '']:
+                raise AnsibleError('host connection rejected by user')
+
+        key._added_by_ansible_this_time = True
+
+        # existing implementation below:
+        client._host_keys.add(hostname, key.get_name(), key)
+
+        # host keys are actually saved in close() function below
+        # in order to control ordering.
+
+
+class Connection(ConnectionBase):
+    """ SSH based connections (paramiko) to WSL """
+
+    transport = 'community.general.wsl'
+    _log_channel: str | None = None
+
+    def __init__(self, play_context, new_stdin, *args, **kwargs):
+        super(Connection, self).__init__(play_context, new_stdin, *args, **kwargs)
+
+    def _set_log_channel(self, name: str) -> None:
+        """ Mimic paramiko.SSHClient.set_log_channel """
+        self._log_channel = name
+
+    def _parse_proxy_command(self, port: int = 22) -> dict[str, t.Any]:
+        proxy_command = self.get_option('proxy_command') or None
+
+        sock_kwarg = {}
+        if proxy_command:
+            replacers = {
+                '%h': self.get_option('remote_addr'),
+                '%p': port,
+                '%r': self.get_option('remote_user')
+            }
+            for find, replace in replacers.items():
+                proxy_command = proxy_command.replace(find, str(replace))
+            try:
+                sock_kwarg = {'sock': paramiko.ProxyCommand(proxy_command)}
+                display.vvv(f'CONFIGURE PROXY COMMAND FOR CONNECTION: {proxy_command}', host=self.get_option('remote_addr'))
+            except AttributeError:
+                display.warning('Paramiko ProxyCommand support unavailable. '
+                                'Please upgrade to Paramiko 1.9.0 or newer. '
+                                'Not using configured ProxyCommand')
+
+        return sock_kwarg
+
+    def _connect(self) -> Connection:
+        """ activates the connection object """
+
+        if paramiko is None:
+            raise AnsibleError(f'paramiko is not installed: {to_native(PARAMIKO_IMPORT_ERR)}')
+
+        port = self.get_option('port')
+        display.vvv(f'ESTABLISH PARAMIKO SSH CONNECTION FOR USER: {self.get_option("remote_user")} on PORT {to_text(port)} TO {self.get_option("remote_addr")}',
+                    host=self.get_option('remote_addr'))
+
+        ssh = paramiko.SSHClient()
+
+        # Set pubkey and hostkey algorithms to disable, the only manipulation allowed currently
+        # is keeping or omitting rsa-sha2 algorithms
+        # default_keys: t.Tuple[str] = ()
+        paramiko_preferred_pubkeys = getattr(paramiko.Transport, '_preferred_pubkeys', ())
+        paramiko_preferred_hostkeys = getattr(paramiko.Transport, '_preferred_keys', ())
+        use_rsa_sha2_algorithms = self.get_option('use_rsa_sha2_algorithms')
+        disabled_algorithms: t.Dict[str, t.Iterable[str]] = {}
+        if not use_rsa_sha2_algorithms:
+            if paramiko_preferred_pubkeys:
+                disabled_algorithms['pubkeys'] = tuple(a for a in paramiko_preferred_pubkeys if 'rsa-sha2' in a)
+            if paramiko_preferred_hostkeys:
+                disabled_algorithms['keys'] = tuple(a for a in paramiko_preferred_hostkeys if 'rsa-sha2' in a)
+
+        # override paramiko's default logger name
+        if self._log_channel is not None:
+            ssh.set_log_channel(self._log_channel)
+
+        self.keyfile = os.path.expanduser('~/.ssh/known_hosts')
+
+        if self.get_option('host_key_checking'):
+            for ssh_known_hosts in ('/etc/ssh/ssh_known_hosts', '/etc/openssh/ssh_known_hosts'):
+                try:
+                    ssh.load_system_host_keys(ssh_known_hosts)
+                    break
+                except IOError:
+                    pass  # file was not found, but not required to function
+                except paramiko.hostkeys.InvalidHostKey as e:
+                    raise AnsibleConnectionFailure(f'Invalid host key: {to_text(e.line)}')
+            try:
+                ssh.load_system_host_keys()
+            except paramiko.hostkeys.InvalidHostKey as e:
+                raise AnsibleConnectionFailure(f'Invalid host key: {to_text(e.line)}')
+
+        ssh_connect_kwargs = self._parse_proxy_command(port)
+        ssh.set_missing_host_key_policy(MyAddPolicy(self))
+        conn_password = self.get_option('password')
+        allow_agent = True
+
+        if conn_password is not None:
+            allow_agent = False
+
+        try:
+            key_filename = None
+            if self.get_option('private_key_file'):
+                key_filename = os.path.expanduser(self.get_option('private_key_file'))
+
+            # paramiko 2.2 introduced auth_timeout parameter
+            if LooseVersion(paramiko.__version__) >= LooseVersion('2.2.0'):
+                ssh_connect_kwargs['auth_timeout'] = self.get_option('timeout')
+
+            # paramiko 1.15 introduced banner timeout parameter
+            if LooseVersion(paramiko.__version__) >= LooseVersion('1.15.0'):
+                ssh_connect_kwargs['banner_timeout'] = self.get_option('banner_timeout')
+
+            ssh.connect(
+                self.get_option('remote_addr').lower(),
+                username=self.get_option('remote_user'),
+                allow_agent=allow_agent,
+                look_for_keys=self.get_option('look_for_keys'),
+                key_filename=key_filename,
+                password=conn_password,
+                timeout=self.get_option('timeout'),
+                port=port,
+                disabled_algorithms=disabled_algorithms,
+                **ssh_connect_kwargs,
+            )
+        except paramiko.ssh_exception.BadHostKeyException as e:
+            raise AnsibleConnectionFailure(f'host key mismatch for {to_text(e.hostname)}')
+        except paramiko.ssh_exception.AuthenticationException as e:
+            msg = f'Failed to authenticate: {e}'
+            raise AnsibleAuthenticationFailure(msg)
+        except Exception as e:
+            msg = to_text(e)
+            if u'PID check failed' in msg:
+                raise AnsibleError('paramiko version issue, please upgrade paramiko on the machine running ansible')
+            elif u'Private key file is encrypted' in msg:
+                msg = f'ssh {self.get_option("remote_user")}@{self.get_options("remote_addr")}:{port} : ' + \
+                    f'{msg}\nTo connect as a different user, use -u <username>.'
+                raise AnsibleConnectionFailure(msg)
+            else:
+                raise AnsibleConnectionFailure(msg)
+        self.ssh = ssh
+        self._connected = True
+        return self
+
+    def _any_keys_added(self) -> bool:
+        for hostname, keys in self.ssh._host_keys.items():
+            for keytype, key in keys.items():
+                added_this_time = getattr(key, '_added_by_ansible_this_time', False)
+                if added_this_time:
+                    return True
+        return False
+
+    def _save_ssh_host_keys(self, filename: str) -> None:
+        """
+        not using the paramiko save_ssh_host_keys function as we want to add new SSH keys at the bottom so folks
+        don't complain about it :)
+        """
+
+        if not self._any_keys_added():
+            return
+
+        path = os.path.expanduser('~/.ssh')
+        makedirs_safe(path)
+
+        with open(filename, 'w') as f:
+            for hostname, keys in self.ssh._host_keys.items():
+                for keytype, key in keys.items():
+                    # was f.write
+                    added_this_time = getattr(key, '_added_by_ansible_this_time', False)
+                    if not added_this_time:
+                        f.write(f'{hostname} {keytype} {key.get_base64()}\n')
+
+            for hostname, keys in self.ssh._host_keys.items():
+                for keytype, key in keys.items():
+                    added_this_time = getattr(key, '_added_by_ansible_this_time', False)
+                    if added_this_time:
+                        f.write(f'{hostname} {keytype} {key.get_base64()}\n')
+
+    def _build_wsl_command(self, cmd: str) -> str:
+        wsl_distribution = self.get_option('wsl_distribution')
+        if not wsl_distribution:
+            raise AnsibleError('A wsl_distribution is required but none was supplied')
path: plugins/connection/wsl.py
user: login: felixfontein
body: Why not simply make `wsl_distribution` a required parameter?
created_at: 2025-02-23T12:26:58Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,758 @@
+# -*- coding: utf-8 -*-
+# Derived from ansible/plugins/connection/proxmox_pct_remote.py (c) 2024 Nils Stein (@mietzen) <github.nstein@mailbox.org>
+# Derived from ansible/plugins/connection/paramiko_ssh.py (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
+# Copyright (c) 2025 Rui Lopes (@rgl) <ruilopes.com>
+# Copyright (c) 2025 Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import annotations
+
+DOCUMENTATION = r"""
+author: Rui Lopes (@rgl) <ruilopes.com>
+name: wsl
+short_description: Run tasks in WSL distribution using wsl.exe CLI via SSH
+requirements:
+  - paramiko
+description:
+  - Run commands or put/fetch files to an existing WSL distribution using wsl.exe CLI via SSH.
+  - Uses the Python SSH implementation (Paramiko) to connect to the WSL host.
+version_added: "10.3.0"
+options:
+  remote_addr:
+    description:
+      - Address of the remote target.
+    default: inventory_hostname
+    type: string
+    vars:
+      - name: inventory_hostname
+      - name: ansible_host
+      - name: ansible_ssh_host
+      - name: ansible_paramiko_host
+  port:
+    description: Remote port to connect to.
+    type: int
+    default: 22
+    ini:
+      - section: defaults
+        key: remote_port
+      - section: paramiko_connection
+        key: remote_port
+    env:
+      - name: ANSIBLE_REMOTE_PORT
+      - name: ANSIBLE_REMOTE_PARAMIKO_PORT
+    vars:
+      - name: ansible_port
+      - name: ansible_ssh_port
+      - name: ansible_paramiko_port
+    keyword:
+      - name: port
+  remote_user:
+    description:
+      - User to login/authenticate as.
+      - Can be set from the CLI via the C(--user) or C(-u) options.
+    type: string
+    vars:
+      - name: ansible_user
+      - name: ansible_ssh_user
+      - name: ansible_paramiko_user
+    env:
+      - name: ANSIBLE_REMOTE_USER
+      - name: ANSIBLE_PARAMIKO_REMOTE_USER
+    ini:
+      - section: defaults
+        key: remote_user
+      - section: paramiko_connection
+        key: remote_user
+    keyword:
+      - name: remote_user
+  password:
+    description:
+      - Secret used to either login the SSH server or as a passphrase for SSH keys that require it.
+      - Can be set from the CLI via the C(--ask-pass) option.
+    type: string
+    vars:
+      - name: ansible_password
+      - name: ansible_ssh_pass
+      - name: ansible_ssh_password
+      - name: ansible_paramiko_pass
+      - name: ansible_paramiko_password
+  use_rsa_sha2_algorithms:
+    description:
+      - Whether or not to enable RSA SHA2 algorithms for pubkeys and hostkeys.
+      - On paramiko versions older than 2.9, this only affects hostkeys.
+      - For behavior matching paramiko<2.9 set this to V(false).
+    vars:
+      - name: ansible_paramiko_use_rsa_sha2_algorithms
+    ini:
+      - {key: use_rsa_sha2_algorithms, section: paramiko_connection}
+    env:
+      - {name: ANSIBLE_PARAMIKO_USE_RSA_SHA2_ALGORITHMS}
+    default: true
+    type: boolean
+  host_key_auto_add:
+    description: "Automatically add host keys to C(~/.ssh/known_hosts)."
+    env:
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_AUTO_ADD
+    ini:
+      - key: host_key_auto_add
+        section: paramiko_connection
+    type: boolean
+  look_for_keys:
+    default: True
+    description: "Set to V(false) to disable searching for private key files in C(~/.ssh/)."
+    env:
+      - name: ANSIBLE_PARAMIKO_LOOK_FOR_KEYS
+    ini:
+      - {key: look_for_keys, section: paramiko_connection}
+    type: boolean
+  proxy_command:
+    default: ""
+    description:
+      - Proxy information for running the connection via a jumphost.
+    type: string
+    env:
+      - name: ANSIBLE_PARAMIKO_PROXY_COMMAND
+    ini:
+      - {key: proxy_command, section: paramiko_connection}
+    vars:
+      - name: ansible_paramiko_proxy_command
+  record_host_keys:
+    default: True
+    description: "Save the host keys to a file."
+    env:
+      - name: ANSIBLE_PARAMIKO_RECORD_HOST_KEYS
+    ini:
+      - section: paramiko_connection
+        key: record_host_keys
+    type: boolean
+  host_key_checking:
+    description: "Set this to V(false) if you want to avoid host key checking by the underlying tools Ansible uses to connect to the host."
+    type: boolean
+    default: true
+    env:
+      - name: ANSIBLE_HOST_KEY_CHECKING
+      - name: ANSIBLE_SSH_HOST_KEY_CHECKING
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_CHECKING
+    ini:
+      - section: defaults
+        key: host_key_checking
+      - section: paramiko_connection
+        key: host_key_checking
+    vars:
+      - name: ansible_host_key_checking
+      - name: ansible_ssh_host_key_checking
+      - name: ansible_paramiko_host_key_checking
+  use_persistent_connections:
+    description: "Toggles the use of persistence for connections."
+    type: boolean
+    default: False
+    env:
+      - name: ANSIBLE_USE_PERSISTENT_CONNECTIONS
+    ini:
+      - section: defaults
+        key: use_persistent_connections
+  banner_timeout:
+    type: float
+    default: 30
+    description:
+      - Configures, in seconds, the amount of time to wait for the SSH
+        banner to be presented. This option is supported by paramiko
+        version 1.15.0 or newer.
+    ini:
+      - section: paramiko_connection
+        key: banner_timeout
+    env:
+      - name: ANSIBLE_PARAMIKO_BANNER_TIMEOUT
+  timeout:
+    type: int
+    default: 10
+    description: Number of seconds until the plugin gives up on failing to establish a TCP connection.
+    ini:
+      - section: defaults
+        key: timeout
+      - section: ssh_connection
+        key: timeout
+      - section: paramiko_connection
+        key: timeout
+    env:
+      - name: ANSIBLE_TIMEOUT
+      - name: ANSIBLE_SSH_TIMEOUT
+      - name: ANSIBLE_PARAMIKO_TIMEOUT
+    vars:
+      - name: ansible_ssh_timeout
+      - name: ansible_paramiko_timeout
+    cli:
+      - name: timeout
+  lock_file_timeout:
+    type: int
+    default: 60
+    description: Number of seconds until the plugin gives up on trying to write a lock file when writing SSH known host keys.
+    vars:
+      - name: ansible_lock_file_timeout
+    env:
+      - name: ANSIBLE_LOCK_FILE_TIMEOUT
+  private_key_file:
+    description:
+      - Path to private key file to use for authentication.
+    type: string
+    ini:
+      - section: defaults
+        key: private_key_file
+      - section: paramiko_connection
+        key: private_key_file
+    env:
+      - name: ANSIBLE_PRIVATE_KEY_FILE
+      - name: ANSIBLE_PARAMIKO_PRIVATE_KEY_FILE
+    vars:
+      - name: ansible_private_key_file
+      - name: ansible_ssh_private_key_file
+      - name: ansible_paramiko_private_key_file
+    cli:
+      - name: private_key_file
+        option: "--private-key"
+  wsl_distribution:
+    description:
+      - WSL distribution name
+    type: string
+    vars:
+      - name: wsl_distribution
+  wsl_user:
+    description:
+      - WSL distribution user
+    type: string
+    vars:
+      - name: wsl_user
+  become_user:
+    description:
+      - WSL distribution user
+    type: string
+    default: root
+    vars:
+      - name: become_user
+      - name: ansible_become_user
+  become:
+    description:
+      - whether to use the user defined by ansible_become_user.
+    type: bool
+    default: false
+    vars:
+      - name: become
+      - name: ansible_become
+"""
+
+EXAMPLES = r"""
+# ------------------------
+# Inventory: inventory.yml
+# ------------------------
+# all:
+#   children:
+#     wsl:
+#       hosts:
+#         ubuntu:
+#           ansible_host: 10.0.0.10
+#           wsl_distribution: ubuntu
+#         debian:
+#           ansible_host: 10.0.0.10
+#           wsl_distribution: debian
+#       vars:
+#         ansible_connection: community.general.wsl
+#         ansible_user: vagrant
+#         wsl_user: wsl
+# ----------------------
+# Playbook: playbook.yml
+# ----------------------
+---
+- hosts: wsl
+  tasks:
+    - name: ping
+      ansible.builtin.ping:
+    - name: id
+      args:
+        executable: /bin/bash
+      ansible.builtin.shell: |
+        exec 2>&1
+        set -x
+        echo "$0"
+        pwd
+        id
+    - name: become id
+      become: true
+      args:
+        executable: /bin/bash
+      ansible.builtin.shell: |
+        exec 2>&1
+        set -x
+        echo "$0"
+        pwd
+        id
+"""
+
+import os
+import pathlib
+import shlex
+import socket
+import tempfile
+import typing as t
+
+from ansible.errors import (
+    AnsibleAuthenticationFailure,
+    AnsibleConnectionFailure,
+    AnsibleError,
+)
+from ansible_collections.community.general.plugins.module_utils._filelock import FileLock, LockTimeout
+from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
+from ansible.module_utils.compat.paramiko import PARAMIKO_IMPORT_ERR, paramiko
+from ansible.module_utils.compat.version import LooseVersion
+from ansible.plugins.connection import ConnectionBase
+from ansible.utils.display import Display
+from ansible.utils.path import makedirs_safe
+from binascii import hexlify
+from subprocess import list2cmdline
+
+
+display = Display()
+
+
+def authenticity_msg(hostname: str, ktype: str, fingerprint: str) -> str:
+    msg = f"""
+    paramiko: The authenticity of host '{hostname}' can't be established.
+    The {ktype} key fingerprint is {fingerprint}.
+    Are you sure you want to continue connecting (yes/no)?
+    """
+    return msg
+
+
+MissingHostKeyPolicy: type = object
+if paramiko:
+    MissingHostKeyPolicy = paramiko.MissingHostKeyPolicy
+
+
+class MyAddPolicy(MissingHostKeyPolicy):
+    """
+    Based on AutoAddPolicy in paramiko so we can determine when keys are added
+
+    and also prompt for input.
+
+    Policy for automatically adding the hostname and new host key to the
+    local L{HostKeys} object, and saving it. This is used by L{SSHClient}.
+    """
+
+    def __init__(self, connection: Connection) -> None:
+        self.connection = connection
+        self._options = connection._options
+
+    def missing_host_key(self, client, hostname, key) -> None:
+
+        if all((self.connection.get_option('host_key_checking'), not self.connection.get_option('host_key_auto_add'))):
+
+            fingerprint = hexlify(key.get_fingerprint())
+            ktype = key.get_name()
+
+            if self.connection.get_option('use_persistent_connections') or self.connection.force_persistence:
+                # don't print the prompt string since the user cannot respond
+                # to the question anyway
+                raise AnsibleError(authenticity_msg(hostname, ktype, fingerprint)[1:92])
+
+            inp = to_text(
+                display.prompt_until(authenticity_msg(hostname, ktype, fingerprint), private=False),
+                errors='surrogate_or_strict'
+            )
+
+            if inp.lower() not in ['yes', 'y', '']:
+                raise AnsibleError('host connection rejected by user')
+
+        key._added_by_ansible_this_time = True
+
+        # existing implementation below:
+        client._host_keys.add(hostname, key.get_name(), key)
+
+        # host keys are actually saved in close() function below
+        # in order to control ordering.
+
+
+class Connection(ConnectionBase):
+    """ SSH based connections (paramiko) to WSL """
+
+    transport = 'community.general.wsl'
+    _log_channel: str | None = None
+
+    def __init__(self, play_context, new_stdin, *args, **kwargs):
+        super(Connection, self).__init__(play_context, new_stdin, *args, **kwargs)
+
+    def _set_log_channel(self, name: str) -> None:
+        """ Mimic paramiko.SSHClient.set_log_channel """
+        self._log_channel = name
+
+    def _parse_proxy_command(self, port: int = 22) -> dict[str, t.Any]:
+        proxy_command = self.get_option('proxy_command') or None
+
+        sock_kwarg = {}
+        if proxy_command:
+            replacers = {
+                '%h': self.get_option('remote_addr'),
+                '%p': port,
+                '%r': self.get_option('remote_user')
+            }
+            for find, replace in replacers.items():
+                proxy_command = proxy_command.replace(find, str(replace))
+            try:
+                sock_kwarg = {'sock': paramiko.ProxyCommand(proxy_command)}
+                display.vvv(f'CONFIGURE PROXY COMMAND FOR CONNECTION: {proxy_command}', host=self.get_option('remote_addr'))
+            except AttributeError:
+                display.warning('Paramiko ProxyCommand support unavailable. '
+                                'Please upgrade to Paramiko 1.9.0 or newer. '
+                                'Not using configured ProxyCommand')
+
+        return sock_kwarg
+
+    def _connect(self) -> Connection:
+        """ activates the connection object """
+
+        if paramiko is None:
+            raise AnsibleError(f'paramiko is not installed: {to_native(PARAMIKO_IMPORT_ERR)}')
+
+        port = self.get_option('port')
+        display.vvv(f'ESTABLISH PARAMIKO SSH CONNECTION FOR USER: {self.get_option("remote_user")} on PORT {to_text(port)} TO {self.get_option("remote_addr")}',
+                    host=self.get_option('remote_addr'))
+
+        ssh = paramiko.SSHClient()
+
+        # Set pubkey and hostkey algorithms to disable, the only manipulation allowed currently
+        # is keeping or omitting rsa-sha2 algorithms
+        # default_keys: t.Tuple[str] = ()
+        paramiko_preferred_pubkeys = getattr(paramiko.Transport, '_preferred_pubkeys', ())
+        paramiko_preferred_hostkeys = getattr(paramiko.Transport, '_preferred_keys', ())
+        use_rsa_sha2_algorithms = self.get_option('use_rsa_sha2_algorithms')
+        disabled_algorithms: t.Dict[str, t.Iterable[str]] = {}
+        if not use_rsa_sha2_algorithms:
+            if paramiko_preferred_pubkeys:
+                disabled_algorithms['pubkeys'] = tuple(a for a in paramiko_preferred_pubkeys if 'rsa-sha2' in a)
+            if paramiko_preferred_hostkeys:
+                disabled_algorithms['keys'] = tuple(a for a in paramiko_preferred_hostkeys if 'rsa-sha2' in a)
+
+        # override paramiko's default logger name
+        if self._log_channel is not None:
+            ssh.set_log_channel(self._log_channel)
+
+        self.keyfile = os.path.expanduser('~/.ssh/known_hosts')
+
+        if self.get_option('host_key_checking'):
+            for ssh_known_hosts in ('/etc/ssh/ssh_known_hosts', '/etc/openssh/ssh_known_hosts'):
+                try:
+                    ssh.load_system_host_keys(ssh_known_hosts)
+                    break
+                except IOError:
+                    pass  # file was not found, but not required to function
+                except paramiko.hostkeys.InvalidHostKey as e:
+                    raise AnsibleConnectionFailure(f'Invalid host key: {to_text(e.line)}')
+            try:
+                ssh.load_system_host_keys()
+            except paramiko.hostkeys.InvalidHostKey as e:
+                raise AnsibleConnectionFailure(f'Invalid host key: {to_text(e.line)}')
+
+        ssh_connect_kwargs = self._parse_proxy_command(port)
+        ssh.set_missing_host_key_policy(MyAddPolicy(self))
+        conn_password = self.get_option('password')
+        allow_agent = True
+
+        if conn_password is not None:
+            allow_agent = False
+
+        try:
+            key_filename = None
+            if self.get_option('private_key_file'):
+                key_filename = os.path.expanduser(self.get_option('private_key_file'))
+
+            # paramiko 2.2 introduced auth_timeout parameter
+            if LooseVersion(paramiko.__version__) >= LooseVersion('2.2.0'):
+                ssh_connect_kwargs['auth_timeout'] = self.get_option('timeout')
+
+            # paramiko 1.15 introduced banner timeout parameter
+            if LooseVersion(paramiko.__version__) >= LooseVersion('1.15.0'):
+                ssh_connect_kwargs['banner_timeout'] = self.get_option('banner_timeout')
+
+            ssh.connect(
+                self.get_option('remote_addr').lower(),
+                username=self.get_option('remote_user'),
+                allow_agent=allow_agent,
+                look_for_keys=self.get_option('look_for_keys'),
+                key_filename=key_filename,
+                password=conn_password,
+                timeout=self.get_option('timeout'),
+                port=port,
+                disabled_algorithms=disabled_algorithms,
+                **ssh_connect_kwargs,
+            )
+        except paramiko.ssh_exception.BadHostKeyException as e:
+            raise AnsibleConnectionFailure(f'host key mismatch for {to_text(e.hostname)}')
+        except paramiko.ssh_exception.AuthenticationException as e:
+            msg = f'Failed to authenticate: {e}'
+            raise AnsibleAuthenticationFailure(msg)
+        except Exception as e:
+            msg = to_text(e)
+            if u'PID check failed' in msg:
+                raise AnsibleError('paramiko version issue, please upgrade paramiko on the machine running ansible')
+            elif u'Private key file is encrypted' in msg:
+                msg = f'ssh {self.get_option("remote_user")}@{self.get_options("remote_addr")}:{port} : ' + \
+                    f'{msg}\nTo connect as a different user, use -u <username>.'
+                raise AnsibleConnectionFailure(msg)
+            else:
+                raise AnsibleConnectionFailure(msg)
+        self.ssh = ssh
+        self._connected = True
+        return self
+
+    def _any_keys_added(self) -> bool:
+        for hostname, keys in self.ssh._host_keys.items():
+            for keytype, key in keys.items():
+                added_this_time = getattr(key, '_added_by_ansible_this_time', False)
+                if added_this_time:
+                    return True
+        return False
+
+    def _save_ssh_host_keys(self, filename: str) -> None:
+        """
+        not using the paramiko save_ssh_host_keys function as we want to add new SSH keys at the bottom so folks
+        don't complain about it :)
+        """
+
+        if not self._any_keys_added():
+            return
+
+        path = os.path.expanduser('~/.ssh')
+        makedirs_safe(path)
+
+        with open(filename, 'w') as f:
+            for hostname, keys in self.ssh._host_keys.items():
+                for keytype, key in keys.items():
+                    # was f.write
+                    added_this_time = getattr(key, '_added_by_ansible_this_time', False)
+                    if not added_this_time:
+                        f.write(f'{hostname} {keytype} {key.get_base64()}\n')
+
+            for hostname, keys in self.ssh._host_keys.items():
+                for keytype, key in keys.items():
+                    added_this_time = getattr(key, '_added_by_ansible_this_time', False)
+                    if added_this_time:
+                        f.write(f'{hostname} {keytype} {key.get_base64()}\n')
+
+    def _build_wsl_command(self, cmd: str) -> str:
+        wsl_distribution = self.get_option('wsl_distribution')
+        if not wsl_distribution:
+            raise AnsibleError('A wsl_distribution is required but none was supplied')
+        become = self.get_option('become')
+        become_user = self.get_option('become_user')
+        if become and become_user:
+            wsl_user = become_user
+        else:
+            wsl_user = self.get_option('wsl_user')
+        args = ['wsl.exe', '--distribution', wsl_distribution]
+        if wsl_user:
+            args.extend(['--user', wsl_user])
+        args.extend(['--'])
+        args.extend(shlex.split(cmd))
+        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]:
path: plugins/connection/wsl.py
user: login: felixfontein
body: `Tuple[]` is (kind of?) deprecated, `tuple[]` is the modern syntax.  Same for `bytes | None` vs. `Optional[bytes]`.
created_at: 2025-02-23T12:27:41Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,759 @@
+# -*- coding: utf-8 -*-
+# Derived from ansible/plugins/connection/proxmox_pct_remote.py (c) 2024 Nils Stein (@mietzen) <github.nstein@mailbox.org>
+# Derived from ansible/plugins/connection/paramiko_ssh.py (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
+# Copyright (c) 2025 Rui Lopes (@rgl) <ruilopes.com>
+# Copyright (c) 2025 Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import annotations
+
+DOCUMENTATION = r"""
+author: Rui Lopes (@rgl) <ruilopes.com>
+name: wsl
+short_description: Run tasks in WSL distribution using wsl.exe CLI via SSH
+requirements:
+  - paramiko
+description:
+  - Run commands or put/fetch files to an existing WSL distribution using wsl.exe CLI via SSH.
+  - Uses the Python SSH implementation (Paramiko) to connect to the WSL host.
+version_added: "10.4.0"
+options:
+  remote_addr:
+    description:
+      - Address of the remote target.
+    default: inventory_hostname
+    type: string
+    vars:
+      - name: inventory_hostname
+      - name: ansible_host
+      - name: ansible_ssh_host
+      - name: ansible_paramiko_host
+  port:
+    description: Remote port to connect to.
+    type: int
+    default: 22
+    ini:
+      - section: defaults
+        key: remote_port
+      - section: paramiko_connection
+        key: remote_port
+    env:
+      - name: ANSIBLE_REMOTE_PORT
+      - name: ANSIBLE_REMOTE_PARAMIKO_PORT
+    vars:
+      - name: ansible_port
+      - name: ansible_ssh_port
+      - name: ansible_paramiko_port
+    keyword:
+      - name: port
+  remote_user:
+    description:
+      - User to login/authenticate as.
+      - Can be set from the CLI via the C(--user) or C(-u) options.
+    type: string
+    vars:
+      - name: ansible_user
+      - name: ansible_ssh_user
+      - name: ansible_paramiko_user
+    env:
+      - name: ANSIBLE_REMOTE_USER
+      - name: ANSIBLE_PARAMIKO_REMOTE_USER
+    ini:
+      - section: defaults
+        key: remote_user
+      - section: paramiko_connection
+        key: remote_user
+    keyword:
+      - name: remote_user
+  password:
+    description:
+      - Secret used to either login the SSH server or as a passphrase for SSH keys that require it.
+      - Can be set from the CLI via the C(--ask-pass) option.
+    type: string
+    vars:
+      - name: ansible_password
+      - name: ansible_ssh_pass
+      - name: ansible_ssh_password
+      - name: ansible_paramiko_pass
+      - name: ansible_paramiko_password
+  use_rsa_sha2_algorithms:
+    description:
+      - Whether or not to enable RSA SHA2 algorithms for pubkeys and hostkeys.
+      - On paramiko versions older than 2.9, this only affects hostkeys.
+      - For behavior matching paramiko<2.9 set this to V(false).
+    vars:
+      - name: ansible_paramiko_use_rsa_sha2_algorithms
+    ini:
+      - {key: use_rsa_sha2_algorithms, section: paramiko_connection}
+    env:
+      - {name: ANSIBLE_PARAMIKO_USE_RSA_SHA2_ALGORITHMS}
+    default: true
+    type: boolean
+  host_key_auto_add:
+    description: "Automatically add host keys to C(~/.ssh/known_hosts)."
+    env:
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_AUTO_ADD
+    ini:
+      - key: host_key_auto_add
+        section: paramiko_connection
+    type: boolean
+  look_for_keys:
+    default: True
+    description: "Set to V(false) to disable searching for private key files in C(~/.ssh/)."
+    env:
+      - name: ANSIBLE_PARAMIKO_LOOK_FOR_KEYS
+    ini:
+      - {key: look_for_keys, section: paramiko_connection}
+    type: boolean
+  proxy_command:
+    default: ""
+    description:
+      - Proxy information for running the connection via a jumphost.
+    type: string
+    env:
+      - name: ANSIBLE_PARAMIKO_PROXY_COMMAND
+    ini:
+      - {key: proxy_command, section: paramiko_connection}
+    vars:
+      - name: ansible_paramiko_proxy_command
+  record_host_keys:
+    default: True
+    description: "Save the host keys to a file."
+    env:
+      - name: ANSIBLE_PARAMIKO_RECORD_HOST_KEYS
+    ini:
+      - section: paramiko_connection
+        key: record_host_keys
+    type: boolean
+  host_key_checking:
+    description: "Set this to V(false) if you want to avoid host key checking by the underlying tools Ansible uses to connect to the host."
+    type: boolean
+    default: true
+    env:
+      - name: ANSIBLE_HOST_KEY_CHECKING
+      - name: ANSIBLE_SSH_HOST_KEY_CHECKING
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_CHECKING
+    ini:
+      - section: defaults
+        key: host_key_checking
+      - section: paramiko_connection
+        key: host_key_checking
+    vars:
+      - name: ansible_host_key_checking
+      - name: ansible_ssh_host_key_checking
+      - name: ansible_paramiko_host_key_checking
+  use_persistent_connections:
+    description: "Toggles the use of persistence for connections."
+    type: boolean
+    default: False
+    env:
+      - name: ANSIBLE_USE_PERSISTENT_CONNECTIONS
+    ini:
+      - section: defaults
+        key: use_persistent_connections
+  banner_timeout:
+    type: float
+    default: 30
+    description:
+      - Configures, in seconds, the amount of time to wait for the SSH
+        banner to be presented. This option is supported by paramiko
+        version 1.15.0 or newer.
+    ini:
+      - section: paramiko_connection
+        key: banner_timeout
+    env:
+      - name: ANSIBLE_PARAMIKO_BANNER_TIMEOUT
+  timeout:
+    type: int
+    default: 10
+    description: Number of seconds until the plugin gives up on failing to establish a TCP connection.
+    ini:
+      - section: defaults
+        key: timeout
+      - section: ssh_connection
+        key: timeout
+      - section: paramiko_connection
+        key: timeout
+    env:
+      - name: ANSIBLE_TIMEOUT
+      - name: ANSIBLE_SSH_TIMEOUT
+      - name: ANSIBLE_PARAMIKO_TIMEOUT
+    vars:
+      - name: ansible_ssh_timeout
+      - name: ansible_paramiko_timeout
+    cli:
+      - name: timeout
+  lock_file_timeout:
+    type: int
+    default: 60
+    description: Number of seconds until the plugin gives up on trying to write a lock file when writing SSH known host keys.
+    vars:
+      - name: ansible_lock_file_timeout
+    env:
+      - name: ANSIBLE_LOCK_FILE_TIMEOUT
+  private_key_file:
+    description:
+      - Path to private key file to use for authentication.
+    type: string
+    ini:
+      - section: defaults
+        key: private_key_file
+      - section: paramiko_connection
+        key: private_key_file
+    env:
+      - name: ANSIBLE_PRIVATE_KEY_FILE
+      - name: ANSIBLE_PARAMIKO_PRIVATE_KEY_FILE
+    vars:
+      - name: ansible_private_key_file
+      - name: ansible_ssh_private_key_file
+      - name: ansible_paramiko_private_key_file
+    cli:
+      - name: private_key_file
+        option: "--private-key"
+  wsl_distribution:
+    description:
+      - WSL distribution name
+    type: string
+    vars:
+      - name: wsl_distribution
+  wsl_user:
+    description:
+      - WSL distribution user
+    type: string
+    vars:
+      - name: wsl_user
+  become_user:
+    description:
+      - WSL distribution user
+    type: string
+    default: root
+    vars:
+      - name: become_user
+      - name: ansible_become_user
+  become:
+    description:
+      - whether to use the user defined by ansible_become_user.
+    type: bool
+    default: false
+    vars:
+      - name: become
+      - name: ansible_become
+"""
+
+EXAMPLES = r"""
+# ------------------------
+# Inventory: inventory.yml
+# ------------------------
+---
+all:
+  children:
+    wsl:
+      hosts:
+        ubuntu:
+          ansible_host: 10.0.0.10
+          wsl_distribution: ubuntu
+        debian:
+          ansible_host: 10.0.0.10
+          wsl_distribution: debian
+      vars:
+        ansible_connection: community.general.wsl
+        ansible_user: vagrant
+        wsl_user: wsl
+# ----------------------
+# Playbook: playbook.yml
+# ----------------------
+---
+- hosts: wsl
+  tasks:
+    - name: ping
+      ansible.builtin.ping:
+    - name: id
+      args:
+        executable: /bin/bash
+      ansible.builtin.shell: |
+        exec 2>&1
+        set -x
+        echo "$0"
+        pwd
+        id
+    - name: become id
+      become: true
+      args:
+        executable: /bin/bash
+      ansible.builtin.shell: |
+        exec 2>&1
+        set -x
+        echo "$0"
+        pwd
+        id
+"""
+
+import os
+import pathlib
+import shlex
+import socket
+import tempfile
+import typing as t
+
+from ansible.errors import (
+    AnsibleAuthenticationFailure,
+    AnsibleConnectionFailure,
+    AnsibleError,
+)
+from ansible_collections.community.general.plugins.module_utils._filelock import FileLock, LockTimeout
+from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
+from ansible.module_utils.compat.paramiko import PARAMIKO_IMPORT_ERR, paramiko
+from ansible.module_utils.compat.version import LooseVersion
+from ansible.plugins.connection import ConnectionBase
+from ansible.utils.display import Display
+from ansible.utils.path import makedirs_safe
+from binascii import hexlify
+from subprocess import list2cmdline
+
+
+display = Display()
+
+
+def authenticity_msg(hostname: str, ktype: str, fingerprint: str) -> str:
+    msg = f"""
+    paramiko: The authenticity of host '{hostname}' can't be established.
+    The {ktype} key fingerprint is {fingerprint}.
+    Are you sure you want to continue connecting (yes/no)?
+    """
+    return msg
+
+
+MissingHostKeyPolicy: type = object
+if paramiko:
+    MissingHostKeyPolicy = paramiko.MissingHostKeyPolicy
+
+
+class MyAddPolicy(MissingHostKeyPolicy):
+    """
+    Based on AutoAddPolicy in paramiko so we can determine when keys are added
+
+    and also prompt for input.
+
+    Policy for automatically adding the hostname and new host key to the
+    local L{HostKeys} object, and saving it. This is used by L{SSHClient}.
+    """
+
+    def __init__(self, connection: Connection) -> None:
+        self.connection = connection
+        self._options = connection._options
+
+    def missing_host_key(self, client, hostname, key) -> None:
+
+        if all((self.connection.get_option('host_key_checking'), not self.connection.get_option('host_key_auto_add'))):
+
+            fingerprint = hexlify(key.get_fingerprint())
+            ktype = key.get_name()
+
+            if self.connection.get_option('use_persistent_connections') or self.connection.force_persistence:
+                # don't print the prompt string since the user cannot respond
+                # to the question anyway
+                raise AnsibleError(authenticity_msg(hostname, ktype, fingerprint)[1:92])
+
+            inp = to_text(
+                display.prompt_until(authenticity_msg(hostname, ktype, fingerprint), private=False),
+                errors='surrogate_or_strict'
+            )
+
+            if inp.lower() not in ['yes', 'y', '']:
+                raise AnsibleError('host connection rejected by user')
+
+        key._added_by_ansible_this_time = True
+
+        # existing implementation below:
+        client._host_keys.add(hostname, key.get_name(), key)
+
+        # host keys are actually saved in close() function below
+        # in order to control ordering.
+
+
+class Connection(ConnectionBase):
+    """ SSH based connections (paramiko) to WSL """
+
+    transport = 'community.general.wsl'
+    _log_channel: str | None = None
+
+    def __init__(self, play_context, new_stdin, *args, **kwargs):
+        super(Connection, self).__init__(play_context, new_stdin, *args, **kwargs)
+
+    def _set_log_channel(self, name: str) -> None:
+        """ Mimic paramiko.SSHClient.set_log_channel """
+        self._log_channel = name
+
+    def _parse_proxy_command(self, port: int = 22) -> dict[str, t.Any]:
+        proxy_command = self.get_option('proxy_command') or None
+
+        sock_kwarg = {}
+        if proxy_command:
+            replacers = {
+                '%h': self.get_option('remote_addr'),
+                '%p': port,
+                '%r': self.get_option('remote_user')
+            }
+            for find, replace in replacers.items():
+                proxy_command = proxy_command.replace(find, str(replace))
+            try:
+                sock_kwarg = {'sock': paramiko.ProxyCommand(proxy_command)}
+                display.vvv(f'CONFIGURE PROXY COMMAND FOR CONNECTION: {proxy_command}', host=self.get_option('remote_addr'))
+            except AttributeError:
+                display.warning('Paramiko ProxyCommand support unavailable. '
+                                'Please upgrade to Paramiko 1.9.0 or newer. '
+                                'Not using configured ProxyCommand')
+
+        return sock_kwarg
+
+    def _connect(self) -> Connection:
+        """ activates the connection object """
+
+        if paramiko is None:
+            raise AnsibleError(f'paramiko is not installed: {to_native(PARAMIKO_IMPORT_ERR)}')
+
+        port = self.get_option('port')
+        display.vvv(f'ESTABLISH PARAMIKO SSH CONNECTION FOR USER: {self.get_option("remote_user")} on PORT {to_text(port)} TO {self.get_option("remote_addr")}',
+                    host=self.get_option('remote_addr'))
+
+        ssh = paramiko.SSHClient()
+
+        # Set pubkey and hostkey algorithms to disable, the only manipulation allowed currently
+        # is keeping or omitting rsa-sha2 algorithms
+        # default_keys: t.Tuple[str] = ()
+        paramiko_preferred_pubkeys = getattr(paramiko.Transport, '_preferred_pubkeys', ())
+        paramiko_preferred_hostkeys = getattr(paramiko.Transport, '_preferred_keys', ())
+        use_rsa_sha2_algorithms = self.get_option('use_rsa_sha2_algorithms')
+        disabled_algorithms: t.Dict[str, t.Iterable[str]] = {}
+        if not use_rsa_sha2_algorithms:
+            if paramiko_preferred_pubkeys:
+                disabled_algorithms['pubkeys'] = tuple(a for a in paramiko_preferred_pubkeys if 'rsa-sha2' in a)
+            if paramiko_preferred_hostkeys:
+                disabled_algorithms['keys'] = tuple(a for a in paramiko_preferred_hostkeys if 'rsa-sha2' in a)
+
+        # override paramiko's default logger name
+        if self._log_channel is not None:
+            ssh.set_log_channel(self._log_channel)
+
+        self.keyfile = os.path.expanduser('~/.ssh/known_hosts')
+
+        if self.get_option('host_key_checking'):
+            for ssh_known_hosts in ('/etc/ssh/ssh_known_hosts', '/etc/openssh/ssh_known_hosts'):
+                try:
+                    ssh.load_system_host_keys(ssh_known_hosts)
+                    break
+                except IOError:
+                    pass  # file was not found, but not required to function
+                except paramiko.hostkeys.InvalidHostKey as e:
+                    raise AnsibleConnectionFailure(f'Invalid host key: {to_text(e.line)}')
+            try:
+                ssh.load_system_host_keys()
+            except paramiko.hostkeys.InvalidHostKey as e:
+                raise AnsibleConnectionFailure(f'Invalid host key: {to_text(e.line)}')
+
+        ssh_connect_kwargs = self._parse_proxy_command(port)
+        ssh.set_missing_host_key_policy(MyAddPolicy(self))
+        conn_password = self.get_option('password')
+        allow_agent = True
+
+        if conn_password is not None:
+            allow_agent = False
+
+        try:
+            key_filename = None
+            if self.get_option('private_key_file'):
+                key_filename = os.path.expanduser(self.get_option('private_key_file'))
+
+            # paramiko 2.2 introduced auth_timeout parameter
+            if LooseVersion(paramiko.__version__) >= LooseVersion('2.2.0'):
+                ssh_connect_kwargs['auth_timeout'] = self.get_option('timeout')
+
+            # paramiko 1.15 introduced banner timeout parameter
+            if LooseVersion(paramiko.__version__) >= LooseVersion('1.15.0'):
+                ssh_connect_kwargs['banner_timeout'] = self.get_option('banner_timeout')
+
+            ssh.connect(
+                self.get_option('remote_addr').lower(),
+                username=self.get_option('remote_user'),
+                allow_agent=allow_agent,
+                look_for_keys=self.get_option('look_for_keys'),
+                key_filename=key_filename,
+                password=conn_password,
+                timeout=self.get_option('timeout'),
+                port=port,
+                disabled_algorithms=disabled_algorithms,
+                **ssh_connect_kwargs,
+            )
+        except paramiko.ssh_exception.BadHostKeyException as e:
+            raise AnsibleConnectionFailure(f'host key mismatch for {to_text(e.hostname)}')
+        except paramiko.ssh_exception.AuthenticationException as e:
+            msg = f'Failed to authenticate: {e}'
+            raise AnsibleAuthenticationFailure(msg)
+        except Exception as e:
+            msg = to_text(e)
+            if u'PID check failed' in msg:
+                raise AnsibleError('paramiko version issue, please upgrade paramiko on the machine running ansible')
+            elif u'Private key file is encrypted' in msg:
+                msg = f'ssh {self.get_option("remote_user")}@{self.get_options("remote_addr")}:{port} : ' + \
+                    f'{msg}\nTo connect as a different user, use -u <username>.'
+                raise AnsibleConnectionFailure(msg)
+            else:
+                raise AnsibleConnectionFailure(msg)
+        self.ssh = ssh
+        self._connected = True
+        return self
+
+    def _any_keys_added(self) -> bool:
+        for hostname, keys in self.ssh._host_keys.items():
+            for keytype, key in keys.items():
+                added_this_time = getattr(key, '_added_by_ansible_this_time', False)
+                if added_this_time:
+                    return True
+        return False
+
+    def _save_ssh_host_keys(self, filename: str) -> None:
+        """
+        not using the paramiko save_ssh_host_keys function as we want to add new SSH keys at the bottom so folks
+        don't complain about it :)
+        """
+
+        if not self._any_keys_added():
+            return
+
+        path = os.path.expanduser('~/.ssh')
+        makedirs_safe(path)
+
+        with open(filename, 'w') as f:
+            for hostname, keys in self.ssh._host_keys.items():
+                for keytype, key in keys.items():
+                    # was f.write
+                    added_this_time = getattr(key, '_added_by_ansible_this_time', False)
+                    if not added_this_time:
+                        f.write(f'{hostname} {keytype} {key.get_base64()}\n')
+
+            for hostname, keys in self.ssh._host_keys.items():
+                for keytype, key in keys.items():
+                    added_this_time = getattr(key, '_added_by_ansible_this_time', False)
+                    if added_this_time:
+                        f.write(f'{hostname} {keytype} {key.get_base64()}\n')
+
+    def _build_wsl_command(self, cmd: str) -> str:
+        wsl_distribution = self.get_option('wsl_distribution')
+        if not wsl_distribution:
+            raise AnsibleError('A wsl_distribution is required but none was supplied')
path: plugins/connection/wsl.py
user: login: rgl
body: ah indeed, much better! I've changed it. please check it.
created_at: 2025-02-23T12:49:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,758 @@
+# -*- coding: utf-8 -*-
+# Derived from ansible/plugins/connection/proxmox_pct_remote.py (c) 2024 Nils Stein (@mietzen) <github.nstein@mailbox.org>
+# Derived from ansible/plugins/connection/paramiko_ssh.py (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
+# Copyright (c) 2025 Rui Lopes (@rgl) <ruilopes.com>
+# Copyright (c) 2025 Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import annotations
+
+DOCUMENTATION = r"""
+author: Rui Lopes (@rgl) <ruilopes.com>
+name: wsl
+short_description: Run tasks in WSL distribution using wsl.exe CLI via SSH
+requirements:
+  - paramiko
+description:
+  - Run commands or put/fetch files to an existing WSL distribution using wsl.exe CLI via SSH.
+  - Uses the Python SSH implementation (Paramiko) to connect to the WSL host.
+version_added: "10.4.0"
+options:
+  remote_addr:
+    description:
+      - Address of the remote target.
+    default: inventory_hostname
+    type: string
+    vars:
+      - name: inventory_hostname
+      - name: ansible_host
+      - name: ansible_ssh_host
+      - name: ansible_paramiko_host
+  port:
+    description: Remote port to connect to.
+    type: int
+    default: 22
+    ini:
+      - section: defaults
+        key: remote_port
+      - section: paramiko_connection
+        key: remote_port
+    env:
+      - name: ANSIBLE_REMOTE_PORT
+      - name: ANSIBLE_REMOTE_PARAMIKO_PORT
+    vars:
+      - name: ansible_port
+      - name: ansible_ssh_port
+      - name: ansible_paramiko_port
+    keyword:
+      - name: port
+  remote_user:
+    description:
+      - User to login/authenticate as.
+      - Can be set from the CLI via the C(--user) or C(-u) options.
+    type: string
+    vars:
+      - name: ansible_user
+      - name: ansible_ssh_user
+      - name: ansible_paramiko_user
+    env:
+      - name: ANSIBLE_REMOTE_USER
+      - name: ANSIBLE_PARAMIKO_REMOTE_USER
+    ini:
+      - section: defaults
+        key: remote_user
+      - section: paramiko_connection
+        key: remote_user
+    keyword:
+      - name: remote_user
+  password:
+    description:
+      - Secret used to either login the SSH server or as a passphrase for SSH keys that require it.
+      - Can be set from the CLI via the C(--ask-pass) option.
+    type: string
+    vars:
+      - name: ansible_password
+      - name: ansible_ssh_pass
+      - name: ansible_ssh_password
+      - name: ansible_paramiko_pass
+      - name: ansible_paramiko_password
+  use_rsa_sha2_algorithms:
+    description:
+      - Whether or not to enable RSA SHA2 algorithms for pubkeys and hostkeys.
+      - On paramiko versions older than 2.9, this only affects hostkeys.
+      - For behavior matching paramiko<2.9 set this to V(false).
+    vars:
+      - name: ansible_paramiko_use_rsa_sha2_algorithms
+    ini:
+      - {key: use_rsa_sha2_algorithms, section: paramiko_connection}
+    env:
+      - {name: ANSIBLE_PARAMIKO_USE_RSA_SHA2_ALGORITHMS}
+    default: true
+    type: boolean
+  host_key_auto_add:
+    description: "Automatically add host keys to C(~/.ssh/known_hosts)."
+    env:
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_AUTO_ADD
+    ini:
+      - key: host_key_auto_add
+        section: paramiko_connection
+    type: boolean
+  look_for_keys:
+    default: True
+    description: "Set to V(false) to disable searching for private key files in C(~/.ssh/)."
+    env:
+      - name: ANSIBLE_PARAMIKO_LOOK_FOR_KEYS
+    ini:
+      - {key: look_for_keys, section: paramiko_connection}
+    type: boolean
+  proxy_command:
+    default: ""
+    description:
+      - Proxy information for running the connection via a jumphost.
+    type: string
+    env:
+      - name: ANSIBLE_PARAMIKO_PROXY_COMMAND
+    ini:
+      - {key: proxy_command, section: paramiko_connection}
+    vars:
+      - name: ansible_paramiko_proxy_command
+  record_host_keys:
+    default: True
+    description: "Save the host keys to a file."
+    env:
+      - name: ANSIBLE_PARAMIKO_RECORD_HOST_KEYS
+    ini:
+      - section: paramiko_connection
+        key: record_host_keys
+    type: boolean
+  host_key_checking:
+    description: "Set this to V(false) if you want to avoid host key checking by the underlying tools Ansible uses to connect to the host."
+    type: boolean
+    default: true
+    env:
+      - name: ANSIBLE_HOST_KEY_CHECKING
+      - name: ANSIBLE_SSH_HOST_KEY_CHECKING
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_CHECKING
+    ini:
+      - section: defaults
+        key: host_key_checking
+      - section: paramiko_connection
+        key: host_key_checking
+    vars:
+      - name: ansible_host_key_checking
+      - name: ansible_ssh_host_key_checking
+      - name: ansible_paramiko_host_key_checking
+  use_persistent_connections:
+    description: "Toggles the use of persistence for connections."
+    type: boolean
+    default: False
+    env:
+      - name: ANSIBLE_USE_PERSISTENT_CONNECTIONS
+    ini:
+      - section: defaults
+        key: use_persistent_connections
+  banner_timeout:
+    type: float
+    default: 30
+    description:
+      - Configures, in seconds, the amount of time to wait for the SSH
+        banner to be presented. This option is supported by paramiko
+        version 1.15.0 or newer.
+    ini:
+      - section: paramiko_connection
+        key: banner_timeout
+    env:
+      - name: ANSIBLE_PARAMIKO_BANNER_TIMEOUT
+  timeout:
+    type: int
+    default: 10
+    description: Number of seconds until the plugin gives up on failing to establish a TCP connection.
+    ini:
+      - section: defaults
+        key: timeout
+      - section: ssh_connection
+        key: timeout
+      - section: paramiko_connection
+        key: timeout
+    env:
+      - name: ANSIBLE_TIMEOUT
+      - name: ANSIBLE_SSH_TIMEOUT
+      - name: ANSIBLE_PARAMIKO_TIMEOUT
+    vars:
+      - name: ansible_ssh_timeout
+      - name: ansible_paramiko_timeout
+    cli:
+      - name: timeout
+  lock_file_timeout:
+    type: int
+    default: 60
+    description: Number of seconds until the plugin gives up on trying to write a lock file when writing SSH known host keys.
+    vars:
+      - name: ansible_lock_file_timeout
+    env:
+      - name: ANSIBLE_LOCK_FILE_TIMEOUT
+  private_key_file:
+    description:
+      - Path to private key file to use for authentication.
+    type: string
+    ini:
+      - section: defaults
+        key: private_key_file
+      - section: paramiko_connection
+        key: private_key_file
+    env:
+      - name: ANSIBLE_PRIVATE_KEY_FILE
+      - name: ANSIBLE_PARAMIKO_PRIVATE_KEY_FILE
+    vars:
+      - name: ansible_private_key_file
+      - name: ansible_ssh_private_key_file
+      - name: ansible_paramiko_private_key_file
+    cli:
+      - name: private_key_file
+        option: "--private-key"
+  wsl_distribution:
+    description:
+      - WSL distribution name
+    type: string
+    required: true
+    vars:
+      - name: wsl_distribution
+  wsl_user:
path: plugins/connection/wsl.py
user: login: rgl
body: should this be called `remove_user` like the incus plugin at https://github.com/ansible-collections/community.general/pull/9743/files#diff-7f31c09aee977074f8ac127e9b35b526768355ce1acae35822288db71bff0a93R52? I'm not really sure about the conventions here. or should all the `wsl.exe` arguments have `wsl_` prefix?
created_at: 2025-02-25T08:31:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,758 @@
+# -*- coding: utf-8 -*-
+# Derived from ansible/plugins/connection/proxmox_pct_remote.py (c) 2024 Nils Stein (@mietzen) <github.nstein@mailbox.org>
+# Derived from ansible/plugins/connection/paramiko_ssh.py (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
+# Copyright (c) 2025 Rui Lopes (@rgl) <ruilopes.com>
+# Copyright (c) 2025 Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import annotations
+
+DOCUMENTATION = r"""
+author: Rui Lopes (@rgl) <ruilopes.com>
+name: wsl
+short_description: Run tasks in WSL distribution using wsl.exe CLI via SSH
+requirements:
+  - paramiko
+description:
+  - Run commands or put/fetch files to an existing WSL distribution using wsl.exe CLI via SSH.
+  - Uses the Python SSH implementation (Paramiko) to connect to the WSL host.
+version_added: "10.4.0"
+options:
+  remote_addr:
+    description:
+      - Address of the remote target.
+    default: inventory_hostname
+    type: string
+    vars:
+      - name: inventory_hostname
+      - name: ansible_host
+      - name: ansible_ssh_host
+      - name: ansible_paramiko_host
+  port:
+    description: Remote port to connect to.
+    type: int
+    default: 22
+    ini:
+      - section: defaults
+        key: remote_port
+      - section: paramiko_connection
+        key: remote_port
+    env:
+      - name: ANSIBLE_REMOTE_PORT
+      - name: ANSIBLE_REMOTE_PARAMIKO_PORT
+    vars:
+      - name: ansible_port
+      - name: ansible_ssh_port
+      - name: ansible_paramiko_port
+    keyword:
+      - name: port
+  remote_user:
+    description:
+      - User to login/authenticate as.
+      - Can be set from the CLI via the C(--user) or C(-u) options.
+    type: string
+    vars:
+      - name: ansible_user
+      - name: ansible_ssh_user
+      - name: ansible_paramiko_user
+    env:
+      - name: ANSIBLE_REMOTE_USER
+      - name: ANSIBLE_PARAMIKO_REMOTE_USER
+    ini:
+      - section: defaults
+        key: remote_user
+      - section: paramiko_connection
+        key: remote_user
+    keyword:
+      - name: remote_user
+  password:
+    description:
+      - Secret used to either login the SSH server or as a passphrase for SSH keys that require it.
+      - Can be set from the CLI via the C(--ask-pass) option.
+    type: string
+    vars:
+      - name: ansible_password
+      - name: ansible_ssh_pass
+      - name: ansible_ssh_password
+      - name: ansible_paramiko_pass
+      - name: ansible_paramiko_password
+  use_rsa_sha2_algorithms:
+    description:
+      - Whether or not to enable RSA SHA2 algorithms for pubkeys and hostkeys.
+      - On paramiko versions older than 2.9, this only affects hostkeys.
+      - For behavior matching paramiko<2.9 set this to V(false).
+    vars:
+      - name: ansible_paramiko_use_rsa_sha2_algorithms
+    ini:
+      - {key: use_rsa_sha2_algorithms, section: paramiko_connection}
+    env:
+      - {name: ANSIBLE_PARAMIKO_USE_RSA_SHA2_ALGORITHMS}
+    default: true
+    type: boolean
+  host_key_auto_add:
+    description: "Automatically add host keys to C(~/.ssh/known_hosts)."
+    env:
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_AUTO_ADD
+    ini:
+      - key: host_key_auto_add
+        section: paramiko_connection
+    type: boolean
+  look_for_keys:
+    default: True
+    description: "Set to V(false) to disable searching for private key files in C(~/.ssh/)."
+    env:
+      - name: ANSIBLE_PARAMIKO_LOOK_FOR_KEYS
+    ini:
+      - {key: look_for_keys, section: paramiko_connection}
+    type: boolean
+  proxy_command:
+    default: ""
+    description:
+      - Proxy information for running the connection via a jumphost.
+    type: string
+    env:
+      - name: ANSIBLE_PARAMIKO_PROXY_COMMAND
+    ini:
+      - {key: proxy_command, section: paramiko_connection}
+    vars:
+      - name: ansible_paramiko_proxy_command
+  record_host_keys:
+    default: True
+    description: "Save the host keys to a file."
+    env:
+      - name: ANSIBLE_PARAMIKO_RECORD_HOST_KEYS
+    ini:
+      - section: paramiko_connection
+        key: record_host_keys
+    type: boolean
+  host_key_checking:
+    description: "Set this to V(false) if you want to avoid host key checking by the underlying tools Ansible uses to connect to the host."
+    type: boolean
+    default: true
+    env:
+      - name: ANSIBLE_HOST_KEY_CHECKING
+      - name: ANSIBLE_SSH_HOST_KEY_CHECKING
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_CHECKING
+    ini:
+      - section: defaults
+        key: host_key_checking
+      - section: paramiko_connection
+        key: host_key_checking
+    vars:
+      - name: ansible_host_key_checking
+      - name: ansible_ssh_host_key_checking
+      - name: ansible_paramiko_host_key_checking
+  use_persistent_connections:
+    description: "Toggles the use of persistence for connections."
+    type: boolean
+    default: False
+    env:
+      - name: ANSIBLE_USE_PERSISTENT_CONNECTIONS
+    ini:
+      - section: defaults
+        key: use_persistent_connections
+  banner_timeout:
+    type: float
+    default: 30
+    description:
+      - Configures, in seconds, the amount of time to wait for the SSH
+        banner to be presented. This option is supported by paramiko
+        version 1.15.0 or newer.
+    ini:
+      - section: paramiko_connection
+        key: banner_timeout
+    env:
+      - name: ANSIBLE_PARAMIKO_BANNER_TIMEOUT
+  timeout:
+    type: int
+    default: 10
+    description: Number of seconds until the plugin gives up on failing to establish a TCP connection.
+    ini:
+      - section: defaults
+        key: timeout
+      - section: ssh_connection
+        key: timeout
+      - section: paramiko_connection
+        key: timeout
+    env:
+      - name: ANSIBLE_TIMEOUT
+      - name: ANSIBLE_SSH_TIMEOUT
+      - name: ANSIBLE_PARAMIKO_TIMEOUT
+    vars:
+      - name: ansible_ssh_timeout
+      - name: ansible_paramiko_timeout
+    cli:
+      - name: timeout
+  lock_file_timeout:
+    type: int
+    default: 60
+    description: Number of seconds until the plugin gives up on trying to write a lock file when writing SSH known host keys.
+    vars:
+      - name: ansible_lock_file_timeout
+    env:
+      - name: ANSIBLE_LOCK_FILE_TIMEOUT
+  private_key_file:
+    description:
+      - Path to private key file to use for authentication.
+    type: string
+    ini:
+      - section: defaults
+        key: private_key_file
+      - section: paramiko_connection
+        key: private_key_file
+    env:
+      - name: ANSIBLE_PRIVATE_KEY_FILE
+      - name: ANSIBLE_PARAMIKO_PRIVATE_KEY_FILE
+    vars:
+      - name: ansible_private_key_file
+      - name: ansible_ssh_private_key_file
+      - name: ansible_paramiko_private_key_file
+    cli:
+      - name: private_key_file
+        option: "--private-key"
+  wsl_distribution:
+    description:
+      - WSL distribution name
+    type: string
+    required: true
+    vars:
+      - name: wsl_distribution
+  wsl_user:
path: plugins/connection/wsl.py
user: login: felixfontein
body: I'm sure you mean `remote_user`, not `remove_user` ;-)

Generally the name of the option does not really matter, since it's only used in the documentation and in the code - you need to supply the same value to `self.get_option()`, but that's it. (The only exceptions are modules and inventory plugins.)

Note that your connection plugin already has an option called `remote_user`, which is used as the SSH user to connect to. `wsl_user` is something else (the user to connect to in WSL).
created_at: 2025-02-26T20:12:24Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,758 @@
+# -*- coding: utf-8 -*-
+# Derived from ansible/plugins/connection/proxmox_pct_remote.py (c) 2024 Nils Stein (@mietzen) <github.nstein@mailbox.org>
+# Derived from ansible/plugins/connection/paramiko_ssh.py (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
+# Copyright (c) 2025 Rui Lopes (@rgl) <ruilopes.com>
+# Copyright (c) 2025 Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import annotations
+
+DOCUMENTATION = r"""
+author: Rui Lopes (@rgl) <ruilopes.com>
+name: wsl
+short_description: Run tasks in WSL distribution using wsl.exe CLI via SSH
+requirements:
+  - paramiko
+description:
+  - Run commands or put/fetch files to an existing WSL distribution using wsl.exe CLI via SSH.
+  - Uses the Python SSH implementation (Paramiko) to connect to the WSL host.
+version_added: "10.4.0"
+options:
+  remote_addr:
+    description:
+      - Address of the remote target.
+    default: inventory_hostname
+    type: string
+    vars:
+      - name: inventory_hostname
+      - name: ansible_host
+      - name: ansible_ssh_host
+      - name: ansible_paramiko_host
+  port:
+    description: Remote port to connect to.
+    type: int
+    default: 22
+    ini:
+      - section: defaults
+        key: remote_port
+      - section: paramiko_connection
+        key: remote_port
+    env:
+      - name: ANSIBLE_REMOTE_PORT
+      - name: ANSIBLE_REMOTE_PARAMIKO_PORT
+    vars:
+      - name: ansible_port
+      - name: ansible_ssh_port
+      - name: ansible_paramiko_port
+    keyword:
+      - name: port
+  remote_user:
+    description:
+      - User to login/authenticate as.
+      - Can be set from the CLI via the C(--user) or C(-u) options.
+    type: string
+    vars:
+      - name: ansible_user
+      - name: ansible_ssh_user
+      - name: ansible_paramiko_user
+    env:
+      - name: ANSIBLE_REMOTE_USER
+      - name: ANSIBLE_PARAMIKO_REMOTE_USER
+    ini:
+      - section: defaults
+        key: remote_user
+      - section: paramiko_connection
+        key: remote_user
+    keyword:
+      - name: remote_user
+  password:
+    description:
+      - Secret used to either login the SSH server or as a passphrase for SSH keys that require it.
+      - Can be set from the CLI via the C(--ask-pass) option.
+    type: string
+    vars:
+      - name: ansible_password
+      - name: ansible_ssh_pass
+      - name: ansible_ssh_password
+      - name: ansible_paramiko_pass
+      - name: ansible_paramiko_password
+  use_rsa_sha2_algorithms:
+    description:
+      - Whether or not to enable RSA SHA2 algorithms for pubkeys and hostkeys.
+      - On paramiko versions older than 2.9, this only affects hostkeys.
+      - For behavior matching paramiko<2.9 set this to V(false).
+    vars:
+      - name: ansible_paramiko_use_rsa_sha2_algorithms
+    ini:
+      - {key: use_rsa_sha2_algorithms, section: paramiko_connection}
+    env:
+      - {name: ANSIBLE_PARAMIKO_USE_RSA_SHA2_ALGORITHMS}
+    default: true
+    type: boolean
+  host_key_auto_add:
+    description: "Automatically add host keys to C(~/.ssh/known_hosts)."
+    env:
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_AUTO_ADD
+    ini:
+      - key: host_key_auto_add
+        section: paramiko_connection
+    type: boolean
+  look_for_keys:
+    default: True
+    description: "Set to V(false) to disable searching for private key files in C(~/.ssh/)."
+    env:
+      - name: ANSIBLE_PARAMIKO_LOOK_FOR_KEYS
+    ini:
+      - {key: look_for_keys, section: paramiko_connection}
+    type: boolean
+  proxy_command:
+    default: ""
+    description:
+      - Proxy information for running the connection via a jumphost.
+    type: string
+    env:
+      - name: ANSIBLE_PARAMIKO_PROXY_COMMAND
+    ini:
+      - {key: proxy_command, section: paramiko_connection}
+    vars:
+      - name: ansible_paramiko_proxy_command
+  record_host_keys:
+    default: True
+    description: "Save the host keys to a file."
+    env:
+      - name: ANSIBLE_PARAMIKO_RECORD_HOST_KEYS
+    ini:
+      - section: paramiko_connection
+        key: record_host_keys
+    type: boolean
+  host_key_checking:
+    description: "Set this to V(false) if you want to avoid host key checking by the underlying tools Ansible uses to connect to the host."
+    type: boolean
+    default: true
+    env:
+      - name: ANSIBLE_HOST_KEY_CHECKING
+      - name: ANSIBLE_SSH_HOST_KEY_CHECKING
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_CHECKING
+    ini:
+      - section: defaults
+        key: host_key_checking
+      - section: paramiko_connection
+        key: host_key_checking
+    vars:
+      - name: ansible_host_key_checking
+      - name: ansible_ssh_host_key_checking
+      - name: ansible_paramiko_host_key_checking
+  use_persistent_connections:
+    description: "Toggles the use of persistence for connections."
+    type: boolean
+    default: False
+    env:
+      - name: ANSIBLE_USE_PERSISTENT_CONNECTIONS
+    ini:
+      - section: defaults
+        key: use_persistent_connections
+  banner_timeout:
+    type: float
+    default: 30
+    description:
+      - Configures, in seconds, the amount of time to wait for the SSH
+        banner to be presented. This option is supported by paramiko
+        version 1.15.0 or newer.
+    ini:
+      - section: paramiko_connection
+        key: banner_timeout
+    env:
+      - name: ANSIBLE_PARAMIKO_BANNER_TIMEOUT
+  timeout:
+    type: int
+    default: 10
+    description: Number of seconds until the plugin gives up on failing to establish a TCP connection.
+    ini:
+      - section: defaults
+        key: timeout
+      - section: ssh_connection
+        key: timeout
+      - section: paramiko_connection
+        key: timeout
+    env:
+      - name: ANSIBLE_TIMEOUT
+      - name: ANSIBLE_SSH_TIMEOUT
+      - name: ANSIBLE_PARAMIKO_TIMEOUT
+    vars:
+      - name: ansible_ssh_timeout
+      - name: ansible_paramiko_timeout
+    cli:
+      - name: timeout
+  lock_file_timeout:
+    type: int
+    default: 60
+    description: Number of seconds until the plugin gives up on trying to write a lock file when writing SSH known host keys.
+    vars:
+      - name: ansible_lock_file_timeout
+    env:
+      - name: ANSIBLE_LOCK_FILE_TIMEOUT
+  private_key_file:
+    description:
+      - Path to private key file to use for authentication.
+    type: string
+    ini:
+      - section: defaults
+        key: private_key_file
+      - section: paramiko_connection
+        key: private_key_file
+    env:
+      - name: ANSIBLE_PRIVATE_KEY_FILE
+      - name: ANSIBLE_PARAMIKO_PRIVATE_KEY_FILE
+    vars:
+      - name: ansible_private_key_file
+      - name: ansible_ssh_private_key_file
+      - name: ansible_paramiko_private_key_file
+    cli:
+      - name: private_key_file
+        option: "--private-key"
+  wsl_distribution:
+    description:
+      - WSL distribution name
+    type: string
+    required: true
+    vars:
+      - name: wsl_distribution
+  wsl_user:
path: plugins/connection/wsl.py
user: login: rgl
body: oh indeed, I've confused myself! scrape this, remote user is the windows host user.
created_at: 2025-02-26T21:54:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,768 @@
+# -*- coding: utf-8 -*-
+# Derived from ansible/plugins/connection/proxmox_pct_remote.py (c) 2024 Nils Stein (@mietzen) <github.nstein@mailbox.org>
+# Derived from ansible/plugins/connection/paramiko_ssh.py (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
+# Copyright (c) 2025 Rui Lopes (@rgl) <ruilopes.com>
+# Copyright (c) 2025 Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import annotations
+
+DOCUMENTATION = r"""
+author: Rui Lopes (@rgl) <ruilopes.com>
+name: wsl
+short_description: Run tasks in WSL distribution using wsl.exe CLI via SSH
+requirements:
+  - paramiko
+description:
+  - Run commands or put/fetch files to an existing WSL distribution using wsl.exe CLI via SSH.
+  - Uses the Python SSH implementation (Paramiko) to connect to the WSL host.
+version_added: "10.5.0"
+options:
+  remote_addr:
+    description:
+      - Address of the remote target.
+    default: inventory_hostname
+    type: string
+    vars:
+      - name: inventory_hostname
+      - name: ansible_host
+      - name: ansible_ssh_host
+      - name: ansible_paramiko_host
+  port:
+    description: Remote port to connect to.
+    type: int
+    default: 22
+    ini:
+      - section: defaults
+        key: remote_port
+      - section: paramiko_connection
+        key: remote_port
+    env:
+      - name: ANSIBLE_REMOTE_PORT
+      - name: ANSIBLE_REMOTE_PARAMIKO_PORT
+    vars:
+      - name: ansible_port
+      - name: ansible_ssh_port
+      - name: ansible_paramiko_port
+    keyword:
+      - name: port
+  remote_user:
+    description:
+      - User to login/authenticate as.
+      - Can be set from the CLI via the C(--user) or C(-u) options.
+    type: string
+    vars:
+      - name: ansible_user
+      - name: ansible_ssh_user
+      - name: ansible_paramiko_user
+    env:
+      - name: ANSIBLE_REMOTE_USER
+      - name: ANSIBLE_PARAMIKO_REMOTE_USER
+    ini:
+      - section: defaults
+        key: remote_user
+      - section: paramiko_connection
+        key: remote_user
+    keyword:
+      - name: remote_user
+  password:
+    description:
+      - Secret used to either login the SSH server or as a passphrase for SSH keys that require it.
+      - Can be set from the CLI via the C(--ask-pass) option.
+    type: string
+    vars:
+      - name: ansible_password
+      - name: ansible_ssh_pass
+      - name: ansible_ssh_password
+      - name: ansible_paramiko_pass
+      - name: ansible_paramiko_password
+  use_rsa_sha2_algorithms:
+    description:
+      - Whether or not to enable RSA SHA2 algorithms for pubkeys and hostkeys.
+      - On paramiko versions older than 2.9, this only affects hostkeys.
+      - For behavior matching paramiko<2.9 set this to V(false).
+    vars:
+      - name: ansible_paramiko_use_rsa_sha2_algorithms
+    ini:
+      - {key: use_rsa_sha2_algorithms, section: paramiko_connection}
+    env:
+      - {name: ANSIBLE_PARAMIKO_USE_RSA_SHA2_ALGORITHMS}
+    default: true
+    type: boolean
+  host_key_auto_add:
+    description: "Automatically add host keys to C(~/.ssh/known_hosts)."
+    env:
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_AUTO_ADD
+    ini:
+      - key: host_key_auto_add
+        section: paramiko_connection
+    type: boolean
+  look_for_keys:
+    default: True
+    description: "Set to V(false) to disable searching for private key files in C(~/.ssh/)."
+    env:
+      - name: ANSIBLE_PARAMIKO_LOOK_FOR_KEYS
+    ini:
+      - {key: look_for_keys, section: paramiko_connection}
+    type: boolean
+  proxy_command:
+    default: ""
+    description:
+      - Proxy information for running the connection via a jumphost.
+    type: string
+    env:
+      - name: ANSIBLE_PARAMIKO_PROXY_COMMAND
+    ini:
+      - {key: proxy_command, section: paramiko_connection}
+    vars:
+      - name: ansible_paramiko_proxy_command
+  record_host_keys:
+    default: True
+    description: "Save the host keys to a file."
+    env:
+      - name: ANSIBLE_PARAMIKO_RECORD_HOST_KEYS
+    ini:
+      - section: paramiko_connection
+        key: record_host_keys
+    type: boolean
+  host_key_checking:
+    description: "Set this to V(false) if you want to avoid host key checking by the underlying tools Ansible uses to connect to the host."
+    type: boolean
+    default: true
+    env:
+      - name: ANSIBLE_HOST_KEY_CHECKING
+      - name: ANSIBLE_SSH_HOST_KEY_CHECKING
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_CHECKING
+    ini:
+      - section: defaults
+        key: host_key_checking
+      - section: paramiko_connection
+        key: host_key_checking
+    vars:
+      - name: ansible_host_key_checking
+      - name: ansible_ssh_host_key_checking
+      - name: ansible_paramiko_host_key_checking
+  use_persistent_connections:
+    description: "Toggles the use of persistence for connections."
+    type: boolean
+    default: False
+    env:
+      - name: ANSIBLE_USE_PERSISTENT_CONNECTIONS
+    ini:
+      - section: defaults
+        key: use_persistent_connections
+  banner_timeout:
+    type: float
+    default: 30
+    description:
+      - Configures, in seconds, the amount of time to wait for the SSH
+        banner to be presented. This option is supported by paramiko
+        version 1.15.0 or newer.
+    ini:
+      - section: paramiko_connection
+        key: banner_timeout
+    env:
+      - name: ANSIBLE_PARAMIKO_BANNER_TIMEOUT
+  timeout:
+    type: int
+    default: 10
+    description: Number of seconds until the plugin gives up on failing to establish a TCP connection.
+    ini:
+      - section: defaults
+        key: timeout
+      - section: ssh_connection
+        key: timeout
+      - section: paramiko_connection
+        key: timeout
+    env:
+      - name: ANSIBLE_TIMEOUT
+      - name: ANSIBLE_SSH_TIMEOUT
+      - name: ANSIBLE_PARAMIKO_TIMEOUT
+    vars:
+      - name: ansible_ssh_timeout
+      - name: ansible_paramiko_timeout
+    cli:
+      - name: timeout
+  lock_file_timeout:
+    type: int
+    default: 60
+    description: Number of seconds until the plugin gives up on trying to write a lock file when writing SSH known host keys.
+    vars:
+      - name: ansible_lock_file_timeout
+    env:
+      - name: ANSIBLE_LOCK_FILE_TIMEOUT
+  private_key_file:
+    description:
+      - Path to private key file to use for authentication.
+    type: string
+    ini:
+      - section: defaults
+        key: private_key_file
+      - section: paramiko_connection
+        key: private_key_file
+    env:
+      - name: ANSIBLE_PRIVATE_KEY_FILE
+      - name: ANSIBLE_PARAMIKO_PRIVATE_KEY_FILE
+    vars:
+      - name: ansible_private_key_file
+      - name: ansible_ssh_private_key_file
+      - name: ansible_paramiko_private_key_file
+    cli:
+      - name: private_key_file
+        option: "--private-key"
+  wsl_distribution:
+    description:
+      - WSL distribution name
+    type: string
+    required: true
+    vars:
+      - name: wsl_distribution
+  wsl_user:
+    description:
+      - WSL distribution user
+    type: string
+    vars:
+      - name: wsl_user
+  become_user:
+    description:
+      - WSL distribution user
+    type: string
+    default: root
+    vars:
+      - name: become_user
+      - name: ansible_become_user
+  become:
+    description:
+      - whether to use the user defined by ansible_become_user.
+    type: bool
+    default: false
+    vars:
+      - name: become
+      - name: ansible_become
+  _wsl_command_use_bash_quoting:
path: plugins/connection/wsl.py
user: login: felixfontein
body: I don't like the idea of using a plugin option for this. This is part of the plugin's public interface and will be visible in the documentation.

How about using a special environment variable instead, that you set in `runme.sh`? For example, `_ANSIBLE_TEST_WSL_CONNECTION_PLUGIN_Waeri5tepheeSha2fae8`. The last 20 characters are a randomly generated password (on my machine), that should avoid any kind of collision with intentionally chosen environment variables. (The name should already do that, it's just to be on the safe side.)
created_at: 2025-03-01T14:29:47Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,768 @@
+# -*- coding: utf-8 -*-
+# Derived from ansible/plugins/connection/proxmox_pct_remote.py (c) 2024 Nils Stein (@mietzen) <github.nstein@mailbox.org>
+# Derived from ansible/plugins/connection/paramiko_ssh.py (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
+# Copyright (c) 2025 Rui Lopes (@rgl) <ruilopes.com>
+# Copyright (c) 2025 Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import annotations
+
+DOCUMENTATION = r"""
+author: Rui Lopes (@rgl) <ruilopes.com>
+name: wsl
+short_description: Run tasks in WSL distribution using wsl.exe CLI via SSH
+requirements:
+  - paramiko
+description:
+  - Run commands or put/fetch files to an existing WSL distribution using wsl.exe CLI via SSH.
+  - Uses the Python SSH implementation (Paramiko) to connect to the WSL host.
+version_added: "10.5.0"
+options:
+  remote_addr:
+    description:
+      - Address of the remote target.
+    default: inventory_hostname
+    type: string
+    vars:
+      - name: inventory_hostname
+      - name: ansible_host
+      - name: ansible_ssh_host
+      - name: ansible_paramiko_host
+  port:
+    description: Remote port to connect to.
+    type: int
+    default: 22
+    ini:
+      - section: defaults
+        key: remote_port
+      - section: paramiko_connection
+        key: remote_port
+    env:
+      - name: ANSIBLE_REMOTE_PORT
+      - name: ANSIBLE_REMOTE_PARAMIKO_PORT
+    vars:
+      - name: ansible_port
+      - name: ansible_ssh_port
+      - name: ansible_paramiko_port
+    keyword:
+      - name: port
+  remote_user:
+    description:
+      - User to login/authenticate as.
+      - Can be set from the CLI via the C(--user) or C(-u) options.
+    type: string
+    vars:
+      - name: ansible_user
+      - name: ansible_ssh_user
+      - name: ansible_paramiko_user
+    env:
+      - name: ANSIBLE_REMOTE_USER
+      - name: ANSIBLE_PARAMIKO_REMOTE_USER
+    ini:
+      - section: defaults
+        key: remote_user
+      - section: paramiko_connection
+        key: remote_user
+    keyword:
+      - name: remote_user
+  password:
+    description:
+      - Secret used to either login the SSH server or as a passphrase for SSH keys that require it.
+      - Can be set from the CLI via the C(--ask-pass) option.
+    type: string
+    vars:
+      - name: ansible_password
+      - name: ansible_ssh_pass
+      - name: ansible_ssh_password
+      - name: ansible_paramiko_pass
+      - name: ansible_paramiko_password
+  use_rsa_sha2_algorithms:
+    description:
+      - Whether or not to enable RSA SHA2 algorithms for pubkeys and hostkeys.
+      - On paramiko versions older than 2.9, this only affects hostkeys.
+      - For behavior matching paramiko<2.9 set this to V(false).
+    vars:
+      - name: ansible_paramiko_use_rsa_sha2_algorithms
+    ini:
+      - {key: use_rsa_sha2_algorithms, section: paramiko_connection}
+    env:
+      - {name: ANSIBLE_PARAMIKO_USE_RSA_SHA2_ALGORITHMS}
+    default: true
+    type: boolean
+  host_key_auto_add:
+    description: "Automatically add host keys to C(~/.ssh/known_hosts)."
+    env:
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_AUTO_ADD
+    ini:
+      - key: host_key_auto_add
+        section: paramiko_connection
+    type: boolean
+  look_for_keys:
+    default: True
+    description: "Set to V(false) to disable searching for private key files in C(~/.ssh/)."
+    env:
+      - name: ANSIBLE_PARAMIKO_LOOK_FOR_KEYS
+    ini:
+      - {key: look_for_keys, section: paramiko_connection}
+    type: boolean
+  proxy_command:
+    default: ""
+    description:
+      - Proxy information for running the connection via a jumphost.
+    type: string
+    env:
+      - name: ANSIBLE_PARAMIKO_PROXY_COMMAND
+    ini:
+      - {key: proxy_command, section: paramiko_connection}
+    vars:
+      - name: ansible_paramiko_proxy_command
+  record_host_keys:
+    default: True
+    description: "Save the host keys to a file."
+    env:
+      - name: ANSIBLE_PARAMIKO_RECORD_HOST_KEYS
+    ini:
+      - section: paramiko_connection
+        key: record_host_keys
+    type: boolean
+  host_key_checking:
+    description: "Set this to V(false) if you want to avoid host key checking by the underlying tools Ansible uses to connect to the host."
+    type: boolean
+    default: true
+    env:
+      - name: ANSIBLE_HOST_KEY_CHECKING
+      - name: ANSIBLE_SSH_HOST_KEY_CHECKING
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_CHECKING
+    ini:
+      - section: defaults
+        key: host_key_checking
+      - section: paramiko_connection
+        key: host_key_checking
+    vars:
+      - name: ansible_host_key_checking
+      - name: ansible_ssh_host_key_checking
+      - name: ansible_paramiko_host_key_checking
+  use_persistent_connections:
+    description: "Toggles the use of persistence for connections."
+    type: boolean
+    default: False
+    env:
+      - name: ANSIBLE_USE_PERSISTENT_CONNECTIONS
+    ini:
+      - section: defaults
+        key: use_persistent_connections
+  banner_timeout:
+    type: float
+    default: 30
+    description:
+      - Configures, in seconds, the amount of time to wait for the SSH
+        banner to be presented. This option is supported by paramiko
+        version 1.15.0 or newer.
+    ini:
+      - section: paramiko_connection
+        key: banner_timeout
+    env:
+      - name: ANSIBLE_PARAMIKO_BANNER_TIMEOUT
+  timeout:
+    type: int
+    default: 10
+    description: Number of seconds until the plugin gives up on failing to establish a TCP connection.
+    ini:
+      - section: defaults
+        key: timeout
+      - section: ssh_connection
+        key: timeout
+      - section: paramiko_connection
+        key: timeout
+    env:
+      - name: ANSIBLE_TIMEOUT
+      - name: ANSIBLE_SSH_TIMEOUT
+      - name: ANSIBLE_PARAMIKO_TIMEOUT
+    vars:
+      - name: ansible_ssh_timeout
+      - name: ansible_paramiko_timeout
+    cli:
+      - name: timeout
+  lock_file_timeout:
+    type: int
+    default: 60
+    description: Number of seconds until the plugin gives up on trying to write a lock file when writing SSH known host keys.
+    vars:
+      - name: ansible_lock_file_timeout
+    env:
+      - name: ANSIBLE_LOCK_FILE_TIMEOUT
+  private_key_file:
+    description:
+      - Path to private key file to use for authentication.
+    type: string
+    ini:
+      - section: defaults
+        key: private_key_file
+      - section: paramiko_connection
+        key: private_key_file
+    env:
+      - name: ANSIBLE_PRIVATE_KEY_FILE
+      - name: ANSIBLE_PARAMIKO_PRIVATE_KEY_FILE
+    vars:
+      - name: ansible_private_key_file
+      - name: ansible_ssh_private_key_file
+      - name: ansible_paramiko_private_key_file
+    cli:
+      - name: private_key_file
+        option: "--private-key"
+  wsl_distribution:
+    description:
+      - WSL distribution name
+    type: string
+    required: true
+    vars:
+      - name: wsl_distribution
+  wsl_user:
+    description:
+      - WSL distribution user
+    type: string
+    vars:
+      - name: wsl_user
+  become_user:
+    description:
+      - WSL distribution user
+    type: string
+    default: root
+    vars:
+      - name: become_user
+      - name: ansible_become_user
+  become:
+    description:
+      - whether to use the user defined by ansible_become_user.
+    type: bool
+    default: false
+    vars:
+      - name: become
+      - name: ansible_become
+  _wsl_command_use_bash_quoting:
path: plugins/connection/wsl.py
user: login: rgl
body: ah, indeed, much better, its now using an environment variable, please check it out :)
created_at: 2025-03-01T14:55:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,758 @@
+# -*- coding: utf-8 -*-
+# Derived from ansible/plugins/connection/proxmox_pct_remote.py (c) 2024 Nils Stein (@mietzen) <github.nstein@mailbox.org>
+# Derived from ansible/plugins/connection/paramiko_ssh.py (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
+# Copyright (c) 2025 Rui Lopes (@rgl) <ruilopes.com>
+# Copyright (c) 2025 Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import annotations
+
+DOCUMENTATION = r"""
+author: Rui Lopes (@rgl) <ruilopes.com>
+name: wsl
+short_description: Run tasks in WSL distribution using wsl.exe CLI via SSH
+requirements:
+  - paramiko
+description:
+  - Run commands or put/fetch files to an existing WSL distribution using wsl.exe CLI via SSH.
+  - Uses the Python SSH implementation (Paramiko) to connect to the WSL host.
+version_added: "10.3.0"
+options:
+  remote_addr:
+    description:
+      - Address of the remote target.
+    default: inventory_hostname
+    type: string
+    vars:
+      - name: inventory_hostname
+      - name: ansible_host
+      - name: ansible_ssh_host
+      - name: ansible_paramiko_host
+  port:
+    description: Remote port to connect to.
+    type: int
+    default: 22
+    ini:
+      - section: defaults
+        key: remote_port
+      - section: paramiko_connection
+        key: remote_port
+    env:
+      - name: ANSIBLE_REMOTE_PORT
+      - name: ANSIBLE_REMOTE_PARAMIKO_PORT
+    vars:
+      - name: ansible_port
+      - name: ansible_ssh_port
+      - name: ansible_paramiko_port
+    keyword:
+      - name: port
+  remote_user:
+    description:
+      - User to login/authenticate as.
+      - Can be set from the CLI via the C(--user) or C(-u) options.
+    type: string
+    vars:
+      - name: ansible_user
+      - name: ansible_ssh_user
+      - name: ansible_paramiko_user
+    env:
+      - name: ANSIBLE_REMOTE_USER
+      - name: ANSIBLE_PARAMIKO_REMOTE_USER
+    ini:
+      - section: defaults
+        key: remote_user
+      - section: paramiko_connection
+        key: remote_user
+    keyword:
+      - name: remote_user
+  password:
+    description:
+      - Secret used to either login the SSH server or as a passphrase for SSH keys that require it.
+      - Can be set from the CLI via the C(--ask-pass) option.
+    type: string
+    vars:
+      - name: ansible_password
+      - name: ansible_ssh_pass
+      - name: ansible_ssh_password
+      - name: ansible_paramiko_pass
+      - name: ansible_paramiko_password
+  use_rsa_sha2_algorithms:
+    description:
+      - Whether or not to enable RSA SHA2 algorithms for pubkeys and hostkeys.
+      - On paramiko versions older than 2.9, this only affects hostkeys.
+      - For behavior matching paramiko<2.9 set this to V(false).
+    vars:
+      - name: ansible_paramiko_use_rsa_sha2_algorithms
+    ini:
+      - {key: use_rsa_sha2_algorithms, section: paramiko_connection}
+    env:
+      - {name: ANSIBLE_PARAMIKO_USE_RSA_SHA2_ALGORITHMS}
+    default: true
+    type: boolean
+  host_key_auto_add:
+    description: "Automatically add host keys to C(~/.ssh/known_hosts)."
+    env:
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_AUTO_ADD
+    ini:
+      - key: host_key_auto_add
+        section: paramiko_connection
+    type: boolean
+  look_for_keys:
+    default: True
+    description: "Set to V(false) to disable searching for private key files in C(~/.ssh/)."
+    env:
+      - name: ANSIBLE_PARAMIKO_LOOK_FOR_KEYS
+    ini:
+      - {key: look_for_keys, section: paramiko_connection}
+    type: boolean
+  proxy_command:
+    default: ""
+    description:
+      - Proxy information for running the connection via a jumphost.
+    type: string
+    env:
+      - name: ANSIBLE_PARAMIKO_PROXY_COMMAND
+    ini:
+      - {key: proxy_command, section: paramiko_connection}
+    vars:
+      - name: ansible_paramiko_proxy_command
+  record_host_keys:
+    default: True
+    description: "Save the host keys to a file."
+    env:
+      - name: ANSIBLE_PARAMIKO_RECORD_HOST_KEYS
+    ini:
+      - section: paramiko_connection
+        key: record_host_keys
+    type: boolean
+  host_key_checking:
+    description: "Set this to V(false) if you want to avoid host key checking by the underlying tools Ansible uses to connect to the host."
+    type: boolean
+    default: true
+    env:
+      - name: ANSIBLE_HOST_KEY_CHECKING
+      - name: ANSIBLE_SSH_HOST_KEY_CHECKING
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_CHECKING
+    ini:
+      - section: defaults
+        key: host_key_checking
+      - section: paramiko_connection
+        key: host_key_checking
+    vars:
+      - name: ansible_host_key_checking
+      - name: ansible_ssh_host_key_checking
+      - name: ansible_paramiko_host_key_checking
+  use_persistent_connections:
+    description: "Toggles the use of persistence for connections."
+    type: boolean
+    default: False
+    env:
+      - name: ANSIBLE_USE_PERSISTENT_CONNECTIONS
+    ini:
+      - section: defaults
+        key: use_persistent_connections
+  banner_timeout:
+    type: float
+    default: 30
+    description:
+      - Configures, in seconds, the amount of time to wait for the SSH
+        banner to be presented. This option is supported by paramiko
+        version 1.15.0 or newer.
+    ini:
+      - section: paramiko_connection
+        key: banner_timeout
+    env:
+      - name: ANSIBLE_PARAMIKO_BANNER_TIMEOUT
+  timeout:
+    type: int
+    default: 10
+    description: Number of seconds until the plugin gives up on failing to establish a TCP connection.
+    ini:
+      - section: defaults
+        key: timeout
+      - section: ssh_connection
+        key: timeout
+      - section: paramiko_connection
+        key: timeout
+    env:
+      - name: ANSIBLE_TIMEOUT
+      - name: ANSIBLE_SSH_TIMEOUT
+      - name: ANSIBLE_PARAMIKO_TIMEOUT
+    vars:
+      - name: ansible_ssh_timeout
+      - name: ansible_paramiko_timeout
+    cli:
+      - name: timeout
+  lock_file_timeout:
+    type: int
+    default: 60
+    description: Number of seconds until the plugin gives up on trying to write a lock file when writing SSH known host keys.
+    vars:
+      - name: ansible_lock_file_timeout
+    env:
+      - name: ANSIBLE_LOCK_FILE_TIMEOUT
+  private_key_file:
+    description:
+      - Path to private key file to use for authentication.
+    type: string
+    ini:
+      - section: defaults
+        key: private_key_file
+      - section: paramiko_connection
+        key: private_key_file
+    env:
+      - name: ANSIBLE_PRIVATE_KEY_FILE
+      - name: ANSIBLE_PARAMIKO_PRIVATE_KEY_FILE
+    vars:
+      - name: ansible_private_key_file
+      - name: ansible_ssh_private_key_file
+      - name: ansible_paramiko_private_key_file
+    cli:
+      - name: private_key_file
+        option: "--private-key"
+  wsl_distribution:
+    description:
+      - WSL distribution name
+    type: string
+    vars:
+      - name: wsl_distribution
+  wsl_user:
+    description:
+      - WSL distribution user
+    type: string
+    vars:
+      - name: wsl_user
+  become_user:
+    description:
+      - WSL distribution user
+    type: string
+    default: root
+    vars:
+      - name: become_user
+      - name: ansible_become_user
+  become:
+    description:
+      - whether to use the user defined by ansible_become_user.
+    type: bool
+    default: false
+    vars:
+      - name: become
+      - name: ansible_become
+"""
+
+EXAMPLES = r"""
+# ------------------------
+# Inventory: inventory.yml
+# ------------------------
+# all:
+#   children:
+#     wsl:
+#       hosts:
+#         ubuntu:
+#           ansible_host: 10.0.0.10
+#           wsl_distribution: ubuntu
+#         debian:
+#           ansible_host: 10.0.0.10
+#           wsl_distribution: debian
+#       vars:
+#         ansible_connection: community.general.wsl
+#         ansible_user: vagrant
+#         wsl_user: wsl
+# ----------------------
+# Playbook: playbook.yml
+# ----------------------
+---
+- hosts: wsl
+  tasks:
+    - name: ping
+      ansible.builtin.ping:
+    - name: id
+      args:
+        executable: /bin/bash
+      ansible.builtin.shell: |
+        exec 2>&1
+        set -x
+        echo "$0"
+        pwd
+        id
+    - name: become id
+      become: true
+      args:
+        executable: /bin/bash
+      ansible.builtin.shell: |
+        exec 2>&1
+        set -x
+        echo "$0"
+        pwd
+        id
+"""
+
+import os
+import pathlib
+import shlex
+import socket
+import tempfile
+import typing as t
+
+from ansible.errors import (
+    AnsibleAuthenticationFailure,
+    AnsibleConnectionFailure,
+    AnsibleError,
+)
+from ansible_collections.community.general.plugins.module_utils._filelock import FileLock, LockTimeout
+from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
+from ansible.module_utils.compat.paramiko import PARAMIKO_IMPORT_ERR, paramiko
+from ansible.module_utils.compat.version import LooseVersion
+from ansible.plugins.connection import ConnectionBase
+from ansible.utils.display import Display
+from ansible.utils.path import makedirs_safe
+from binascii import hexlify
+from subprocess import list2cmdline
+
+
+display = Display()
+
+
+def authenticity_msg(hostname: str, ktype: str, fingerprint: str) -> str:
+    msg = f"""
+    paramiko: The authenticity of host '{hostname}' can't be established.
+    The {ktype} key fingerprint is {fingerprint}.
+    Are you sure you want to continue connecting (yes/no)?
+    """
+    return msg
+
+
+MissingHostKeyPolicy: type = object
+if paramiko:
+    MissingHostKeyPolicy = paramiko.MissingHostKeyPolicy
+
+
+class MyAddPolicy(MissingHostKeyPolicy):
+    """
+    Based on AutoAddPolicy in paramiko so we can determine when keys are added
+
+    and also prompt for input.
+
+    Policy for automatically adding the hostname and new host key to the
+    local L{HostKeys} object, and saving it. This is used by L{SSHClient}.
+    """
+
+    def __init__(self, connection: Connection) -> None:
+        self.connection = connection
+        self._options = connection._options
+
+    def missing_host_key(self, client, hostname, key) -> None:
+
+        if all((self.connection.get_option('host_key_checking'), not self.connection.get_option('host_key_auto_add'))):
+
+            fingerprint = hexlify(key.get_fingerprint())
+            ktype = key.get_name()
+
+            if self.connection.get_option('use_persistent_connections') or self.connection.force_persistence:
+                # don't print the prompt string since the user cannot respond
+                # to the question anyway
+                raise AnsibleError(authenticity_msg(hostname, ktype, fingerprint)[1:92])
+
+            inp = to_text(
+                display.prompt_until(authenticity_msg(hostname, ktype, fingerprint), private=False),
+                errors='surrogate_or_strict'
+            )
+
+            if inp.lower() not in ['yes', 'y', '']:
+                raise AnsibleError('host connection rejected by user')
+
+        key._added_by_ansible_this_time = True
+
+        # existing implementation below:
+        client._host_keys.add(hostname, key.get_name(), key)
+
+        # host keys are actually saved in close() function below
+        # in order to control ordering.
+
+
+class Connection(ConnectionBase):
+    """ SSH based connections (paramiko) to WSL """
+
+    transport = 'community.general.wsl'
+    _log_channel: str | None = None
+
+    def __init__(self, play_context, new_stdin, *args, **kwargs):
+        super(Connection, self).__init__(play_context, new_stdin, *args, **kwargs)
+
+    def _set_log_channel(self, name: str) -> None:
+        """ Mimic paramiko.SSHClient.set_log_channel """
+        self._log_channel = name
+
+    def _parse_proxy_command(self, port: int = 22) -> dict[str, t.Any]:
+        proxy_command = self.get_option('proxy_command') or None
+
+        sock_kwarg = {}
+        if proxy_command:
+            replacers = {
+                '%h': self.get_option('remote_addr'),
+                '%p': port,
+                '%r': self.get_option('remote_user')
+            }
+            for find, replace in replacers.items():
+                proxy_command = proxy_command.replace(find, str(replace))
+            try:
+                sock_kwarg = {'sock': paramiko.ProxyCommand(proxy_command)}
+                display.vvv(f'CONFIGURE PROXY COMMAND FOR CONNECTION: {proxy_command}', host=self.get_option('remote_addr'))
+            except AttributeError:
+                display.warning('Paramiko ProxyCommand support unavailable. '
+                                'Please upgrade to Paramiko 1.9.0 or newer. '
+                                'Not using configured ProxyCommand')
+
+        return sock_kwarg
+
+    def _connect(self) -> Connection:
+        """ activates the connection object """
+
+        if paramiko is None:
+            raise AnsibleError(f'paramiko is not installed: {to_native(PARAMIKO_IMPORT_ERR)}')
+
+        port = self.get_option('port')
+        display.vvv(f'ESTABLISH PARAMIKO SSH CONNECTION FOR USER: {self.get_option("remote_user")} on PORT {to_text(port)} TO {self.get_option("remote_addr")}',
+                    host=self.get_option('remote_addr'))
+
+        ssh = paramiko.SSHClient()
+
+        # Set pubkey and hostkey algorithms to disable, the only manipulation allowed currently
+        # is keeping or omitting rsa-sha2 algorithms
+        # default_keys: t.Tuple[str] = ()
+        paramiko_preferred_pubkeys = getattr(paramiko.Transport, '_preferred_pubkeys', ())
+        paramiko_preferred_hostkeys = getattr(paramiko.Transport, '_preferred_keys', ())
+        use_rsa_sha2_algorithms = self.get_option('use_rsa_sha2_algorithms')
+        disabled_algorithms: t.Dict[str, t.Iterable[str]] = {}
+        if not use_rsa_sha2_algorithms:
+            if paramiko_preferred_pubkeys:
+                disabled_algorithms['pubkeys'] = tuple(a for a in paramiko_preferred_pubkeys if 'rsa-sha2' in a)
+            if paramiko_preferred_hostkeys:
+                disabled_algorithms['keys'] = tuple(a for a in paramiko_preferred_hostkeys if 'rsa-sha2' in a)
+
+        # override paramiko's default logger name
+        if self._log_channel is not None:
+            ssh.set_log_channel(self._log_channel)
+
+        self.keyfile = os.path.expanduser('~/.ssh/known_hosts')
+
+        if self.get_option('host_key_checking'):
+            for ssh_known_hosts in ('/etc/ssh/ssh_known_hosts', '/etc/openssh/ssh_known_hosts'):
+                try:
+                    ssh.load_system_host_keys(ssh_known_hosts)
+                    break
+                except IOError:
+                    pass  # file was not found, but not required to function
+                except paramiko.hostkeys.InvalidHostKey as e:
+                    raise AnsibleConnectionFailure(f'Invalid host key: {to_text(e.line)}')
+            try:
+                ssh.load_system_host_keys()
+            except paramiko.hostkeys.InvalidHostKey as e:
+                raise AnsibleConnectionFailure(f'Invalid host key: {to_text(e.line)}')
+
+        ssh_connect_kwargs = self._parse_proxy_command(port)
+        ssh.set_missing_host_key_policy(MyAddPolicy(self))
+        conn_password = self.get_option('password')
+        allow_agent = True
+
+        if conn_password is not None:
+            allow_agent = False
+
+        try:
+            key_filename = None
+            if self.get_option('private_key_file'):
+                key_filename = os.path.expanduser(self.get_option('private_key_file'))
+
+            # paramiko 2.2 introduced auth_timeout parameter
+            if LooseVersion(paramiko.__version__) >= LooseVersion('2.2.0'):
+                ssh_connect_kwargs['auth_timeout'] = self.get_option('timeout')
path: plugins/connection/wsl.py
user: login: rgl
body: done. please verify.
created_at: 2025-03-05T18:53:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,758 @@
+# -*- coding: utf-8 -*-
+# Derived from ansible/plugins/connection/proxmox_pct_remote.py (c) 2024 Nils Stein (@mietzen) <github.nstein@mailbox.org>
+# Derived from ansible/plugins/connection/paramiko_ssh.py (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
+# Copyright (c) 2025 Rui Lopes (@rgl) <ruilopes.com>
+# Copyright (c) 2025 Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import annotations
+
+DOCUMENTATION = r"""
+author: Rui Lopes (@rgl) <ruilopes.com>
+name: wsl
+short_description: Run tasks in WSL distribution using wsl.exe CLI via SSH
+requirements:
+  - paramiko
+description:
+  - Run commands or put/fetch files to an existing WSL distribution using wsl.exe CLI via SSH.
+  - Uses the Python SSH implementation (Paramiko) to connect to the WSL host.
+version_added: "10.3.0"
+options:
+  remote_addr:
+    description:
+      - Address of the remote target.
+    default: inventory_hostname
+    type: string
+    vars:
+      - name: inventory_hostname
+      - name: ansible_host
+      - name: ansible_ssh_host
+      - name: ansible_paramiko_host
+  port:
+    description: Remote port to connect to.
+    type: int
+    default: 22
+    ini:
+      - section: defaults
+        key: remote_port
+      - section: paramiko_connection
+        key: remote_port
+    env:
+      - name: ANSIBLE_REMOTE_PORT
+      - name: ANSIBLE_REMOTE_PARAMIKO_PORT
+    vars:
+      - name: ansible_port
+      - name: ansible_ssh_port
+      - name: ansible_paramiko_port
+    keyword:
+      - name: port
+  remote_user:
+    description:
+      - User to login/authenticate as.
+      - Can be set from the CLI via the C(--user) or C(-u) options.
+    type: string
+    vars:
+      - name: ansible_user
+      - name: ansible_ssh_user
+      - name: ansible_paramiko_user
+    env:
+      - name: ANSIBLE_REMOTE_USER
+      - name: ANSIBLE_PARAMIKO_REMOTE_USER
+    ini:
+      - section: defaults
+        key: remote_user
+      - section: paramiko_connection
+        key: remote_user
+    keyword:
+      - name: remote_user
+  password:
+    description:
+      - Secret used to either login the SSH server or as a passphrase for SSH keys that require it.
+      - Can be set from the CLI via the C(--ask-pass) option.
+    type: string
+    vars:
+      - name: ansible_password
+      - name: ansible_ssh_pass
+      - name: ansible_ssh_password
+      - name: ansible_paramiko_pass
+      - name: ansible_paramiko_password
+  use_rsa_sha2_algorithms:
+    description:
+      - Whether or not to enable RSA SHA2 algorithms for pubkeys and hostkeys.
+      - On paramiko versions older than 2.9, this only affects hostkeys.
+      - For behavior matching paramiko<2.9 set this to V(false).
+    vars:
+      - name: ansible_paramiko_use_rsa_sha2_algorithms
+    ini:
+      - {key: use_rsa_sha2_algorithms, section: paramiko_connection}
+    env:
+      - {name: ANSIBLE_PARAMIKO_USE_RSA_SHA2_ALGORITHMS}
+    default: true
+    type: boolean
+  host_key_auto_add:
+    description: "Automatically add host keys to C(~/.ssh/known_hosts)."
+    env:
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_AUTO_ADD
+    ini:
+      - key: host_key_auto_add
+        section: paramiko_connection
+    type: boolean
+  look_for_keys:
+    default: True
+    description: "Set to V(false) to disable searching for private key files in C(~/.ssh/)."
+    env:
+      - name: ANSIBLE_PARAMIKO_LOOK_FOR_KEYS
+    ini:
+      - {key: look_for_keys, section: paramiko_connection}
+    type: boolean
+  proxy_command:
+    default: ""
+    description:
+      - Proxy information for running the connection via a jumphost.
+    type: string
+    env:
+      - name: ANSIBLE_PARAMIKO_PROXY_COMMAND
+    ini:
+      - {key: proxy_command, section: paramiko_connection}
+    vars:
+      - name: ansible_paramiko_proxy_command
+  record_host_keys:
+    default: True
+    description: "Save the host keys to a file."
+    env:
+      - name: ANSIBLE_PARAMIKO_RECORD_HOST_KEYS
+    ini:
+      - section: paramiko_connection
+        key: record_host_keys
+    type: boolean
+  host_key_checking:
+    description: "Set this to V(false) if you want to avoid host key checking by the underlying tools Ansible uses to connect to the host."
+    type: boolean
+    default: true
+    env:
+      - name: ANSIBLE_HOST_KEY_CHECKING
+      - name: ANSIBLE_SSH_HOST_KEY_CHECKING
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_CHECKING
+    ini:
+      - section: defaults
+        key: host_key_checking
+      - section: paramiko_connection
+        key: host_key_checking
+    vars:
+      - name: ansible_host_key_checking
+      - name: ansible_ssh_host_key_checking
+      - name: ansible_paramiko_host_key_checking
+  use_persistent_connections:
+    description: "Toggles the use of persistence for connections."
+    type: boolean
+    default: False
+    env:
+      - name: ANSIBLE_USE_PERSISTENT_CONNECTIONS
+    ini:
+      - section: defaults
+        key: use_persistent_connections
+  banner_timeout:
+    type: float
+    default: 30
+    description:
+      - Configures, in seconds, the amount of time to wait for the SSH
+        banner to be presented. This option is supported by paramiko
+        version 1.15.0 or newer.
+    ini:
+      - section: paramiko_connection
+        key: banner_timeout
+    env:
+      - name: ANSIBLE_PARAMIKO_BANNER_TIMEOUT
+  timeout:
+    type: int
+    default: 10
+    description: Number of seconds until the plugin gives up on failing to establish a TCP connection.
+    ini:
+      - section: defaults
+        key: timeout
+      - section: ssh_connection
+        key: timeout
+      - section: paramiko_connection
+        key: timeout
+    env:
+      - name: ANSIBLE_TIMEOUT
+      - name: ANSIBLE_SSH_TIMEOUT
+      - name: ANSIBLE_PARAMIKO_TIMEOUT
+    vars:
+      - name: ansible_ssh_timeout
+      - name: ansible_paramiko_timeout
+    cli:
+      - name: timeout
+  lock_file_timeout:
+    type: int
+    default: 60
+    description: Number of seconds until the plugin gives up on trying to write a lock file when writing SSH known host keys.
+    vars:
+      - name: ansible_lock_file_timeout
+    env:
+      - name: ANSIBLE_LOCK_FILE_TIMEOUT
+  private_key_file:
+    description:
+      - Path to private key file to use for authentication.
+    type: string
+    ini:
+      - section: defaults
+        key: private_key_file
+      - section: paramiko_connection
+        key: private_key_file
+    env:
+      - name: ANSIBLE_PRIVATE_KEY_FILE
+      - name: ANSIBLE_PARAMIKO_PRIVATE_KEY_FILE
+    vars:
+      - name: ansible_private_key_file
+      - name: ansible_ssh_private_key_file
+      - name: ansible_paramiko_private_key_file
+    cli:
+      - name: private_key_file
+        option: "--private-key"
+  wsl_distribution:
+    description:
+      - WSL distribution name
+    type: string
+    vars:
+      - name: wsl_distribution
+  wsl_user:
+    description:
+      - WSL distribution user
+    type: string
+    vars:
+      - name: wsl_user
+  become_user:
+    description:
+      - WSL distribution user
+    type: string
+    default: root
+    vars:
+      - name: become_user
+      - name: ansible_become_user
+  become:
+    description:
+      - whether to use the user defined by ansible_become_user.
+    type: bool
+    default: false
+    vars:
+      - name: become
+      - name: ansible_become
+"""
+
+EXAMPLES = r"""
+# ------------------------
+# Inventory: inventory.yml
+# ------------------------
+# all:
+#   children:
+#     wsl:
+#       hosts:
+#         ubuntu:
+#           ansible_host: 10.0.0.10
+#           wsl_distribution: ubuntu
+#         debian:
+#           ansible_host: 10.0.0.10
+#           wsl_distribution: debian
+#       vars:
+#         ansible_connection: community.general.wsl
+#         ansible_user: vagrant
+#         wsl_user: wsl
+# ----------------------
+# Playbook: playbook.yml
+# ----------------------
+---
+- hosts: wsl
+  tasks:
+    - name: ping
+      ansible.builtin.ping:
+    - name: id
+      args:
+        executable: /bin/bash
+      ansible.builtin.shell: |
+        exec 2>&1
+        set -x
+        echo "$0"
+        pwd
+        id
+    - name: become id
+      become: true
+      args:
+        executable: /bin/bash
+      ansible.builtin.shell: |
+        exec 2>&1
+        set -x
+        echo "$0"
+        pwd
+        id
+"""
+
+import os
+import pathlib
+import shlex
+import socket
+import tempfile
+import typing as t
+
+from ansible.errors import (
+    AnsibleAuthenticationFailure,
+    AnsibleConnectionFailure,
+    AnsibleError,
+)
+from ansible_collections.community.general.plugins.module_utils._filelock import FileLock, LockTimeout
+from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
+from ansible.module_utils.compat.paramiko import PARAMIKO_IMPORT_ERR, paramiko
+from ansible.module_utils.compat.version import LooseVersion
+from ansible.plugins.connection import ConnectionBase
+from ansible.utils.display import Display
+from ansible.utils.path import makedirs_safe
+from binascii import hexlify
+from subprocess import list2cmdline
+
+
+display = Display()
+
+
+def authenticity_msg(hostname: str, ktype: str, fingerprint: str) -> str:
+    msg = f"""
+    paramiko: The authenticity of host '{hostname}' can't be established.
+    The {ktype} key fingerprint is {fingerprint}.
+    Are you sure you want to continue connecting (yes/no)?
+    """
+    return msg
+
+
+MissingHostKeyPolicy: type = object
+if paramiko:
+    MissingHostKeyPolicy = paramiko.MissingHostKeyPolicy
+
+
+class MyAddPolicy(MissingHostKeyPolicy):
+    """
+    Based on AutoAddPolicy in paramiko so we can determine when keys are added
+
+    and also prompt for input.
+
+    Policy for automatically adding the hostname and new host key to the
+    local L{HostKeys} object, and saving it. This is used by L{SSHClient}.
+    """
+
+    def __init__(self, connection: Connection) -> None:
+        self.connection = connection
+        self._options = connection._options
+
+    def missing_host_key(self, client, hostname, key) -> None:
+
+        if all((self.connection.get_option('host_key_checking'), not self.connection.get_option('host_key_auto_add'))):
+
+            fingerprint = hexlify(key.get_fingerprint())
+            ktype = key.get_name()
+
+            if self.connection.get_option('use_persistent_connections') or self.connection.force_persistence:
+                # don't print the prompt string since the user cannot respond
+                # to the question anyway
+                raise AnsibleError(authenticity_msg(hostname, ktype, fingerprint)[1:92])
+
+            inp = to_text(
+                display.prompt_until(authenticity_msg(hostname, ktype, fingerprint), private=False),
+                errors='surrogate_or_strict'
+            )
+
+            if inp.lower() not in ['yes', 'y', '']:
+                raise AnsibleError('host connection rejected by user')
+
+        key._added_by_ansible_this_time = True
+
+        # existing implementation below:
+        client._host_keys.add(hostname, key.get_name(), key)
+
+        # host keys are actually saved in close() function below
+        # in order to control ordering.
+
+
+class Connection(ConnectionBase):
+    """ SSH based connections (paramiko) to WSL """
+
+    transport = 'community.general.wsl'
+    _log_channel: str | None = None
+
+    def __init__(self, play_context, new_stdin, *args, **kwargs):
+        super(Connection, self).__init__(play_context, new_stdin, *args, **kwargs)
+
+    def _set_log_channel(self, name: str) -> None:
+        """ Mimic paramiko.SSHClient.set_log_channel """
+        self._log_channel = name
+
+    def _parse_proxy_command(self, port: int = 22) -> dict[str, t.Any]:
+        proxy_command = self.get_option('proxy_command') or None
+
+        sock_kwarg = {}
+        if proxy_command:
+            replacers = {
+                '%h': self.get_option('remote_addr'),
+                '%p': port,
+                '%r': self.get_option('remote_user')
+            }
+            for find, replace in replacers.items():
+                proxy_command = proxy_command.replace(find, str(replace))
+            try:
+                sock_kwarg = {'sock': paramiko.ProxyCommand(proxy_command)}
+                display.vvv(f'CONFIGURE PROXY COMMAND FOR CONNECTION: {proxy_command}', host=self.get_option('remote_addr'))
+            except AttributeError:
+                display.warning('Paramiko ProxyCommand support unavailable. '
+                                'Please upgrade to Paramiko 1.9.0 or newer. '
+                                'Not using configured ProxyCommand')
path: plugins/connection/wsl.py
user: login: rgl
body: done. please verify.
created_at: 2025-03-05T18:53:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,758 @@
+# -*- coding: utf-8 -*-
+# Derived from ansible/plugins/connection/proxmox_pct_remote.py (c) 2024 Nils Stein (@mietzen) <github.nstein@mailbox.org>
+# Derived from ansible/plugins/connection/paramiko_ssh.py (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
+# Copyright (c) 2025 Rui Lopes (@rgl) <ruilopes.com>
+# Copyright (c) 2025 Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import annotations
+
+DOCUMENTATION = r"""
+author: Rui Lopes (@rgl) <ruilopes.com>
+name: wsl
+short_description: Run tasks in WSL distribution using wsl.exe CLI via SSH
+requirements:
+  - paramiko
+description:
+  - Run commands or put/fetch files to an existing WSL distribution using wsl.exe CLI via SSH.
+  - Uses the Python SSH implementation (Paramiko) to connect to the WSL host.
+version_added: "10.3.0"
+options:
+  remote_addr:
+    description:
+      - Address of the remote target.
+    default: inventory_hostname
+    type: string
+    vars:
+      - name: inventory_hostname
+      - name: ansible_host
+      - name: ansible_ssh_host
+      - name: ansible_paramiko_host
+  port:
+    description: Remote port to connect to.
+    type: int
+    default: 22
+    ini:
+      - section: defaults
+        key: remote_port
+      - section: paramiko_connection
+        key: remote_port
+    env:
+      - name: ANSIBLE_REMOTE_PORT
+      - name: ANSIBLE_REMOTE_PARAMIKO_PORT
+    vars:
+      - name: ansible_port
+      - name: ansible_ssh_port
+      - name: ansible_paramiko_port
+    keyword:
+      - name: port
+  remote_user:
+    description:
+      - User to login/authenticate as.
+      - Can be set from the CLI via the C(--user) or C(-u) options.
+    type: string
+    vars:
+      - name: ansible_user
+      - name: ansible_ssh_user
+      - name: ansible_paramiko_user
+    env:
+      - name: ANSIBLE_REMOTE_USER
+      - name: ANSIBLE_PARAMIKO_REMOTE_USER
+    ini:
+      - section: defaults
+        key: remote_user
+      - section: paramiko_connection
+        key: remote_user
+    keyword:
+      - name: remote_user
+  password:
+    description:
+      - Secret used to either login the SSH server or as a passphrase for SSH keys that require it.
+      - Can be set from the CLI via the C(--ask-pass) option.
+    type: string
+    vars:
+      - name: ansible_password
+      - name: ansible_ssh_pass
+      - name: ansible_ssh_password
+      - name: ansible_paramiko_pass
+      - name: ansible_paramiko_password
+  use_rsa_sha2_algorithms:
+    description:
+      - Whether or not to enable RSA SHA2 algorithms for pubkeys and hostkeys.
+      - On paramiko versions older than 2.9, this only affects hostkeys.
+      - For behavior matching paramiko<2.9 set this to V(false).
+    vars:
+      - name: ansible_paramiko_use_rsa_sha2_algorithms
+    ini:
+      - {key: use_rsa_sha2_algorithms, section: paramiko_connection}
+    env:
+      - {name: ANSIBLE_PARAMIKO_USE_RSA_SHA2_ALGORITHMS}
+    default: true
+    type: boolean
+  host_key_auto_add:
+    description: "Automatically add host keys to C(~/.ssh/known_hosts)."
+    env:
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_AUTO_ADD
+    ini:
+      - key: host_key_auto_add
+        section: paramiko_connection
+    type: boolean
+  look_for_keys:
+    default: True
+    description: "Set to V(false) to disable searching for private key files in C(~/.ssh/)."
+    env:
+      - name: ANSIBLE_PARAMIKO_LOOK_FOR_KEYS
+    ini:
+      - {key: look_for_keys, section: paramiko_connection}
+    type: boolean
+  proxy_command:
+    default: ""
+    description:
+      - Proxy information for running the connection via a jumphost.
+    type: string
+    env:
+      - name: ANSIBLE_PARAMIKO_PROXY_COMMAND
+    ini:
+      - {key: proxy_command, section: paramiko_connection}
+    vars:
+      - name: ansible_paramiko_proxy_command
+  record_host_keys:
+    default: True
+    description: "Save the host keys to a file."
+    env:
+      - name: ANSIBLE_PARAMIKO_RECORD_HOST_KEYS
+    ini:
+      - section: paramiko_connection
+        key: record_host_keys
+    type: boolean
+  host_key_checking:
+    description: "Set this to V(false) if you want to avoid host key checking by the underlying tools Ansible uses to connect to the host."
+    type: boolean
+    default: true
+    env:
+      - name: ANSIBLE_HOST_KEY_CHECKING
+      - name: ANSIBLE_SSH_HOST_KEY_CHECKING
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_CHECKING
+    ini:
+      - section: defaults
+        key: host_key_checking
+      - section: paramiko_connection
+        key: host_key_checking
+    vars:
+      - name: ansible_host_key_checking
+      - name: ansible_ssh_host_key_checking
+      - name: ansible_paramiko_host_key_checking
+  use_persistent_connections:
+    description: "Toggles the use of persistence for connections."
+    type: boolean
+    default: False
+    env:
+      - name: ANSIBLE_USE_PERSISTENT_CONNECTIONS
+    ini:
+      - section: defaults
+        key: use_persistent_connections
+  banner_timeout:
+    type: float
+    default: 30
+    description:
+      - Configures, in seconds, the amount of time to wait for the SSH
+        banner to be presented. This option is supported by paramiko
+        version 1.15.0 or newer.
+    ini:
+      - section: paramiko_connection
+        key: banner_timeout
+    env:
+      - name: ANSIBLE_PARAMIKO_BANNER_TIMEOUT
+  timeout:
+    type: int
+    default: 10
+    description: Number of seconds until the plugin gives up on failing to establish a TCP connection.
+    ini:
+      - section: defaults
+        key: timeout
+      - section: ssh_connection
+        key: timeout
+      - section: paramiko_connection
+        key: timeout
+    env:
+      - name: ANSIBLE_TIMEOUT
+      - name: ANSIBLE_SSH_TIMEOUT
+      - name: ANSIBLE_PARAMIKO_TIMEOUT
+    vars:
+      - name: ansible_ssh_timeout
+      - name: ansible_paramiko_timeout
+    cli:
+      - name: timeout
+  lock_file_timeout:
+    type: int
+    default: 60
+    description: Number of seconds until the plugin gives up on trying to write a lock file when writing SSH known host keys.
+    vars:
+      - name: ansible_lock_file_timeout
+    env:
+      - name: ANSIBLE_LOCK_FILE_TIMEOUT
+  private_key_file:
+    description:
+      - Path to private key file to use for authentication.
+    type: string
+    ini:
+      - section: defaults
+        key: private_key_file
+      - section: paramiko_connection
+        key: private_key_file
+    env:
+      - name: ANSIBLE_PRIVATE_KEY_FILE
+      - name: ANSIBLE_PARAMIKO_PRIVATE_KEY_FILE
+    vars:
+      - name: ansible_private_key_file
+      - name: ansible_ssh_private_key_file
+      - name: ansible_paramiko_private_key_file
+    cli:
+      - name: private_key_file
+        option: "--private-key"
+  wsl_distribution:
+    description:
+      - WSL distribution name
+    type: string
+    vars:
+      - name: wsl_distribution
+  wsl_user:
+    description:
+      - WSL distribution user
+    type: string
+    vars:
+      - name: wsl_user
+  become_user:
+    description:
+      - WSL distribution user
+    type: string
+    default: root
+    vars:
+      - name: become_user
+      - name: ansible_become_user
+  become:
+    description:
+      - whether to use the user defined by ansible_become_user.
+    type: bool
+    default: false
+    vars:
+      - name: become
+      - name: ansible_become
+"""
+
+EXAMPLES = r"""
+# ------------------------
+# Inventory: inventory.yml
+# ------------------------
+# all:
+#   children:
+#     wsl:
+#       hosts:
+#         ubuntu:
+#           ansible_host: 10.0.0.10
+#           wsl_distribution: ubuntu
+#         debian:
+#           ansible_host: 10.0.0.10
+#           wsl_distribution: debian
+#       vars:
+#         ansible_connection: community.general.wsl
+#         ansible_user: vagrant
+#         wsl_user: wsl
path: plugins/connection/wsl.py
user: login: rgl
body: done. please verify.
created_at: 2025-03-05T18:53:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,758 @@
+# -*- coding: utf-8 -*-
+# Derived from ansible/plugins/connection/proxmox_pct_remote.py (c) 2024 Nils Stein (@mietzen) <github.nstein@mailbox.org>
+# Derived from ansible/plugins/connection/paramiko_ssh.py (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
+# Copyright (c) 2025 Rui Lopes (@rgl) <ruilopes.com>
+# Copyright (c) 2025 Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import annotations
+
+DOCUMENTATION = r"""
+author: Rui Lopes (@rgl) <ruilopes.com>
+name: wsl
+short_description: Run tasks in WSL distribution using wsl.exe CLI via SSH
+requirements:
+  - paramiko
+description:
+  - Run commands or put/fetch files to an existing WSL distribution using wsl.exe CLI via SSH.
+  - Uses the Python SSH implementation (Paramiko) to connect to the WSL host.
+version_added: "10.3.0"
+options:
+  remote_addr:
+    description:
+      - Address of the remote target.
+    default: inventory_hostname
+    type: string
+    vars:
+      - name: inventory_hostname
+      - name: ansible_host
+      - name: ansible_ssh_host
+      - name: ansible_paramiko_host
+  port:
+    description: Remote port to connect to.
+    type: int
+    default: 22
+    ini:
+      - section: defaults
+        key: remote_port
+      - section: paramiko_connection
+        key: remote_port
+    env:
+      - name: ANSIBLE_REMOTE_PORT
+      - name: ANSIBLE_REMOTE_PARAMIKO_PORT
+    vars:
+      - name: ansible_port
+      - name: ansible_ssh_port
+      - name: ansible_paramiko_port
+    keyword:
+      - name: port
+  remote_user:
+    description:
+      - User to login/authenticate as.
+      - Can be set from the CLI via the C(--user) or C(-u) options.
+    type: string
+    vars:
+      - name: ansible_user
+      - name: ansible_ssh_user
+      - name: ansible_paramiko_user
+    env:
+      - name: ANSIBLE_REMOTE_USER
+      - name: ANSIBLE_PARAMIKO_REMOTE_USER
+    ini:
+      - section: defaults
+        key: remote_user
+      - section: paramiko_connection
+        key: remote_user
+    keyword:
+      - name: remote_user
+  password:
+    description:
+      - Secret used to either login the SSH server or as a passphrase for SSH keys that require it.
+      - Can be set from the CLI via the C(--ask-pass) option.
+    type: string
+    vars:
+      - name: ansible_password
+      - name: ansible_ssh_pass
+      - name: ansible_ssh_password
+      - name: ansible_paramiko_pass
+      - name: ansible_paramiko_password
+  use_rsa_sha2_algorithms:
+    description:
+      - Whether or not to enable RSA SHA2 algorithms for pubkeys and hostkeys.
+      - On paramiko versions older than 2.9, this only affects hostkeys.
+      - For behavior matching paramiko<2.9 set this to V(false).
+    vars:
+      - name: ansible_paramiko_use_rsa_sha2_algorithms
+    ini:
+      - {key: use_rsa_sha2_algorithms, section: paramiko_connection}
+    env:
+      - {name: ANSIBLE_PARAMIKO_USE_RSA_SHA2_ALGORITHMS}
+    default: true
+    type: boolean
+  host_key_auto_add:
+    description: "Automatically add host keys to C(~/.ssh/known_hosts)."
+    env:
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_AUTO_ADD
+    ini:
+      - key: host_key_auto_add
+        section: paramiko_connection
+    type: boolean
+  look_for_keys:
+    default: True
+    description: "Set to V(false) to disable searching for private key files in C(~/.ssh/)."
+    env:
+      - name: ANSIBLE_PARAMIKO_LOOK_FOR_KEYS
+    ini:
+      - {key: look_for_keys, section: paramiko_connection}
+    type: boolean
+  proxy_command:
+    default: ""
+    description:
+      - Proxy information for running the connection via a jumphost.
+    type: string
+    env:
+      - name: ANSIBLE_PARAMIKO_PROXY_COMMAND
+    ini:
+      - {key: proxy_command, section: paramiko_connection}
+    vars:
+      - name: ansible_paramiko_proxy_command
+  record_host_keys:
+    default: True
+    description: "Save the host keys to a file."
+    env:
+      - name: ANSIBLE_PARAMIKO_RECORD_HOST_KEYS
+    ini:
+      - section: paramiko_connection
+        key: record_host_keys
+    type: boolean
+  host_key_checking:
+    description: "Set this to V(false) if you want to avoid host key checking by the underlying tools Ansible uses to connect to the host."
+    type: boolean
+    default: true
+    env:
+      - name: ANSIBLE_HOST_KEY_CHECKING
+      - name: ANSIBLE_SSH_HOST_KEY_CHECKING
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_CHECKING
+    ini:
+      - section: defaults
+        key: host_key_checking
+      - section: paramiko_connection
+        key: host_key_checking
+    vars:
+      - name: ansible_host_key_checking
+      - name: ansible_ssh_host_key_checking
+      - name: ansible_paramiko_host_key_checking
+  use_persistent_connections:
+    description: "Toggles the use of persistence for connections."
+    type: boolean
+    default: False
+    env:
+      - name: ANSIBLE_USE_PERSISTENT_CONNECTIONS
+    ini:
+      - section: defaults
+        key: use_persistent_connections
+  banner_timeout:
+    type: float
+    default: 30
+    description:
+      - Configures, in seconds, the amount of time to wait for the SSH
+        banner to be presented. This option is supported by paramiko
+        version 1.15.0 or newer.
+    ini:
+      - section: paramiko_connection
+        key: banner_timeout
+    env:
+      - name: ANSIBLE_PARAMIKO_BANNER_TIMEOUT
+  timeout:
+    type: int
+    default: 10
+    description: Number of seconds until the plugin gives up on failing to establish a TCP connection.
+    ini:
+      - section: defaults
+        key: timeout
+      - section: ssh_connection
+        key: timeout
+      - section: paramiko_connection
+        key: timeout
+    env:
+      - name: ANSIBLE_TIMEOUT
+      - name: ANSIBLE_SSH_TIMEOUT
+      - name: ANSIBLE_PARAMIKO_TIMEOUT
+    vars:
+      - name: ansible_ssh_timeout
+      - name: ansible_paramiko_timeout
+    cli:
+      - name: timeout
+  lock_file_timeout:
+    type: int
+    default: 60
+    description: Number of seconds until the plugin gives up on trying to write a lock file when writing SSH known host keys.
+    vars:
+      - name: ansible_lock_file_timeout
+    env:
+      - name: ANSIBLE_LOCK_FILE_TIMEOUT
+  private_key_file:
+    description:
+      - Path to private key file to use for authentication.
+    type: string
+    ini:
+      - section: defaults
+        key: private_key_file
+      - section: paramiko_connection
+        key: private_key_file
+    env:
+      - name: ANSIBLE_PRIVATE_KEY_FILE
+      - name: ANSIBLE_PARAMIKO_PRIVATE_KEY_FILE
+    vars:
+      - name: ansible_private_key_file
+      - name: ansible_ssh_private_key_file
+      - name: ansible_paramiko_private_key_file
+    cli:
+      - name: private_key_file
+        option: "--private-key"
+  wsl_distribution:
+    description:
+      - WSL distribution name
+    type: string
+    vars:
+      - name: wsl_distribution
+  wsl_user:
+    description:
+      - WSL distribution user
+    type: string
+    vars:
+      - name: wsl_user
+  become_user:
+    description:
+      - WSL distribution user
+    type: string
+    default: root
+    vars:
+      - name: become_user
+      - name: ansible_become_user
+  become:
+    description:
+      - whether to use the user defined by ansible_become_user.
+    type: bool
+    default: false
+    vars:
+      - name: become
+      - name: ansible_become
+"""
+
+EXAMPLES = r"""
+# ------------------------
+# Inventory: inventory.yml
+# ------------------------
+# all:
+#   children:
+#     wsl:
+#       hosts:
+#         ubuntu:
+#           ansible_host: 10.0.0.10
+#           wsl_distribution: ubuntu
+#         debian:
+#           ansible_host: 10.0.0.10
+#           wsl_distribution: debian
+#       vars:
+#         ansible_connection: community.general.wsl
+#         ansible_user: vagrant
+#         wsl_user: wsl
+# ----------------------
+# Playbook: playbook.yml
+# ----------------------
+---
+- hosts: wsl
+  tasks:
+    - name: ping
+      ansible.builtin.ping:
+    - name: id
+      args:
+        executable: /bin/bash
+      ansible.builtin.shell: |
+        exec 2>&1
+        set -x
+        echo "$0"
+        pwd
+        id
+    - name: become id
+      become: true
+      args:
+        executable: /bin/bash
+      ansible.builtin.shell: |
+        exec 2>&1
+        set -x
+        echo "$0"
+        pwd
+        id
+"""
+
+import os
+import pathlib
+import shlex
+import socket
+import tempfile
+import typing as t
path: plugins/connection/wsl.py
user: login: rgl
body: it's now used in more places, since ansible core (at least the parts I peeked at) seems to use `t.` I kept it like that too.
created_at: 2025-03-05T18:54:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,758 @@
+# -*- coding: utf-8 -*-
+# Derived from ansible/plugins/connection/proxmox_pct_remote.py (c) 2024 Nils Stein (@mietzen) <github.nstein@mailbox.org>
+# Derived from ansible/plugins/connection/paramiko_ssh.py (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
+# Copyright (c) 2025 Rui Lopes (@rgl) <ruilopes.com>
+# Copyright (c) 2025 Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import annotations
+
+DOCUMENTATION = r"""
+author: Rui Lopes (@rgl) <ruilopes.com>
+name: wsl
+short_description: Run tasks in WSL distribution using wsl.exe CLI via SSH
+requirements:
+  - paramiko
+description:
+  - Run commands or put/fetch files to an existing WSL distribution using wsl.exe CLI via SSH.
+  - Uses the Python SSH implementation (Paramiko) to connect to the WSL host.
+version_added: "10.3.0"
+options:
+  remote_addr:
+    description:
+      - Address of the remote target.
+    default: inventory_hostname
+    type: string
+    vars:
+      - name: inventory_hostname
+      - name: ansible_host
+      - name: ansible_ssh_host
+      - name: ansible_paramiko_host
+  port:
+    description: Remote port to connect to.
+    type: int
+    default: 22
+    ini:
+      - section: defaults
+        key: remote_port
+      - section: paramiko_connection
+        key: remote_port
+    env:
+      - name: ANSIBLE_REMOTE_PORT
+      - name: ANSIBLE_REMOTE_PARAMIKO_PORT
+    vars:
+      - name: ansible_port
+      - name: ansible_ssh_port
+      - name: ansible_paramiko_port
+    keyword:
+      - name: port
+  remote_user:
+    description:
+      - User to login/authenticate as.
+      - Can be set from the CLI via the C(--user) or C(-u) options.
+    type: string
+    vars:
+      - name: ansible_user
+      - name: ansible_ssh_user
+      - name: ansible_paramiko_user
+    env:
+      - name: ANSIBLE_REMOTE_USER
+      - name: ANSIBLE_PARAMIKO_REMOTE_USER
+    ini:
+      - section: defaults
+        key: remote_user
+      - section: paramiko_connection
+        key: remote_user
+    keyword:
+      - name: remote_user
+  password:
+    description:
+      - Secret used to either login the SSH server or as a passphrase for SSH keys that require it.
+      - Can be set from the CLI via the C(--ask-pass) option.
+    type: string
+    vars:
+      - name: ansible_password
+      - name: ansible_ssh_pass
+      - name: ansible_ssh_password
+      - name: ansible_paramiko_pass
+      - name: ansible_paramiko_password
+  use_rsa_sha2_algorithms:
+    description:
+      - Whether or not to enable RSA SHA2 algorithms for pubkeys and hostkeys.
+      - On paramiko versions older than 2.9, this only affects hostkeys.
+      - For behavior matching paramiko<2.9 set this to V(false).
+    vars:
+      - name: ansible_paramiko_use_rsa_sha2_algorithms
+    ini:
+      - {key: use_rsa_sha2_algorithms, section: paramiko_connection}
+    env:
+      - {name: ANSIBLE_PARAMIKO_USE_RSA_SHA2_ALGORITHMS}
+    default: true
+    type: boolean
+  host_key_auto_add:
+    description: "Automatically add host keys to C(~/.ssh/known_hosts)."
+    env:
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_AUTO_ADD
+    ini:
+      - key: host_key_auto_add
+        section: paramiko_connection
+    type: boolean
+  look_for_keys:
+    default: True
+    description: "Set to V(false) to disable searching for private key files in C(~/.ssh/)."
+    env:
+      - name: ANSIBLE_PARAMIKO_LOOK_FOR_KEYS
+    ini:
+      - {key: look_for_keys, section: paramiko_connection}
+    type: boolean
+  proxy_command:
+    default: ""
+    description:
+      - Proxy information for running the connection via a jumphost.
+    type: string
+    env:
+      - name: ANSIBLE_PARAMIKO_PROXY_COMMAND
+    ini:
+      - {key: proxy_command, section: paramiko_connection}
+    vars:
+      - name: ansible_paramiko_proxy_command
+  record_host_keys:
+    default: True
+    description: "Save the host keys to a file."
+    env:
+      - name: ANSIBLE_PARAMIKO_RECORD_HOST_KEYS
+    ini:
+      - section: paramiko_connection
+        key: record_host_keys
+    type: boolean
+  host_key_checking:
+    description: "Set this to V(false) if you want to avoid host key checking by the underlying tools Ansible uses to connect to the host."
+    type: boolean
+    default: true
+    env:
+      - name: ANSIBLE_HOST_KEY_CHECKING
+      - name: ANSIBLE_SSH_HOST_KEY_CHECKING
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_CHECKING
+    ini:
+      - section: defaults
+        key: host_key_checking
+      - section: paramiko_connection
+        key: host_key_checking
+    vars:
+      - name: ansible_host_key_checking
+      - name: ansible_ssh_host_key_checking
+      - name: ansible_paramiko_host_key_checking
+  use_persistent_connections:
+    description: "Toggles the use of persistence for connections."
+    type: boolean
+    default: False
+    env:
+      - name: ANSIBLE_USE_PERSISTENT_CONNECTIONS
+    ini:
+      - section: defaults
+        key: use_persistent_connections
+  banner_timeout:
+    type: float
+    default: 30
+    description:
+      - Configures, in seconds, the amount of time to wait for the SSH
+        banner to be presented. This option is supported by paramiko
+        version 1.15.0 or newer.
+    ini:
+      - section: paramiko_connection
+        key: banner_timeout
+    env:
+      - name: ANSIBLE_PARAMIKO_BANNER_TIMEOUT
+  timeout:
+    type: int
+    default: 10
+    description: Number of seconds until the plugin gives up on failing to establish a TCP connection.
+    ini:
+      - section: defaults
+        key: timeout
+      - section: ssh_connection
+        key: timeout
+      - section: paramiko_connection
+        key: timeout
+    env:
+      - name: ANSIBLE_TIMEOUT
+      - name: ANSIBLE_SSH_TIMEOUT
+      - name: ANSIBLE_PARAMIKO_TIMEOUT
+    vars:
+      - name: ansible_ssh_timeout
+      - name: ansible_paramiko_timeout
+    cli:
+      - name: timeout
+  lock_file_timeout:
+    type: int
+    default: 60
+    description: Number of seconds until the plugin gives up on trying to write a lock file when writing SSH known host keys.
+    vars:
+      - name: ansible_lock_file_timeout
+    env:
+      - name: ANSIBLE_LOCK_FILE_TIMEOUT
+  private_key_file:
+    description:
+      - Path to private key file to use for authentication.
+    type: string
+    ini:
+      - section: defaults
+        key: private_key_file
+      - section: paramiko_connection
+        key: private_key_file
+    env:
+      - name: ANSIBLE_PRIVATE_KEY_FILE
+      - name: ANSIBLE_PARAMIKO_PRIVATE_KEY_FILE
+    vars:
+      - name: ansible_private_key_file
+      - name: ansible_ssh_private_key_file
+      - name: ansible_paramiko_private_key_file
+    cli:
+      - name: private_key_file
+        option: "--private-key"
+  wsl_distribution:
+    description:
+      - WSL distribution name
+    type: string
+    vars:
+      - name: wsl_distribution
+  wsl_user:
+    description:
+      - WSL distribution user
+    type: string
+    vars:
+      - name: wsl_user
+  become_user:
+    description:
+      - WSL distribution user
+    type: string
+    default: root
+    vars:
+      - name: become_user
+      - name: ansible_become_user
+  become:
+    description:
+      - whether to use the user defined by ansible_become_user.
+    type: bool
+    default: false
+    vars:
+      - name: become
+      - name: ansible_become
+"""
+
+EXAMPLES = r"""
+# ------------------------
+# Inventory: inventory.yml
+# ------------------------
+# all:
+#   children:
+#     wsl:
+#       hosts:
+#         ubuntu:
+#           ansible_host: 10.0.0.10
+#           wsl_distribution: ubuntu
+#         debian:
+#           ansible_host: 10.0.0.10
+#           wsl_distribution: debian
+#       vars:
+#         ansible_connection: community.general.wsl
+#         ansible_user: vagrant
+#         wsl_user: wsl
+# ----------------------
+# Playbook: playbook.yml
+# ----------------------
+---
+- hosts: wsl
+  tasks:
+    - name: ping
+      ansible.builtin.ping:
+    - name: id
+      args:
+        executable: /bin/bash
+      ansible.builtin.shell: |
+        exec 2>&1
+        set -x
+        echo "$0"
+        pwd
+        id
+    - name: become id
+      become: true
+      args:
+        executable: /bin/bash
+      ansible.builtin.shell: |
+        exec 2>&1
+        set -x
+        echo "$0"
+        pwd
+        id
+"""
+
+import os
+import pathlib
+import shlex
+import socket
+import tempfile
+import typing as t
+
+from ansible.errors import (
+    AnsibleAuthenticationFailure,
+    AnsibleConnectionFailure,
+    AnsibleError,
+)
+from ansible_collections.community.general.plugins.module_utils._filelock import FileLock, LockTimeout
+from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
+from ansible.module_utils.compat.paramiko import PARAMIKO_IMPORT_ERR, paramiko
+from ansible.module_utils.compat.version import LooseVersion
+from ansible.plugins.connection import ConnectionBase
+from ansible.utils.display import Display
+from ansible.utils.path import makedirs_safe
+from binascii import hexlify
+from subprocess import list2cmdline
+
+
+display = Display()
+
+
+def authenticity_msg(hostname: str, ktype: str, fingerprint: str) -> str:
+    msg = f"""
+    paramiko: The authenticity of host '{hostname}' can't be established.
+    The {ktype} key fingerprint is {fingerprint}.
+    Are you sure you want to continue connecting (yes/no)?
+    """
+    return msg
+
+
+MissingHostKeyPolicy: type = object
+if paramiko:
+    MissingHostKeyPolicy = paramiko.MissingHostKeyPolicy
+
+
+class MyAddPolicy(MissingHostKeyPolicy):
+    """
+    Based on AutoAddPolicy in paramiko so we can determine when keys are added
+
+    and also prompt for input.
+
+    Policy for automatically adding the hostname and new host key to the
+    local L{HostKeys} object, and saving it. This is used by L{SSHClient}.
+    """
+
+    def __init__(self, connection: Connection) -> None:
+        self.connection = connection
+        self._options = connection._options
+
+    def missing_host_key(self, client, hostname, key) -> None:
path: plugins/connection/wsl.py
user: login: rgl
body: done. please verify.

tho, this was somewhat odd, since paramiko does not have type hints, so, these might not stay in sync with it.
created_at: 2025-03-05T18:55:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,758 @@
+# -*- coding: utf-8 -*-
+# Derived from ansible/plugins/connection/proxmox_pct_remote.py (c) 2024 Nils Stein (@mietzen) <github.nstein@mailbox.org>
+# Derived from ansible/plugins/connection/paramiko_ssh.py (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
+# Copyright (c) 2025 Rui Lopes (@rgl) <ruilopes.com>
+# Copyright (c) 2025 Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import annotations
+
+DOCUMENTATION = r"""
+author: Rui Lopes (@rgl) <ruilopes.com>
+name: wsl
+short_description: Run tasks in WSL distribution using wsl.exe CLI via SSH
+requirements:
+  - paramiko
+description:
+  - Run commands or put/fetch files to an existing WSL distribution using wsl.exe CLI via SSH.
+  - Uses the Python SSH implementation (Paramiko) to connect to the WSL host.
+version_added: "10.3.0"
+options:
+  remote_addr:
+    description:
+      - Address of the remote target.
+    default: inventory_hostname
+    type: string
+    vars:
+      - name: inventory_hostname
+      - name: ansible_host
+      - name: ansible_ssh_host
+      - name: ansible_paramiko_host
+  port:
+    description: Remote port to connect to.
+    type: int
+    default: 22
+    ini:
+      - section: defaults
+        key: remote_port
+      - section: paramiko_connection
+        key: remote_port
+    env:
+      - name: ANSIBLE_REMOTE_PORT
+      - name: ANSIBLE_REMOTE_PARAMIKO_PORT
+    vars:
+      - name: ansible_port
+      - name: ansible_ssh_port
+      - name: ansible_paramiko_port
+    keyword:
+      - name: port
+  remote_user:
+    description:
+      - User to login/authenticate as.
+      - Can be set from the CLI via the C(--user) or C(-u) options.
+    type: string
+    vars:
+      - name: ansible_user
+      - name: ansible_ssh_user
+      - name: ansible_paramiko_user
+    env:
+      - name: ANSIBLE_REMOTE_USER
+      - name: ANSIBLE_PARAMIKO_REMOTE_USER
+    ini:
+      - section: defaults
+        key: remote_user
+      - section: paramiko_connection
+        key: remote_user
+    keyword:
+      - name: remote_user
+  password:
+    description:
+      - Secret used to either login the SSH server or as a passphrase for SSH keys that require it.
+      - Can be set from the CLI via the C(--ask-pass) option.
+    type: string
+    vars:
+      - name: ansible_password
+      - name: ansible_ssh_pass
+      - name: ansible_ssh_password
+      - name: ansible_paramiko_pass
+      - name: ansible_paramiko_password
+  use_rsa_sha2_algorithms:
+    description:
+      - Whether or not to enable RSA SHA2 algorithms for pubkeys and hostkeys.
+      - On paramiko versions older than 2.9, this only affects hostkeys.
+      - For behavior matching paramiko<2.9 set this to V(false).
+    vars:
+      - name: ansible_paramiko_use_rsa_sha2_algorithms
+    ini:
+      - {key: use_rsa_sha2_algorithms, section: paramiko_connection}
+    env:
+      - {name: ANSIBLE_PARAMIKO_USE_RSA_SHA2_ALGORITHMS}
+    default: true
+    type: boolean
+  host_key_auto_add:
+    description: "Automatically add host keys to C(~/.ssh/known_hosts)."
+    env:
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_AUTO_ADD
+    ini:
+      - key: host_key_auto_add
+        section: paramiko_connection
+    type: boolean
+  look_for_keys:
+    default: True
+    description: "Set to V(false) to disable searching for private key files in C(~/.ssh/)."
+    env:
+      - name: ANSIBLE_PARAMIKO_LOOK_FOR_KEYS
+    ini:
+      - {key: look_for_keys, section: paramiko_connection}
+    type: boolean
+  proxy_command:
+    default: ""
+    description:
+      - Proxy information for running the connection via a jumphost.
+    type: string
+    env:
+      - name: ANSIBLE_PARAMIKO_PROXY_COMMAND
+    ini:
+      - {key: proxy_command, section: paramiko_connection}
+    vars:
+      - name: ansible_paramiko_proxy_command
+  record_host_keys:
+    default: True
+    description: "Save the host keys to a file."
+    env:
+      - name: ANSIBLE_PARAMIKO_RECORD_HOST_KEYS
+    ini:
+      - section: paramiko_connection
+        key: record_host_keys
+    type: boolean
+  host_key_checking:
+    description: "Set this to V(false) if you want to avoid host key checking by the underlying tools Ansible uses to connect to the host."
+    type: boolean
+    default: true
+    env:
+      - name: ANSIBLE_HOST_KEY_CHECKING
+      - name: ANSIBLE_SSH_HOST_KEY_CHECKING
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_CHECKING
+    ini:
+      - section: defaults
+        key: host_key_checking
+      - section: paramiko_connection
+        key: host_key_checking
+    vars:
+      - name: ansible_host_key_checking
+      - name: ansible_ssh_host_key_checking
+      - name: ansible_paramiko_host_key_checking
+  use_persistent_connections:
+    description: "Toggles the use of persistence for connections."
+    type: boolean
+    default: False
+    env:
+      - name: ANSIBLE_USE_PERSISTENT_CONNECTIONS
+    ini:
+      - section: defaults
+        key: use_persistent_connections
+  banner_timeout:
+    type: float
+    default: 30
+    description:
+      - Configures, in seconds, the amount of time to wait for the SSH
+        banner to be presented. This option is supported by paramiko
+        version 1.15.0 or newer.
+    ini:
+      - section: paramiko_connection
+        key: banner_timeout
+    env:
+      - name: ANSIBLE_PARAMIKO_BANNER_TIMEOUT
+  timeout:
+    type: int
+    default: 10
+    description: Number of seconds until the plugin gives up on failing to establish a TCP connection.
+    ini:
+      - section: defaults
+        key: timeout
+      - section: ssh_connection
+        key: timeout
+      - section: paramiko_connection
+        key: timeout
+    env:
+      - name: ANSIBLE_TIMEOUT
+      - name: ANSIBLE_SSH_TIMEOUT
+      - name: ANSIBLE_PARAMIKO_TIMEOUT
+    vars:
+      - name: ansible_ssh_timeout
+      - name: ansible_paramiko_timeout
+    cli:
+      - name: timeout
+  lock_file_timeout:
+    type: int
+    default: 60
+    description: Number of seconds until the plugin gives up on trying to write a lock file when writing SSH known host keys.
+    vars:
+      - name: ansible_lock_file_timeout
+    env:
+      - name: ANSIBLE_LOCK_FILE_TIMEOUT
+  private_key_file:
+    description:
+      - Path to private key file to use for authentication.
+    type: string
+    ini:
+      - section: defaults
+        key: private_key_file
+      - section: paramiko_connection
+        key: private_key_file
+    env:
+      - name: ANSIBLE_PRIVATE_KEY_FILE
+      - name: ANSIBLE_PARAMIKO_PRIVATE_KEY_FILE
+    vars:
+      - name: ansible_private_key_file
+      - name: ansible_ssh_private_key_file
+      - name: ansible_paramiko_private_key_file
+    cli:
+      - name: private_key_file
+        option: "--private-key"
+  wsl_distribution:
+    description:
+      - WSL distribution name
+    type: string
+    vars:
+      - name: wsl_distribution
+  wsl_user:
+    description:
+      - WSL distribution user
+    type: string
+    vars:
+      - name: wsl_user
+  become_user:
+    description:
+      - WSL distribution user
+    type: string
+    default: root
+    vars:
+      - name: become_user
+      - name: ansible_become_user
+  become:
+    description:
+      - whether to use the user defined by ansible_become_user.
+    type: bool
+    default: false
+    vars:
+      - name: become
+      - name: ansible_become
+"""
+
+EXAMPLES = r"""
+# ------------------------
+# Inventory: inventory.yml
+# ------------------------
+# all:
+#   children:
+#     wsl:
+#       hosts:
+#         ubuntu:
+#           ansible_host: 10.0.0.10
+#           wsl_distribution: ubuntu
+#         debian:
+#           ansible_host: 10.0.0.10
+#           wsl_distribution: debian
+#       vars:
+#         ansible_connection: community.general.wsl
+#         ansible_user: vagrant
+#         wsl_user: wsl
+# ----------------------
+# Playbook: playbook.yml
+# ----------------------
+---
+- hosts: wsl
+  tasks:
+    - name: ping
+      ansible.builtin.ping:
+    - name: id
+      args:
+        executable: /bin/bash
+      ansible.builtin.shell: |
+        exec 2>&1
+        set -x
+        echo "$0"
+        pwd
+        id
+    - name: become id
+      become: true
+      args:
+        executable: /bin/bash
+      ansible.builtin.shell: |
+        exec 2>&1
+        set -x
+        echo "$0"
+        pwd
+        id
+"""
+
+import os
+import pathlib
+import shlex
+import socket
+import tempfile
+import typing as t
+
+from ansible.errors import (
+    AnsibleAuthenticationFailure,
+    AnsibleConnectionFailure,
+    AnsibleError,
+)
+from ansible_collections.community.general.plugins.module_utils._filelock import FileLock, LockTimeout
+from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
+from ansible.module_utils.compat.paramiko import PARAMIKO_IMPORT_ERR, paramiko
+from ansible.module_utils.compat.version import LooseVersion
+from ansible.plugins.connection import ConnectionBase
+from ansible.utils.display import Display
+from ansible.utils.path import makedirs_safe
+from binascii import hexlify
+from subprocess import list2cmdline
+
+
+display = Display()
+
+
+def authenticity_msg(hostname: str, ktype: str, fingerprint: str) -> str:
+    msg = f"""
+    paramiko: The authenticity of host '{hostname}' can't be established.
+    The {ktype} key fingerprint is {fingerprint}.
+    Are you sure you want to continue connecting (yes/no)?
+    """
+    return msg
+
+
+MissingHostKeyPolicy: type = object
+if paramiko:
+    MissingHostKeyPolicy = paramiko.MissingHostKeyPolicy
+
+
+class MyAddPolicy(MissingHostKeyPolicy):
+    """
+    Based on AutoAddPolicy in paramiko so we can determine when keys are added
+
+    and also prompt for input.
+
+    Policy for automatically adding the hostname and new host key to the
+    local L{HostKeys} object, and saving it. This is used by L{SSHClient}.
+    """
+
+    def __init__(self, connection: Connection) -> None:
+        self.connection = connection
+        self._options = connection._options
+
+    def missing_host_key(self, client, hostname, key) -> None:
+
+        if all((self.connection.get_option('host_key_checking'), not self.connection.get_option('host_key_auto_add'))):
+
+            fingerprint = hexlify(key.get_fingerprint())
+            ktype = key.get_name()
+
+            if self.connection.get_option('use_persistent_connections') or self.connection.force_persistence:
+                # don't print the prompt string since the user cannot respond
+                # to the question anyway
+                raise AnsibleError(authenticity_msg(hostname, ktype, fingerprint)[1:92])
+
+            inp = to_text(
+                display.prompt_until(authenticity_msg(hostname, ktype, fingerprint), private=False),
+                errors='surrogate_or_strict'
+            )
+
+            if inp.lower() not in ['yes', 'y', '']:
+                raise AnsibleError('host connection rejected by user')
+
+        key._added_by_ansible_this_time = True
+
+        # existing implementation below:
+        client._host_keys.add(hostname, key.get_name(), key)
+
+        # host keys are actually saved in close() function below
+        # in order to control ordering.
+
+
+class Connection(ConnectionBase):
+    """ SSH based connections (paramiko) to WSL """
+
+    transport = 'community.general.wsl'
+    _log_channel: str | None = None
+
+    def __init__(self, play_context, new_stdin, *args, **kwargs):
path: plugins/connection/wsl.py
user: login: rgl
body: done. please verify.
created_at: 2025-03-05T18:55:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,758 @@
+# -*- coding: utf-8 -*-
+# Derived from ansible/plugins/connection/proxmox_pct_remote.py (c) 2024 Nils Stein (@mietzen) <github.nstein@mailbox.org>
+# Derived from ansible/plugins/connection/paramiko_ssh.py (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
+# Copyright (c) 2025 Rui Lopes (@rgl) <ruilopes.com>
+# Copyright (c) 2025 Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import annotations
+
+DOCUMENTATION = r"""
+author: Rui Lopes (@rgl) <ruilopes.com>
+name: wsl
+short_description: Run tasks in WSL distribution using wsl.exe CLI via SSH
+requirements:
+  - paramiko
+description:
+  - Run commands or put/fetch files to an existing WSL distribution using wsl.exe CLI via SSH.
+  - Uses the Python SSH implementation (Paramiko) to connect to the WSL host.
+version_added: "10.3.0"
+options:
+  remote_addr:
+    description:
+      - Address of the remote target.
+    default: inventory_hostname
+    type: string
+    vars:
+      - name: inventory_hostname
+      - name: ansible_host
+      - name: ansible_ssh_host
+      - name: ansible_paramiko_host
+  port:
+    description: Remote port to connect to.
+    type: int
+    default: 22
+    ini:
+      - section: defaults
+        key: remote_port
+      - section: paramiko_connection
+        key: remote_port
+    env:
+      - name: ANSIBLE_REMOTE_PORT
+      - name: ANSIBLE_REMOTE_PARAMIKO_PORT
+    vars:
+      - name: ansible_port
+      - name: ansible_ssh_port
+      - name: ansible_paramiko_port
+    keyword:
+      - name: port
+  remote_user:
+    description:
+      - User to login/authenticate as.
+      - Can be set from the CLI via the C(--user) or C(-u) options.
+    type: string
+    vars:
+      - name: ansible_user
+      - name: ansible_ssh_user
+      - name: ansible_paramiko_user
+    env:
+      - name: ANSIBLE_REMOTE_USER
+      - name: ANSIBLE_PARAMIKO_REMOTE_USER
+    ini:
+      - section: defaults
+        key: remote_user
+      - section: paramiko_connection
+        key: remote_user
+    keyword:
+      - name: remote_user
+  password:
+    description:
+      - Secret used to either login the SSH server or as a passphrase for SSH keys that require it.
+      - Can be set from the CLI via the C(--ask-pass) option.
+    type: string
+    vars:
+      - name: ansible_password
+      - name: ansible_ssh_pass
+      - name: ansible_ssh_password
+      - name: ansible_paramiko_pass
+      - name: ansible_paramiko_password
+  use_rsa_sha2_algorithms:
+    description:
+      - Whether or not to enable RSA SHA2 algorithms for pubkeys and hostkeys.
+      - On paramiko versions older than 2.9, this only affects hostkeys.
+      - For behavior matching paramiko<2.9 set this to V(false).
+    vars:
+      - name: ansible_paramiko_use_rsa_sha2_algorithms
+    ini:
+      - {key: use_rsa_sha2_algorithms, section: paramiko_connection}
+    env:
+      - {name: ANSIBLE_PARAMIKO_USE_RSA_SHA2_ALGORITHMS}
+    default: true
+    type: boolean
+  host_key_auto_add:
+    description: "Automatically add host keys to C(~/.ssh/known_hosts)."
+    env:
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_AUTO_ADD
+    ini:
+      - key: host_key_auto_add
+        section: paramiko_connection
+    type: boolean
+  look_for_keys:
+    default: True
+    description: "Set to V(false) to disable searching for private key files in C(~/.ssh/)."
+    env:
+      - name: ANSIBLE_PARAMIKO_LOOK_FOR_KEYS
+    ini:
+      - {key: look_for_keys, section: paramiko_connection}
+    type: boolean
+  proxy_command:
+    default: ""
+    description:
+      - Proxy information for running the connection via a jumphost.
+    type: string
+    env:
+      - name: ANSIBLE_PARAMIKO_PROXY_COMMAND
+    ini:
+      - {key: proxy_command, section: paramiko_connection}
+    vars:
+      - name: ansible_paramiko_proxy_command
+  record_host_keys:
+    default: True
+    description: "Save the host keys to a file."
+    env:
+      - name: ANSIBLE_PARAMIKO_RECORD_HOST_KEYS
+    ini:
+      - section: paramiko_connection
+        key: record_host_keys
+    type: boolean
+  host_key_checking:
+    description: "Set this to V(false) if you want to avoid host key checking by the underlying tools Ansible uses to connect to the host."
+    type: boolean
+    default: true
+    env:
+      - name: ANSIBLE_HOST_KEY_CHECKING
+      - name: ANSIBLE_SSH_HOST_KEY_CHECKING
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_CHECKING
+    ini:
+      - section: defaults
+        key: host_key_checking
+      - section: paramiko_connection
+        key: host_key_checking
+    vars:
+      - name: ansible_host_key_checking
+      - name: ansible_ssh_host_key_checking
+      - name: ansible_paramiko_host_key_checking
+  use_persistent_connections:
+    description: "Toggles the use of persistence for connections."
+    type: boolean
+    default: False
+    env:
+      - name: ANSIBLE_USE_PERSISTENT_CONNECTIONS
+    ini:
+      - section: defaults
+        key: use_persistent_connections
+  banner_timeout:
+    type: float
+    default: 30
+    description:
+      - Configures, in seconds, the amount of time to wait for the SSH
+        banner to be presented. This option is supported by paramiko
+        version 1.15.0 or newer.
+    ini:
+      - section: paramiko_connection
+        key: banner_timeout
+    env:
+      - name: ANSIBLE_PARAMIKO_BANNER_TIMEOUT
+  timeout:
+    type: int
+    default: 10
+    description: Number of seconds until the plugin gives up on failing to establish a TCP connection.
+    ini:
+      - section: defaults
+        key: timeout
+      - section: ssh_connection
+        key: timeout
+      - section: paramiko_connection
+        key: timeout
+    env:
+      - name: ANSIBLE_TIMEOUT
+      - name: ANSIBLE_SSH_TIMEOUT
+      - name: ANSIBLE_PARAMIKO_TIMEOUT
+    vars:
+      - name: ansible_ssh_timeout
+      - name: ansible_paramiko_timeout
+    cli:
+      - name: timeout
+  lock_file_timeout:
+    type: int
+    default: 60
+    description: Number of seconds until the plugin gives up on trying to write a lock file when writing SSH known host keys.
+    vars:
+      - name: ansible_lock_file_timeout
+    env:
+      - name: ANSIBLE_LOCK_FILE_TIMEOUT
+  private_key_file:
+    description:
+      - Path to private key file to use for authentication.
+    type: string
+    ini:
+      - section: defaults
+        key: private_key_file
+      - section: paramiko_connection
+        key: private_key_file
+    env:
+      - name: ANSIBLE_PRIVATE_KEY_FILE
+      - name: ANSIBLE_PARAMIKO_PRIVATE_KEY_FILE
+    vars:
+      - name: ansible_private_key_file
+      - name: ansible_ssh_private_key_file
+      - name: ansible_paramiko_private_key_file
+    cli:
+      - name: private_key_file
+        option: "--private-key"
+  wsl_distribution:
+    description:
+      - WSL distribution name
+    type: string
+    vars:
+      - name: wsl_distribution
+  wsl_user:
+    description:
+      - WSL distribution user
+    type: string
+    vars:
+      - name: wsl_user
+  become_user:
+    description:
+      - WSL distribution user
+    type: string
+    default: root
+    vars:
+      - name: become_user
+      - name: ansible_become_user
+  become:
+    description:
+      - whether to use the user defined by ansible_become_user.
+    type: bool
+    default: false
+    vars:
+      - name: become
+      - name: ansible_become
+"""
+
+EXAMPLES = r"""
+# ------------------------
+# Inventory: inventory.yml
+# ------------------------
+# all:
+#   children:
+#     wsl:
+#       hosts:
+#         ubuntu:
+#           ansible_host: 10.0.0.10
+#           wsl_distribution: ubuntu
+#         debian:
+#           ansible_host: 10.0.0.10
+#           wsl_distribution: debian
+#       vars:
+#         ansible_connection: community.general.wsl
+#         ansible_user: vagrant
+#         wsl_user: wsl
+# ----------------------
+# Playbook: playbook.yml
+# ----------------------
+---
+- hosts: wsl
+  tasks:
+    - name: ping
+      ansible.builtin.ping:
+    - name: id
+      args:
+        executable: /bin/bash
+      ansible.builtin.shell: |
+        exec 2>&1
+        set -x
+        echo "$0"
+        pwd
+        id
+    - name: become id
+      become: true
+      args:
+        executable: /bin/bash
+      ansible.builtin.shell: |
+        exec 2>&1
+        set -x
+        echo "$0"
+        pwd
+        id
+"""
+
+import os
+import pathlib
+import shlex
+import socket
+import tempfile
+import typing as t
+
+from ansible.errors import (
+    AnsibleAuthenticationFailure,
+    AnsibleConnectionFailure,
+    AnsibleError,
+)
+from ansible_collections.community.general.plugins.module_utils._filelock import FileLock, LockTimeout
+from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
+from ansible.module_utils.compat.paramiko import PARAMIKO_IMPORT_ERR, paramiko
+from ansible.module_utils.compat.version import LooseVersion
+from ansible.plugins.connection import ConnectionBase
+from ansible.utils.display import Display
+from ansible.utils.path import makedirs_safe
+from binascii import hexlify
+from subprocess import list2cmdline
+
+
+display = Display()
+
+
+def authenticity_msg(hostname: str, ktype: str, fingerprint: str) -> str:
+    msg = f"""
+    paramiko: The authenticity of host '{hostname}' can't be established.
+    The {ktype} key fingerprint is {fingerprint}.
+    Are you sure you want to continue connecting (yes/no)?
+    """
+    return msg
+
+
+MissingHostKeyPolicy: type = object
+if paramiko:
+    MissingHostKeyPolicy = paramiko.MissingHostKeyPolicy
+
+
+class MyAddPolicy(MissingHostKeyPolicy):
+    """
+    Based on AutoAddPolicy in paramiko so we can determine when keys are added
+
+    and also prompt for input.
+
+    Policy for automatically adding the hostname and new host key to the
+    local L{HostKeys} object, and saving it. This is used by L{SSHClient}.
+    """
+
+    def __init__(self, connection: Connection) -> None:
+        self.connection = connection
+        self._options = connection._options
+
+    def missing_host_key(self, client, hostname, key) -> None:
+
+        if all((self.connection.get_option('host_key_checking'), not self.connection.get_option('host_key_auto_add'))):
+
+            fingerprint = hexlify(key.get_fingerprint())
+            ktype = key.get_name()
+
+            if self.connection.get_option('use_persistent_connections') or self.connection.force_persistence:
+                # don't print the prompt string since the user cannot respond
+                # to the question anyway
+                raise AnsibleError(authenticity_msg(hostname, ktype, fingerprint)[1:92])
+
+            inp = to_text(
+                display.prompt_until(authenticity_msg(hostname, ktype, fingerprint), private=False),
+                errors='surrogate_or_strict'
+            )
+
+            if inp.lower() not in ['yes', 'y', '']:
+                raise AnsibleError('host connection rejected by user')
+
+        key._added_by_ansible_this_time = True
+
+        # existing implementation below:
+        client._host_keys.add(hostname, key.get_name(), key)
+
+        # host keys are actually saved in close() function below
+        # in order to control ordering.
+
+
+class Connection(ConnectionBase):
+    """ SSH based connections (paramiko) to WSL """
+
+    transport = 'community.general.wsl'
+    _log_channel: str | None = None
+
+    def __init__(self, play_context, new_stdin, *args, **kwargs):
+        super(Connection, self).__init__(play_context, new_stdin, *args, **kwargs)
+
+    def _set_log_channel(self, name: str) -> None:
+        """ Mimic paramiko.SSHClient.set_log_channel """
+        self._log_channel = name
+
+    def _parse_proxy_command(self, port: int = 22) -> dict[str, t.Any]:
+        proxy_command = self.get_option('proxy_command') or None
+
+        sock_kwarg = {}
+        if proxy_command:
+            replacers = {
+                '%h': self.get_option('remote_addr'),
+                '%p': port,
+                '%r': self.get_option('remote_user')
+            }
+            for find, replace in replacers.items():
+                proxy_command = proxy_command.replace(find, str(replace))
path: plugins/connection/wsl.py
user: login: rgl
body: done. please verify.
created_at: 2025-03-05T18:56:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,758 @@
+# -*- coding: utf-8 -*-
+# Derived from ansible/plugins/connection/proxmox_pct_remote.py (c) 2024 Nils Stein (@mietzen) <github.nstein@mailbox.org>
+# Derived from ansible/plugins/connection/paramiko_ssh.py (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
+# Copyright (c) 2025 Rui Lopes (@rgl) <ruilopes.com>
+# Copyright (c) 2025 Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import annotations
+
+DOCUMENTATION = r"""
+author: Rui Lopes (@rgl) <ruilopes.com>
+name: wsl
+short_description: Run tasks in WSL distribution using wsl.exe CLI via SSH
+requirements:
+  - paramiko
+description:
+  - Run commands or put/fetch files to an existing WSL distribution using wsl.exe CLI via SSH.
+  - Uses the Python SSH implementation (Paramiko) to connect to the WSL host.
+version_added: "10.3.0"
+options:
+  remote_addr:
+    description:
+      - Address of the remote target.
+    default: inventory_hostname
+    type: string
+    vars:
+      - name: inventory_hostname
+      - name: ansible_host
+      - name: ansible_ssh_host
+      - name: ansible_paramiko_host
+  port:
+    description: Remote port to connect to.
+    type: int
+    default: 22
+    ini:
+      - section: defaults
+        key: remote_port
+      - section: paramiko_connection
+        key: remote_port
+    env:
+      - name: ANSIBLE_REMOTE_PORT
+      - name: ANSIBLE_REMOTE_PARAMIKO_PORT
+    vars:
+      - name: ansible_port
+      - name: ansible_ssh_port
+      - name: ansible_paramiko_port
+    keyword:
+      - name: port
+  remote_user:
+    description:
+      - User to login/authenticate as.
+      - Can be set from the CLI via the C(--user) or C(-u) options.
+    type: string
+    vars:
+      - name: ansible_user
+      - name: ansible_ssh_user
+      - name: ansible_paramiko_user
+    env:
+      - name: ANSIBLE_REMOTE_USER
+      - name: ANSIBLE_PARAMIKO_REMOTE_USER
+    ini:
+      - section: defaults
+        key: remote_user
+      - section: paramiko_connection
+        key: remote_user
+    keyword:
+      - name: remote_user
+  password:
+    description:
+      - Secret used to either login the SSH server or as a passphrase for SSH keys that require it.
+      - Can be set from the CLI via the C(--ask-pass) option.
+    type: string
+    vars:
+      - name: ansible_password
+      - name: ansible_ssh_pass
+      - name: ansible_ssh_password
+      - name: ansible_paramiko_pass
+      - name: ansible_paramiko_password
+  use_rsa_sha2_algorithms:
+    description:
+      - Whether or not to enable RSA SHA2 algorithms for pubkeys and hostkeys.
+      - On paramiko versions older than 2.9, this only affects hostkeys.
+      - For behavior matching paramiko<2.9 set this to V(false).
+    vars:
+      - name: ansible_paramiko_use_rsa_sha2_algorithms
+    ini:
+      - {key: use_rsa_sha2_algorithms, section: paramiko_connection}
+    env:
+      - {name: ANSIBLE_PARAMIKO_USE_RSA_SHA2_ALGORITHMS}
+    default: true
+    type: boolean
+  host_key_auto_add:
+    description: "Automatically add host keys to C(~/.ssh/known_hosts)."
+    env:
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_AUTO_ADD
+    ini:
+      - key: host_key_auto_add
+        section: paramiko_connection
+    type: boolean
+  look_for_keys:
+    default: True
+    description: "Set to V(false) to disable searching for private key files in C(~/.ssh/)."
+    env:
+      - name: ANSIBLE_PARAMIKO_LOOK_FOR_KEYS
+    ini:
+      - {key: look_for_keys, section: paramiko_connection}
+    type: boolean
+  proxy_command:
+    default: ""
+    description:
+      - Proxy information for running the connection via a jumphost.
+    type: string
+    env:
+      - name: ANSIBLE_PARAMIKO_PROXY_COMMAND
+    ini:
+      - {key: proxy_command, section: paramiko_connection}
+    vars:
+      - name: ansible_paramiko_proxy_command
+  record_host_keys:
+    default: True
+    description: "Save the host keys to a file."
+    env:
+      - name: ANSIBLE_PARAMIKO_RECORD_HOST_KEYS
+    ini:
+      - section: paramiko_connection
+        key: record_host_keys
+    type: boolean
+  host_key_checking:
+    description: "Set this to V(false) if you want to avoid host key checking by the underlying tools Ansible uses to connect to the host."
+    type: boolean
+    default: true
+    env:
+      - name: ANSIBLE_HOST_KEY_CHECKING
+      - name: ANSIBLE_SSH_HOST_KEY_CHECKING
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_CHECKING
+    ini:
+      - section: defaults
+        key: host_key_checking
+      - section: paramiko_connection
+        key: host_key_checking
+    vars:
+      - name: ansible_host_key_checking
+      - name: ansible_ssh_host_key_checking
+      - name: ansible_paramiko_host_key_checking
+  use_persistent_connections:
+    description: "Toggles the use of persistence for connections."
+    type: boolean
+    default: False
+    env:
+      - name: ANSIBLE_USE_PERSISTENT_CONNECTIONS
+    ini:
+      - section: defaults
+        key: use_persistent_connections
+  banner_timeout:
+    type: float
+    default: 30
+    description:
+      - Configures, in seconds, the amount of time to wait for the SSH
+        banner to be presented. This option is supported by paramiko
+        version 1.15.0 or newer.
+    ini:
+      - section: paramiko_connection
+        key: banner_timeout
+    env:
+      - name: ANSIBLE_PARAMIKO_BANNER_TIMEOUT
+  timeout:
+    type: int
+    default: 10
+    description: Number of seconds until the plugin gives up on failing to establish a TCP connection.
+    ini:
+      - section: defaults
+        key: timeout
+      - section: ssh_connection
+        key: timeout
+      - section: paramiko_connection
+        key: timeout
+    env:
+      - name: ANSIBLE_TIMEOUT
+      - name: ANSIBLE_SSH_TIMEOUT
+      - name: ANSIBLE_PARAMIKO_TIMEOUT
+    vars:
+      - name: ansible_ssh_timeout
+      - name: ansible_paramiko_timeout
+    cli:
+      - name: timeout
+  lock_file_timeout:
+    type: int
+    default: 60
+    description: Number of seconds until the plugin gives up on trying to write a lock file when writing SSH known host keys.
+    vars:
+      - name: ansible_lock_file_timeout
+    env:
+      - name: ANSIBLE_LOCK_FILE_TIMEOUT
+  private_key_file:
+    description:
+      - Path to private key file to use for authentication.
+    type: string
+    ini:
+      - section: defaults
+        key: private_key_file
+      - section: paramiko_connection
+        key: private_key_file
+    env:
+      - name: ANSIBLE_PRIVATE_KEY_FILE
+      - name: ANSIBLE_PARAMIKO_PRIVATE_KEY_FILE
+    vars:
+      - name: ansible_private_key_file
+      - name: ansible_ssh_private_key_file
+      - name: ansible_paramiko_private_key_file
+    cli:
+      - name: private_key_file
+        option: "--private-key"
+  wsl_distribution:
+    description:
+      - WSL distribution name
+    type: string
+    vars:
+      - name: wsl_distribution
+  wsl_user:
+    description:
+      - WSL distribution user
+    type: string
+    vars:
+      - name: wsl_user
+  become_user:
+    description:
+      - WSL distribution user
+    type: string
+    default: root
+    vars:
+      - name: become_user
+      - name: ansible_become_user
+  become:
+    description:
+      - whether to use the user defined by ansible_become_user.
+    type: bool
+    default: false
+    vars:
+      - name: become
+      - name: ansible_become
+"""
+
+EXAMPLES = r"""
+# ------------------------
+# Inventory: inventory.yml
+# ------------------------
+# all:
+#   children:
+#     wsl:
+#       hosts:
+#         ubuntu:
+#           ansible_host: 10.0.0.10
+#           wsl_distribution: ubuntu
+#         debian:
+#           ansible_host: 10.0.0.10
+#           wsl_distribution: debian
+#       vars:
+#         ansible_connection: community.general.wsl
+#         ansible_user: vagrant
+#         wsl_user: wsl
+# ----------------------
+# Playbook: playbook.yml
+# ----------------------
+---
+- hosts: wsl
+  tasks:
+    - name: ping
+      ansible.builtin.ping:
+    - name: id
+      args:
+        executable: /bin/bash
+      ansible.builtin.shell: |
+        exec 2>&1
+        set -x
+        echo "$0"
+        pwd
+        id
+    - name: become id
+      become: true
+      args:
+        executable: /bin/bash
+      ansible.builtin.shell: |
+        exec 2>&1
+        set -x
+        echo "$0"
+        pwd
+        id
+"""
+
+import os
+import pathlib
+import shlex
+import socket
+import tempfile
+import typing as t
+
+from ansible.errors import (
+    AnsibleAuthenticationFailure,
+    AnsibleConnectionFailure,
+    AnsibleError,
+)
+from ansible_collections.community.general.plugins.module_utils._filelock import FileLock, LockTimeout
+from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
+from ansible.module_utils.compat.paramiko import PARAMIKO_IMPORT_ERR, paramiko
+from ansible.module_utils.compat.version import LooseVersion
+from ansible.plugins.connection import ConnectionBase
+from ansible.utils.display import Display
+from ansible.utils.path import makedirs_safe
+from binascii import hexlify
+from subprocess import list2cmdline
+
+
+display = Display()
+
+
+def authenticity_msg(hostname: str, ktype: str, fingerprint: str) -> str:
+    msg = f"""
+    paramiko: The authenticity of host '{hostname}' can't be established.
+    The {ktype} key fingerprint is {fingerprint}.
+    Are you sure you want to continue connecting (yes/no)?
+    """
+    return msg
+
+
+MissingHostKeyPolicy: type = object
+if paramiko:
+    MissingHostKeyPolicy = paramiko.MissingHostKeyPolicy
+
+
+class MyAddPolicy(MissingHostKeyPolicy):
+    """
+    Based on AutoAddPolicy in paramiko so we can determine when keys are added
+
+    and also prompt for input.
+
+    Policy for automatically adding the hostname and new host key to the
+    local L{HostKeys} object, and saving it. This is used by L{SSHClient}.
+    """
+
+    def __init__(self, connection: Connection) -> None:
+        self.connection = connection
+        self._options = connection._options
+
+    def missing_host_key(self, client, hostname, key) -> None:
+
+        if all((self.connection.get_option('host_key_checking'), not self.connection.get_option('host_key_auto_add'))):
+
+            fingerprint = hexlify(key.get_fingerprint())
+            ktype = key.get_name()
+
+            if self.connection.get_option('use_persistent_connections') or self.connection.force_persistence:
+                # don't print the prompt string since the user cannot respond
+                # to the question anyway
+                raise AnsibleError(authenticity_msg(hostname, ktype, fingerprint)[1:92])
+
+            inp = to_text(
+                display.prompt_until(authenticity_msg(hostname, ktype, fingerprint), private=False),
+                errors='surrogate_or_strict'
+            )
+
+            if inp.lower() not in ['yes', 'y', '']:
+                raise AnsibleError('host connection rejected by user')
+
+        key._added_by_ansible_this_time = True
+
+        # existing implementation below:
+        client._host_keys.add(hostname, key.get_name(), key)
+
+        # host keys are actually saved in close() function below
+        # in order to control ordering.
+
+
+class Connection(ConnectionBase):
+    """ SSH based connections (paramiko) to WSL """
+
+    transport = 'community.general.wsl'
+    _log_channel: str | None = None
+
+    def __init__(self, play_context, new_stdin, *args, **kwargs):
+        super(Connection, self).__init__(play_context, new_stdin, *args, **kwargs)
+
+    def _set_log_channel(self, name: str) -> None:
+        """ Mimic paramiko.SSHClient.set_log_channel """
+        self._log_channel = name
+
+    def _parse_proxy_command(self, port: int = 22) -> dict[str, t.Any]:
+        proxy_command = self.get_option('proxy_command') or None
+
+        sock_kwarg = {}
+        if proxy_command:
+            replacers = {
+                '%h': self.get_option('remote_addr'),
+                '%p': port,
+                '%r': self.get_option('remote_user')
+            }
+            for find, replace in replacers.items():
+                proxy_command = proxy_command.replace(find, str(replace))
+            try:
+                sock_kwarg = {'sock': paramiko.ProxyCommand(proxy_command)}
+                display.vvv(f'CONFIGURE PROXY COMMAND FOR CONNECTION: {proxy_command}', host=self.get_option('remote_addr'))
+            except AttributeError:
+                display.warning('Paramiko ProxyCommand support unavailable. '
+                                'Please upgrade to Paramiko 1.9.0 or newer. '
+                                'Not using configured ProxyCommand')
+
+        return sock_kwarg
+
+    def _connect(self) -> Connection:
+        """ activates the connection object """
+
+        if paramiko is None:
+            raise AnsibleError(f'paramiko is not installed: {to_native(PARAMIKO_IMPORT_ERR)}')
+
+        port = self.get_option('port')
+        display.vvv(f'ESTABLISH PARAMIKO SSH CONNECTION FOR USER: {self.get_option("remote_user")} on PORT {to_text(port)} TO {self.get_option("remote_addr")}',
+                    host=self.get_option('remote_addr'))
+
+        ssh = paramiko.SSHClient()
+
+        # Set pubkey and hostkey algorithms to disable, the only manipulation allowed currently
+        # is keeping or omitting rsa-sha2 algorithms
+        # default_keys: t.Tuple[str] = ()
+        paramiko_preferred_pubkeys = getattr(paramiko.Transport, '_preferred_pubkeys', ())
+        paramiko_preferred_hostkeys = getattr(paramiko.Transport, '_preferred_keys', ())
+        use_rsa_sha2_algorithms = self.get_option('use_rsa_sha2_algorithms')
+        disabled_algorithms: t.Dict[str, t.Iterable[str]] = {}
+        if not use_rsa_sha2_algorithms:
+            if paramiko_preferred_pubkeys:
+                disabled_algorithms['pubkeys'] = tuple(a for a in paramiko_preferred_pubkeys if 'rsa-sha2' in a)
+            if paramiko_preferred_hostkeys:
+                disabled_algorithms['keys'] = tuple(a for a in paramiko_preferred_hostkeys if 'rsa-sha2' in a)
+
+        # override paramiko's default logger name
+        if self._log_channel is not None:
+            ssh.set_log_channel(self._log_channel)
+
+        self.keyfile = os.path.expanduser('~/.ssh/known_hosts')
+
+        if self.get_option('host_key_checking'):
+            for ssh_known_hosts in ('/etc/ssh/ssh_known_hosts', '/etc/openssh/ssh_known_hosts'):
+                try:
+                    ssh.load_system_host_keys(ssh_known_hosts)
+                    break
+                except IOError:
+                    pass  # file was not found, but not required to function
path: plugins/connection/wsl.py
user: login: rgl
body: Not sure either.

This was introduced by @mietzen in https://github.com/ansible-collections/community.general/pull/8424.

@mietzen, maybe you can tell us more about this?
created_at: 2025-03-05T18:58:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,758 @@
+# -*- coding: utf-8 -*-
+# Derived from ansible/plugins/connection/proxmox_pct_remote.py (c) 2024 Nils Stein (@mietzen) <github.nstein@mailbox.org>
+# Derived from ansible/plugins/connection/paramiko_ssh.py (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
+# Copyright (c) 2025 Rui Lopes (@rgl) <ruilopes.com>
+# Copyright (c) 2025 Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import annotations
+
+DOCUMENTATION = r"""
+author: Rui Lopes (@rgl) <ruilopes.com>
+name: wsl
+short_description: Run tasks in WSL distribution using wsl.exe CLI via SSH
+requirements:
+  - paramiko
+description:
+  - Run commands or put/fetch files to an existing WSL distribution using wsl.exe CLI via SSH.
+  - Uses the Python SSH implementation (Paramiko) to connect to the WSL host.
+version_added: "10.3.0"
+options:
+  remote_addr:
+    description:
+      - Address of the remote target.
+    default: inventory_hostname
+    type: string
+    vars:
+      - name: inventory_hostname
+      - name: ansible_host
+      - name: ansible_ssh_host
+      - name: ansible_paramiko_host
+  port:
+    description: Remote port to connect to.
+    type: int
+    default: 22
+    ini:
+      - section: defaults
+        key: remote_port
+      - section: paramiko_connection
+        key: remote_port
+    env:
+      - name: ANSIBLE_REMOTE_PORT
+      - name: ANSIBLE_REMOTE_PARAMIKO_PORT
+    vars:
+      - name: ansible_port
+      - name: ansible_ssh_port
+      - name: ansible_paramiko_port
+    keyword:
+      - name: port
+  remote_user:
+    description:
+      - User to login/authenticate as.
+      - Can be set from the CLI via the C(--user) or C(-u) options.
+    type: string
+    vars:
+      - name: ansible_user
+      - name: ansible_ssh_user
+      - name: ansible_paramiko_user
+    env:
+      - name: ANSIBLE_REMOTE_USER
+      - name: ANSIBLE_PARAMIKO_REMOTE_USER
+    ini:
+      - section: defaults
+        key: remote_user
+      - section: paramiko_connection
+        key: remote_user
+    keyword:
+      - name: remote_user
+  password:
+    description:
+      - Secret used to either login the SSH server or as a passphrase for SSH keys that require it.
+      - Can be set from the CLI via the C(--ask-pass) option.
+    type: string
+    vars:
+      - name: ansible_password
+      - name: ansible_ssh_pass
+      - name: ansible_ssh_password
+      - name: ansible_paramiko_pass
+      - name: ansible_paramiko_password
+  use_rsa_sha2_algorithms:
+    description:
+      - Whether or not to enable RSA SHA2 algorithms for pubkeys and hostkeys.
+      - On paramiko versions older than 2.9, this only affects hostkeys.
+      - For behavior matching paramiko<2.9 set this to V(false).
+    vars:
+      - name: ansible_paramiko_use_rsa_sha2_algorithms
+    ini:
+      - {key: use_rsa_sha2_algorithms, section: paramiko_connection}
+    env:
+      - {name: ANSIBLE_PARAMIKO_USE_RSA_SHA2_ALGORITHMS}
+    default: true
+    type: boolean
+  host_key_auto_add:
+    description: "Automatically add host keys to C(~/.ssh/known_hosts)."
+    env:
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_AUTO_ADD
+    ini:
+      - key: host_key_auto_add
+        section: paramiko_connection
+    type: boolean
+  look_for_keys:
+    default: True
+    description: "Set to V(false) to disable searching for private key files in C(~/.ssh/)."
+    env:
+      - name: ANSIBLE_PARAMIKO_LOOK_FOR_KEYS
+    ini:
+      - {key: look_for_keys, section: paramiko_connection}
+    type: boolean
+  proxy_command:
+    default: ""
+    description:
+      - Proxy information for running the connection via a jumphost.
+    type: string
+    env:
+      - name: ANSIBLE_PARAMIKO_PROXY_COMMAND
+    ini:
+      - {key: proxy_command, section: paramiko_connection}
+    vars:
+      - name: ansible_paramiko_proxy_command
+  record_host_keys:
+    default: True
+    description: "Save the host keys to a file."
+    env:
+      - name: ANSIBLE_PARAMIKO_RECORD_HOST_KEYS
+    ini:
+      - section: paramiko_connection
+        key: record_host_keys
+    type: boolean
+  host_key_checking:
+    description: "Set this to V(false) if you want to avoid host key checking by the underlying tools Ansible uses to connect to the host."
+    type: boolean
+    default: true
+    env:
+      - name: ANSIBLE_HOST_KEY_CHECKING
+      - name: ANSIBLE_SSH_HOST_KEY_CHECKING
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_CHECKING
+    ini:
+      - section: defaults
+        key: host_key_checking
+      - section: paramiko_connection
+        key: host_key_checking
+    vars:
+      - name: ansible_host_key_checking
+      - name: ansible_ssh_host_key_checking
+      - name: ansible_paramiko_host_key_checking
+  use_persistent_connections:
+    description: "Toggles the use of persistence for connections."
+    type: boolean
+    default: False
+    env:
+      - name: ANSIBLE_USE_PERSISTENT_CONNECTIONS
+    ini:
+      - section: defaults
+        key: use_persistent_connections
+  banner_timeout:
+    type: float
+    default: 30
+    description:
+      - Configures, in seconds, the amount of time to wait for the SSH
+        banner to be presented. This option is supported by paramiko
+        version 1.15.0 or newer.
+    ini:
+      - section: paramiko_connection
+        key: banner_timeout
+    env:
+      - name: ANSIBLE_PARAMIKO_BANNER_TIMEOUT
+  timeout:
+    type: int
+    default: 10
+    description: Number of seconds until the plugin gives up on failing to establish a TCP connection.
+    ini:
+      - section: defaults
+        key: timeout
+      - section: ssh_connection
+        key: timeout
+      - section: paramiko_connection
+        key: timeout
+    env:
+      - name: ANSIBLE_TIMEOUT
+      - name: ANSIBLE_SSH_TIMEOUT
+      - name: ANSIBLE_PARAMIKO_TIMEOUT
+    vars:
+      - name: ansible_ssh_timeout
+      - name: ansible_paramiko_timeout
+    cli:
+      - name: timeout
+  lock_file_timeout:
+    type: int
+    default: 60
+    description: Number of seconds until the plugin gives up on trying to write a lock file when writing SSH known host keys.
+    vars:
+      - name: ansible_lock_file_timeout
+    env:
+      - name: ANSIBLE_LOCK_FILE_TIMEOUT
+  private_key_file:
+    description:
+      - Path to private key file to use for authentication.
+    type: string
+    ini:
+      - section: defaults
+        key: private_key_file
+      - section: paramiko_connection
+        key: private_key_file
+    env:
+      - name: ANSIBLE_PRIVATE_KEY_FILE
+      - name: ANSIBLE_PARAMIKO_PRIVATE_KEY_FILE
+    vars:
+      - name: ansible_private_key_file
+      - name: ansible_ssh_private_key_file
+      - name: ansible_paramiko_private_key_file
+    cli:
+      - name: private_key_file
+        option: "--private-key"
+  wsl_distribution:
+    description:
+      - WSL distribution name
+    type: string
+    vars:
+      - name: wsl_distribution
+  wsl_user:
+    description:
+      - WSL distribution user
+    type: string
+    vars:
+      - name: wsl_user
+  become_user:
+    description:
+      - WSL distribution user
+    type: string
+    default: root
+    vars:
+      - name: become_user
+      - name: ansible_become_user
+  become:
+    description:
+      - whether to use the user defined by ansible_become_user.
+    type: bool
+    default: false
+    vars:
+      - name: become
+      - name: ansible_become
+"""
+
+EXAMPLES = r"""
+# ------------------------
+# Inventory: inventory.yml
+# ------------------------
+# all:
+#   children:
+#     wsl:
+#       hosts:
+#         ubuntu:
+#           ansible_host: 10.0.0.10
+#           wsl_distribution: ubuntu
+#         debian:
+#           ansible_host: 10.0.0.10
+#           wsl_distribution: debian
+#       vars:
+#         ansible_connection: community.general.wsl
+#         ansible_user: vagrant
+#         wsl_user: wsl
+# ----------------------
+# Playbook: playbook.yml
+# ----------------------
+---
+- hosts: wsl
+  tasks:
+    - name: ping
+      ansible.builtin.ping:
+    - name: id
+      args:
+        executable: /bin/bash
+      ansible.builtin.shell: |
+        exec 2>&1
+        set -x
+        echo "$0"
+        pwd
+        id
+    - name: become id
+      become: true
+      args:
+        executable: /bin/bash
+      ansible.builtin.shell: |
+        exec 2>&1
+        set -x
+        echo "$0"
+        pwd
+        id
+"""
+
+import os
+import pathlib
+import shlex
+import socket
+import tempfile
+import typing as t
+
+from ansible.errors import (
+    AnsibleAuthenticationFailure,
+    AnsibleConnectionFailure,
+    AnsibleError,
+)
+from ansible_collections.community.general.plugins.module_utils._filelock import FileLock, LockTimeout
+from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
+from ansible.module_utils.compat.paramiko import PARAMIKO_IMPORT_ERR, paramiko
+from ansible.module_utils.compat.version import LooseVersion
+from ansible.plugins.connection import ConnectionBase
+from ansible.utils.display import Display
+from ansible.utils.path import makedirs_safe
+from binascii import hexlify
+from subprocess import list2cmdline
+
+
+display = Display()
+
+
+def authenticity_msg(hostname: str, ktype: str, fingerprint: str) -> str:
+    msg = f"""
+    paramiko: The authenticity of host '{hostname}' can't be established.
+    The {ktype} key fingerprint is {fingerprint}.
+    Are you sure you want to continue connecting (yes/no)?
+    """
+    return msg
+
+
+MissingHostKeyPolicy: type = object
+if paramiko:
+    MissingHostKeyPolicy = paramiko.MissingHostKeyPolicy
+
+
+class MyAddPolicy(MissingHostKeyPolicy):
+    """
+    Based on AutoAddPolicy in paramiko so we can determine when keys are added
+
+    and also prompt for input.
+
+    Policy for automatically adding the hostname and new host key to the
+    local L{HostKeys} object, and saving it. This is used by L{SSHClient}.
+    """
+
+    def __init__(self, connection: Connection) -> None:
+        self.connection = connection
+        self._options = connection._options
+
+    def missing_host_key(self, client, hostname, key) -> None:
+
+        if all((self.connection.get_option('host_key_checking'), not self.connection.get_option('host_key_auto_add'))):
+
+            fingerprint = hexlify(key.get_fingerprint())
+            ktype = key.get_name()
+
+            if self.connection.get_option('use_persistent_connections') or self.connection.force_persistence:
+                # don't print the prompt string since the user cannot respond
+                # to the question anyway
+                raise AnsibleError(authenticity_msg(hostname, ktype, fingerprint)[1:92])
+
+            inp = to_text(
+                display.prompt_until(authenticity_msg(hostname, ktype, fingerprint), private=False),
+                errors='surrogate_or_strict'
+            )
+
+            if inp.lower() not in ['yes', 'y', '']:
+                raise AnsibleError('host connection rejected by user')
+
+        key._added_by_ansible_this_time = True
+
+        # existing implementation below:
+        client._host_keys.add(hostname, key.get_name(), key)
+
+        # host keys are actually saved in close() function below
+        # in order to control ordering.
+
+
+class Connection(ConnectionBase):
+    """ SSH based connections (paramiko) to WSL """
+
+    transport = 'community.general.wsl'
+    _log_channel: str | None = None
+
+    def __init__(self, play_context, new_stdin, *args, **kwargs):
+        super(Connection, self).__init__(play_context, new_stdin, *args, **kwargs)
+
+    def _set_log_channel(self, name: str) -> None:
+        """ Mimic paramiko.SSHClient.set_log_channel """
+        self._log_channel = name
+
+    def _parse_proxy_command(self, port: int = 22) -> dict[str, t.Any]:
+        proxy_command = self.get_option('proxy_command') or None
+
+        sock_kwarg = {}
+        if proxy_command:
+            replacers = {
+                '%h': self.get_option('remote_addr'),
+                '%p': port,
+                '%r': self.get_option('remote_user')
+            }
+            for find, replace in replacers.items():
+                proxy_command = proxy_command.replace(find, str(replace))
+            try:
+                sock_kwarg = {'sock': paramiko.ProxyCommand(proxy_command)}
+                display.vvv(f'CONFIGURE PROXY COMMAND FOR CONNECTION: {proxy_command}', host=self.get_option('remote_addr'))
+            except AttributeError:
+                display.warning('Paramiko ProxyCommand support unavailable. '
+                                'Please upgrade to Paramiko 1.9.0 or newer. '
+                                'Not using configured ProxyCommand')
+
+        return sock_kwarg
+
+    def _connect(self) -> Connection:
+        """ activates the connection object """
+
+        if paramiko is None:
+            raise AnsibleError(f'paramiko is not installed: {to_native(PARAMIKO_IMPORT_ERR)}')
+
+        port = self.get_option('port')
+        display.vvv(f'ESTABLISH PARAMIKO SSH CONNECTION FOR USER: {self.get_option("remote_user")} on PORT {to_text(port)} TO {self.get_option("remote_addr")}',
+                    host=self.get_option('remote_addr'))
+
+        ssh = paramiko.SSHClient()
+
+        # Set pubkey and hostkey algorithms to disable, the only manipulation allowed currently
+        # is keeping or omitting rsa-sha2 algorithms
+        # default_keys: t.Tuple[str] = ()
+        paramiko_preferred_pubkeys = getattr(paramiko.Transport, '_preferred_pubkeys', ())
+        paramiko_preferred_hostkeys = getattr(paramiko.Transport, '_preferred_keys', ())
+        use_rsa_sha2_algorithms = self.get_option('use_rsa_sha2_algorithms')
+        disabled_algorithms: t.Dict[str, t.Iterable[str]] = {}
+        if not use_rsa_sha2_algorithms:
+            if paramiko_preferred_pubkeys:
+                disabled_algorithms['pubkeys'] = tuple(a for a in paramiko_preferred_pubkeys if 'rsa-sha2' in a)
+            if paramiko_preferred_hostkeys:
+                disabled_algorithms['keys'] = tuple(a for a in paramiko_preferred_hostkeys if 'rsa-sha2' in a)
+
+        # override paramiko's default logger name
+        if self._log_channel is not None:
+            ssh.set_log_channel(self._log_channel)
+
+        self.keyfile = os.path.expanduser('~/.ssh/known_hosts')
+
+        if self.get_option('host_key_checking'):
+            for ssh_known_hosts in ('/etc/ssh/ssh_known_hosts', '/etc/openssh/ssh_known_hosts'):
+                try:
+                    ssh.load_system_host_keys(ssh_known_hosts)
+                    break
+                except IOError:
+                    pass  # file was not found, but not required to function
+                except paramiko.hostkeys.InvalidHostKey as e:
+                    raise AnsibleConnectionFailure(f'Invalid host key: {to_text(e.line)}')
+            try:
+                ssh.load_system_host_keys()
+            except paramiko.hostkeys.InvalidHostKey as e:
+                raise AnsibleConnectionFailure(f'Invalid host key: {to_text(e.line)}')
+
+        ssh_connect_kwargs = self._parse_proxy_command(port)
+        ssh.set_missing_host_key_policy(MyAddPolicy(self))
+        conn_password = self.get_option('password')
+        allow_agent = True
+
+        if conn_password is not None:
+            allow_agent = False
+
+        try:
+            key_filename = None
+            if self.get_option('private_key_file'):
+                key_filename = os.path.expanduser(self.get_option('private_key_file'))
+
+            # paramiko 2.2 introduced auth_timeout parameter
+            if LooseVersion(paramiko.__version__) >= LooseVersion('2.2.0'):
+                ssh_connect_kwargs['auth_timeout'] = self.get_option('timeout')
+
+            # paramiko 1.15 introduced banner timeout parameter
+            if LooseVersion(paramiko.__version__) >= LooseVersion('1.15.0'):
+                ssh_connect_kwargs['banner_timeout'] = self.get_option('banner_timeout')
+
+            ssh.connect(
+                self.get_option('remote_addr').lower(),
+                username=self.get_option('remote_user'),
+                allow_agent=allow_agent,
+                look_for_keys=self.get_option('look_for_keys'),
+                key_filename=key_filename,
+                password=conn_password,
+                timeout=self.get_option('timeout'),
+                port=port,
+                disabled_algorithms=disabled_algorithms,
+                **ssh_connect_kwargs,
+            )
+        except paramiko.ssh_exception.BadHostKeyException as e:
+            raise AnsibleConnectionFailure(f'host key mismatch for {to_text(e.hostname)}')
+        except paramiko.ssh_exception.AuthenticationException as e:
+            msg = f'Failed to authenticate: {e}'
+            raise AnsibleAuthenticationFailure(msg)
+        except Exception as e:
+            msg = to_text(e)
+            if u'PID check failed' in msg:
+                raise AnsibleError('paramiko version issue, please upgrade paramiko on the machine running ansible')
path: plugins/connection/wsl.py
user: login: rgl
body: I have no idea from where this message comes from.

This was introduced by @mietzen in https://github.com/ansible-collections/community.general/pull/8424.

@mietzen, maybe you can tell us more about this?
created_at: 2025-03-05T18:58:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,758 @@
+# -*- coding: utf-8 -*-
+# Derived from ansible/plugins/connection/proxmox_pct_remote.py (c) 2024 Nils Stein (@mietzen) <github.nstein@mailbox.org>
+# Derived from ansible/plugins/connection/paramiko_ssh.py (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
+# Copyright (c) 2025 Rui Lopes (@rgl) <ruilopes.com>
+# Copyright (c) 2025 Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import annotations
+
+DOCUMENTATION = r"""
+author: Rui Lopes (@rgl) <ruilopes.com>
+name: wsl
+short_description: Run tasks in WSL distribution using wsl.exe CLI via SSH
+requirements:
+  - paramiko
+description:
+  - Run commands or put/fetch files to an existing WSL distribution using wsl.exe CLI via SSH.
+  - Uses the Python SSH implementation (Paramiko) to connect to the WSL host.
+version_added: "10.3.0"
+options:
+  remote_addr:
+    description:
+      - Address of the remote target.
+    default: inventory_hostname
+    type: string
+    vars:
+      - name: inventory_hostname
+      - name: ansible_host
+      - name: ansible_ssh_host
+      - name: ansible_paramiko_host
+  port:
+    description: Remote port to connect to.
+    type: int
+    default: 22
+    ini:
+      - section: defaults
+        key: remote_port
+      - section: paramiko_connection
+        key: remote_port
+    env:
+      - name: ANSIBLE_REMOTE_PORT
+      - name: ANSIBLE_REMOTE_PARAMIKO_PORT
+    vars:
+      - name: ansible_port
+      - name: ansible_ssh_port
+      - name: ansible_paramiko_port
+    keyword:
+      - name: port
+  remote_user:
+    description:
+      - User to login/authenticate as.
+      - Can be set from the CLI via the C(--user) or C(-u) options.
+    type: string
+    vars:
+      - name: ansible_user
+      - name: ansible_ssh_user
+      - name: ansible_paramiko_user
+    env:
+      - name: ANSIBLE_REMOTE_USER
+      - name: ANSIBLE_PARAMIKO_REMOTE_USER
+    ini:
+      - section: defaults
+        key: remote_user
+      - section: paramiko_connection
+        key: remote_user
+    keyword:
+      - name: remote_user
+  password:
+    description:
+      - Secret used to either login the SSH server or as a passphrase for SSH keys that require it.
+      - Can be set from the CLI via the C(--ask-pass) option.
+    type: string
+    vars:
+      - name: ansible_password
+      - name: ansible_ssh_pass
+      - name: ansible_ssh_password
+      - name: ansible_paramiko_pass
+      - name: ansible_paramiko_password
+  use_rsa_sha2_algorithms:
+    description:
+      - Whether or not to enable RSA SHA2 algorithms for pubkeys and hostkeys.
+      - On paramiko versions older than 2.9, this only affects hostkeys.
+      - For behavior matching paramiko<2.9 set this to V(false).
+    vars:
+      - name: ansible_paramiko_use_rsa_sha2_algorithms
+    ini:
+      - {key: use_rsa_sha2_algorithms, section: paramiko_connection}
+    env:
+      - {name: ANSIBLE_PARAMIKO_USE_RSA_SHA2_ALGORITHMS}
+    default: true
+    type: boolean
+  host_key_auto_add:
+    description: "Automatically add host keys to C(~/.ssh/known_hosts)."
+    env:
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_AUTO_ADD
+    ini:
+      - key: host_key_auto_add
+        section: paramiko_connection
+    type: boolean
+  look_for_keys:
+    default: True
+    description: "Set to V(false) to disable searching for private key files in C(~/.ssh/)."
+    env:
+      - name: ANSIBLE_PARAMIKO_LOOK_FOR_KEYS
+    ini:
+      - {key: look_for_keys, section: paramiko_connection}
+    type: boolean
+  proxy_command:
+    default: ""
+    description:
+      - Proxy information for running the connection via a jumphost.
+    type: string
+    env:
+      - name: ANSIBLE_PARAMIKO_PROXY_COMMAND
+    ini:
+      - {key: proxy_command, section: paramiko_connection}
+    vars:
+      - name: ansible_paramiko_proxy_command
+  record_host_keys:
+    default: True
+    description: "Save the host keys to a file."
+    env:
+      - name: ANSIBLE_PARAMIKO_RECORD_HOST_KEYS
+    ini:
+      - section: paramiko_connection
+        key: record_host_keys
+    type: boolean
+  host_key_checking:
+    description: "Set this to V(false) if you want to avoid host key checking by the underlying tools Ansible uses to connect to the host."
+    type: boolean
+    default: true
+    env:
+      - name: ANSIBLE_HOST_KEY_CHECKING
+      - name: ANSIBLE_SSH_HOST_KEY_CHECKING
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_CHECKING
+    ini:
+      - section: defaults
+        key: host_key_checking
+      - section: paramiko_connection
+        key: host_key_checking
+    vars:
+      - name: ansible_host_key_checking
+      - name: ansible_ssh_host_key_checking
+      - name: ansible_paramiko_host_key_checking
+  use_persistent_connections:
+    description: "Toggles the use of persistence for connections."
+    type: boolean
+    default: False
+    env:
+      - name: ANSIBLE_USE_PERSISTENT_CONNECTIONS
+    ini:
+      - section: defaults
+        key: use_persistent_connections
+  banner_timeout:
+    type: float
+    default: 30
+    description:
+      - Configures, in seconds, the amount of time to wait for the SSH
+        banner to be presented. This option is supported by paramiko
+        version 1.15.0 or newer.
+    ini:
+      - section: paramiko_connection
+        key: banner_timeout
+    env:
+      - name: ANSIBLE_PARAMIKO_BANNER_TIMEOUT
+  timeout:
+    type: int
+    default: 10
+    description: Number of seconds until the plugin gives up on failing to establish a TCP connection.
+    ini:
+      - section: defaults
+        key: timeout
+      - section: ssh_connection
+        key: timeout
+      - section: paramiko_connection
+        key: timeout
+    env:
+      - name: ANSIBLE_TIMEOUT
+      - name: ANSIBLE_SSH_TIMEOUT
+      - name: ANSIBLE_PARAMIKO_TIMEOUT
+    vars:
+      - name: ansible_ssh_timeout
+      - name: ansible_paramiko_timeout
+    cli:
+      - name: timeout
+  lock_file_timeout:
+    type: int
+    default: 60
+    description: Number of seconds until the plugin gives up on trying to write a lock file when writing SSH known host keys.
+    vars:
+      - name: ansible_lock_file_timeout
+    env:
+      - name: ANSIBLE_LOCK_FILE_TIMEOUT
+  private_key_file:
+    description:
+      - Path to private key file to use for authentication.
+    type: string
+    ini:
+      - section: defaults
+        key: private_key_file
+      - section: paramiko_connection
+        key: private_key_file
+    env:
+      - name: ANSIBLE_PRIVATE_KEY_FILE
+      - name: ANSIBLE_PARAMIKO_PRIVATE_KEY_FILE
+    vars:
+      - name: ansible_private_key_file
+      - name: ansible_ssh_private_key_file
+      - name: ansible_paramiko_private_key_file
+    cli:
+      - name: private_key_file
+        option: "--private-key"
+  wsl_distribution:
+    description:
+      - WSL distribution name
+    type: string
+    vars:
+      - name: wsl_distribution
+  wsl_user:
+    description:
+      - WSL distribution user
+    type: string
+    vars:
+      - name: wsl_user
+  become_user:
+    description:
+      - WSL distribution user
+    type: string
+    default: root
+    vars:
+      - name: become_user
+      - name: ansible_become_user
+  become:
+    description:
+      - whether to use the user defined by ansible_become_user.
+    type: bool
+    default: false
+    vars:
+      - name: become
+      - name: ansible_become
+"""
+
+EXAMPLES = r"""
+# ------------------------
+# Inventory: inventory.yml
+# ------------------------
+# all:
+#   children:
+#     wsl:
+#       hosts:
+#         ubuntu:
+#           ansible_host: 10.0.0.10
+#           wsl_distribution: ubuntu
+#         debian:
+#           ansible_host: 10.0.0.10
+#           wsl_distribution: debian
+#       vars:
+#         ansible_connection: community.general.wsl
+#         ansible_user: vagrant
+#         wsl_user: wsl
+# ----------------------
+# Playbook: playbook.yml
+# ----------------------
+---
+- hosts: wsl
+  tasks:
+    - name: ping
+      ansible.builtin.ping:
+    - name: id
+      args:
+        executable: /bin/bash
+      ansible.builtin.shell: |
+        exec 2>&1
+        set -x
+        echo "$0"
+        pwd
+        id
+    - name: become id
+      become: true
+      args:
+        executable: /bin/bash
+      ansible.builtin.shell: |
+        exec 2>&1
+        set -x
+        echo "$0"
+        pwd
+        id
+"""
+
+import os
+import pathlib
+import shlex
+import socket
+import tempfile
+import typing as t
+
+from ansible.errors import (
+    AnsibleAuthenticationFailure,
+    AnsibleConnectionFailure,
+    AnsibleError,
+)
+from ansible_collections.community.general.plugins.module_utils._filelock import FileLock, LockTimeout
+from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
+from ansible.module_utils.compat.paramiko import PARAMIKO_IMPORT_ERR, paramiko
+from ansible.module_utils.compat.version import LooseVersion
+from ansible.plugins.connection import ConnectionBase
+from ansible.utils.display import Display
+from ansible.utils.path import makedirs_safe
+from binascii import hexlify
+from subprocess import list2cmdline
+
+
+display = Display()
+
+
+def authenticity_msg(hostname: str, ktype: str, fingerprint: str) -> str:
+    msg = f"""
+    paramiko: The authenticity of host '{hostname}' can't be established.
+    The {ktype} key fingerprint is {fingerprint}.
+    Are you sure you want to continue connecting (yes/no)?
+    """
+    return msg
+
+
+MissingHostKeyPolicy: type = object
+if paramiko:
+    MissingHostKeyPolicy = paramiko.MissingHostKeyPolicy
+
+
+class MyAddPolicy(MissingHostKeyPolicy):
+    """
+    Based on AutoAddPolicy in paramiko so we can determine when keys are added
+
+    and also prompt for input.
+
+    Policy for automatically adding the hostname and new host key to the
+    local L{HostKeys} object, and saving it. This is used by L{SSHClient}.
+    """
+
+    def __init__(self, connection: Connection) -> None:
+        self.connection = connection
+        self._options = connection._options
+
+    def missing_host_key(self, client, hostname, key) -> None:
+
+        if all((self.connection.get_option('host_key_checking'), not self.connection.get_option('host_key_auto_add'))):
+
+            fingerprint = hexlify(key.get_fingerprint())
+            ktype = key.get_name()
+
+            if self.connection.get_option('use_persistent_connections') or self.connection.force_persistence:
+                # don't print the prompt string since the user cannot respond
+                # to the question anyway
+                raise AnsibleError(authenticity_msg(hostname, ktype, fingerprint)[1:92])
+
+            inp = to_text(
+                display.prompt_until(authenticity_msg(hostname, ktype, fingerprint), private=False),
+                errors='surrogate_or_strict'
+            )
+
+            if inp.lower() not in ['yes', 'y', '']:
+                raise AnsibleError('host connection rejected by user')
+
+        key._added_by_ansible_this_time = True
+
+        # existing implementation below:
+        client._host_keys.add(hostname, key.get_name(), key)
+
+        # host keys are actually saved in close() function below
+        # in order to control ordering.
+
+
+class Connection(ConnectionBase):
+    """ SSH based connections (paramiko) to WSL """
+
+    transport = 'community.general.wsl'
+    _log_channel: str | None = None
+
+    def __init__(self, play_context, new_stdin, *args, **kwargs):
+        super(Connection, self).__init__(play_context, new_stdin, *args, **kwargs)
+
+    def _set_log_channel(self, name: str) -> None:
+        """ Mimic paramiko.SSHClient.set_log_channel """
+        self._log_channel = name
+
+    def _parse_proxy_command(self, port: int = 22) -> dict[str, t.Any]:
+        proxy_command = self.get_option('proxy_command') or None
+
+        sock_kwarg = {}
+        if proxy_command:
+            replacers = {
+                '%h': self.get_option('remote_addr'),
+                '%p': port,
+                '%r': self.get_option('remote_user')
+            }
+            for find, replace in replacers.items():
+                proxy_command = proxy_command.replace(find, str(replace))
+            try:
+                sock_kwarg = {'sock': paramiko.ProxyCommand(proxy_command)}
+                display.vvv(f'CONFIGURE PROXY COMMAND FOR CONNECTION: {proxy_command}', host=self.get_option('remote_addr'))
+            except AttributeError:
+                display.warning('Paramiko ProxyCommand support unavailable. '
+                                'Please upgrade to Paramiko 1.9.0 or newer. '
+                                'Not using configured ProxyCommand')
+
+        return sock_kwarg
+
+    def _connect(self) -> Connection:
+        """ activates the connection object """
+
+        if paramiko is None:
+            raise AnsibleError(f'paramiko is not installed: {to_native(PARAMIKO_IMPORT_ERR)}')
+
+        port = self.get_option('port')
+        display.vvv(f'ESTABLISH PARAMIKO SSH CONNECTION FOR USER: {self.get_option("remote_user")} on PORT {to_text(port)} TO {self.get_option("remote_addr")}',
+                    host=self.get_option('remote_addr'))
+
+        ssh = paramiko.SSHClient()
+
+        # Set pubkey and hostkey algorithms to disable, the only manipulation allowed currently
+        # is keeping or omitting rsa-sha2 algorithms
+        # default_keys: t.Tuple[str] = ()
+        paramiko_preferred_pubkeys = getattr(paramiko.Transport, '_preferred_pubkeys', ())
+        paramiko_preferred_hostkeys = getattr(paramiko.Transport, '_preferred_keys', ())
+        use_rsa_sha2_algorithms = self.get_option('use_rsa_sha2_algorithms')
+        disabled_algorithms: t.Dict[str, t.Iterable[str]] = {}
+        if not use_rsa_sha2_algorithms:
+            if paramiko_preferred_pubkeys:
+                disabled_algorithms['pubkeys'] = tuple(a for a in paramiko_preferred_pubkeys if 'rsa-sha2' in a)
+            if paramiko_preferred_hostkeys:
+                disabled_algorithms['keys'] = tuple(a for a in paramiko_preferred_hostkeys if 'rsa-sha2' in a)
+
+        # override paramiko's default logger name
+        if self._log_channel is not None:
+            ssh.set_log_channel(self._log_channel)
+
+        self.keyfile = os.path.expanduser('~/.ssh/known_hosts')
+
+        if self.get_option('host_key_checking'):
+            for ssh_known_hosts in ('/etc/ssh/ssh_known_hosts', '/etc/openssh/ssh_known_hosts'):
+                try:
+                    ssh.load_system_host_keys(ssh_known_hosts)
+                    break
+                except IOError:
+                    pass  # file was not found, but not required to function
+                except paramiko.hostkeys.InvalidHostKey as e:
+                    raise AnsibleConnectionFailure(f'Invalid host key: {to_text(e.line)}')
+            try:
+                ssh.load_system_host_keys()
+            except paramiko.hostkeys.InvalidHostKey as e:
+                raise AnsibleConnectionFailure(f'Invalid host key: {to_text(e.line)}')
+
+        ssh_connect_kwargs = self._parse_proxy_command(port)
+        ssh.set_missing_host_key_policy(MyAddPolicy(self))
+        conn_password = self.get_option('password')
+        allow_agent = True
+
+        if conn_password is not None:
+            allow_agent = False
+
+        try:
+            key_filename = None
+            if self.get_option('private_key_file'):
+                key_filename = os.path.expanduser(self.get_option('private_key_file'))
+
+            # paramiko 2.2 introduced auth_timeout parameter
+            if LooseVersion(paramiko.__version__) >= LooseVersion('2.2.0'):
+                ssh_connect_kwargs['auth_timeout'] = self.get_option('timeout')
+
+            # paramiko 1.15 introduced banner timeout parameter
+            if LooseVersion(paramiko.__version__) >= LooseVersion('1.15.0'):
+                ssh_connect_kwargs['banner_timeout'] = self.get_option('banner_timeout')
+
+            ssh.connect(
+                self.get_option('remote_addr').lower(),
+                username=self.get_option('remote_user'),
+                allow_agent=allow_agent,
+                look_for_keys=self.get_option('look_for_keys'),
+                key_filename=key_filename,
+                password=conn_password,
+                timeout=self.get_option('timeout'),
+                port=port,
+                disabled_algorithms=disabled_algorithms,
+                **ssh_connect_kwargs,
+            )
+        except paramiko.ssh_exception.BadHostKeyException as e:
+            raise AnsibleConnectionFailure(f'host key mismatch for {to_text(e.hostname)}')
+        except paramiko.ssh_exception.AuthenticationException as e:
+            msg = f'Failed to authenticate: {e}'
+            raise AnsibleAuthenticationFailure(msg)
+        except Exception as e:
+            msg = to_text(e)
+            if u'PID check failed' in msg:
+                raise AnsibleError('paramiko version issue, please upgrade paramiko on the machine running ansible')
+            elif u'Private key file is encrypted' in msg:
+                msg = f'ssh {self.get_option("remote_user")}@{self.get_options("remote_addr")}:{port} : ' + \
+                    f'{msg}\nTo connect as a different user, use -u <username>.'
+                raise AnsibleConnectionFailure(msg)
+            else:
+                raise AnsibleConnectionFailure(msg)
+        self.ssh = ssh
+        self._connected = True
+        return self
+
+    def _any_keys_added(self) -> bool:
+        for hostname, keys in self.ssh._host_keys.items():
+            for keytype, key in keys.items():
+                added_this_time = getattr(key, '_added_by_ansible_this_time', False)
+                if added_this_time:
+                    return True
+        return False
+
+    def _save_ssh_host_keys(self, filename: str) -> None:
+        """
+        not using the paramiko save_ssh_host_keys function as we want to add new SSH keys at the bottom so folks
+        don't complain about it :)
+        """
+
+        if not self._any_keys_added():
+            return
+
+        path = os.path.expanduser('~/.ssh')
+        makedirs_safe(path)
+
+        with open(filename, 'w') as f:
+            for hostname, keys in self.ssh._host_keys.items():
+                for keytype, key in keys.items():
+                    # was f.write
+                    added_this_time = getattr(key, '_added_by_ansible_this_time', False)
+                    if not added_this_time:
+                        f.write(f'{hostname} {keytype} {key.get_base64()}\n')
+
+            for hostname, keys in self.ssh._host_keys.items():
+                for keytype, key in keys.items():
+                    added_this_time = getattr(key, '_added_by_ansible_this_time', False)
+                    if added_this_time:
+                        f.write(f'{hostname} {keytype} {key.get_base64()}\n')
+
+    def _build_wsl_command(self, cmd: str) -> str:
+        wsl_distribution = self.get_option('wsl_distribution')
+        if not wsl_distribution:
+            raise AnsibleError('A wsl_distribution is required but none was supplied')
+        become = self.get_option('become')
+        become_user = self.get_option('become_user')
+        if become and become_user:
+            wsl_user = become_user
+        else:
+            wsl_user = self.get_option('wsl_user')
+        args = ['wsl.exe', '--distribution', wsl_distribution]
+        if wsl_user:
+            args.extend(['--user', wsl_user])
+        args.extend(['--'])
+        args.extend(shlex.split(cmd))
+        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 """
+
+        cmd = self._build_wsl_command(cmd)
+
+        super(Connection, self).exec_command(cmd, in_data=in_data, sudoable=sudoable)
+
+        bufsize = 4096
+
+        try:
+            self.ssh.get_transport().set_keepalive(5)
+            chan = self.ssh.get_transport().open_session()
+        except Exception as e:
+            text_e = to_text(e)
+            msg = 'Failed to open session'
+            if text_e:
+                msg += f': {text_e}'
+            raise AnsibleConnectionFailure(to_native(msg))
+
+        display.vvv(f'EXEC {cmd}', host=self.get_option('remote_addr'))
+
+        cmd = to_bytes(cmd, errors='surrogate_or_strict')
+
+        no_prompt_out = b''
+        no_prompt_err = b''
+        become_output = b''
+
+        try:
+            chan.exec_command(cmd)
+            if self.become and self.become.expect_prompt():
+                password_prompt = False
+                become_success = False
+                while not (become_success or password_prompt):
+                    display.debug('Waiting for Privilege Escalation input')
+
+                    chunk = chan.recv(bufsize)
+                    display.debug(f'chunk is: {to_text(chunk)}')
+                    if not chunk:
+                        if b'unknown user' in become_output:
+                            n_become_user = to_native(self.become.get_option('become_user'))
+                            raise AnsibleError(f'user {n_become_user} does not exist')
+                        else:
+                            break
+                            # raise AnsibleError('ssh connection closed waiting for password prompt')
+                    become_output += chunk
+
+                    # need to check every line because we might get lectured
+                    # and we might get the middle of a line in a chunk
+                    for line in become_output.splitlines(True):
+                        if self.become.check_success(line):
+                            become_success = True
+                            break
+                        elif self.become.check_password_prompt(line):
+                            password_prompt = True
+                            break
+
+                if password_prompt:
+                    if self.become:
+                        become_pass = self.become.get_option('become_pass')
+                        chan.sendall(to_bytes(become_pass, errors='surrogate_or_strict') + b'\n')
+                    else:
+                        raise AnsibleError('A password is required but none was supplied')
+                else:
+                    no_prompt_out += become_output
+                    no_prompt_err += become_output
+
+            if in_data:
+                for i in range(0, len(in_data), bufsize):
+                    chan.send(in_data[i:i + bufsize])
+                chan.shutdown_write()
+            elif in_data == b'':
+                chan.shutdown_write()
+
+        except socket.timeout:
+            raise AnsibleError('ssh timed out waiting for privilege escalation.\n' + to_text(become_output))
+
+        stdout = b''.join(chan.makefile('rb', bufsize))
+        stderr = b''.join(chan.makefile_stderr('rb', bufsize))
+        returncode = chan.recv_exit_status()
+
+        # NB the full english error message is:
path: plugins/connection/wsl.py
user: login: rgl
body: it probably is subject to localization settings, thou, not sure how to handle it, please advise.
created_at: 2025-03-05T18:59:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,758 @@
+# -*- coding: utf-8 -*-
+# Derived from ansible/plugins/connection/proxmox_pct_remote.py (c) 2024 Nils Stein (@mietzen) <github.nstein@mailbox.org>
+# Derived from ansible/plugins/connection/paramiko_ssh.py (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
+# Copyright (c) 2025 Rui Lopes (@rgl) <ruilopes.com>
+# Copyright (c) 2025 Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import annotations
+
+DOCUMENTATION = r"""
+author: Rui Lopes (@rgl) <ruilopes.com>
+name: wsl
+short_description: Run tasks in WSL distribution using wsl.exe CLI via SSH
+requirements:
+  - paramiko
+description:
+  - Run commands or put/fetch files to an existing WSL distribution using wsl.exe CLI via SSH.
+  - Uses the Python SSH implementation (Paramiko) to connect to the WSL host.
+version_added: "10.3.0"
+options:
+  remote_addr:
+    description:
+      - Address of the remote target.
+    default: inventory_hostname
+    type: string
+    vars:
+      - name: inventory_hostname
+      - name: ansible_host
+      - name: ansible_ssh_host
+      - name: ansible_paramiko_host
+  port:
+    description: Remote port to connect to.
+    type: int
+    default: 22
+    ini:
+      - section: defaults
+        key: remote_port
+      - section: paramiko_connection
+        key: remote_port
+    env:
+      - name: ANSIBLE_REMOTE_PORT
+      - name: ANSIBLE_REMOTE_PARAMIKO_PORT
+    vars:
+      - name: ansible_port
+      - name: ansible_ssh_port
+      - name: ansible_paramiko_port
+    keyword:
+      - name: port
+  remote_user:
+    description:
+      - User to login/authenticate as.
+      - Can be set from the CLI via the C(--user) or C(-u) options.
+    type: string
+    vars:
+      - name: ansible_user
+      - name: ansible_ssh_user
+      - name: ansible_paramiko_user
+    env:
+      - name: ANSIBLE_REMOTE_USER
+      - name: ANSIBLE_PARAMIKO_REMOTE_USER
+    ini:
+      - section: defaults
+        key: remote_user
+      - section: paramiko_connection
+        key: remote_user
+    keyword:
+      - name: remote_user
+  password:
+    description:
+      - Secret used to either login the SSH server or as a passphrase for SSH keys that require it.
+      - Can be set from the CLI via the C(--ask-pass) option.
+    type: string
+    vars:
+      - name: ansible_password
+      - name: ansible_ssh_pass
+      - name: ansible_ssh_password
+      - name: ansible_paramiko_pass
+      - name: ansible_paramiko_password
+  use_rsa_sha2_algorithms:
+    description:
+      - Whether or not to enable RSA SHA2 algorithms for pubkeys and hostkeys.
+      - On paramiko versions older than 2.9, this only affects hostkeys.
+      - For behavior matching paramiko<2.9 set this to V(false).
+    vars:
+      - name: ansible_paramiko_use_rsa_sha2_algorithms
+    ini:
+      - {key: use_rsa_sha2_algorithms, section: paramiko_connection}
+    env:
+      - {name: ANSIBLE_PARAMIKO_USE_RSA_SHA2_ALGORITHMS}
+    default: true
+    type: boolean
+  host_key_auto_add:
+    description: "Automatically add host keys to C(~/.ssh/known_hosts)."
+    env:
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_AUTO_ADD
+    ini:
+      - key: host_key_auto_add
+        section: paramiko_connection
+    type: boolean
+  look_for_keys:
+    default: True
+    description: "Set to V(false) to disable searching for private key files in C(~/.ssh/)."
+    env:
+      - name: ANSIBLE_PARAMIKO_LOOK_FOR_KEYS
+    ini:
+      - {key: look_for_keys, section: paramiko_connection}
+    type: boolean
+  proxy_command:
+    default: ""
+    description:
+      - Proxy information for running the connection via a jumphost.
+    type: string
+    env:
+      - name: ANSIBLE_PARAMIKO_PROXY_COMMAND
+    ini:
+      - {key: proxy_command, section: paramiko_connection}
+    vars:
+      - name: ansible_paramiko_proxy_command
+  record_host_keys:
+    default: True
+    description: "Save the host keys to a file."
+    env:
+      - name: ANSIBLE_PARAMIKO_RECORD_HOST_KEYS
+    ini:
+      - section: paramiko_connection
+        key: record_host_keys
+    type: boolean
+  host_key_checking:
+    description: "Set this to V(false) if you want to avoid host key checking by the underlying tools Ansible uses to connect to the host."
+    type: boolean
+    default: true
+    env:
+      - name: ANSIBLE_HOST_KEY_CHECKING
+      - name: ANSIBLE_SSH_HOST_KEY_CHECKING
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_CHECKING
+    ini:
+      - section: defaults
+        key: host_key_checking
+      - section: paramiko_connection
+        key: host_key_checking
+    vars:
+      - name: ansible_host_key_checking
+      - name: ansible_ssh_host_key_checking
+      - name: ansible_paramiko_host_key_checking
+  use_persistent_connections:
+    description: "Toggles the use of persistence for connections."
+    type: boolean
+    default: False
+    env:
+      - name: ANSIBLE_USE_PERSISTENT_CONNECTIONS
+    ini:
+      - section: defaults
+        key: use_persistent_connections
+  banner_timeout:
+    type: float
+    default: 30
+    description:
+      - Configures, in seconds, the amount of time to wait for the SSH
+        banner to be presented. This option is supported by paramiko
+        version 1.15.0 or newer.
+    ini:
+      - section: paramiko_connection
+        key: banner_timeout
+    env:
+      - name: ANSIBLE_PARAMIKO_BANNER_TIMEOUT
+  timeout:
+    type: int
+    default: 10
+    description: Number of seconds until the plugin gives up on failing to establish a TCP connection.
+    ini:
+      - section: defaults
+        key: timeout
+      - section: ssh_connection
+        key: timeout
+      - section: paramiko_connection
+        key: timeout
+    env:
+      - name: ANSIBLE_TIMEOUT
+      - name: ANSIBLE_SSH_TIMEOUT
+      - name: ANSIBLE_PARAMIKO_TIMEOUT
+    vars:
+      - name: ansible_ssh_timeout
+      - name: ansible_paramiko_timeout
+    cli:
+      - name: timeout
+  lock_file_timeout:
+    type: int
+    default: 60
+    description: Number of seconds until the plugin gives up on trying to write a lock file when writing SSH known host keys.
+    vars:
+      - name: ansible_lock_file_timeout
+    env:
+      - name: ANSIBLE_LOCK_FILE_TIMEOUT
+  private_key_file:
+    description:
+      - Path to private key file to use for authentication.
+    type: string
+    ini:
+      - section: defaults
+        key: private_key_file
+      - section: paramiko_connection
+        key: private_key_file
+    env:
+      - name: ANSIBLE_PRIVATE_KEY_FILE
+      - name: ANSIBLE_PARAMIKO_PRIVATE_KEY_FILE
+    vars:
+      - name: ansible_private_key_file
+      - name: ansible_ssh_private_key_file
+      - name: ansible_paramiko_private_key_file
+    cli:
+      - name: private_key_file
+        option: "--private-key"
+  wsl_distribution:
+    description:
+      - WSL distribution name
+    type: string
+    vars:
+      - name: wsl_distribution
+  wsl_user:
+    description:
+      - WSL distribution user
+    type: string
+    vars:
+      - name: wsl_user
+  become_user:
+    description:
+      - WSL distribution user
+    type: string
+    default: root
+    vars:
+      - name: become_user
+      - name: ansible_become_user
+  become:
+    description:
+      - whether to use the user defined by ansible_become_user.
+    type: bool
+    default: false
+    vars:
+      - name: become
+      - name: ansible_become
+"""
+
+EXAMPLES = r"""
+# ------------------------
+# Inventory: inventory.yml
+# ------------------------
+# all:
+#   children:
+#     wsl:
+#       hosts:
+#         ubuntu:
+#           ansible_host: 10.0.0.10
+#           wsl_distribution: ubuntu
+#         debian:
+#           ansible_host: 10.0.0.10
+#           wsl_distribution: debian
+#       vars:
+#         ansible_connection: community.general.wsl
+#         ansible_user: vagrant
+#         wsl_user: wsl
+# ----------------------
+# Playbook: playbook.yml
+# ----------------------
+---
+- hosts: wsl
+  tasks:
+    - name: ping
+      ansible.builtin.ping:
+    - name: id
+      args:
+        executable: /bin/bash
+      ansible.builtin.shell: |
+        exec 2>&1
+        set -x
+        echo "$0"
+        pwd
+        id
+    - name: become id
+      become: true
+      args:
+        executable: /bin/bash
+      ansible.builtin.shell: |
+        exec 2>&1
+        set -x
+        echo "$0"
+        pwd
+        id
+"""
+
+import os
+import pathlib
+import shlex
+import socket
+import tempfile
+import typing as t
+
+from ansible.errors import (
+    AnsibleAuthenticationFailure,
+    AnsibleConnectionFailure,
+    AnsibleError,
+)
+from ansible_collections.community.general.plugins.module_utils._filelock import FileLock, LockTimeout
+from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
+from ansible.module_utils.compat.paramiko import PARAMIKO_IMPORT_ERR, paramiko
+from ansible.module_utils.compat.version import LooseVersion
+from ansible.plugins.connection import ConnectionBase
+from ansible.utils.display import Display
+from ansible.utils.path import makedirs_safe
+from binascii import hexlify
+from subprocess import list2cmdline
+
+
+display = Display()
+
+
+def authenticity_msg(hostname: str, ktype: str, fingerprint: str) -> str:
+    msg = f"""
+    paramiko: The authenticity of host '{hostname}' can't be established.
+    The {ktype} key fingerprint is {fingerprint}.
+    Are you sure you want to continue connecting (yes/no)?
+    """
+    return msg
+
+
+MissingHostKeyPolicy: type = object
+if paramiko:
+    MissingHostKeyPolicy = paramiko.MissingHostKeyPolicy
+
+
+class MyAddPolicy(MissingHostKeyPolicy):
+    """
+    Based on AutoAddPolicy in paramiko so we can determine when keys are added
+
+    and also prompt for input.
+
+    Policy for automatically adding the hostname and new host key to the
+    local L{HostKeys} object, and saving it. This is used by L{SSHClient}.
+    """
+
+    def __init__(self, connection: Connection) -> None:
+        self.connection = connection
+        self._options = connection._options
+
+    def missing_host_key(self, client, hostname, key) -> None:
+
+        if all((self.connection.get_option('host_key_checking'), not self.connection.get_option('host_key_auto_add'))):
+
+            fingerprint = hexlify(key.get_fingerprint())
+            ktype = key.get_name()
+
+            if self.connection.get_option('use_persistent_connections') or self.connection.force_persistence:
+                # don't print the prompt string since the user cannot respond
+                # to the question anyway
+                raise AnsibleError(authenticity_msg(hostname, ktype, fingerprint)[1:92])
+
+            inp = to_text(
+                display.prompt_until(authenticity_msg(hostname, ktype, fingerprint), private=False),
+                errors='surrogate_or_strict'
+            )
+
+            if inp.lower() not in ['yes', 'y', '']:
+                raise AnsibleError('host connection rejected by user')
+
+        key._added_by_ansible_this_time = True
+
+        # existing implementation below:
+        client._host_keys.add(hostname, key.get_name(), key)
+
+        # host keys are actually saved in close() function below
+        # in order to control ordering.
+
+
+class Connection(ConnectionBase):
+    """ SSH based connections (paramiko) to WSL """
+
+    transport = 'community.general.wsl'
+    _log_channel: str | None = None
+
+    def __init__(self, play_context, new_stdin, *args, **kwargs):
+        super(Connection, self).__init__(play_context, new_stdin, *args, **kwargs)
+
+    def _set_log_channel(self, name: str) -> None:
+        """ Mimic paramiko.SSHClient.set_log_channel """
+        self._log_channel = name
+
+    def _parse_proxy_command(self, port: int = 22) -> dict[str, t.Any]:
+        proxy_command = self.get_option('proxy_command') or None
+
+        sock_kwarg = {}
+        if proxy_command:
+            replacers = {
+                '%h': self.get_option('remote_addr'),
+                '%p': port,
+                '%r': self.get_option('remote_user')
+            }
+            for find, replace in replacers.items():
+                proxy_command = proxy_command.replace(find, str(replace))
+            try:
+                sock_kwarg = {'sock': paramiko.ProxyCommand(proxy_command)}
+                display.vvv(f'CONFIGURE PROXY COMMAND FOR CONNECTION: {proxy_command}', host=self.get_option('remote_addr'))
+            except AttributeError:
+                display.warning('Paramiko ProxyCommand support unavailable. '
+                                'Please upgrade to Paramiko 1.9.0 or newer. '
+                                'Not using configured ProxyCommand')
+
+        return sock_kwarg
+
+    def _connect(self) -> Connection:
+        """ activates the connection object """
+
+        if paramiko is None:
+            raise AnsibleError(f'paramiko is not installed: {to_native(PARAMIKO_IMPORT_ERR)}')
+
+        port = self.get_option('port')
+        display.vvv(f'ESTABLISH PARAMIKO SSH CONNECTION FOR USER: {self.get_option("remote_user")} on PORT {to_text(port)} TO {self.get_option("remote_addr")}',
+                    host=self.get_option('remote_addr'))
+
+        ssh = paramiko.SSHClient()
+
+        # Set pubkey and hostkey algorithms to disable, the only manipulation allowed currently
+        # is keeping or omitting rsa-sha2 algorithms
+        # default_keys: t.Tuple[str] = ()
+        paramiko_preferred_pubkeys = getattr(paramiko.Transport, '_preferred_pubkeys', ())
+        paramiko_preferred_hostkeys = getattr(paramiko.Transport, '_preferred_keys', ())
+        use_rsa_sha2_algorithms = self.get_option('use_rsa_sha2_algorithms')
+        disabled_algorithms: t.Dict[str, t.Iterable[str]] = {}
+        if not use_rsa_sha2_algorithms:
+            if paramiko_preferred_pubkeys:
+                disabled_algorithms['pubkeys'] = tuple(a for a in paramiko_preferred_pubkeys if 'rsa-sha2' in a)
+            if paramiko_preferred_hostkeys:
+                disabled_algorithms['keys'] = tuple(a for a in paramiko_preferred_hostkeys if 'rsa-sha2' in a)
+
+        # override paramiko's default logger name
+        if self._log_channel is not None:
+            ssh.set_log_channel(self._log_channel)
+
+        self.keyfile = os.path.expanduser('~/.ssh/known_hosts')
+
+        if self.get_option('host_key_checking'):
+            for ssh_known_hosts in ('/etc/ssh/ssh_known_hosts', '/etc/openssh/ssh_known_hosts'):
+                try:
+                    ssh.load_system_host_keys(ssh_known_hosts)
+                    break
+                except IOError:
+                    pass  # file was not found, but not required to function
+                except paramiko.hostkeys.InvalidHostKey as e:
+                    raise AnsibleConnectionFailure(f'Invalid host key: {to_text(e.line)}')
+            try:
+                ssh.load_system_host_keys()
+            except paramiko.hostkeys.InvalidHostKey as e:
+                raise AnsibleConnectionFailure(f'Invalid host key: {to_text(e.line)}')
+
+        ssh_connect_kwargs = self._parse_proxy_command(port)
+        ssh.set_missing_host_key_policy(MyAddPolicy(self))
+        conn_password = self.get_option('password')
+        allow_agent = True
+
+        if conn_password is not None:
+            allow_agent = False
+
+        try:
+            key_filename = None
+            if self.get_option('private_key_file'):
+                key_filename = os.path.expanduser(self.get_option('private_key_file'))
+
+            # paramiko 2.2 introduced auth_timeout parameter
+            if LooseVersion(paramiko.__version__) >= LooseVersion('2.2.0'):
+                ssh_connect_kwargs['auth_timeout'] = self.get_option('timeout')
+
+            # paramiko 1.15 introduced banner timeout parameter
+            if LooseVersion(paramiko.__version__) >= LooseVersion('1.15.0'):
+                ssh_connect_kwargs['banner_timeout'] = self.get_option('banner_timeout')
+
+            ssh.connect(
+                self.get_option('remote_addr').lower(),
+                username=self.get_option('remote_user'),
+                allow_agent=allow_agent,
+                look_for_keys=self.get_option('look_for_keys'),
+                key_filename=key_filename,
+                password=conn_password,
+                timeout=self.get_option('timeout'),
+                port=port,
+                disabled_algorithms=disabled_algorithms,
+                **ssh_connect_kwargs,
+            )
+        except paramiko.ssh_exception.BadHostKeyException as e:
+            raise AnsibleConnectionFailure(f'host key mismatch for {to_text(e.hostname)}')
+        except paramiko.ssh_exception.AuthenticationException as e:
+            msg = f'Failed to authenticate: {e}'
+            raise AnsibleAuthenticationFailure(msg)
+        except Exception as e:
+            msg = to_text(e)
+            if u'PID check failed' in msg:
+                raise AnsibleError('paramiko version issue, please upgrade paramiko on the machine running ansible')
+            elif u'Private key file is encrypted' in msg:
+                msg = f'ssh {self.get_option("remote_user")}@{self.get_options("remote_addr")}:{port} : ' + \
+                    f'{msg}\nTo connect as a different user, use -u <username>.'
+                raise AnsibleConnectionFailure(msg)
+            else:
+                raise AnsibleConnectionFailure(msg)
+        self.ssh = ssh
+        self._connected = True
+        return self
+
+    def _any_keys_added(self) -> bool:
+        for hostname, keys in self.ssh._host_keys.items():
+            for keytype, key in keys.items():
+                added_this_time = getattr(key, '_added_by_ansible_this_time', False)
+                if added_this_time:
+                    return True
+        return False
+
+    def _save_ssh_host_keys(self, filename: str) -> None:
+        """
+        not using the paramiko save_ssh_host_keys function as we want to add new SSH keys at the bottom so folks
+        don't complain about it :)
+        """
+
+        if not self._any_keys_added():
+            return
+
+        path = os.path.expanduser('~/.ssh')
+        makedirs_safe(path)
+
+        with open(filename, 'w') as f:
+            for hostname, keys in self.ssh._host_keys.items():
+                for keytype, key in keys.items():
+                    # was f.write
+                    added_this_time = getattr(key, '_added_by_ansible_this_time', False)
+                    if not added_this_time:
+                        f.write(f'{hostname} {keytype} {key.get_base64()}\n')
+
+            for hostname, keys in self.ssh._host_keys.items():
+                for keytype, key in keys.items():
+                    added_this_time = getattr(key, '_added_by_ansible_this_time', False)
+                    if added_this_time:
+                        f.write(f'{hostname} {keytype} {key.get_base64()}\n')
+
+    def _build_wsl_command(self, cmd: str) -> str:
+        wsl_distribution = self.get_option('wsl_distribution')
+        if not wsl_distribution:
+            raise AnsibleError('A wsl_distribution is required but none was supplied')
+        become = self.get_option('become')
+        become_user = self.get_option('become_user')
+        if become and become_user:
+            wsl_user = become_user
+        else:
+            wsl_user = self.get_option('wsl_user')
+        args = ['wsl.exe', '--distribution', wsl_distribution]
+        if wsl_user:
+            args.extend(['--user', wsl_user])
+        args.extend(['--'])
+        args.extend(shlex.split(cmd))
+        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 """
+
+        cmd = self._build_wsl_command(cmd)
+
+        super(Connection, self).exec_command(cmd, in_data=in_data, sudoable=sudoable)
+
+        bufsize = 4096
+
+        try:
+            self.ssh.get_transport().set_keepalive(5)
+            chan = self.ssh.get_transport().open_session()
+        except Exception as e:
+            text_e = to_text(e)
+            msg = 'Failed to open session'
+            if text_e:
+                msg += f': {text_e}'
+            raise AnsibleConnectionFailure(to_native(msg))
+
+        display.vvv(f'EXEC {cmd}', host=self.get_option('remote_addr'))
+
+        cmd = to_bytes(cmd, errors='surrogate_or_strict')
+
+        no_prompt_out = b''
+        no_prompt_err = b''
+        become_output = b''
+
+        try:
+            chan.exec_command(cmd)
+            if self.become and self.become.expect_prompt():
+                password_prompt = False
+                become_success = False
+                while not (become_success or password_prompt):
+                    display.debug('Waiting for Privilege Escalation input')
+
+                    chunk = chan.recv(bufsize)
+                    display.debug(f'chunk is: {to_text(chunk)}')
+                    if not chunk:
+                        if b'unknown user' in become_output:
+                            n_become_user = to_native(self.become.get_option('become_user'))
+                            raise AnsibleError(f'user {n_become_user} does not exist')
+                        else:
+                            break
+                            # raise AnsibleError('ssh connection closed waiting for password prompt')
+                    become_output += chunk
+
+                    # need to check every line because we might get lectured
+                    # and we might get the middle of a line in a chunk
+                    for line in become_output.splitlines(True):
+                        if self.become.check_success(line):
+                            become_success = True
+                            break
+                        elif self.become.check_password_prompt(line):
+                            password_prompt = True
+                            break
+
+                if password_prompt:
+                    if self.become:
+                        become_pass = self.become.get_option('become_pass')
+                        chan.sendall(to_bytes(become_pass, errors='surrogate_or_strict') + b'\n')
+                    else:
+                        raise AnsibleError('A password is required but none was supplied')
+                else:
+                    no_prompt_out += become_output
+                    no_prompt_err += become_output
+
+            if in_data:
+                for i in range(0, len(in_data), bufsize):
+                    chan.send(in_data[i:i + bufsize])
+                chan.shutdown_write()
+            elif in_data == b'':
+                chan.shutdown_write()
+
+        except socket.timeout:
+            raise AnsibleError('ssh timed out waiting for privilege escalation.\n' + to_text(become_output))
+
+        stdout = b''.join(chan.makefile('rb', bufsize))
+        stderr = b''.join(chan.makefile_stderr('rb', bufsize))
+        returncode = chan.recv_exit_status()
+
+        # NB the full english error message is:
+        #     'wsl.exe' is not recognized as an internal or external command,
+        #     operable program or batch file.
+        if "'wsl.exe' is not recognized" in stderr.decode('utf-8'):
+            raise AnsibleError(
+                f'wsl.exe not found in path of host: {to_text(self.get_option("remote_addr"))}')
+
+        return (returncode, no_prompt_out + stdout, no_prompt_out + stderr)
+
+    def put_file(self, in_path: str, out_path: str) -> None:
+        """ transfer a file from local to remote """
+
+        display.vvv(f'PUT {in_path} TO {out_path}', host=self.get_option('remote_addr'))
+        try:
+            with open(in_path, 'rb') as f:
+                data = f.read()
+                returncode, stdout, stderr = self.exec_command(
+                    ' '.join([
+                        self._shell.executable, '-c',
+                        self._shell.quote(f'cat > {out_path}')]),
+                    in_data=data,
+                    sudoable=False)
+            if returncode != 0:
+                if 'cat: not found' in stderr.decode('utf-8'):
+                    raise AnsibleError(
+                        f'cat not found in path of WSL distribution: {to_text(self.get_option("wsl_distribution"))}')
+                raise AnsibleError(
+                    f'{to_text(stdout)}\n{to_text(stderr)}')
+        except Exception as e:
+            raise AnsibleError(
+                f'error occurred while putting file from {in_path} to {out_path}!\n{to_text(e)}')
+
+    def fetch_file(self, in_path: str, out_path: str) -> None:
+        """ save a remote file to the specified path """
+
+        display.vvv(f'FETCH {in_path} TO {out_path}', host=self.get_option('remote_addr'))
+        try:
+            returncode, stdout, stderr = self.exec_command(
+                ' '.join([
+                    self._shell.executable, '-c',
+                    self._shell.quote(f'cat {in_path}')]),
+                sudoable=False)
+            if returncode != 0:
+                if 'cat: not found' in stderr.decode('utf-8'):
+                    raise AnsibleError(
+                        f'cat not found in path of WSL distribution: {to_text(self.get_option("wsl_distribution"))}')
+                raise AnsibleError(
+                    f'{to_text(stdout)}\n{to_text(stderr)}')
+            with open(out_path, 'wb') as f:
+                f.write(stdout)
+        except Exception as e:
+            raise AnsibleError(
+                f'error occurred while fetching file from {in_path} to {out_path}!\n{to_text(e)}')
+
+    def reset(self) -> None:
+        """ reset the connection """
+
+        if not self._connected:
+            return
+        self.close()
+        self._connect()
+
+    def close(self) -> None:
+        """ terminate the connection """
+
+        if self.get_option('host_key_checking') and self.get_option('record_host_keys') and self._any_keys_added():
+            # add any new SSH host keys -- warning -- this could be slow
+            # (This doesn't acquire the connection lock because it needs
+            # to exclude only other known_hosts writers, not connections
+            # that are starting up.)
+            lockfile = os.path.basename(self.keyfile)
+            dirname = os.path.dirname(self.keyfile)
+            makedirs_safe(dirname)
+            tmp_keyfile_name = None
+            try:
+                with FileLock().lock_file(lockfile, dirname, self.get_option('lock_file_timeout')):
+                    # just in case any were added recently
+
+                    self.ssh.load_system_host_keys()
+                    self.ssh._host_keys.update(self.ssh._system_host_keys)
+
+                    # gather information about the current key file, so
+                    # we can ensure the new file has the correct mode/owner
+
+                    key_dir = os.path.dirname(self.keyfile)
+                    if os.path.exists(self.keyfile):
+                        key_stat = os.stat(self.keyfile)
+                        mode = key_stat.st_mode & 0o777
+                        uid = key_stat.st_uid
+                        gid = key_stat.st_gid
+                    else:
+                        mode = 0o644
+                        uid = os.getuid()
+                        gid = os.getgid()
+
+                    # Save the new keys to a temporary file and move it into place
+                    # rather than rewriting the file. We set delete=False because
+                    # the file will be moved into place rather than cleaned up.
+
+                    with tempfile.NamedTemporaryFile(dir=key_dir, delete=False) as tmp_keyfile:
+                        tmp_keyfile_name = tmp_keyfile.name
+                        os.chmod(tmp_keyfile_name, mode)
+                        os.chown(tmp_keyfile_name, uid, gid)
+                        self._save_ssh_host_keys(tmp_keyfile_name)
+
+                    os.rename(tmp_keyfile_name, self.keyfile)
path: plugins/connection/wsl.py
user: login: rgl
body: It seems, this loads the entire known hosts file, adds the inventory hosts, then saves it to disk. Something that needs to be done in a single transaction, that is, its there to prevent multiple concurrent executions of this section of the code, which seems fine. Tho, I have no idea weather the same procedure is followed by other apps (e.g. by the `ssh` command), so it might not protect against those modifying the known hosts file at the same time.

Indeed, at a first glance, the last part, seems to implement something similar to `AnsibleModule.atomic_move`.

This was introduced by @mietzen in https://github.com/ansible-collections/community.general/pull/8424.

@mietzen, maybe you can tell us more about this?
created_at: 2025-03-05T19:01:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,758 @@
+# -*- coding: utf-8 -*-
+# Derived from ansible/plugins/connection/proxmox_pct_remote.py (c) 2024 Nils Stein (@mietzen) <github.nstein@mailbox.org>
+# Derived from ansible/plugins/connection/paramiko_ssh.py (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
+# Copyright (c) 2025 Rui Lopes (@rgl) <ruilopes.com>
+# Copyright (c) 2025 Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import annotations
+
+DOCUMENTATION = r"""
+author: Rui Lopes (@rgl) <ruilopes.com>
+name: wsl
+short_description: Run tasks in WSL distribution using wsl.exe CLI via SSH
+requirements:
+  - paramiko
+description:
+  - Run commands or put/fetch files to an existing WSL distribution using wsl.exe CLI via SSH.
+  - Uses the Python SSH implementation (Paramiko) to connect to the WSL host.
+version_added: "10.3.0"
+options:
+  remote_addr:
+    description:
+      - Address of the remote target.
+    default: inventory_hostname
+    type: string
+    vars:
+      - name: inventory_hostname
+      - name: ansible_host
+      - name: ansible_ssh_host
+      - name: ansible_paramiko_host
+  port:
+    description: Remote port to connect to.
+    type: int
+    default: 22
+    ini:
+      - section: defaults
+        key: remote_port
+      - section: paramiko_connection
+        key: remote_port
+    env:
+      - name: ANSIBLE_REMOTE_PORT
+      - name: ANSIBLE_REMOTE_PARAMIKO_PORT
+    vars:
+      - name: ansible_port
+      - name: ansible_ssh_port
+      - name: ansible_paramiko_port
+    keyword:
+      - name: port
+  remote_user:
+    description:
+      - User to login/authenticate as.
+      - Can be set from the CLI via the C(--user) or C(-u) options.
+    type: string
+    vars:
+      - name: ansible_user
+      - name: ansible_ssh_user
+      - name: ansible_paramiko_user
+    env:
+      - name: ANSIBLE_REMOTE_USER
+      - name: ANSIBLE_PARAMIKO_REMOTE_USER
+    ini:
+      - section: defaults
+        key: remote_user
+      - section: paramiko_connection
+        key: remote_user
+    keyword:
+      - name: remote_user
+  password:
+    description:
+      - Secret used to either login the SSH server or as a passphrase for SSH keys that require it.
+      - Can be set from the CLI via the C(--ask-pass) option.
+    type: string
+    vars:
+      - name: ansible_password
+      - name: ansible_ssh_pass
+      - name: ansible_ssh_password
+      - name: ansible_paramiko_pass
+      - name: ansible_paramiko_password
+  use_rsa_sha2_algorithms:
+    description:
+      - Whether or not to enable RSA SHA2 algorithms for pubkeys and hostkeys.
+      - On paramiko versions older than 2.9, this only affects hostkeys.
+      - For behavior matching paramiko<2.9 set this to V(false).
+    vars:
+      - name: ansible_paramiko_use_rsa_sha2_algorithms
+    ini:
+      - {key: use_rsa_sha2_algorithms, section: paramiko_connection}
+    env:
+      - {name: ANSIBLE_PARAMIKO_USE_RSA_SHA2_ALGORITHMS}
+    default: true
+    type: boolean
+  host_key_auto_add:
+    description: "Automatically add host keys to C(~/.ssh/known_hosts)."
+    env:
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_AUTO_ADD
+    ini:
+      - key: host_key_auto_add
+        section: paramiko_connection
+    type: boolean
+  look_for_keys:
+    default: True
+    description: "Set to V(false) to disable searching for private key files in C(~/.ssh/)."
+    env:
+      - name: ANSIBLE_PARAMIKO_LOOK_FOR_KEYS
+    ini:
+      - {key: look_for_keys, section: paramiko_connection}
+    type: boolean
+  proxy_command:
+    default: ""
+    description:
+      - Proxy information for running the connection via a jumphost.
+    type: string
+    env:
+      - name: ANSIBLE_PARAMIKO_PROXY_COMMAND
+    ini:
+      - {key: proxy_command, section: paramiko_connection}
+    vars:
+      - name: ansible_paramiko_proxy_command
+  record_host_keys:
+    default: True
+    description: "Save the host keys to a file."
+    env:
+      - name: ANSIBLE_PARAMIKO_RECORD_HOST_KEYS
+    ini:
+      - section: paramiko_connection
+        key: record_host_keys
+    type: boolean
+  host_key_checking:
+    description: "Set this to V(false) if you want to avoid host key checking by the underlying tools Ansible uses to connect to the host."
+    type: boolean
+    default: true
+    env:
+      - name: ANSIBLE_HOST_KEY_CHECKING
+      - name: ANSIBLE_SSH_HOST_KEY_CHECKING
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_CHECKING
+    ini:
+      - section: defaults
+        key: host_key_checking
+      - section: paramiko_connection
+        key: host_key_checking
+    vars:
+      - name: ansible_host_key_checking
+      - name: ansible_ssh_host_key_checking
+      - name: ansible_paramiko_host_key_checking
+  use_persistent_connections:
+    description: "Toggles the use of persistence for connections."
+    type: boolean
+    default: False
+    env:
+      - name: ANSIBLE_USE_PERSISTENT_CONNECTIONS
+    ini:
+      - section: defaults
+        key: use_persistent_connections
+  banner_timeout:
+    type: float
+    default: 30
+    description:
+      - Configures, in seconds, the amount of time to wait for the SSH
+        banner to be presented. This option is supported by paramiko
+        version 1.15.0 or newer.
+    ini:
+      - section: paramiko_connection
+        key: banner_timeout
+    env:
+      - name: ANSIBLE_PARAMIKO_BANNER_TIMEOUT
+  timeout:
+    type: int
+    default: 10
+    description: Number of seconds until the plugin gives up on failing to establish a TCP connection.
+    ini:
+      - section: defaults
+        key: timeout
+      - section: ssh_connection
+        key: timeout
+      - section: paramiko_connection
+        key: timeout
+    env:
+      - name: ANSIBLE_TIMEOUT
+      - name: ANSIBLE_SSH_TIMEOUT
+      - name: ANSIBLE_PARAMIKO_TIMEOUT
+    vars:
+      - name: ansible_ssh_timeout
+      - name: ansible_paramiko_timeout
+    cli:
+      - name: timeout
+  lock_file_timeout:
+    type: int
+    default: 60
+    description: Number of seconds until the plugin gives up on trying to write a lock file when writing SSH known host keys.
+    vars:
+      - name: ansible_lock_file_timeout
+    env:
+      - name: ANSIBLE_LOCK_FILE_TIMEOUT
+  private_key_file:
+    description:
+      - Path to private key file to use for authentication.
+    type: string
+    ini:
+      - section: defaults
+        key: private_key_file
+      - section: paramiko_connection
+        key: private_key_file
+    env:
+      - name: ANSIBLE_PRIVATE_KEY_FILE
+      - name: ANSIBLE_PARAMIKO_PRIVATE_KEY_FILE
+    vars:
+      - name: ansible_private_key_file
+      - name: ansible_ssh_private_key_file
+      - name: ansible_paramiko_private_key_file
+    cli:
+      - name: private_key_file
+        option: "--private-key"
+  wsl_distribution:
+    description:
+      - WSL distribution name
+    type: string
+    vars:
+      - name: wsl_distribution
+  wsl_user:
+    description:
+      - WSL distribution user
+    type: string
+    vars:
+      - name: wsl_user
+  become_user:
+    description:
+      - WSL distribution user
+    type: string
+    default: root
+    vars:
+      - name: become_user
+      - name: ansible_become_user
+  become:
+    description:
+      - whether to use the user defined by ansible_become_user.
+    type: bool
+    default: false
+    vars:
+      - name: become
+      - name: ansible_become
+"""
+
+EXAMPLES = r"""
+# ------------------------
+# Inventory: inventory.yml
+# ------------------------
+# all:
+#   children:
+#     wsl:
+#       hosts:
+#         ubuntu:
+#           ansible_host: 10.0.0.10
+#           wsl_distribution: ubuntu
+#         debian:
+#           ansible_host: 10.0.0.10
+#           wsl_distribution: debian
+#       vars:
+#         ansible_connection: community.general.wsl
+#         ansible_user: vagrant
+#         wsl_user: wsl
+# ----------------------
+# Playbook: playbook.yml
+# ----------------------
+---
+- hosts: wsl
+  tasks:
+    - name: ping
+      ansible.builtin.ping:
+    - name: id
+      args:
+        executable: /bin/bash
+      ansible.builtin.shell: |
+        exec 2>&1
+        set -x
+        echo "$0"
+        pwd
+        id
+    - name: become id
+      become: true
+      args:
+        executable: /bin/bash
+      ansible.builtin.shell: |
+        exec 2>&1
+        set -x
+        echo "$0"
+        pwd
+        id
+"""
+
+import os
+import pathlib
+import shlex
+import socket
+import tempfile
+import typing as t
+
+from ansible.errors import (
+    AnsibleAuthenticationFailure,
+    AnsibleConnectionFailure,
+    AnsibleError,
+)
+from ansible_collections.community.general.plugins.module_utils._filelock import FileLock, LockTimeout
+from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
+from ansible.module_utils.compat.paramiko import PARAMIKO_IMPORT_ERR, paramiko
+from ansible.module_utils.compat.version import LooseVersion
+from ansible.plugins.connection import ConnectionBase
+from ansible.utils.display import Display
+from ansible.utils.path import makedirs_safe
+from binascii import hexlify
+from subprocess import list2cmdline
+
+
+display = Display()
+
+
+def authenticity_msg(hostname: str, ktype: str, fingerprint: str) -> str:
+    msg = f"""
+    paramiko: The authenticity of host '{hostname}' can't be established.
+    The {ktype} key fingerprint is {fingerprint}.
+    Are you sure you want to continue connecting (yes/no)?
+    """
+    return msg
+
+
+MissingHostKeyPolicy: type = object
+if paramiko:
+    MissingHostKeyPolicy = paramiko.MissingHostKeyPolicy
+
+
+class MyAddPolicy(MissingHostKeyPolicy):
+    """
+    Based on AutoAddPolicy in paramiko so we can determine when keys are added
+
+    and also prompt for input.
+
+    Policy for automatically adding the hostname and new host key to the
+    local L{HostKeys} object, and saving it. This is used by L{SSHClient}.
+    """
+
+    def __init__(self, connection: Connection) -> None:
+        self.connection = connection
+        self._options = connection._options
+
+    def missing_host_key(self, client, hostname, key) -> None:
+
+        if all((self.connection.get_option('host_key_checking'), not self.connection.get_option('host_key_auto_add'))):
+
+            fingerprint = hexlify(key.get_fingerprint())
+            ktype = key.get_name()
+
+            if self.connection.get_option('use_persistent_connections') or self.connection.force_persistence:
+                # don't print the prompt string since the user cannot respond
+                # to the question anyway
+                raise AnsibleError(authenticity_msg(hostname, ktype, fingerprint)[1:92])
+
+            inp = to_text(
+                display.prompt_until(authenticity_msg(hostname, ktype, fingerprint), private=False),
+                errors='surrogate_or_strict'
+            )
+
+            if inp.lower() not in ['yes', 'y', '']:
+                raise AnsibleError('host connection rejected by user')
+
+        key._added_by_ansible_this_time = True
+
+        # existing implementation below:
+        client._host_keys.add(hostname, key.get_name(), key)
+
+        # host keys are actually saved in close() function below
+        # in order to control ordering.
+
+
+class Connection(ConnectionBase):
+    """ SSH based connections (paramiko) to WSL """
+
+    transport = 'community.general.wsl'
+    _log_channel: str | None = None
+
+    def __init__(self, play_context, new_stdin, *args, **kwargs):
+        super(Connection, self).__init__(play_context, new_stdin, *args, **kwargs)
+
+    def _set_log_channel(self, name: str) -> None:
+        """ Mimic paramiko.SSHClient.set_log_channel """
+        self._log_channel = name
+
+    def _parse_proxy_command(self, port: int = 22) -> dict[str, t.Any]:
+        proxy_command = self.get_option('proxy_command') or None
+
+        sock_kwarg = {}
+        if proxy_command:
+            replacers = {
+                '%h': self.get_option('remote_addr'),
+                '%p': port,
+                '%r': self.get_option('remote_user')
+            }
+            for find, replace in replacers.items():
+                proxy_command = proxy_command.replace(find, str(replace))
+            try:
+                sock_kwarg = {'sock': paramiko.ProxyCommand(proxy_command)}
+                display.vvv(f'CONFIGURE PROXY COMMAND FOR CONNECTION: {proxy_command}', host=self.get_option('remote_addr'))
+            except AttributeError:
+                display.warning('Paramiko ProxyCommand support unavailable. '
+                                'Please upgrade to Paramiko 1.9.0 or newer. '
+                                'Not using configured ProxyCommand')
+
+        return sock_kwarg
+
+    def _connect(self) -> Connection:
+        """ activates the connection object """
+
+        if paramiko is None:
+            raise AnsibleError(f'paramiko is not installed: {to_native(PARAMIKO_IMPORT_ERR)}')
+
+        port = self.get_option('port')
+        display.vvv(f'ESTABLISH PARAMIKO SSH CONNECTION FOR USER: {self.get_option("remote_user")} on PORT {to_text(port)} TO {self.get_option("remote_addr")}',
+                    host=self.get_option('remote_addr'))
+
+        ssh = paramiko.SSHClient()
+
+        # Set pubkey and hostkey algorithms to disable, the only manipulation allowed currently
+        # is keeping or omitting rsa-sha2 algorithms
+        # default_keys: t.Tuple[str] = ()
+        paramiko_preferred_pubkeys = getattr(paramiko.Transport, '_preferred_pubkeys', ())
+        paramiko_preferred_hostkeys = getattr(paramiko.Transport, '_preferred_keys', ())
+        use_rsa_sha2_algorithms = self.get_option('use_rsa_sha2_algorithms')
+        disabled_algorithms: t.Dict[str, t.Iterable[str]] = {}
+        if not use_rsa_sha2_algorithms:
+            if paramiko_preferred_pubkeys:
+                disabled_algorithms['pubkeys'] = tuple(a for a in paramiko_preferred_pubkeys if 'rsa-sha2' in a)
+            if paramiko_preferred_hostkeys:
+                disabled_algorithms['keys'] = tuple(a for a in paramiko_preferred_hostkeys if 'rsa-sha2' in a)
+
+        # override paramiko's default logger name
+        if self._log_channel is not None:
+            ssh.set_log_channel(self._log_channel)
+
+        self.keyfile = os.path.expanduser('~/.ssh/known_hosts')
+
+        if self.get_option('host_key_checking'):
+            for ssh_known_hosts in ('/etc/ssh/ssh_known_hosts', '/etc/openssh/ssh_known_hosts'):
+                try:
+                    ssh.load_system_host_keys(ssh_known_hosts)
+                    break
+                except IOError:
+                    pass  # file was not found, but not required to function
+                except paramiko.hostkeys.InvalidHostKey as e:
+                    raise AnsibleConnectionFailure(f'Invalid host key: {to_text(e.line)}')
+            try:
+                ssh.load_system_host_keys()
+            except paramiko.hostkeys.InvalidHostKey as e:
+                raise AnsibleConnectionFailure(f'Invalid host key: {to_text(e.line)}')
+
+        ssh_connect_kwargs = self._parse_proxy_command(port)
+        ssh.set_missing_host_key_policy(MyAddPolicy(self))
+        conn_password = self.get_option('password')
+        allow_agent = True
+
+        if conn_password is not None:
+            allow_agent = False
+
+        try:
+            key_filename = None
+            if self.get_option('private_key_file'):
+                key_filename = os.path.expanduser(self.get_option('private_key_file'))
+
+            # paramiko 2.2 introduced auth_timeout parameter
+            if LooseVersion(paramiko.__version__) >= LooseVersion('2.2.0'):
+                ssh_connect_kwargs['auth_timeout'] = self.get_option('timeout')
+
+            # paramiko 1.15 introduced banner timeout parameter
+            if LooseVersion(paramiko.__version__) >= LooseVersion('1.15.0'):
+                ssh_connect_kwargs['banner_timeout'] = self.get_option('banner_timeout')
+
+            ssh.connect(
+                self.get_option('remote_addr').lower(),
+                username=self.get_option('remote_user'),
+                allow_agent=allow_agent,
+                look_for_keys=self.get_option('look_for_keys'),
+                key_filename=key_filename,
+                password=conn_password,
+                timeout=self.get_option('timeout'),
+                port=port,
+                disabled_algorithms=disabled_algorithms,
+                **ssh_connect_kwargs,
+            )
+        except paramiko.ssh_exception.BadHostKeyException as e:
+            raise AnsibleConnectionFailure(f'host key mismatch for {to_text(e.hostname)}')
+        except paramiko.ssh_exception.AuthenticationException as e:
+            msg = f'Failed to authenticate: {e}'
+            raise AnsibleAuthenticationFailure(msg)
+        except Exception as e:
+            msg = to_text(e)
+            if u'PID check failed' in msg:
+                raise AnsibleError('paramiko version issue, please upgrade paramiko on the machine running ansible')
+            elif u'Private key file is encrypted' in msg:
+                msg = f'ssh {self.get_option("remote_user")}@{self.get_options("remote_addr")}:{port} : ' + \
+                    f'{msg}\nTo connect as a different user, use -u <username>.'
+                raise AnsibleConnectionFailure(msg)
+            else:
+                raise AnsibleConnectionFailure(msg)
+        self.ssh = ssh
+        self._connected = True
+        return self
+
+    def _any_keys_added(self) -> bool:
+        for hostname, keys in self.ssh._host_keys.items():
+            for keytype, key in keys.items():
+                added_this_time = getattr(key, '_added_by_ansible_this_time', False)
+                if added_this_time:
+                    return True
+        return False
+
+    def _save_ssh_host_keys(self, filename: str) -> None:
+        """
+        not using the paramiko save_ssh_host_keys function as we want to add new SSH keys at the bottom so folks
+        don't complain about it :)
+        """
+
+        if not self._any_keys_added():
+            return
+
+        path = os.path.expanduser('~/.ssh')
+        makedirs_safe(path)
+
+        with open(filename, 'w') as f:
+            for hostname, keys in self.ssh._host_keys.items():
+                for keytype, key in keys.items():
+                    # was f.write
+                    added_this_time = getattr(key, '_added_by_ansible_this_time', False)
+                    if not added_this_time:
+                        f.write(f'{hostname} {keytype} {key.get_base64()}\n')
+
+            for hostname, keys in self.ssh._host_keys.items():
+                for keytype, key in keys.items():
+                    added_this_time = getattr(key, '_added_by_ansible_this_time', False)
+                    if added_this_time:
+                        f.write(f'{hostname} {keytype} {key.get_base64()}\n')
+
+    def _build_wsl_command(self, cmd: str) -> str:
+        wsl_distribution = self.get_option('wsl_distribution')
+        if not wsl_distribution:
+            raise AnsibleError('A wsl_distribution is required but none was supplied')
+        become = self.get_option('become')
+        become_user = self.get_option('become_user')
+        if become and become_user:
+            wsl_user = become_user
+        else:
+            wsl_user = self.get_option('wsl_user')
+        args = ['wsl.exe', '--distribution', wsl_distribution]
+        if wsl_user:
+            args.extend(['--user', wsl_user])
+        args.extend(['--'])
+        args.extend(shlex.split(cmd))
+        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 """
+
+        cmd = self._build_wsl_command(cmd)
+
+        super(Connection, self).exec_command(cmd, in_data=in_data, sudoable=sudoable)
+
+        bufsize = 4096
+
+        try:
+            self.ssh.get_transport().set_keepalive(5)
+            chan = self.ssh.get_transport().open_session()
+        except Exception as e:
+            text_e = to_text(e)
+            msg = 'Failed to open session'
+            if text_e:
+                msg += f': {text_e}'
+            raise AnsibleConnectionFailure(to_native(msg))
+
+        display.vvv(f'EXEC {cmd}', host=self.get_option('remote_addr'))
+
+        cmd = to_bytes(cmd, errors='surrogate_or_strict')
+
+        no_prompt_out = b''
+        no_prompt_err = b''
+        become_output = b''
+
+        try:
+            chan.exec_command(cmd)
+            if self.become and self.become.expect_prompt():
+                password_prompt = False
+                become_success = False
+                while not (become_success or password_prompt):
+                    display.debug('Waiting for Privilege Escalation input')
+
+                    chunk = chan.recv(bufsize)
+                    display.debug(f'chunk is: {to_text(chunk)}')
+                    if not chunk:
+                        if b'unknown user' in become_output:
+                            n_become_user = to_native(self.become.get_option('become_user'))
+                            raise AnsibleError(f'user {n_become_user} does not exist')
+                        else:
+                            break
+                            # raise AnsibleError('ssh connection closed waiting for password prompt')
+                    become_output += chunk
+
+                    # need to check every line because we might get lectured
+                    # and we might get the middle of a line in a chunk
+                    for line in become_output.splitlines(True):
+                        if self.become.check_success(line):
+                            become_success = True
+                            break
+                        elif self.become.check_password_prompt(line):
+                            password_prompt = True
+                            break
+
+                if password_prompt:
+                    if self.become:
+                        become_pass = self.become.get_option('become_pass')
+                        chan.sendall(to_bytes(become_pass, errors='surrogate_or_strict') + b'\n')
path: plugins/connection/wsl.py
user: login: rgl
body: done. please verify.
created_at: 2025-03-05T19:01:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,758 @@
+# -*- coding: utf-8 -*-
+# Derived from ansible/plugins/connection/proxmox_pct_remote.py (c) 2024 Nils Stein (@mietzen) <github.nstein@mailbox.org>
+# Derived from ansible/plugins/connection/paramiko_ssh.py (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
+# Copyright (c) 2025 Rui Lopes (@rgl) <ruilopes.com>
+# Copyright (c) 2025 Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import annotations
+
+DOCUMENTATION = r"""
+author: Rui Lopes (@rgl) <ruilopes.com>
+name: wsl
+short_description: Run tasks in WSL distribution using wsl.exe CLI via SSH
+requirements:
+  - paramiko
+description:
+  - Run commands or put/fetch files to an existing WSL distribution using wsl.exe CLI via SSH.
+  - Uses the Python SSH implementation (Paramiko) to connect to the WSL host.
+version_added: "10.3.0"
+options:
+  remote_addr:
+    description:
+      - Address of the remote target.
+    default: inventory_hostname
+    type: string
+    vars:
+      - name: inventory_hostname
+      - name: ansible_host
+      - name: ansible_ssh_host
+      - name: ansible_paramiko_host
+  port:
+    description: Remote port to connect to.
+    type: int
+    default: 22
+    ini:
+      - section: defaults
+        key: remote_port
+      - section: paramiko_connection
+        key: remote_port
+    env:
+      - name: ANSIBLE_REMOTE_PORT
+      - name: ANSIBLE_REMOTE_PARAMIKO_PORT
+    vars:
+      - name: ansible_port
+      - name: ansible_ssh_port
+      - name: ansible_paramiko_port
+    keyword:
+      - name: port
+  remote_user:
+    description:
+      - User to login/authenticate as.
+      - Can be set from the CLI via the C(--user) or C(-u) options.
+    type: string
+    vars:
+      - name: ansible_user
+      - name: ansible_ssh_user
+      - name: ansible_paramiko_user
+    env:
+      - name: ANSIBLE_REMOTE_USER
+      - name: ANSIBLE_PARAMIKO_REMOTE_USER
+    ini:
+      - section: defaults
+        key: remote_user
+      - section: paramiko_connection
+        key: remote_user
+    keyword:
+      - name: remote_user
+  password:
+    description:
+      - Secret used to either login the SSH server or as a passphrase for SSH keys that require it.
+      - Can be set from the CLI via the C(--ask-pass) option.
+    type: string
+    vars:
+      - name: ansible_password
+      - name: ansible_ssh_pass
+      - name: ansible_ssh_password
+      - name: ansible_paramiko_pass
+      - name: ansible_paramiko_password
+  use_rsa_sha2_algorithms:
+    description:
+      - Whether or not to enable RSA SHA2 algorithms for pubkeys and hostkeys.
+      - On paramiko versions older than 2.9, this only affects hostkeys.
+      - For behavior matching paramiko<2.9 set this to V(false).
+    vars:
+      - name: ansible_paramiko_use_rsa_sha2_algorithms
+    ini:
+      - {key: use_rsa_sha2_algorithms, section: paramiko_connection}
+    env:
+      - {name: ANSIBLE_PARAMIKO_USE_RSA_SHA2_ALGORITHMS}
+    default: true
+    type: boolean
+  host_key_auto_add:
+    description: "Automatically add host keys to C(~/.ssh/known_hosts)."
+    env:
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_AUTO_ADD
+    ini:
+      - key: host_key_auto_add
+        section: paramiko_connection
+    type: boolean
+  look_for_keys:
+    default: True
+    description: "Set to V(false) to disable searching for private key files in C(~/.ssh/)."
+    env:
+      - name: ANSIBLE_PARAMIKO_LOOK_FOR_KEYS
+    ini:
+      - {key: look_for_keys, section: paramiko_connection}
+    type: boolean
+  proxy_command:
+    default: ""
+    description:
+      - Proxy information for running the connection via a jumphost.
+    type: string
+    env:
+      - name: ANSIBLE_PARAMIKO_PROXY_COMMAND
+    ini:
+      - {key: proxy_command, section: paramiko_connection}
+    vars:
+      - name: ansible_paramiko_proxy_command
+  record_host_keys:
+    default: True
+    description: "Save the host keys to a file."
+    env:
+      - name: ANSIBLE_PARAMIKO_RECORD_HOST_KEYS
+    ini:
+      - section: paramiko_connection
+        key: record_host_keys
+    type: boolean
+  host_key_checking:
+    description: "Set this to V(false) if you want to avoid host key checking by the underlying tools Ansible uses to connect to the host."
+    type: boolean
+    default: true
+    env:
+      - name: ANSIBLE_HOST_KEY_CHECKING
+      - name: ANSIBLE_SSH_HOST_KEY_CHECKING
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_CHECKING
+    ini:
+      - section: defaults
+        key: host_key_checking
+      - section: paramiko_connection
+        key: host_key_checking
+    vars:
+      - name: ansible_host_key_checking
+      - name: ansible_ssh_host_key_checking
+      - name: ansible_paramiko_host_key_checking
+  use_persistent_connections:
+    description: "Toggles the use of persistence for connections."
+    type: boolean
+    default: False
+    env:
+      - name: ANSIBLE_USE_PERSISTENT_CONNECTIONS
+    ini:
+      - section: defaults
+        key: use_persistent_connections
+  banner_timeout:
+    type: float
+    default: 30
+    description:
+      - Configures, in seconds, the amount of time to wait for the SSH
+        banner to be presented. This option is supported by paramiko
+        version 1.15.0 or newer.
+    ini:
+      - section: paramiko_connection
+        key: banner_timeout
+    env:
+      - name: ANSIBLE_PARAMIKO_BANNER_TIMEOUT
+  timeout:
+    type: int
+    default: 10
+    description: Number of seconds until the plugin gives up on failing to establish a TCP connection.
+    ini:
+      - section: defaults
+        key: timeout
+      - section: ssh_connection
+        key: timeout
+      - section: paramiko_connection
+        key: timeout
+    env:
+      - name: ANSIBLE_TIMEOUT
+      - name: ANSIBLE_SSH_TIMEOUT
+      - name: ANSIBLE_PARAMIKO_TIMEOUT
+    vars:
+      - name: ansible_ssh_timeout
+      - name: ansible_paramiko_timeout
+    cli:
+      - name: timeout
+  lock_file_timeout:
+    type: int
+    default: 60
+    description: Number of seconds until the plugin gives up on trying to write a lock file when writing SSH known host keys.
+    vars:
+      - name: ansible_lock_file_timeout
+    env:
+      - name: ANSIBLE_LOCK_FILE_TIMEOUT
+  private_key_file:
+    description:
+      - Path to private key file to use for authentication.
+    type: string
+    ini:
+      - section: defaults
+        key: private_key_file
+      - section: paramiko_connection
+        key: private_key_file
+    env:
+      - name: ANSIBLE_PRIVATE_KEY_FILE
+      - name: ANSIBLE_PARAMIKO_PRIVATE_KEY_FILE
+    vars:
+      - name: ansible_private_key_file
+      - name: ansible_ssh_private_key_file
+      - name: ansible_paramiko_private_key_file
+    cli:
+      - name: private_key_file
+        option: "--private-key"
+  wsl_distribution:
+    description:
+      - WSL distribution name
+    type: string
+    vars:
+      - name: wsl_distribution
+  wsl_user:
+    description:
+      - WSL distribution user
+    type: string
+    vars:
+      - name: wsl_user
+  become_user:
+    description:
+      - WSL distribution user
+    type: string
+    default: root
+    vars:
+      - name: become_user
+      - name: ansible_become_user
+  become:
+    description:
+      - whether to use the user defined by ansible_become_user.
+    type: bool
+    default: false
+    vars:
+      - name: become
+      - name: ansible_become
+"""
+
+EXAMPLES = r"""
+# ------------------------
+# Inventory: inventory.yml
+# ------------------------
+# all:
+#   children:
+#     wsl:
+#       hosts:
+#         ubuntu:
+#           ansible_host: 10.0.0.10
+#           wsl_distribution: ubuntu
+#         debian:
+#           ansible_host: 10.0.0.10
+#           wsl_distribution: debian
+#       vars:
+#         ansible_connection: community.general.wsl
+#         ansible_user: vagrant
+#         wsl_user: wsl
+# ----------------------
+# Playbook: playbook.yml
+# ----------------------
+---
+- hosts: wsl
+  tasks:
+    - name: ping
+      ansible.builtin.ping:
+    - name: id
+      args:
+        executable: /bin/bash
+      ansible.builtin.shell: |
+        exec 2>&1
+        set -x
+        echo "$0"
+        pwd
+        id
+    - name: become id
+      become: true
+      args:
+        executable: /bin/bash
+      ansible.builtin.shell: |
+        exec 2>&1
+        set -x
+        echo "$0"
+        pwd
+        id
+"""
+
+import os
+import pathlib
+import shlex
+import socket
+import tempfile
+import typing as t
path: plugins/connection/wsl.py
user: login: felixfontein
body: Importing `typing` as `t` is a rather common pattern. I would not deviate from it just because it's only used a few times. (If you don't want to use it for another reason that's totally fine, I'm just saying that if you want to use it, I would use it consequently, and not only if `t.` is used often enough.)
created_at: 2025-03-05T20:02:21Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,758 @@
+# -*- coding: utf-8 -*-
+# Derived from ansible/plugins/connection/proxmox_pct_remote.py (c) 2024 Nils Stein (@mietzen) <github.nstein@mailbox.org>
+# Derived from ansible/plugins/connection/paramiko_ssh.py (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
+# Copyright (c) 2025 Rui Lopes (@rgl) <ruilopes.com>
+# Copyright (c) 2025 Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import annotations
+
+DOCUMENTATION = r"""
+author: Rui Lopes (@rgl) <ruilopes.com>
+name: wsl
+short_description: Run tasks in WSL distribution using wsl.exe CLI via SSH
+requirements:
+  - paramiko
+description:
+  - Run commands or put/fetch files to an existing WSL distribution using wsl.exe CLI via SSH.
+  - Uses the Python SSH implementation (Paramiko) to connect to the WSL host.
+version_added: "10.3.0"
+options:
+  remote_addr:
+    description:
+      - Address of the remote target.
+    default: inventory_hostname
+    type: string
+    vars:
+      - name: inventory_hostname
+      - name: ansible_host
+      - name: ansible_ssh_host
+      - name: ansible_paramiko_host
+  port:
+    description: Remote port to connect to.
+    type: int
+    default: 22
+    ini:
+      - section: defaults
+        key: remote_port
+      - section: paramiko_connection
+        key: remote_port
+    env:
+      - name: ANSIBLE_REMOTE_PORT
+      - name: ANSIBLE_REMOTE_PARAMIKO_PORT
+    vars:
+      - name: ansible_port
+      - name: ansible_ssh_port
+      - name: ansible_paramiko_port
+    keyword:
+      - name: port
+  remote_user:
+    description:
+      - User to login/authenticate as.
+      - Can be set from the CLI via the C(--user) or C(-u) options.
+    type: string
+    vars:
+      - name: ansible_user
+      - name: ansible_ssh_user
+      - name: ansible_paramiko_user
+    env:
+      - name: ANSIBLE_REMOTE_USER
+      - name: ANSIBLE_PARAMIKO_REMOTE_USER
+    ini:
+      - section: defaults
+        key: remote_user
+      - section: paramiko_connection
+        key: remote_user
+    keyword:
+      - name: remote_user
+  password:
+    description:
+      - Secret used to either login the SSH server or as a passphrase for SSH keys that require it.
+      - Can be set from the CLI via the C(--ask-pass) option.
+    type: string
+    vars:
+      - name: ansible_password
+      - name: ansible_ssh_pass
+      - name: ansible_ssh_password
+      - name: ansible_paramiko_pass
+      - name: ansible_paramiko_password
+  use_rsa_sha2_algorithms:
+    description:
+      - Whether or not to enable RSA SHA2 algorithms for pubkeys and hostkeys.
+      - On paramiko versions older than 2.9, this only affects hostkeys.
+      - For behavior matching paramiko<2.9 set this to V(false).
+    vars:
+      - name: ansible_paramiko_use_rsa_sha2_algorithms
+    ini:
+      - {key: use_rsa_sha2_algorithms, section: paramiko_connection}
+    env:
+      - {name: ANSIBLE_PARAMIKO_USE_RSA_SHA2_ALGORITHMS}
+    default: true
+    type: boolean
+  host_key_auto_add:
+    description: "Automatically add host keys to C(~/.ssh/known_hosts)."
+    env:
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_AUTO_ADD
+    ini:
+      - key: host_key_auto_add
+        section: paramiko_connection
+    type: boolean
+  look_for_keys:
+    default: True
+    description: "Set to V(false) to disable searching for private key files in C(~/.ssh/)."
+    env:
+      - name: ANSIBLE_PARAMIKO_LOOK_FOR_KEYS
+    ini:
+      - {key: look_for_keys, section: paramiko_connection}
+    type: boolean
+  proxy_command:
+    default: ""
+    description:
+      - Proxy information for running the connection via a jumphost.
+    type: string
+    env:
+      - name: ANSIBLE_PARAMIKO_PROXY_COMMAND
+    ini:
+      - {key: proxy_command, section: paramiko_connection}
+    vars:
+      - name: ansible_paramiko_proxy_command
+  record_host_keys:
+    default: True
+    description: "Save the host keys to a file."
+    env:
+      - name: ANSIBLE_PARAMIKO_RECORD_HOST_KEYS
+    ini:
+      - section: paramiko_connection
+        key: record_host_keys
+    type: boolean
+  host_key_checking:
+    description: "Set this to V(false) if you want to avoid host key checking by the underlying tools Ansible uses to connect to the host."
+    type: boolean
+    default: true
+    env:
+      - name: ANSIBLE_HOST_KEY_CHECKING
+      - name: ANSIBLE_SSH_HOST_KEY_CHECKING
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_CHECKING
+    ini:
+      - section: defaults
+        key: host_key_checking
+      - section: paramiko_connection
+        key: host_key_checking
+    vars:
+      - name: ansible_host_key_checking
+      - name: ansible_ssh_host_key_checking
+      - name: ansible_paramiko_host_key_checking
+  use_persistent_connections:
+    description: "Toggles the use of persistence for connections."
+    type: boolean
+    default: False
+    env:
+      - name: ANSIBLE_USE_PERSISTENT_CONNECTIONS
+    ini:
+      - section: defaults
+        key: use_persistent_connections
+  banner_timeout:
+    type: float
+    default: 30
+    description:
+      - Configures, in seconds, the amount of time to wait for the SSH
+        banner to be presented. This option is supported by paramiko
+        version 1.15.0 or newer.
+    ini:
+      - section: paramiko_connection
+        key: banner_timeout
+    env:
+      - name: ANSIBLE_PARAMIKO_BANNER_TIMEOUT
+  timeout:
+    type: int
+    default: 10
+    description: Number of seconds until the plugin gives up on failing to establish a TCP connection.
+    ini:
+      - section: defaults
+        key: timeout
+      - section: ssh_connection
+        key: timeout
+      - section: paramiko_connection
+        key: timeout
+    env:
+      - name: ANSIBLE_TIMEOUT
+      - name: ANSIBLE_SSH_TIMEOUT
+      - name: ANSIBLE_PARAMIKO_TIMEOUT
+    vars:
+      - name: ansible_ssh_timeout
+      - name: ansible_paramiko_timeout
+    cli:
+      - name: timeout
+  lock_file_timeout:
+    type: int
+    default: 60
+    description: Number of seconds until the plugin gives up on trying to write a lock file when writing SSH known host keys.
+    vars:
+      - name: ansible_lock_file_timeout
+    env:
+      - name: ANSIBLE_LOCK_FILE_TIMEOUT
+  private_key_file:
+    description:
+      - Path to private key file to use for authentication.
+    type: string
+    ini:
+      - section: defaults
+        key: private_key_file
+      - section: paramiko_connection
+        key: private_key_file
+    env:
+      - name: ANSIBLE_PRIVATE_KEY_FILE
+      - name: ANSIBLE_PARAMIKO_PRIVATE_KEY_FILE
+    vars:
+      - name: ansible_private_key_file
+      - name: ansible_ssh_private_key_file
+      - name: ansible_paramiko_private_key_file
+    cli:
+      - name: private_key_file
+        option: "--private-key"
+  wsl_distribution:
+    description:
+      - WSL distribution name
+    type: string
+    vars:
+      - name: wsl_distribution
+  wsl_user:
+    description:
+      - WSL distribution user
+    type: string
+    vars:
+      - name: wsl_user
+  become_user:
+    description:
+      - WSL distribution user
+    type: string
+    default: root
+    vars:
+      - name: become_user
+      - name: ansible_become_user
+  become:
+    description:
+      - whether to use the user defined by ansible_become_user.
+    type: bool
+    default: false
+    vars:
+      - name: become
+      - name: ansible_become
+"""
+
+EXAMPLES = r"""
+# ------------------------
+# Inventory: inventory.yml
+# ------------------------
+# all:
+#   children:
+#     wsl:
+#       hosts:
+#         ubuntu:
+#           ansible_host: 10.0.0.10
+#           wsl_distribution: ubuntu
+#         debian:
+#           ansible_host: 10.0.0.10
+#           wsl_distribution: debian
+#       vars:
+#         ansible_connection: community.general.wsl
+#         ansible_user: vagrant
+#         wsl_user: wsl
+# ----------------------
+# Playbook: playbook.yml
+# ----------------------
+---
+- hosts: wsl
+  tasks:
+    - name: ping
+      ansible.builtin.ping:
+    - name: id
+      args:
+        executable: /bin/bash
+      ansible.builtin.shell: |
+        exec 2>&1
+        set -x
+        echo "$0"
+        pwd
+        id
+    - name: become id
+      become: true
+      args:
+        executable: /bin/bash
+      ansible.builtin.shell: |
+        exec 2>&1
+        set -x
+        echo "$0"
+        pwd
+        id
+"""
+
+import os
+import pathlib
+import shlex
+import socket
+import tempfile
+import typing as t
+
+from ansible.errors import (
+    AnsibleAuthenticationFailure,
+    AnsibleConnectionFailure,
+    AnsibleError,
+)
+from ansible_collections.community.general.plugins.module_utils._filelock import FileLock, LockTimeout
+from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
+from ansible.module_utils.compat.paramiko import PARAMIKO_IMPORT_ERR, paramiko
+from ansible.module_utils.compat.version import LooseVersion
+from ansible.plugins.connection import ConnectionBase
+from ansible.utils.display import Display
+from ansible.utils.path import makedirs_safe
+from binascii import hexlify
+from subprocess import list2cmdline
+
+
+display = Display()
+
+
+def authenticity_msg(hostname: str, ktype: str, fingerprint: str) -> str:
+    msg = f"""
+    paramiko: The authenticity of host '{hostname}' can't be established.
+    The {ktype} key fingerprint is {fingerprint}.
+    Are you sure you want to continue connecting (yes/no)?
+    """
+    return msg
+
+
+MissingHostKeyPolicy: type = object
+if paramiko:
+    MissingHostKeyPolicy = paramiko.MissingHostKeyPolicy
+
+
+class MyAddPolicy(MissingHostKeyPolicy):
+    """
+    Based on AutoAddPolicy in paramiko so we can determine when keys are added
+
+    and also prompt for input.
+
+    Policy for automatically adding the hostname and new host key to the
+    local L{HostKeys} object, and saving it. This is used by L{SSHClient}.
+    """
+
+    def __init__(self, connection: Connection) -> None:
+        self.connection = connection
+        self._options = connection._options
+
+    def missing_host_key(self, client, hostname, key) -> None:
+
+        if all((self.connection.get_option('host_key_checking'), not self.connection.get_option('host_key_auto_add'))):
+
+            fingerprint = hexlify(key.get_fingerprint())
+            ktype = key.get_name()
+
+            if self.connection.get_option('use_persistent_connections') or self.connection.force_persistence:
+                # don't print the prompt string since the user cannot respond
+                # to the question anyway
+                raise AnsibleError(authenticity_msg(hostname, ktype, fingerprint)[1:92])
+
+            inp = to_text(
+                display.prompt_until(authenticity_msg(hostname, ktype, fingerprint), private=False),
+                errors='surrogate_or_strict'
+            )
+
+            if inp.lower() not in ['yes', 'y', '']:
+                raise AnsibleError('host connection rejected by user')
+
+        key._added_by_ansible_this_time = True
+
+        # existing implementation below:
+        client._host_keys.add(hostname, key.get_name(), key)
+
+        # host keys are actually saved in close() function below
+        # in order to control ordering.
+
+
+class Connection(ConnectionBase):
+    """ SSH based connections (paramiko) to WSL """
+
+    transport = 'community.general.wsl'
+    _log_channel: str | None = None
+
+    def __init__(self, play_context, new_stdin, *args, **kwargs):
+        super(Connection, self).__init__(play_context, new_stdin, *args, **kwargs)
+
+    def _set_log_channel(self, name: str) -> None:
+        """ Mimic paramiko.SSHClient.set_log_channel """
+        self._log_channel = name
+
+    def _parse_proxy_command(self, port: int = 22) -> dict[str, t.Any]:
+        proxy_command = self.get_option('proxy_command') or None
+
+        sock_kwarg = {}
+        if proxy_command:
+            replacers = {
+                '%h': self.get_option('remote_addr'),
+                '%p': port,
+                '%r': self.get_option('remote_user')
+            }
+            for find, replace in replacers.items():
+                proxy_command = proxy_command.replace(find, str(replace))
+            try:
+                sock_kwarg = {'sock': paramiko.ProxyCommand(proxy_command)}
+                display.vvv(f'CONFIGURE PROXY COMMAND FOR CONNECTION: {proxy_command}', host=self.get_option('remote_addr'))
+            except AttributeError:
+                display.warning('Paramiko ProxyCommand support unavailable. '
+                                'Please upgrade to Paramiko 1.9.0 or newer. '
+                                'Not using configured ProxyCommand')
+
+        return sock_kwarg
+
+    def _connect(self) -> Connection:
+        """ activates the connection object """
+
+        if paramiko is None:
+            raise AnsibleError(f'paramiko is not installed: {to_native(PARAMIKO_IMPORT_ERR)}')
+
+        port = self.get_option('port')
+        display.vvv(f'ESTABLISH PARAMIKO SSH CONNECTION FOR USER: {self.get_option("remote_user")} on PORT {to_text(port)} TO {self.get_option("remote_addr")}',
+                    host=self.get_option('remote_addr'))
+
+        ssh = paramiko.SSHClient()
+
+        # Set pubkey and hostkey algorithms to disable, the only manipulation allowed currently
+        # is keeping or omitting rsa-sha2 algorithms
+        # default_keys: t.Tuple[str] = ()
+        paramiko_preferred_pubkeys = getattr(paramiko.Transport, '_preferred_pubkeys', ())
+        paramiko_preferred_hostkeys = getattr(paramiko.Transport, '_preferred_keys', ())
+        use_rsa_sha2_algorithms = self.get_option('use_rsa_sha2_algorithms')
+        disabled_algorithms: t.Dict[str, t.Iterable[str]] = {}
+        if not use_rsa_sha2_algorithms:
+            if paramiko_preferred_pubkeys:
+                disabled_algorithms['pubkeys'] = tuple(a for a in paramiko_preferred_pubkeys if 'rsa-sha2' in a)
+            if paramiko_preferred_hostkeys:
+                disabled_algorithms['keys'] = tuple(a for a in paramiko_preferred_hostkeys if 'rsa-sha2' in a)
+
+        # override paramiko's default logger name
+        if self._log_channel is not None:
+            ssh.set_log_channel(self._log_channel)
+
+        self.keyfile = os.path.expanduser('~/.ssh/known_hosts')
+
+        if self.get_option('host_key_checking'):
+            for ssh_known_hosts in ('/etc/ssh/ssh_known_hosts', '/etc/openssh/ssh_known_hosts'):
+                try:
+                    ssh.load_system_host_keys(ssh_known_hosts)
+                    break
+                except IOError:
+                    pass  # file was not found, but not required to function
path: plugins/connection/wsl.py
user: login: mietzen
body: I also only copied it from: [paramiko_ssh](https://github.com/ansible/ansible/blob/c02ad3516fced3c06486bd4b920e85f32e8e4f07/lib/ansible/plugins/connection/paramiko_ssh.py#L409)

I guessed this would cover some edge case, so I left it in.
created_at: 2025-03-06T07:19:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,758 @@
+# -*- coding: utf-8 -*-
+# Derived from ansible/plugins/connection/proxmox_pct_remote.py (c) 2024 Nils Stein (@mietzen) <github.nstein@mailbox.org>
+# Derived from ansible/plugins/connection/paramiko_ssh.py (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
+# Copyright (c) 2025 Rui Lopes (@rgl) <ruilopes.com>
+# Copyright (c) 2025 Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import annotations
+
+DOCUMENTATION = r"""
+author: Rui Lopes (@rgl) <ruilopes.com>
+name: wsl
+short_description: Run tasks in WSL distribution using wsl.exe CLI via SSH
+requirements:
+  - paramiko
+description:
+  - Run commands or put/fetch files to an existing WSL distribution using wsl.exe CLI via SSH.
+  - Uses the Python SSH implementation (Paramiko) to connect to the WSL host.
+version_added: "10.3.0"
+options:
+  remote_addr:
+    description:
+      - Address of the remote target.
+    default: inventory_hostname
+    type: string
+    vars:
+      - name: inventory_hostname
+      - name: ansible_host
+      - name: ansible_ssh_host
+      - name: ansible_paramiko_host
+  port:
+    description: Remote port to connect to.
+    type: int
+    default: 22
+    ini:
+      - section: defaults
+        key: remote_port
+      - section: paramiko_connection
+        key: remote_port
+    env:
+      - name: ANSIBLE_REMOTE_PORT
+      - name: ANSIBLE_REMOTE_PARAMIKO_PORT
+    vars:
+      - name: ansible_port
+      - name: ansible_ssh_port
+      - name: ansible_paramiko_port
+    keyword:
+      - name: port
+  remote_user:
+    description:
+      - User to login/authenticate as.
+      - Can be set from the CLI via the C(--user) or C(-u) options.
+    type: string
+    vars:
+      - name: ansible_user
+      - name: ansible_ssh_user
+      - name: ansible_paramiko_user
+    env:
+      - name: ANSIBLE_REMOTE_USER
+      - name: ANSIBLE_PARAMIKO_REMOTE_USER
+    ini:
+      - section: defaults
+        key: remote_user
+      - section: paramiko_connection
+        key: remote_user
+    keyword:
+      - name: remote_user
+  password:
+    description:
+      - Secret used to either login the SSH server or as a passphrase for SSH keys that require it.
+      - Can be set from the CLI via the C(--ask-pass) option.
+    type: string
+    vars:
+      - name: ansible_password
+      - name: ansible_ssh_pass
+      - name: ansible_ssh_password
+      - name: ansible_paramiko_pass
+      - name: ansible_paramiko_password
+  use_rsa_sha2_algorithms:
+    description:
+      - Whether or not to enable RSA SHA2 algorithms for pubkeys and hostkeys.
+      - On paramiko versions older than 2.9, this only affects hostkeys.
+      - For behavior matching paramiko<2.9 set this to V(false).
+    vars:
+      - name: ansible_paramiko_use_rsa_sha2_algorithms
+    ini:
+      - {key: use_rsa_sha2_algorithms, section: paramiko_connection}
+    env:
+      - {name: ANSIBLE_PARAMIKO_USE_RSA_SHA2_ALGORITHMS}
+    default: true
+    type: boolean
+  host_key_auto_add:
+    description: "Automatically add host keys to C(~/.ssh/known_hosts)."
+    env:
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_AUTO_ADD
+    ini:
+      - key: host_key_auto_add
+        section: paramiko_connection
+    type: boolean
+  look_for_keys:
+    default: True
+    description: "Set to V(false) to disable searching for private key files in C(~/.ssh/)."
+    env:
+      - name: ANSIBLE_PARAMIKO_LOOK_FOR_KEYS
+    ini:
+      - {key: look_for_keys, section: paramiko_connection}
+    type: boolean
+  proxy_command:
+    default: ""
+    description:
+      - Proxy information for running the connection via a jumphost.
+    type: string
+    env:
+      - name: ANSIBLE_PARAMIKO_PROXY_COMMAND
+    ini:
+      - {key: proxy_command, section: paramiko_connection}
+    vars:
+      - name: ansible_paramiko_proxy_command
+  record_host_keys:
+    default: True
+    description: "Save the host keys to a file."
+    env:
+      - name: ANSIBLE_PARAMIKO_RECORD_HOST_KEYS
+    ini:
+      - section: paramiko_connection
+        key: record_host_keys
+    type: boolean
+  host_key_checking:
+    description: "Set this to V(false) if you want to avoid host key checking by the underlying tools Ansible uses to connect to the host."
+    type: boolean
+    default: true
+    env:
+      - name: ANSIBLE_HOST_KEY_CHECKING
+      - name: ANSIBLE_SSH_HOST_KEY_CHECKING
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_CHECKING
+    ini:
+      - section: defaults
+        key: host_key_checking
+      - section: paramiko_connection
+        key: host_key_checking
+    vars:
+      - name: ansible_host_key_checking
+      - name: ansible_ssh_host_key_checking
+      - name: ansible_paramiko_host_key_checking
+  use_persistent_connections:
+    description: "Toggles the use of persistence for connections."
+    type: boolean
+    default: False
+    env:
+      - name: ANSIBLE_USE_PERSISTENT_CONNECTIONS
+    ini:
+      - section: defaults
+        key: use_persistent_connections
+  banner_timeout:
+    type: float
+    default: 30
+    description:
+      - Configures, in seconds, the amount of time to wait for the SSH
+        banner to be presented. This option is supported by paramiko
+        version 1.15.0 or newer.
+    ini:
+      - section: paramiko_connection
+        key: banner_timeout
+    env:
+      - name: ANSIBLE_PARAMIKO_BANNER_TIMEOUT
+  timeout:
+    type: int
+    default: 10
+    description: Number of seconds until the plugin gives up on failing to establish a TCP connection.
+    ini:
+      - section: defaults
+        key: timeout
+      - section: ssh_connection
+        key: timeout
+      - section: paramiko_connection
+        key: timeout
+    env:
+      - name: ANSIBLE_TIMEOUT
+      - name: ANSIBLE_SSH_TIMEOUT
+      - name: ANSIBLE_PARAMIKO_TIMEOUT
+    vars:
+      - name: ansible_ssh_timeout
+      - name: ansible_paramiko_timeout
+    cli:
+      - name: timeout
+  lock_file_timeout:
+    type: int
+    default: 60
+    description: Number of seconds until the plugin gives up on trying to write a lock file when writing SSH known host keys.
+    vars:
+      - name: ansible_lock_file_timeout
+    env:
+      - name: ANSIBLE_LOCK_FILE_TIMEOUT
+  private_key_file:
+    description:
+      - Path to private key file to use for authentication.
+    type: string
+    ini:
+      - section: defaults
+        key: private_key_file
+      - section: paramiko_connection
+        key: private_key_file
+    env:
+      - name: ANSIBLE_PRIVATE_KEY_FILE
+      - name: ANSIBLE_PARAMIKO_PRIVATE_KEY_FILE
+    vars:
+      - name: ansible_private_key_file
+      - name: ansible_ssh_private_key_file
+      - name: ansible_paramiko_private_key_file
+    cli:
+      - name: private_key_file
+        option: "--private-key"
+  wsl_distribution:
+    description:
+      - WSL distribution name
+    type: string
+    vars:
+      - name: wsl_distribution
+  wsl_user:
+    description:
+      - WSL distribution user
+    type: string
+    vars:
+      - name: wsl_user
+  become_user:
+    description:
+      - WSL distribution user
+    type: string
+    default: root
+    vars:
+      - name: become_user
+      - name: ansible_become_user
+  become:
+    description:
+      - whether to use the user defined by ansible_become_user.
+    type: bool
+    default: false
+    vars:
+      - name: become
+      - name: ansible_become
+"""
+
+EXAMPLES = r"""
+# ------------------------
+# Inventory: inventory.yml
+# ------------------------
+# all:
+#   children:
+#     wsl:
+#       hosts:
+#         ubuntu:
+#           ansible_host: 10.0.0.10
+#           wsl_distribution: ubuntu
+#         debian:
+#           ansible_host: 10.0.0.10
+#           wsl_distribution: debian
+#       vars:
+#         ansible_connection: community.general.wsl
+#         ansible_user: vagrant
+#         wsl_user: wsl
+# ----------------------
+# Playbook: playbook.yml
+# ----------------------
+---
+- hosts: wsl
+  tasks:
+    - name: ping
+      ansible.builtin.ping:
+    - name: id
+      args:
+        executable: /bin/bash
+      ansible.builtin.shell: |
+        exec 2>&1
+        set -x
+        echo "$0"
+        pwd
+        id
+    - name: become id
+      become: true
+      args:
+        executable: /bin/bash
+      ansible.builtin.shell: |
+        exec 2>&1
+        set -x
+        echo "$0"
+        pwd
+        id
+"""
+
+import os
+import pathlib
+import shlex
+import socket
+import tempfile
+import typing as t
+
+from ansible.errors import (
+    AnsibleAuthenticationFailure,
+    AnsibleConnectionFailure,
+    AnsibleError,
+)
+from ansible_collections.community.general.plugins.module_utils._filelock import FileLock, LockTimeout
+from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
+from ansible.module_utils.compat.paramiko import PARAMIKO_IMPORT_ERR, paramiko
+from ansible.module_utils.compat.version import LooseVersion
+from ansible.plugins.connection import ConnectionBase
+from ansible.utils.display import Display
+from ansible.utils.path import makedirs_safe
+from binascii import hexlify
+from subprocess import list2cmdline
+
+
+display = Display()
+
+
+def authenticity_msg(hostname: str, ktype: str, fingerprint: str) -> str:
+    msg = f"""
+    paramiko: The authenticity of host '{hostname}' can't be established.
+    The {ktype} key fingerprint is {fingerprint}.
+    Are you sure you want to continue connecting (yes/no)?
+    """
+    return msg
+
+
+MissingHostKeyPolicy: type = object
+if paramiko:
+    MissingHostKeyPolicy = paramiko.MissingHostKeyPolicy
+
+
+class MyAddPolicy(MissingHostKeyPolicy):
+    """
+    Based on AutoAddPolicy in paramiko so we can determine when keys are added
+
+    and also prompt for input.
+
+    Policy for automatically adding the hostname and new host key to the
+    local L{HostKeys} object, and saving it. This is used by L{SSHClient}.
+    """
+
+    def __init__(self, connection: Connection) -> None:
+        self.connection = connection
+        self._options = connection._options
+
+    def missing_host_key(self, client, hostname, key) -> None:
+
+        if all((self.connection.get_option('host_key_checking'), not self.connection.get_option('host_key_auto_add'))):
+
+            fingerprint = hexlify(key.get_fingerprint())
+            ktype = key.get_name()
+
+            if self.connection.get_option('use_persistent_connections') or self.connection.force_persistence:
+                # don't print the prompt string since the user cannot respond
+                # to the question anyway
+                raise AnsibleError(authenticity_msg(hostname, ktype, fingerprint)[1:92])
+
+            inp = to_text(
+                display.prompt_until(authenticity_msg(hostname, ktype, fingerprint), private=False),
+                errors='surrogate_or_strict'
+            )
+
+            if inp.lower() not in ['yes', 'y', '']:
+                raise AnsibleError('host connection rejected by user')
+
+        key._added_by_ansible_this_time = True
+
+        # existing implementation below:
+        client._host_keys.add(hostname, key.get_name(), key)
+
+        # host keys are actually saved in close() function below
+        # in order to control ordering.
+
+
+class Connection(ConnectionBase):
+    """ SSH based connections (paramiko) to WSL """
+
+    transport = 'community.general.wsl'
+    _log_channel: str | None = None
+
+    def __init__(self, play_context, new_stdin, *args, **kwargs):
+        super(Connection, self).__init__(play_context, new_stdin, *args, **kwargs)
+
+    def _set_log_channel(self, name: str) -> None:
+        """ Mimic paramiko.SSHClient.set_log_channel """
+        self._log_channel = name
+
+    def _parse_proxy_command(self, port: int = 22) -> dict[str, t.Any]:
+        proxy_command = self.get_option('proxy_command') or None
+
+        sock_kwarg = {}
+        if proxy_command:
+            replacers = {
+                '%h': self.get_option('remote_addr'),
+                '%p': port,
+                '%r': self.get_option('remote_user')
+            }
+            for find, replace in replacers.items():
+                proxy_command = proxy_command.replace(find, str(replace))
+            try:
+                sock_kwarg = {'sock': paramiko.ProxyCommand(proxy_command)}
+                display.vvv(f'CONFIGURE PROXY COMMAND FOR CONNECTION: {proxy_command}', host=self.get_option('remote_addr'))
+            except AttributeError:
+                display.warning('Paramiko ProxyCommand support unavailable. '
+                                'Please upgrade to Paramiko 1.9.0 or newer. '
+                                'Not using configured ProxyCommand')
+
+        return sock_kwarg
+
+    def _connect(self) -> Connection:
+        """ activates the connection object """
+
+        if paramiko is None:
+            raise AnsibleError(f'paramiko is not installed: {to_native(PARAMIKO_IMPORT_ERR)}')
+
+        port = self.get_option('port')
+        display.vvv(f'ESTABLISH PARAMIKO SSH CONNECTION FOR USER: {self.get_option("remote_user")} on PORT {to_text(port)} TO {self.get_option("remote_addr")}',
+                    host=self.get_option('remote_addr'))
+
+        ssh = paramiko.SSHClient()
+
+        # Set pubkey and hostkey algorithms to disable, the only manipulation allowed currently
+        # is keeping or omitting rsa-sha2 algorithms
+        # default_keys: t.Tuple[str] = ()
+        paramiko_preferred_pubkeys = getattr(paramiko.Transport, '_preferred_pubkeys', ())
+        paramiko_preferred_hostkeys = getattr(paramiko.Transport, '_preferred_keys', ())
+        use_rsa_sha2_algorithms = self.get_option('use_rsa_sha2_algorithms')
+        disabled_algorithms: t.Dict[str, t.Iterable[str]] = {}
+        if not use_rsa_sha2_algorithms:
+            if paramiko_preferred_pubkeys:
+                disabled_algorithms['pubkeys'] = tuple(a for a in paramiko_preferred_pubkeys if 'rsa-sha2' in a)
+            if paramiko_preferred_hostkeys:
+                disabled_algorithms['keys'] = tuple(a for a in paramiko_preferred_hostkeys if 'rsa-sha2' in a)
+
+        # override paramiko's default logger name
+        if self._log_channel is not None:
+            ssh.set_log_channel(self._log_channel)
+
+        self.keyfile = os.path.expanduser('~/.ssh/known_hosts')
+
+        if self.get_option('host_key_checking'):
+            for ssh_known_hosts in ('/etc/ssh/ssh_known_hosts', '/etc/openssh/ssh_known_hosts'):
+                try:
+                    ssh.load_system_host_keys(ssh_known_hosts)
+                    break
+                except IOError:
+                    pass  # file was not found, but not required to function
+                except paramiko.hostkeys.InvalidHostKey as e:
+                    raise AnsibleConnectionFailure(f'Invalid host key: {to_text(e.line)}')
+            try:
+                ssh.load_system_host_keys()
+            except paramiko.hostkeys.InvalidHostKey as e:
+                raise AnsibleConnectionFailure(f'Invalid host key: {to_text(e.line)}')
+
+        ssh_connect_kwargs = self._parse_proxy_command(port)
+        ssh.set_missing_host_key_policy(MyAddPolicy(self))
+        conn_password = self.get_option('password')
+        allow_agent = True
+
+        if conn_password is not None:
+            allow_agent = False
+
+        try:
+            key_filename = None
+            if self.get_option('private_key_file'):
+                key_filename = os.path.expanduser(self.get_option('private_key_file'))
+
+            # paramiko 2.2 introduced auth_timeout parameter
+            if LooseVersion(paramiko.__version__) >= LooseVersion('2.2.0'):
+                ssh_connect_kwargs['auth_timeout'] = self.get_option('timeout')
+
+            # paramiko 1.15 introduced banner timeout parameter
+            if LooseVersion(paramiko.__version__) >= LooseVersion('1.15.0'):
+                ssh_connect_kwargs['banner_timeout'] = self.get_option('banner_timeout')
+
+            ssh.connect(
+                self.get_option('remote_addr').lower(),
+                username=self.get_option('remote_user'),
+                allow_agent=allow_agent,
+                look_for_keys=self.get_option('look_for_keys'),
+                key_filename=key_filename,
+                password=conn_password,
+                timeout=self.get_option('timeout'),
+                port=port,
+                disabled_algorithms=disabled_algorithms,
+                **ssh_connect_kwargs,
+            )
+        except paramiko.ssh_exception.BadHostKeyException as e:
+            raise AnsibleConnectionFailure(f'host key mismatch for {to_text(e.hostname)}')
+        except paramiko.ssh_exception.AuthenticationException as e:
+            msg = f'Failed to authenticate: {e}'
+            raise AnsibleAuthenticationFailure(msg)
+        except Exception as e:
+            msg = to_text(e)
+            if u'PID check failed' in msg:
+                raise AnsibleError('paramiko version issue, please upgrade paramiko on the machine running ansible')
+            elif u'Private key file is encrypted' in msg:
+                msg = f'ssh {self.get_option("remote_user")}@{self.get_options("remote_addr")}:{port} : ' + \
+                    f'{msg}\nTo connect as a different user, use -u <username>.'
+                raise AnsibleConnectionFailure(msg)
+            else:
+                raise AnsibleConnectionFailure(msg)
+        self.ssh = ssh
+        self._connected = True
+        return self
+
+    def _any_keys_added(self) -> bool:
+        for hostname, keys in self.ssh._host_keys.items():
+            for keytype, key in keys.items():
+                added_this_time = getattr(key, '_added_by_ansible_this_time', False)
+                if added_this_time:
+                    return True
+        return False
+
+    def _save_ssh_host_keys(self, filename: str) -> None:
+        """
+        not using the paramiko save_ssh_host_keys function as we want to add new SSH keys at the bottom so folks
+        don't complain about it :)
+        """
+
+        if not self._any_keys_added():
+            return
+
+        path = os.path.expanduser('~/.ssh')
+        makedirs_safe(path)
+
+        with open(filename, 'w') as f:
+            for hostname, keys in self.ssh._host_keys.items():
+                for keytype, key in keys.items():
+                    # was f.write
+                    added_this_time = getattr(key, '_added_by_ansible_this_time', False)
+                    if not added_this_time:
+                        f.write(f'{hostname} {keytype} {key.get_base64()}\n')
+
+            for hostname, keys in self.ssh._host_keys.items():
+                for keytype, key in keys.items():
+                    added_this_time = getattr(key, '_added_by_ansible_this_time', False)
+                    if added_this_time:
+                        f.write(f'{hostname} {keytype} {key.get_base64()}\n')
+
+    def _build_wsl_command(self, cmd: str) -> str:
+        wsl_distribution = self.get_option('wsl_distribution')
+        if not wsl_distribution:
+            raise AnsibleError('A wsl_distribution is required but none was supplied')
+        become = self.get_option('become')
+        become_user = self.get_option('become_user')
+        if become and become_user:
+            wsl_user = become_user
+        else:
+            wsl_user = self.get_option('wsl_user')
+        args = ['wsl.exe', '--distribution', wsl_distribution]
+        if wsl_user:
+            args.extend(['--user', wsl_user])
+        args.extend(['--'])
+        args.extend(shlex.split(cmd))
+        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 """
+
+        cmd = self._build_wsl_command(cmd)
+
+        super(Connection, self).exec_command(cmd, in_data=in_data, sudoable=sudoable)
+
+        bufsize = 4096
+
+        try:
+            self.ssh.get_transport().set_keepalive(5)
+            chan = self.ssh.get_transport().open_session()
+        except Exception as e:
+            text_e = to_text(e)
+            msg = 'Failed to open session'
+            if text_e:
+                msg += f': {text_e}'
+            raise AnsibleConnectionFailure(to_native(msg))
+
+        display.vvv(f'EXEC {cmd}', host=self.get_option('remote_addr'))
+
+        cmd = to_bytes(cmd, errors='surrogate_or_strict')
+
+        no_prompt_out = b''
+        no_prompt_err = b''
+        become_output = b''
+
+        try:
+            chan.exec_command(cmd)
+            if self.become and self.become.expect_prompt():
+                password_prompt = False
+                become_success = False
+                while not (become_success or password_prompt):
+                    display.debug('Waiting for Privilege Escalation input')
+
+                    chunk = chan.recv(bufsize)
+                    display.debug(f'chunk is: {to_text(chunk)}')
+                    if not chunk:
+                        if b'unknown user' in become_output:
+                            n_become_user = to_native(self.become.get_option('become_user'))
+                            raise AnsibleError(f'user {n_become_user} does not exist')
+                        else:
+                            break
+                            # raise AnsibleError('ssh connection closed waiting for password prompt')
+                    become_output += chunk
+
+                    # need to check every line because we might get lectured
+                    # and we might get the middle of a line in a chunk
+                    for line in become_output.splitlines(True):
+                        if self.become.check_success(line):
+                            become_success = True
+                            break
+                        elif self.become.check_password_prompt(line):
+                            password_prompt = True
+                            break
+
+                if password_prompt:
+                    if self.become:
+                        become_pass = self.become.get_option('become_pass')
+                        chan.sendall(to_bytes(become_pass, errors='surrogate_or_strict') + b'\n')
+                    else:
+                        raise AnsibleError('A password is required but none was supplied')
+                else:
+                    no_prompt_out += become_output
+                    no_prompt_err += become_output
+
+            if in_data:
+                for i in range(0, len(in_data), bufsize):
+                    chan.send(in_data[i:i + bufsize])
+                chan.shutdown_write()
+            elif in_data == b'':
+                chan.shutdown_write()
+
+        except socket.timeout:
+            raise AnsibleError('ssh timed out waiting for privilege escalation.\n' + to_text(become_output))
+
+        stdout = b''.join(chan.makefile('rb', bufsize))
+        stderr = b''.join(chan.makefile_stderr('rb', bufsize))
+        returncode = chan.recv_exit_status()
+
+        # NB the full english error message is:
+        #     'wsl.exe' is not recognized as an internal or external command,
+        #     operable program or batch file.
+        if "'wsl.exe' is not recognized" in stderr.decode('utf-8'):
+            raise AnsibleError(
+                f'wsl.exe not found in path of host: {to_text(self.get_option("remote_addr"))}')
+
+        return (returncode, no_prompt_out + stdout, no_prompt_out + stderr)
+
+    def put_file(self, in_path: str, out_path: str) -> None:
+        """ transfer a file from local to remote """
+
+        display.vvv(f'PUT {in_path} TO {out_path}', host=self.get_option('remote_addr'))
+        try:
+            with open(in_path, 'rb') as f:
+                data = f.read()
+                returncode, stdout, stderr = self.exec_command(
+                    ' '.join([
+                        self._shell.executable, '-c',
+                        self._shell.quote(f'cat > {out_path}')]),
+                    in_data=data,
+                    sudoable=False)
+            if returncode != 0:
+                if 'cat: not found' in stderr.decode('utf-8'):
+                    raise AnsibleError(
+                        f'cat not found in path of WSL distribution: {to_text(self.get_option("wsl_distribution"))}')
+                raise AnsibleError(
+                    f'{to_text(stdout)}\n{to_text(stderr)}')
+        except Exception as e:
+            raise AnsibleError(
+                f'error occurred while putting file from {in_path} to {out_path}!\n{to_text(e)}')
+
+    def fetch_file(self, in_path: str, out_path: str) -> None:
+        """ save a remote file to the specified path """
+
+        display.vvv(f'FETCH {in_path} TO {out_path}', host=self.get_option('remote_addr'))
+        try:
+            returncode, stdout, stderr = self.exec_command(
+                ' '.join([
+                    self._shell.executable, '-c',
+                    self._shell.quote(f'cat {in_path}')]),
+                sudoable=False)
+            if returncode != 0:
+                if 'cat: not found' in stderr.decode('utf-8'):
+                    raise AnsibleError(
+                        f'cat not found in path of WSL distribution: {to_text(self.get_option("wsl_distribution"))}')
+                raise AnsibleError(
+                    f'{to_text(stdout)}\n{to_text(stderr)}')
+            with open(out_path, 'wb') as f:
+                f.write(stdout)
+        except Exception as e:
+            raise AnsibleError(
+                f'error occurred while fetching file from {in_path} to {out_path}!\n{to_text(e)}')
+
+    def reset(self) -> None:
+        """ reset the connection """
+
+        if not self._connected:
+            return
+        self.close()
+        self._connect()
+
+    def close(self) -> None:
+        """ terminate the connection """
+
+        if self.get_option('host_key_checking') and self.get_option('record_host_keys') and self._any_keys_added():
+            # add any new SSH host keys -- warning -- this could be slow
+            # (This doesn't acquire the connection lock because it needs
+            # to exclude only other known_hosts writers, not connections
+            # that are starting up.)
+            lockfile = os.path.basename(self.keyfile)
+            dirname = os.path.dirname(self.keyfile)
+            makedirs_safe(dirname)
+            tmp_keyfile_name = None
+            try:
+                with FileLock().lock_file(lockfile, dirname, self.get_option('lock_file_timeout')):
+                    # just in case any were added recently
+
+                    self.ssh.load_system_host_keys()
+                    self.ssh._host_keys.update(self.ssh._system_host_keys)
+
+                    # gather information about the current key file, so
+                    # we can ensure the new file has the correct mode/owner
+
+                    key_dir = os.path.dirname(self.keyfile)
+                    if os.path.exists(self.keyfile):
+                        key_stat = os.stat(self.keyfile)
+                        mode = key_stat.st_mode & 0o777
+                        uid = key_stat.st_uid
+                        gid = key_stat.st_gid
+                    else:
+                        mode = 0o644
+                        uid = os.getuid()
+                        gid = os.getgid()
+
+                    # Save the new keys to a temporary file and move it into place
+                    # rather than rewriting the file. We set delete=False because
+                    # the file will be moved into place rather than cleaned up.
+
+                    with tempfile.NamedTemporaryFile(dir=key_dir, delete=False) as tmp_keyfile:
+                        tmp_keyfile_name = tmp_keyfile.name
+                        os.chmod(tmp_keyfile_name, mode)
+                        os.chown(tmp_keyfile_name, uid, gid)
+                        self._save_ssh_host_keys(tmp_keyfile_name)
+
+                    os.rename(tmp_keyfile_name, self.keyfile)
path: plugins/connection/wsl.py
user: login: mietzen
body: > It seems, this loads the entire known hosts file, adds the inventory hosts, then saves it to disk. Something that needs to be done in a single transaction, that is, its there to prevent multiple concurrent executions of this section of the code, which seems fine.

Yes this is what it should do.

> Tho, I have no idea weather the same procedure is followed by other apps (e.g. by the ssh command), so it might not protect against those modifying the known hosts file at the same time.

Essentially I just refactored the close method of [paramiko_ssh](https://github.com/ansible/ansible/blob/c02ad3516fced3c06486bd4b920e85f32e8e4f07/lib/ansible/plugins/connection/paramiko_ssh.py#L649).

> Indeed, at a first glance, the last part, seems to implement something similar to `AnsibleModule.atomic_move`.

I didn't knew about `AnsibleModule.atomic_move`, at the time I wrote that code.
created_at: 2025-03-06T07:25:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,758 @@
+# -*- coding: utf-8 -*-
+# Derived from ansible/plugins/connection/proxmox_pct_remote.py (c) 2024 Nils Stein (@mietzen) <github.nstein@mailbox.org>
+# Derived from ansible/plugins/connection/paramiko_ssh.py (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
+# Copyright (c) 2025 Rui Lopes (@rgl) <ruilopes.com>
+# Copyright (c) 2025 Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import annotations
+
+DOCUMENTATION = r"""
+author: Rui Lopes (@rgl) <ruilopes.com>
+name: wsl
+short_description: Run tasks in WSL distribution using wsl.exe CLI via SSH
+requirements:
+  - paramiko
+description:
+  - Run commands or put/fetch files to an existing WSL distribution using wsl.exe CLI via SSH.
+  - Uses the Python SSH implementation (Paramiko) to connect to the WSL host.
+version_added: "10.3.0"
+options:
+  remote_addr:
+    description:
+      - Address of the remote target.
+    default: inventory_hostname
+    type: string
+    vars:
+      - name: inventory_hostname
+      - name: ansible_host
+      - name: ansible_ssh_host
+      - name: ansible_paramiko_host
+  port:
+    description: Remote port to connect to.
+    type: int
+    default: 22
+    ini:
+      - section: defaults
+        key: remote_port
+      - section: paramiko_connection
+        key: remote_port
+    env:
+      - name: ANSIBLE_REMOTE_PORT
+      - name: ANSIBLE_REMOTE_PARAMIKO_PORT
+    vars:
+      - name: ansible_port
+      - name: ansible_ssh_port
+      - name: ansible_paramiko_port
+    keyword:
+      - name: port
+  remote_user:
+    description:
+      - User to login/authenticate as.
+      - Can be set from the CLI via the C(--user) or C(-u) options.
+    type: string
+    vars:
+      - name: ansible_user
+      - name: ansible_ssh_user
+      - name: ansible_paramiko_user
+    env:
+      - name: ANSIBLE_REMOTE_USER
+      - name: ANSIBLE_PARAMIKO_REMOTE_USER
+    ini:
+      - section: defaults
+        key: remote_user
+      - section: paramiko_connection
+        key: remote_user
+    keyword:
+      - name: remote_user
+  password:
+    description:
+      - Secret used to either login the SSH server or as a passphrase for SSH keys that require it.
+      - Can be set from the CLI via the C(--ask-pass) option.
+    type: string
+    vars:
+      - name: ansible_password
+      - name: ansible_ssh_pass
+      - name: ansible_ssh_password
+      - name: ansible_paramiko_pass
+      - name: ansible_paramiko_password
+  use_rsa_sha2_algorithms:
+    description:
+      - Whether or not to enable RSA SHA2 algorithms for pubkeys and hostkeys.
+      - On paramiko versions older than 2.9, this only affects hostkeys.
+      - For behavior matching paramiko<2.9 set this to V(false).
+    vars:
+      - name: ansible_paramiko_use_rsa_sha2_algorithms
+    ini:
+      - {key: use_rsa_sha2_algorithms, section: paramiko_connection}
+    env:
+      - {name: ANSIBLE_PARAMIKO_USE_RSA_SHA2_ALGORITHMS}
+    default: true
+    type: boolean
+  host_key_auto_add:
+    description: "Automatically add host keys to C(~/.ssh/known_hosts)."
+    env:
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_AUTO_ADD
+    ini:
+      - key: host_key_auto_add
+        section: paramiko_connection
+    type: boolean
+  look_for_keys:
+    default: True
+    description: "Set to V(false) to disable searching for private key files in C(~/.ssh/)."
+    env:
+      - name: ANSIBLE_PARAMIKO_LOOK_FOR_KEYS
+    ini:
+      - {key: look_for_keys, section: paramiko_connection}
+    type: boolean
+  proxy_command:
+    default: ""
+    description:
+      - Proxy information for running the connection via a jumphost.
+    type: string
+    env:
+      - name: ANSIBLE_PARAMIKO_PROXY_COMMAND
+    ini:
+      - {key: proxy_command, section: paramiko_connection}
+    vars:
+      - name: ansible_paramiko_proxy_command
+  record_host_keys:
+    default: True
+    description: "Save the host keys to a file."
+    env:
+      - name: ANSIBLE_PARAMIKO_RECORD_HOST_KEYS
+    ini:
+      - section: paramiko_connection
+        key: record_host_keys
+    type: boolean
+  host_key_checking:
+    description: "Set this to V(false) if you want to avoid host key checking by the underlying tools Ansible uses to connect to the host."
+    type: boolean
+    default: true
+    env:
+      - name: ANSIBLE_HOST_KEY_CHECKING
+      - name: ANSIBLE_SSH_HOST_KEY_CHECKING
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_CHECKING
+    ini:
+      - section: defaults
+        key: host_key_checking
+      - section: paramiko_connection
+        key: host_key_checking
+    vars:
+      - name: ansible_host_key_checking
+      - name: ansible_ssh_host_key_checking
+      - name: ansible_paramiko_host_key_checking
+  use_persistent_connections:
+    description: "Toggles the use of persistence for connections."
+    type: boolean
+    default: False
+    env:
+      - name: ANSIBLE_USE_PERSISTENT_CONNECTIONS
+    ini:
+      - section: defaults
+        key: use_persistent_connections
+  banner_timeout:
+    type: float
+    default: 30
+    description:
+      - Configures, in seconds, the amount of time to wait for the SSH
+        banner to be presented. This option is supported by paramiko
+        version 1.15.0 or newer.
+    ini:
+      - section: paramiko_connection
+        key: banner_timeout
+    env:
+      - name: ANSIBLE_PARAMIKO_BANNER_TIMEOUT
+  timeout:
+    type: int
+    default: 10
+    description: Number of seconds until the plugin gives up on failing to establish a TCP connection.
+    ini:
+      - section: defaults
+        key: timeout
+      - section: ssh_connection
+        key: timeout
+      - section: paramiko_connection
+        key: timeout
+    env:
+      - name: ANSIBLE_TIMEOUT
+      - name: ANSIBLE_SSH_TIMEOUT
+      - name: ANSIBLE_PARAMIKO_TIMEOUT
+    vars:
+      - name: ansible_ssh_timeout
+      - name: ansible_paramiko_timeout
+    cli:
+      - name: timeout
+  lock_file_timeout:
+    type: int
+    default: 60
+    description: Number of seconds until the plugin gives up on trying to write a lock file when writing SSH known host keys.
+    vars:
+      - name: ansible_lock_file_timeout
+    env:
+      - name: ANSIBLE_LOCK_FILE_TIMEOUT
+  private_key_file:
+    description:
+      - Path to private key file to use for authentication.
+    type: string
+    ini:
+      - section: defaults
+        key: private_key_file
+      - section: paramiko_connection
+        key: private_key_file
+    env:
+      - name: ANSIBLE_PRIVATE_KEY_FILE
+      - name: ANSIBLE_PARAMIKO_PRIVATE_KEY_FILE
+    vars:
+      - name: ansible_private_key_file
+      - name: ansible_ssh_private_key_file
+      - name: ansible_paramiko_private_key_file
+    cli:
+      - name: private_key_file
+        option: "--private-key"
+  wsl_distribution:
+    description:
+      - WSL distribution name
+    type: string
+    vars:
+      - name: wsl_distribution
+  wsl_user:
+    description:
+      - WSL distribution user
+    type: string
+    vars:
+      - name: wsl_user
+  become_user:
+    description:
+      - WSL distribution user
+    type: string
+    default: root
+    vars:
+      - name: become_user
+      - name: ansible_become_user
+  become:
+    description:
+      - whether to use the user defined by ansible_become_user.
+    type: bool
+    default: false
+    vars:
+      - name: become
+      - name: ansible_become
+"""
+
+EXAMPLES = r"""
+# ------------------------
+# Inventory: inventory.yml
+# ------------------------
+# all:
+#   children:
+#     wsl:
+#       hosts:
+#         ubuntu:
+#           ansible_host: 10.0.0.10
+#           wsl_distribution: ubuntu
+#         debian:
+#           ansible_host: 10.0.0.10
+#           wsl_distribution: debian
+#       vars:
+#         ansible_connection: community.general.wsl
+#         ansible_user: vagrant
+#         wsl_user: wsl
+# ----------------------
+# Playbook: playbook.yml
+# ----------------------
+---
+- hosts: wsl
+  tasks:
+    - name: ping
+      ansible.builtin.ping:
+    - name: id
+      args:
+        executable: /bin/bash
+      ansible.builtin.shell: |
+        exec 2>&1
+        set -x
+        echo "$0"
+        pwd
+        id
+    - name: become id
+      become: true
+      args:
+        executable: /bin/bash
+      ansible.builtin.shell: |
+        exec 2>&1
+        set -x
+        echo "$0"
+        pwd
+        id
+"""
+
+import os
+import pathlib
+import shlex
+import socket
+import tempfile
+import typing as t
+
+from ansible.errors import (
+    AnsibleAuthenticationFailure,
+    AnsibleConnectionFailure,
+    AnsibleError,
+)
+from ansible_collections.community.general.plugins.module_utils._filelock import FileLock, LockTimeout
+from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
+from ansible.module_utils.compat.paramiko import PARAMIKO_IMPORT_ERR, paramiko
+from ansible.module_utils.compat.version import LooseVersion
+from ansible.plugins.connection import ConnectionBase
+from ansible.utils.display import Display
+from ansible.utils.path import makedirs_safe
+from binascii import hexlify
+from subprocess import list2cmdline
+
+
+display = Display()
+
+
+def authenticity_msg(hostname: str, ktype: str, fingerprint: str) -> str:
+    msg = f"""
+    paramiko: The authenticity of host '{hostname}' can't be established.
+    The {ktype} key fingerprint is {fingerprint}.
+    Are you sure you want to continue connecting (yes/no)?
+    """
+    return msg
+
+
+MissingHostKeyPolicy: type = object
+if paramiko:
+    MissingHostKeyPolicy = paramiko.MissingHostKeyPolicy
+
+
+class MyAddPolicy(MissingHostKeyPolicy):
+    """
+    Based on AutoAddPolicy in paramiko so we can determine when keys are added
+
+    and also prompt for input.
+
+    Policy for automatically adding the hostname and new host key to the
+    local L{HostKeys} object, and saving it. This is used by L{SSHClient}.
+    """
+
+    def __init__(self, connection: Connection) -> None:
+        self.connection = connection
+        self._options = connection._options
+
+    def missing_host_key(self, client, hostname, key) -> None:
+
+        if all((self.connection.get_option('host_key_checking'), not self.connection.get_option('host_key_auto_add'))):
+
+            fingerprint = hexlify(key.get_fingerprint())
+            ktype = key.get_name()
+
+            if self.connection.get_option('use_persistent_connections') or self.connection.force_persistence:
+                # don't print the prompt string since the user cannot respond
+                # to the question anyway
+                raise AnsibleError(authenticity_msg(hostname, ktype, fingerprint)[1:92])
+
+            inp = to_text(
+                display.prompt_until(authenticity_msg(hostname, ktype, fingerprint), private=False),
+                errors='surrogate_or_strict'
+            )
+
+            if inp.lower() not in ['yes', 'y', '']:
+                raise AnsibleError('host connection rejected by user')
+
+        key._added_by_ansible_this_time = True
+
+        # existing implementation below:
+        client._host_keys.add(hostname, key.get_name(), key)
+
+        # host keys are actually saved in close() function below
+        # in order to control ordering.
+
+
+class Connection(ConnectionBase):
+    """ SSH based connections (paramiko) to WSL """
+
+    transport = 'community.general.wsl'
+    _log_channel: str | None = None
+
+    def __init__(self, play_context, new_stdin, *args, **kwargs):
+        super(Connection, self).__init__(play_context, new_stdin, *args, **kwargs)
+
+    def _set_log_channel(self, name: str) -> None:
+        """ Mimic paramiko.SSHClient.set_log_channel """
+        self._log_channel = name
+
+    def _parse_proxy_command(self, port: int = 22) -> dict[str, t.Any]:
+        proxy_command = self.get_option('proxy_command') or None
+
+        sock_kwarg = {}
+        if proxy_command:
+            replacers = {
+                '%h': self.get_option('remote_addr'),
+                '%p': port,
+                '%r': self.get_option('remote_user')
+            }
+            for find, replace in replacers.items():
+                proxy_command = proxy_command.replace(find, str(replace))
+            try:
+                sock_kwarg = {'sock': paramiko.ProxyCommand(proxy_command)}
+                display.vvv(f'CONFIGURE PROXY COMMAND FOR CONNECTION: {proxy_command}', host=self.get_option('remote_addr'))
+            except AttributeError:
+                display.warning('Paramiko ProxyCommand support unavailable. '
+                                'Please upgrade to Paramiko 1.9.0 or newer. '
+                                'Not using configured ProxyCommand')
+
+        return sock_kwarg
+
+    def _connect(self) -> Connection:
+        """ activates the connection object """
+
+        if paramiko is None:
+            raise AnsibleError(f'paramiko is not installed: {to_native(PARAMIKO_IMPORT_ERR)}')
+
+        port = self.get_option('port')
+        display.vvv(f'ESTABLISH PARAMIKO SSH CONNECTION FOR USER: {self.get_option("remote_user")} on PORT {to_text(port)} TO {self.get_option("remote_addr")}',
+                    host=self.get_option('remote_addr'))
+
+        ssh = paramiko.SSHClient()
+
+        # Set pubkey and hostkey algorithms to disable, the only manipulation allowed currently
+        # is keeping or omitting rsa-sha2 algorithms
+        # default_keys: t.Tuple[str] = ()
+        paramiko_preferred_pubkeys = getattr(paramiko.Transport, '_preferred_pubkeys', ())
+        paramiko_preferred_hostkeys = getattr(paramiko.Transport, '_preferred_keys', ())
+        use_rsa_sha2_algorithms = self.get_option('use_rsa_sha2_algorithms')
+        disabled_algorithms: t.Dict[str, t.Iterable[str]] = {}
+        if not use_rsa_sha2_algorithms:
+            if paramiko_preferred_pubkeys:
+                disabled_algorithms['pubkeys'] = tuple(a for a in paramiko_preferred_pubkeys if 'rsa-sha2' in a)
+            if paramiko_preferred_hostkeys:
+                disabled_algorithms['keys'] = tuple(a for a in paramiko_preferred_hostkeys if 'rsa-sha2' in a)
+
+        # override paramiko's default logger name
+        if self._log_channel is not None:
+            ssh.set_log_channel(self._log_channel)
+
+        self.keyfile = os.path.expanduser('~/.ssh/known_hosts')
+
+        if self.get_option('host_key_checking'):
+            for ssh_known_hosts in ('/etc/ssh/ssh_known_hosts', '/etc/openssh/ssh_known_hosts'):
+                try:
+                    ssh.load_system_host_keys(ssh_known_hosts)
+                    break
+                except IOError:
+                    pass  # file was not found, but not required to function
+                except paramiko.hostkeys.InvalidHostKey as e:
+                    raise AnsibleConnectionFailure(f'Invalid host key: {to_text(e.line)}')
+            try:
+                ssh.load_system_host_keys()
+            except paramiko.hostkeys.InvalidHostKey as e:
+                raise AnsibleConnectionFailure(f'Invalid host key: {to_text(e.line)}')
+
+        ssh_connect_kwargs = self._parse_proxy_command(port)
+        ssh.set_missing_host_key_policy(MyAddPolicy(self))
+        conn_password = self.get_option('password')
+        allow_agent = True
+
+        if conn_password is not None:
+            allow_agent = False
+
+        try:
+            key_filename = None
+            if self.get_option('private_key_file'):
+                key_filename = os.path.expanduser(self.get_option('private_key_file'))
+
+            # paramiko 2.2 introduced auth_timeout parameter
+            if LooseVersion(paramiko.__version__) >= LooseVersion('2.2.0'):
+                ssh_connect_kwargs['auth_timeout'] = self.get_option('timeout')
+
+            # paramiko 1.15 introduced banner timeout parameter
+            if LooseVersion(paramiko.__version__) >= LooseVersion('1.15.0'):
+                ssh_connect_kwargs['banner_timeout'] = self.get_option('banner_timeout')
+
+            ssh.connect(
+                self.get_option('remote_addr').lower(),
+                username=self.get_option('remote_user'),
+                allow_agent=allow_agent,
+                look_for_keys=self.get_option('look_for_keys'),
+                key_filename=key_filename,
+                password=conn_password,
+                timeout=self.get_option('timeout'),
+                port=port,
+                disabled_algorithms=disabled_algorithms,
+                **ssh_connect_kwargs,
+            )
+        except paramiko.ssh_exception.BadHostKeyException as e:
+            raise AnsibleConnectionFailure(f'host key mismatch for {to_text(e.hostname)}')
+        except paramiko.ssh_exception.AuthenticationException as e:
+            msg = f'Failed to authenticate: {e}'
+            raise AnsibleAuthenticationFailure(msg)
+        except Exception as e:
+            msg = to_text(e)
+            if u'PID check failed' in msg:
+                raise AnsibleError('paramiko version issue, please upgrade paramiko on the machine running ansible')
path: plugins/connection/wsl.py
user: login: mietzen
body: I also copied it from here: [paramiko_ssh](https://github.com/ansible/ansible/blob/c02ad3516fced3c06486bd4b920e85f32e8e4f07/lib/ansible/plugins/connection/paramiko_ssh.py#L456).

I think it would be better to switch to the regular `ssh` plugin.

I based `proxmox_pct_remote` on it because I thought it would be easier to have a pure Python implementation, but I later realized that `ssh` would have been the better choice.  

Refactoring `proxmox_pct_remote` to use `ssh` is on my TODO list, but I won’t be able to do it in the next couple of months.  
created_at: 2025-03-06T07:40:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,758 @@
+# -*- coding: utf-8 -*-
+# Derived from ansible/plugins/connection/proxmox_pct_remote.py (c) 2024 Nils Stein (@mietzen) <github.nstein@mailbox.org>
+# Derived from ansible/plugins/connection/paramiko_ssh.py (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
+# Copyright (c) 2025 Rui Lopes (@rgl) <ruilopes.com>
+# Copyright (c) 2025 Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import annotations
+
+DOCUMENTATION = r"""
+author: Rui Lopes (@rgl) <ruilopes.com>
+name: wsl
+short_description: Run tasks in WSL distribution using wsl.exe CLI via SSH
+requirements:
+  - paramiko
+description:
+  - Run commands or put/fetch files to an existing WSL distribution using wsl.exe CLI via SSH.
+  - Uses the Python SSH implementation (Paramiko) to connect to the WSL host.
+version_added: "10.3.0"
+options:
+  remote_addr:
+    description:
+      - Address of the remote target.
+    default: inventory_hostname
+    type: string
+    vars:
+      - name: inventory_hostname
+      - name: ansible_host
+      - name: ansible_ssh_host
+      - name: ansible_paramiko_host
+  port:
+    description: Remote port to connect to.
+    type: int
+    default: 22
+    ini:
+      - section: defaults
+        key: remote_port
+      - section: paramiko_connection
+        key: remote_port
+    env:
+      - name: ANSIBLE_REMOTE_PORT
+      - name: ANSIBLE_REMOTE_PARAMIKO_PORT
+    vars:
+      - name: ansible_port
+      - name: ansible_ssh_port
+      - name: ansible_paramiko_port
+    keyword:
+      - name: port
+  remote_user:
+    description:
+      - User to login/authenticate as.
+      - Can be set from the CLI via the C(--user) or C(-u) options.
+    type: string
+    vars:
+      - name: ansible_user
+      - name: ansible_ssh_user
+      - name: ansible_paramiko_user
+    env:
+      - name: ANSIBLE_REMOTE_USER
+      - name: ANSIBLE_PARAMIKO_REMOTE_USER
+    ini:
+      - section: defaults
+        key: remote_user
+      - section: paramiko_connection
+        key: remote_user
+    keyword:
+      - name: remote_user
+  password:
+    description:
+      - Secret used to either login the SSH server or as a passphrase for SSH keys that require it.
+      - Can be set from the CLI via the C(--ask-pass) option.
+    type: string
+    vars:
+      - name: ansible_password
+      - name: ansible_ssh_pass
+      - name: ansible_ssh_password
+      - name: ansible_paramiko_pass
+      - name: ansible_paramiko_password
+  use_rsa_sha2_algorithms:
+    description:
+      - Whether or not to enable RSA SHA2 algorithms for pubkeys and hostkeys.
+      - On paramiko versions older than 2.9, this only affects hostkeys.
+      - For behavior matching paramiko<2.9 set this to V(false).
+    vars:
+      - name: ansible_paramiko_use_rsa_sha2_algorithms
+    ini:
+      - {key: use_rsa_sha2_algorithms, section: paramiko_connection}
+    env:
+      - {name: ANSIBLE_PARAMIKO_USE_RSA_SHA2_ALGORITHMS}
+    default: true
+    type: boolean
+  host_key_auto_add:
+    description: "Automatically add host keys to C(~/.ssh/known_hosts)."
+    env:
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_AUTO_ADD
+    ini:
+      - key: host_key_auto_add
+        section: paramiko_connection
+    type: boolean
+  look_for_keys:
+    default: True
+    description: "Set to V(false) to disable searching for private key files in C(~/.ssh/)."
+    env:
+      - name: ANSIBLE_PARAMIKO_LOOK_FOR_KEYS
+    ini:
+      - {key: look_for_keys, section: paramiko_connection}
+    type: boolean
+  proxy_command:
+    default: ""
+    description:
+      - Proxy information for running the connection via a jumphost.
+    type: string
+    env:
+      - name: ANSIBLE_PARAMIKO_PROXY_COMMAND
+    ini:
+      - {key: proxy_command, section: paramiko_connection}
+    vars:
+      - name: ansible_paramiko_proxy_command
+  record_host_keys:
+    default: True
+    description: "Save the host keys to a file."
+    env:
+      - name: ANSIBLE_PARAMIKO_RECORD_HOST_KEYS
+    ini:
+      - section: paramiko_connection
+        key: record_host_keys
+    type: boolean
+  host_key_checking:
+    description: "Set this to V(false) if you want to avoid host key checking by the underlying tools Ansible uses to connect to the host."
+    type: boolean
+    default: true
+    env:
+      - name: ANSIBLE_HOST_KEY_CHECKING
+      - name: ANSIBLE_SSH_HOST_KEY_CHECKING
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_CHECKING
+    ini:
+      - section: defaults
+        key: host_key_checking
+      - section: paramiko_connection
+        key: host_key_checking
+    vars:
+      - name: ansible_host_key_checking
+      - name: ansible_ssh_host_key_checking
+      - name: ansible_paramiko_host_key_checking
+  use_persistent_connections:
+    description: "Toggles the use of persistence for connections."
+    type: boolean
+    default: False
+    env:
+      - name: ANSIBLE_USE_PERSISTENT_CONNECTIONS
+    ini:
+      - section: defaults
+        key: use_persistent_connections
+  banner_timeout:
+    type: float
+    default: 30
+    description:
+      - Configures, in seconds, the amount of time to wait for the SSH
+        banner to be presented. This option is supported by paramiko
+        version 1.15.0 or newer.
+    ini:
+      - section: paramiko_connection
+        key: banner_timeout
+    env:
+      - name: ANSIBLE_PARAMIKO_BANNER_TIMEOUT
+  timeout:
+    type: int
+    default: 10
+    description: Number of seconds until the plugin gives up on failing to establish a TCP connection.
+    ini:
+      - section: defaults
+        key: timeout
+      - section: ssh_connection
+        key: timeout
+      - section: paramiko_connection
+        key: timeout
+    env:
+      - name: ANSIBLE_TIMEOUT
+      - name: ANSIBLE_SSH_TIMEOUT
+      - name: ANSIBLE_PARAMIKO_TIMEOUT
+    vars:
+      - name: ansible_ssh_timeout
+      - name: ansible_paramiko_timeout
+    cli:
+      - name: timeout
+  lock_file_timeout:
+    type: int
+    default: 60
+    description: Number of seconds until the plugin gives up on trying to write a lock file when writing SSH known host keys.
+    vars:
+      - name: ansible_lock_file_timeout
+    env:
+      - name: ANSIBLE_LOCK_FILE_TIMEOUT
+  private_key_file:
+    description:
+      - Path to private key file to use for authentication.
+    type: string
+    ini:
+      - section: defaults
+        key: private_key_file
+      - section: paramiko_connection
+        key: private_key_file
+    env:
+      - name: ANSIBLE_PRIVATE_KEY_FILE
+      - name: ANSIBLE_PARAMIKO_PRIVATE_KEY_FILE
+    vars:
+      - name: ansible_private_key_file
+      - name: ansible_ssh_private_key_file
+      - name: ansible_paramiko_private_key_file
+    cli:
+      - name: private_key_file
+        option: "--private-key"
+  wsl_distribution:
+    description:
+      - WSL distribution name
+    type: string
+    vars:
+      - name: wsl_distribution
+  wsl_user:
+    description:
+      - WSL distribution user
+    type: string
+    vars:
+      - name: wsl_user
+  become_user:
+    description:
+      - WSL distribution user
+    type: string
+    default: root
+    vars:
+      - name: become_user
+      - name: ansible_become_user
+  become:
+    description:
+      - whether to use the user defined by ansible_become_user.
+    type: bool
+    default: false
+    vars:
+      - name: become
+      - name: ansible_become
+"""
+
+EXAMPLES = r"""
+# ------------------------
+# Inventory: inventory.yml
+# ------------------------
+# all:
+#   children:
+#     wsl:
+#       hosts:
+#         ubuntu:
+#           ansible_host: 10.0.0.10
+#           wsl_distribution: ubuntu
+#         debian:
+#           ansible_host: 10.0.0.10
+#           wsl_distribution: debian
+#       vars:
+#         ansible_connection: community.general.wsl
+#         ansible_user: vagrant
+#         wsl_user: wsl
+# ----------------------
+# Playbook: playbook.yml
+# ----------------------
+---
+- hosts: wsl
+  tasks:
+    - name: ping
+      ansible.builtin.ping:
+    - name: id
+      args:
+        executable: /bin/bash
+      ansible.builtin.shell: |
+        exec 2>&1
+        set -x
+        echo "$0"
+        pwd
+        id
+    - name: become id
+      become: true
+      args:
+        executable: /bin/bash
+      ansible.builtin.shell: |
+        exec 2>&1
+        set -x
+        echo "$0"
+        pwd
+        id
+"""
+
+import os
+import pathlib
+import shlex
+import socket
+import tempfile
+import typing as t
+
+from ansible.errors import (
+    AnsibleAuthenticationFailure,
+    AnsibleConnectionFailure,
+    AnsibleError,
+)
+from ansible_collections.community.general.plugins.module_utils._filelock import FileLock, LockTimeout
+from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
+from ansible.module_utils.compat.paramiko import PARAMIKO_IMPORT_ERR, paramiko
+from ansible.module_utils.compat.version import LooseVersion
+from ansible.plugins.connection import ConnectionBase
+from ansible.utils.display import Display
+from ansible.utils.path import makedirs_safe
+from binascii import hexlify
+from subprocess import list2cmdline
+
+
+display = Display()
+
+
+def authenticity_msg(hostname: str, ktype: str, fingerprint: str) -> str:
+    msg = f"""
+    paramiko: The authenticity of host '{hostname}' can't be established.
+    The {ktype} key fingerprint is {fingerprint}.
+    Are you sure you want to continue connecting (yes/no)?
+    """
+    return msg
+
+
+MissingHostKeyPolicy: type = object
+if paramiko:
+    MissingHostKeyPolicy = paramiko.MissingHostKeyPolicy
+
+
+class MyAddPolicy(MissingHostKeyPolicy):
+    """
+    Based on AutoAddPolicy in paramiko so we can determine when keys are added
+
+    and also prompt for input.
+
+    Policy for automatically adding the hostname and new host key to the
+    local L{HostKeys} object, and saving it. This is used by L{SSHClient}.
+    """
+
+    def __init__(self, connection: Connection) -> None:
+        self.connection = connection
+        self._options = connection._options
+
+    def missing_host_key(self, client, hostname, key) -> None:
+
+        if all((self.connection.get_option('host_key_checking'), not self.connection.get_option('host_key_auto_add'))):
+
+            fingerprint = hexlify(key.get_fingerprint())
+            ktype = key.get_name()
+
+            if self.connection.get_option('use_persistent_connections') or self.connection.force_persistence:
+                # don't print the prompt string since the user cannot respond
+                # to the question anyway
+                raise AnsibleError(authenticity_msg(hostname, ktype, fingerprint)[1:92])
+
+            inp = to_text(
+                display.prompt_until(authenticity_msg(hostname, ktype, fingerprint), private=False),
+                errors='surrogate_or_strict'
+            )
+
+            if inp.lower() not in ['yes', 'y', '']:
+                raise AnsibleError('host connection rejected by user')
+
+        key._added_by_ansible_this_time = True
+
+        # existing implementation below:
+        client._host_keys.add(hostname, key.get_name(), key)
+
+        # host keys are actually saved in close() function below
+        # in order to control ordering.
+
+
+class Connection(ConnectionBase):
+    """ SSH based connections (paramiko) to WSL """
+
+    transport = 'community.general.wsl'
+    _log_channel: str | None = None
+
+    def __init__(self, play_context, new_stdin, *args, **kwargs):
+        super(Connection, self).__init__(play_context, new_stdin, *args, **kwargs)
+
+    def _set_log_channel(self, name: str) -> None:
+        """ Mimic paramiko.SSHClient.set_log_channel """
+        self._log_channel = name
+
+    def _parse_proxy_command(self, port: int = 22) -> dict[str, t.Any]:
+        proxy_command = self.get_option('proxy_command') or None
+
+        sock_kwarg = {}
+        if proxy_command:
+            replacers = {
+                '%h': self.get_option('remote_addr'),
+                '%p': port,
+                '%r': self.get_option('remote_user')
+            }
+            for find, replace in replacers.items():
+                proxy_command = proxy_command.replace(find, str(replace))
+            try:
+                sock_kwarg = {'sock': paramiko.ProxyCommand(proxy_command)}
+                display.vvv(f'CONFIGURE PROXY COMMAND FOR CONNECTION: {proxy_command}', host=self.get_option('remote_addr'))
+            except AttributeError:
+                display.warning('Paramiko ProxyCommand support unavailable. '
+                                'Please upgrade to Paramiko 1.9.0 or newer. '
+                                'Not using configured ProxyCommand')
+
+        return sock_kwarg
+
+    def _connect(self) -> Connection:
+        """ activates the connection object """
+
+        if paramiko is None:
+            raise AnsibleError(f'paramiko is not installed: {to_native(PARAMIKO_IMPORT_ERR)}')
+
+        port = self.get_option('port')
+        display.vvv(f'ESTABLISH PARAMIKO SSH CONNECTION FOR USER: {self.get_option("remote_user")} on PORT {to_text(port)} TO {self.get_option("remote_addr")}',
+                    host=self.get_option('remote_addr'))
+
+        ssh = paramiko.SSHClient()
+
+        # Set pubkey and hostkey algorithms to disable, the only manipulation allowed currently
+        # is keeping or omitting rsa-sha2 algorithms
+        # default_keys: t.Tuple[str] = ()
+        paramiko_preferred_pubkeys = getattr(paramiko.Transport, '_preferred_pubkeys', ())
+        paramiko_preferred_hostkeys = getattr(paramiko.Transport, '_preferred_keys', ())
+        use_rsa_sha2_algorithms = self.get_option('use_rsa_sha2_algorithms')
+        disabled_algorithms: t.Dict[str, t.Iterable[str]] = {}
+        if not use_rsa_sha2_algorithms:
+            if paramiko_preferred_pubkeys:
+                disabled_algorithms['pubkeys'] = tuple(a for a in paramiko_preferred_pubkeys if 'rsa-sha2' in a)
+            if paramiko_preferred_hostkeys:
+                disabled_algorithms['keys'] = tuple(a for a in paramiko_preferred_hostkeys if 'rsa-sha2' in a)
+
+        # override paramiko's default logger name
+        if self._log_channel is not None:
+            ssh.set_log_channel(self._log_channel)
+
+        self.keyfile = os.path.expanduser('~/.ssh/known_hosts')
+
+        if self.get_option('host_key_checking'):
+            for ssh_known_hosts in ('/etc/ssh/ssh_known_hosts', '/etc/openssh/ssh_known_hosts'):
+                try:
+                    ssh.load_system_host_keys(ssh_known_hosts)
+                    break
+                except IOError:
+                    pass  # file was not found, but not required to function
path: plugins/connection/wsl.py
user: login: rgl
body: FWIW, that code change was introduced in 2015 by @bcoca in https://github.com/ansible/ansible/commit/b9710b4a3459d3941b8f8ef37c0c294ae344d900 anda its still present in ansible 2.18.3.
created_at: 2025-03-06T09:04:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,791 @@
+# -*- coding: utf-8 -*-
+# Derived from ansible/plugins/connection/proxmox_pct_remote.py (c) 2024 Nils Stein (@mietzen) <github.nstein@mailbox.org>
+# Derived from ansible/plugins/connection/paramiko_ssh.py (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
+# Copyright (c) 2025 Rui Lopes (@rgl) <ruilopes.com>
+# Copyright (c) 2025 Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import annotations
+
+DOCUMENTATION = r"""
+author: Rui Lopes (@rgl) <ruilopes.com>
+name: wsl
+short_description: Run tasks in WSL distribution using wsl.exe CLI via SSH
+requirements:
+  - paramiko
+description:
+  - Run commands or put/fetch files to an existing WSL distribution using wsl.exe CLI via SSH.
+  - Uses the Python SSH implementation (Paramiko) to connect to the WSL host.
+version_added: "10.5.0"
+options:
+  remote_addr:
+    description:
+      - Address of the remote target.
+    default: inventory_hostname
+    type: string
+    vars:
+      - name: inventory_hostname
+      - name: ansible_host
+      - name: ansible_ssh_host
+      - name: ansible_paramiko_host
+  port:
+    description: Remote port to connect to.
+    type: int
+    default: 22
+    ini:
+      - section: defaults
+        key: remote_port
+      - section: paramiko_connection
+        key: remote_port
+    env:
+      - name: ANSIBLE_REMOTE_PORT
+      - name: ANSIBLE_REMOTE_PARAMIKO_PORT
+    vars:
+      - name: ansible_port
+      - name: ansible_ssh_port
+      - name: ansible_paramiko_port
+    keyword:
+      - name: port
+  remote_user:
+    description:
+      - User to login/authenticate as.
+      - Can be set from the CLI via the C(--user) or C(-u) options.
+    type: string
+    vars:
+      - name: ansible_user
+      - name: ansible_ssh_user
+      - name: ansible_paramiko_user
+    env:
+      - name: ANSIBLE_REMOTE_USER
+      - name: ANSIBLE_PARAMIKO_REMOTE_USER
+    ini:
+      - section: defaults
+        key: remote_user
+      - section: paramiko_connection
+        key: remote_user
+    keyword:
+      - name: remote_user
+  password:
+    description:
+      - Secret used to either login the SSH server or as a passphrase for SSH keys that require it.
+      - Can be set from the CLI via the C(--ask-pass) option.
+    type: string
+    vars:
+      - name: ansible_password
+      - name: ansible_ssh_pass
+      - name: ansible_ssh_password
+      - name: ansible_paramiko_pass
+      - name: ansible_paramiko_password
+  use_rsa_sha2_algorithms:
+    description:
+      - Whether or not to enable RSA SHA2 algorithms for pubkeys and hostkeys.
+      - On paramiko versions older than 2.9, this only affects hostkeys.
+      - For behavior matching paramiko<2.9 set this to V(false).
+    vars:
+      - name: ansible_paramiko_use_rsa_sha2_algorithms
+    ini:
+      - {key: use_rsa_sha2_algorithms, section: paramiko_connection}
+    env:
+      - {name: ANSIBLE_PARAMIKO_USE_RSA_SHA2_ALGORITHMS}
+    default: true
+    type: boolean
+  host_key_auto_add:
+    description: "Automatically add host keys to C(~/.ssh/known_hosts)."
+    env:
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_AUTO_ADD
+    ini:
+      - key: host_key_auto_add
+        section: paramiko_connection
+    type: boolean
+  look_for_keys:
+    default: True
+    description: "Set to V(false) to disable searching for private key files in C(~/.ssh/)."
+    env:
+      - name: ANSIBLE_PARAMIKO_LOOK_FOR_KEYS
+    ini:
+      - {key: look_for_keys, section: paramiko_connection}
+    type: boolean
+  proxy_command:
+    default: ""
+    description:
+      - Proxy information for running the connection via a jumphost.
+      - This option is supported by paramiko version 1.9.0 or newer.
+    type: string
+    env:
+      - name: ANSIBLE_PARAMIKO_PROXY_COMMAND
+    ini:
+      - {key: proxy_command, section: paramiko_connection}
+    vars:
+      - name: ansible_paramiko_proxy_command
+  record_host_keys:
+    default: True
+    description: "Save the host keys to a file."
+    env:
+      - name: ANSIBLE_PARAMIKO_RECORD_HOST_KEYS
+    ini:
+      - section: paramiko_connection
+        key: record_host_keys
+    type: boolean
+  host_key_checking:
+    description: "Set this to V(false) if you want to avoid host key checking by the underlying tools Ansible uses to connect to the host."
+    type: boolean
+    default: true
+    env:
+      - name: ANSIBLE_HOST_KEY_CHECKING
+      - name: ANSIBLE_SSH_HOST_KEY_CHECKING
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_CHECKING
+    ini:
+      - section: defaults
+        key: host_key_checking
+      - section: paramiko_connection
+        key: host_key_checking
+    vars:
+      - name: ansible_host_key_checking
+      - name: ansible_ssh_host_key_checking
+      - name: ansible_paramiko_host_key_checking
+  use_persistent_connections:
+    description: "Toggles the use of persistence for connections."
+    type: boolean
+    default: False
+    env:
+      - name: ANSIBLE_USE_PERSISTENT_CONNECTIONS
+    ini:
+      - section: defaults
+        key: use_persistent_connections
+  banner_timeout:
+    type: float
+    default: 30
+    description:
+      - Configures, in seconds, the amount of time to wait for the SSH
+        banner to be presented.
+      - This option is supported by paramiko version 1.15.0 or newer.
+    ini:
+      - section: paramiko_connection
+        key: banner_timeout
+    env:
+      - name: ANSIBLE_PARAMIKO_BANNER_TIMEOUT
+  timeout:
+    type: int
+    default: 10
+    description:
+      - Number of seconds until the plugin gives up on failing to establish a TCP connection.
+      - This option is supported by paramiko version 2.2.0 or newer.
+    ini:
+      - section: defaults
+        key: timeout
+      - section: ssh_connection
+        key: timeout
+      - section: paramiko_connection
+        key: timeout
+    env:
+      - name: ANSIBLE_TIMEOUT
+      - name: ANSIBLE_SSH_TIMEOUT
+      - name: ANSIBLE_PARAMIKO_TIMEOUT
+    vars:
+      - name: ansible_ssh_timeout
+      - name: ansible_paramiko_timeout
+    cli:
+      - name: timeout
+  lock_file_timeout:
+    type: int
+    default: 60
+    description: Number of seconds until the plugin gives up on trying to write a lock file when writing SSH known host keys.
+    vars:
+      - name: ansible_lock_file_timeout
+    env:
+      - name: ANSIBLE_LOCK_FILE_TIMEOUT
+  private_key_file:
+    description:
+      - Path to private key file to use for authentication.
+    type: string
+    ini:
+      - section: defaults
+        key: private_key_file
+      - section: paramiko_connection
+        key: private_key_file
+    env:
+      - name: ANSIBLE_PRIVATE_KEY_FILE
+      - name: ANSIBLE_PARAMIKO_PRIVATE_KEY_FILE
+    vars:
+      - name: ansible_private_key_file
+      - name: ansible_ssh_private_key_file
+      - name: ansible_paramiko_private_key_file
+    cli:
+      - name: private_key_file
+        option: "--private-key"
+  user_known_hosts_file:
+    description:
+      - Path to the user known hosts file.
+      - Used to verify the ssh hosts keys.
+    type: string
+    default: ~/.ssh/known_hosts
+    ini:
+      - section: paramiko_connection
+        key: user_known_hosts_file
+    vars:
+      - name: ansible_paramiko_user_known_hosts_file
+  wsl_distribution:
+    description:
+      - WSL distribution name
+    type: string
+    required: true
+    vars:
+      - name: wsl_distribution
+  wsl_user:
+    description:
+      - WSL distribution user
+    type: string
+    vars:
+      - name: wsl_user
+  become_user:
+    description:
+      - WSL distribution user
+    type: string
+    default: root
+    vars:
+      - name: become_user
+      - name: ansible_become_user
+  become:
+    description:
+      - whether to use the user defined by ansible_become_user.
+    type: bool
+    default: false
+    vars:
+      - name: become
+      - name: ansible_become
+"""
+
+EXAMPLES = r"""
+# ------------------------
+# Inventory: inventory.yml
+# ------------------------
+---
+all:
+  children:
+    wsl:
+      hosts:
+        example-wsl-ubuntu:
+          ansible_host: 10.0.0.10
+          wsl_distribution: ubuntu
+          wsl_user: ubuntu
+      vars:
+        ansible_connection: community.general.wsl
+        ansible_user: vagrant
+# ----------------------
+# Playbook: playbook.yml
+# ----------------------
+---
+- name: WSL Example
+  hosts: wsl
+  gather_facts: true
+  become: true
+  tasks:
+    - name: Ping
+      ansible.builtin.ping:
+    - name: Id (with become false)
+      become: false
+      changed_when: false
+      args:
+        executable: /bin/bash
+      ansible.builtin.shell: |
+        exec 2>&1
+        set -x
+        echo "$0"
+        pwd
+        id
+    - name: Id (with become true)
+      changed_when: false
+      args:
+        executable: /bin/bash
+      ansible.builtin.shell: |
+        exec 2>&1
+        set -x
+        echo "$0"
+        pwd
+        id
+    - name: Reboot
+      ansible.builtin.reboot:
+        boot_time_command: systemctl show -p ActiveEnterTimestamp init.scope
+"""
+
+import io
+import os
+import pathlib
+import shlex
+import socket
+import tempfile
+import typing as t
+
+from ansible.errors import (
+    AnsibleAuthenticationFailure,
+    AnsibleConnectionFailure,
+    AnsibleError,
+)
+from ansible_collections.community.general.plugins.module_utils._filelock import FileLock, LockTimeout
+from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
+from ansible.module_utils.compat.paramiko import PARAMIKO_IMPORT_ERR, paramiko
+from ansible.module_utils.compat.version import LooseVersion
+from ansible.playbook.play_context import PlayContext
+from ansible.plugins.connection import ConnectionBase
+from ansible.utils.display import Display
+from ansible.utils.path import makedirs_safe
+from binascii import hexlify
+from subprocess import list2cmdline
+from paramiko.client import SSHClient
+from paramiko.pkey import PKey
path: plugins/connection/wsl.py
user: login: felixfontein
body: These paramiko imports will not work if paramiko is not present. You must only do them if `paramiko` is truish:
```suggestion

if paramiko:
    from paramiko.client import SSHClient
    from paramiko.pkey import PKey
```
Also since you only need this for typing, you should only import them if type-checking is done:
```suggestion

if t.TYPE_CHECKING and paramiko:
    from paramiko.client import SSHClient
    from paramiko.pkey import PKey
```

created_at: 2025-03-15T07:20:24Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,791 @@
+# -*- coding: utf-8 -*-
+# Derived from ansible/plugins/connection/proxmox_pct_remote.py (c) 2024 Nils Stein (@mietzen) <github.nstein@mailbox.org>
+# Derived from ansible/plugins/connection/paramiko_ssh.py (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
+# Copyright (c) 2025 Rui Lopes (@rgl) <ruilopes.com>
+# Copyright (c) 2025 Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import annotations
+
+DOCUMENTATION = r"""
+author: Rui Lopes (@rgl) <ruilopes.com>
+name: wsl
+short_description: Run tasks in WSL distribution using wsl.exe CLI via SSH
+requirements:
+  - paramiko
+description:
+  - Run commands or put/fetch files to an existing WSL distribution using wsl.exe CLI via SSH.
+  - Uses the Python SSH implementation (Paramiko) to connect to the WSL host.
+version_added: "10.5.0"
+options:
+  remote_addr:
+    description:
+      - Address of the remote target.
+    default: inventory_hostname
+    type: string
+    vars:
+      - name: inventory_hostname
+      - name: ansible_host
+      - name: ansible_ssh_host
+      - name: ansible_paramiko_host
+  port:
+    description: Remote port to connect to.
+    type: int
+    default: 22
+    ini:
+      - section: defaults
+        key: remote_port
+      - section: paramiko_connection
+        key: remote_port
+    env:
+      - name: ANSIBLE_REMOTE_PORT
+      - name: ANSIBLE_REMOTE_PARAMIKO_PORT
+    vars:
+      - name: ansible_port
+      - name: ansible_ssh_port
+      - name: ansible_paramiko_port
+    keyword:
+      - name: port
+  remote_user:
+    description:
+      - User to login/authenticate as.
+      - Can be set from the CLI via the C(--user) or C(-u) options.
+    type: string
+    vars:
+      - name: ansible_user
+      - name: ansible_ssh_user
+      - name: ansible_paramiko_user
+    env:
+      - name: ANSIBLE_REMOTE_USER
+      - name: ANSIBLE_PARAMIKO_REMOTE_USER
+    ini:
+      - section: defaults
+        key: remote_user
+      - section: paramiko_connection
+        key: remote_user
+    keyword:
+      - name: remote_user
+  password:
+    description:
+      - Secret used to either login the SSH server or as a passphrase for SSH keys that require it.
+      - Can be set from the CLI via the C(--ask-pass) option.
+    type: string
+    vars:
+      - name: ansible_password
+      - name: ansible_ssh_pass
+      - name: ansible_ssh_password
+      - name: ansible_paramiko_pass
+      - name: ansible_paramiko_password
+  use_rsa_sha2_algorithms:
+    description:
+      - Whether or not to enable RSA SHA2 algorithms for pubkeys and hostkeys.
+      - On paramiko versions older than 2.9, this only affects hostkeys.
+      - For behavior matching paramiko<2.9 set this to V(false).
+    vars:
+      - name: ansible_paramiko_use_rsa_sha2_algorithms
+    ini:
+      - {key: use_rsa_sha2_algorithms, section: paramiko_connection}
+    env:
+      - {name: ANSIBLE_PARAMIKO_USE_RSA_SHA2_ALGORITHMS}
+    default: true
+    type: boolean
+  host_key_auto_add:
+    description: "Automatically add host keys to C(~/.ssh/known_hosts)."
+    env:
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_AUTO_ADD
+    ini:
+      - key: host_key_auto_add
+        section: paramiko_connection
+    type: boolean
+  look_for_keys:
+    default: True
+    description: "Set to V(false) to disable searching for private key files in C(~/.ssh/)."
+    env:
+      - name: ANSIBLE_PARAMIKO_LOOK_FOR_KEYS
+    ini:
+      - {key: look_for_keys, section: paramiko_connection}
+    type: boolean
+  proxy_command:
+    default: ""
+    description:
+      - Proxy information for running the connection via a jumphost.
+      - This option is supported by paramiko version 1.9.0 or newer.
+    type: string
+    env:
+      - name: ANSIBLE_PARAMIKO_PROXY_COMMAND
+    ini:
+      - {key: proxy_command, section: paramiko_connection}
+    vars:
+      - name: ansible_paramiko_proxy_command
+  record_host_keys:
+    default: True
+    description: "Save the host keys to a file."
+    env:
+      - name: ANSIBLE_PARAMIKO_RECORD_HOST_KEYS
+    ini:
+      - section: paramiko_connection
+        key: record_host_keys
+    type: boolean
+  host_key_checking:
+    description: "Set this to V(false) if you want to avoid host key checking by the underlying tools Ansible uses to connect to the host."
+    type: boolean
+    default: true
+    env:
+      - name: ANSIBLE_HOST_KEY_CHECKING
+      - name: ANSIBLE_SSH_HOST_KEY_CHECKING
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_CHECKING
+    ini:
+      - section: defaults
+        key: host_key_checking
+      - section: paramiko_connection
+        key: host_key_checking
+    vars:
+      - name: ansible_host_key_checking
+      - name: ansible_ssh_host_key_checking
+      - name: ansible_paramiko_host_key_checking
+  use_persistent_connections:
+    description: "Toggles the use of persistence for connections."
+    type: boolean
+    default: False
+    env:
+      - name: ANSIBLE_USE_PERSISTENT_CONNECTIONS
+    ini:
+      - section: defaults
+        key: use_persistent_connections
+  banner_timeout:
+    type: float
+    default: 30
+    description:
+      - Configures, in seconds, the amount of time to wait for the SSH
+        banner to be presented.
+      - This option is supported by paramiko version 1.15.0 or newer.
+    ini:
+      - section: paramiko_connection
+        key: banner_timeout
+    env:
+      - name: ANSIBLE_PARAMIKO_BANNER_TIMEOUT
+  timeout:
+    type: int
+    default: 10
+    description:
+      - Number of seconds until the plugin gives up on failing to establish a TCP connection.
+      - This option is supported by paramiko version 2.2.0 or newer.
+    ini:
+      - section: defaults
+        key: timeout
+      - section: ssh_connection
+        key: timeout
+      - section: paramiko_connection
+        key: timeout
+    env:
+      - name: ANSIBLE_TIMEOUT
+      - name: ANSIBLE_SSH_TIMEOUT
+      - name: ANSIBLE_PARAMIKO_TIMEOUT
+    vars:
+      - name: ansible_ssh_timeout
+      - name: ansible_paramiko_timeout
+    cli:
+      - name: timeout
+  lock_file_timeout:
+    type: int
+    default: 60
+    description: Number of seconds until the plugin gives up on trying to write a lock file when writing SSH known host keys.
+    vars:
+      - name: ansible_lock_file_timeout
+    env:
+      - name: ANSIBLE_LOCK_FILE_TIMEOUT
+  private_key_file:
+    description:
+      - Path to private key file to use for authentication.
+    type: string
+    ini:
+      - section: defaults
+        key: private_key_file
+      - section: paramiko_connection
+        key: private_key_file
+    env:
+      - name: ANSIBLE_PRIVATE_KEY_FILE
+      - name: ANSIBLE_PARAMIKO_PRIVATE_KEY_FILE
+    vars:
+      - name: ansible_private_key_file
+      - name: ansible_ssh_private_key_file
+      - name: ansible_paramiko_private_key_file
+    cli:
+      - name: private_key_file
+        option: "--private-key"
+  user_known_hosts_file:
+    description:
+      - Path to the user known hosts file.
+      - Used to verify the ssh hosts keys.
+    type: string
+    default: ~/.ssh/known_hosts
+    ini:
+      - section: paramiko_connection
+        key: user_known_hosts_file
+    vars:
+      - name: ansible_paramiko_user_known_hosts_file
+  wsl_distribution:
+    description:
+      - WSL distribution name
+    type: string
+    required: true
+    vars:
+      - name: wsl_distribution
+  wsl_user:
+    description:
+      - WSL distribution user
+    type: string
+    vars:
+      - name: wsl_user
+  become_user:
+    description:
+      - WSL distribution user
+    type: string
+    default: root
+    vars:
+      - name: become_user
+      - name: ansible_become_user
+  become:
+    description:
+      - whether to use the user defined by ansible_become_user.
+    type: bool
+    default: false
+    vars:
+      - name: become
+      - name: ansible_become
+"""
+
+EXAMPLES = r"""
+# ------------------------
+# Inventory: inventory.yml
+# ------------------------
+---
+all:
+  children:
+    wsl:
+      hosts:
+        example-wsl-ubuntu:
+          ansible_host: 10.0.0.10
+          wsl_distribution: ubuntu
+          wsl_user: ubuntu
+      vars:
+        ansible_connection: community.general.wsl
+        ansible_user: vagrant
+# ----------------------
+# Playbook: playbook.yml
+# ----------------------
+---
+- name: WSL Example
+  hosts: wsl
+  gather_facts: true
+  become: true
+  tasks:
+    - name: Ping
+      ansible.builtin.ping:
+    - name: Id (with become false)
+      become: false
+      changed_when: false
+      args:
+        executable: /bin/bash
+      ansible.builtin.shell: |
+        exec 2>&1
+        set -x
+        echo "$0"
+        pwd
+        id
+    - name: Id (with become true)
+      changed_when: false
+      args:
+        executable: /bin/bash
+      ansible.builtin.shell: |
+        exec 2>&1
+        set -x
+        echo "$0"
+        pwd
+        id
+    - name: Reboot
+      ansible.builtin.reboot:
+        boot_time_command: systemctl show -p ActiveEnterTimestamp init.scope
+"""
+
+import io
+import os
+import pathlib
+import shlex
+import socket
+import tempfile
+import typing as t
+
+from ansible.errors import (
+    AnsibleAuthenticationFailure,
+    AnsibleConnectionFailure,
+    AnsibleError,
+)
+from ansible_collections.community.general.plugins.module_utils._filelock import FileLock, LockTimeout
+from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
+from ansible.module_utils.compat.paramiko import PARAMIKO_IMPORT_ERR, paramiko
+from ansible.module_utils.compat.version import LooseVersion
+from ansible.playbook.play_context import PlayContext
+from ansible.plugins.connection import ConnectionBase
+from ansible.utils.display import Display
+from ansible.utils.path import makedirs_safe
+from binascii import hexlify
+from subprocess import list2cmdline
+from paramiko.client import SSHClient
+from paramiko.pkey import PKey
path: plugins/connection/wsl.py
user: login: rgl
body: I've made the changes. Please check it now.
created_at: 2025-04-13T11:52:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,589 @@
+# -*- coding: utf-8 -*-
+# Derived from test_proxmox_pct_remote.py (c) 2024 Nils Stein (@mietzen) <github.nstein@mailbox.org>
+# Copyright (c) 2025 Rui Lopes (@rgl) <ruilopes.com>
+# Copyright (c) 2025 Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import (annotations, absolute_import, division, print_function)
+__metaclass__ = type
path: tests/unit/plugins/connection/test_wsl.py
user: login: russoz
body: I believe this is no longer necessary
created_at: 2025-04-13T21:34:56Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,14 @@
+# Copyright (c) 2025 Nils Stein (@mietzen) <github.nstein@mailbox.org>
path: tests/integration/targets/connection_wsl/test_connection.inventory
user: login: russoz
body: I believe this needs adjusting
created_at: 2025-04-13T21:35:21Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,32 @@
+---
+# Copyright (c) Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+- hosts: "{{ target_hosts }}"
+  gather_facts: false
+  serial: 1
path: tests/integration/targets/connection_wsl/plugin-specific-tests.yml
user: login: russoz
body: Is this necessary? If so, why?
created_at: 2025-04-13T21:36:01Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,12 @@
+# Copyright (c) 2025 Nils Stein (@mietzen) <github.nstein@mailbox.org>
+# Copyright (c) 2025 Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+azp/posix/3
+destructive
+needs/root
+needs/target/connection
+skip/docker
+skip/alpine
+skip/macos
path: tests/integration/targets/connection_wsl/aliases
user: login: russoz
body: As far as I understand it, it depends on paramiko, which, in theory, could be installed in any of those platforms. Any specific reason not to test on them?
created_at: 2025-04-13T21:37:22Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,758 @@
+# -*- coding: utf-8 -*-
+# Derived from ansible/plugins/connection/proxmox_pct_remote.py (c) 2024 Nils Stein (@mietzen) <github.nstein@mailbox.org>
+# Derived from ansible/plugins/connection/paramiko_ssh.py (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
+# Copyright (c) 2025 Rui Lopes (@rgl) <ruilopes.com>
+# Copyright (c) 2025 Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import annotations
+
+DOCUMENTATION = r"""
+author: Rui Lopes (@rgl) <ruilopes.com>
+name: wsl
+short_description: Run tasks in WSL distribution using wsl.exe CLI via SSH
+requirements:
+  - paramiko
+description:
+  - Run commands or put/fetch files to an existing WSL distribution using wsl.exe CLI via SSH.
+  - Uses the Python SSH implementation (Paramiko) to connect to the WSL host.
+version_added: "10.3.0"
+options:
+  remote_addr:
+    description:
+      - Address of the remote target.
+    default: inventory_hostname
+    type: string
+    vars:
+      - name: inventory_hostname
+      - name: ansible_host
+      - name: ansible_ssh_host
+      - name: ansible_paramiko_host
+  port:
+    description: Remote port to connect to.
+    type: int
+    default: 22
+    ini:
+      - section: defaults
+        key: remote_port
+      - section: paramiko_connection
+        key: remote_port
+    env:
+      - name: ANSIBLE_REMOTE_PORT
+      - name: ANSIBLE_REMOTE_PARAMIKO_PORT
+    vars:
+      - name: ansible_port
+      - name: ansible_ssh_port
+      - name: ansible_paramiko_port
+    keyword:
+      - name: port
+  remote_user:
+    description:
+      - User to login/authenticate as.
+      - Can be set from the CLI via the C(--user) or C(-u) options.
+    type: string
+    vars:
+      - name: ansible_user
+      - name: ansible_ssh_user
+      - name: ansible_paramiko_user
+    env:
+      - name: ANSIBLE_REMOTE_USER
+      - name: ANSIBLE_PARAMIKO_REMOTE_USER
+    ini:
+      - section: defaults
+        key: remote_user
+      - section: paramiko_connection
+        key: remote_user
+    keyword:
+      - name: remote_user
+  password:
+    description:
+      - Secret used to either login the SSH server or as a passphrase for SSH keys that require it.
+      - Can be set from the CLI via the C(--ask-pass) option.
+    type: string
+    vars:
+      - name: ansible_password
+      - name: ansible_ssh_pass
+      - name: ansible_ssh_password
+      - name: ansible_paramiko_pass
+      - name: ansible_paramiko_password
+  use_rsa_sha2_algorithms:
+    description:
+      - Whether or not to enable RSA SHA2 algorithms for pubkeys and hostkeys.
+      - On paramiko versions older than 2.9, this only affects hostkeys.
+      - For behavior matching paramiko<2.9 set this to V(false).
+    vars:
+      - name: ansible_paramiko_use_rsa_sha2_algorithms
+    ini:
+      - {key: use_rsa_sha2_algorithms, section: paramiko_connection}
+    env:
+      - {name: ANSIBLE_PARAMIKO_USE_RSA_SHA2_ALGORITHMS}
+    default: true
+    type: boolean
+  host_key_auto_add:
+    description: "Automatically add host keys to C(~/.ssh/known_hosts)."
+    env:
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_AUTO_ADD
+    ini:
+      - key: host_key_auto_add
+        section: paramiko_connection
+    type: boolean
+  look_for_keys:
+    default: True
+    description: "Set to V(false) to disable searching for private key files in C(~/.ssh/)."
+    env:
+      - name: ANSIBLE_PARAMIKO_LOOK_FOR_KEYS
+    ini:
+      - {key: look_for_keys, section: paramiko_connection}
+    type: boolean
+  proxy_command:
+    default: ""
+    description:
+      - Proxy information for running the connection via a jumphost.
+    type: string
+    env:
+      - name: ANSIBLE_PARAMIKO_PROXY_COMMAND
+    ini:
+      - {key: proxy_command, section: paramiko_connection}
+    vars:
+      - name: ansible_paramiko_proxy_command
+  record_host_keys:
+    default: True
+    description: "Save the host keys to a file."
+    env:
+      - name: ANSIBLE_PARAMIKO_RECORD_HOST_KEYS
+    ini:
+      - section: paramiko_connection
+        key: record_host_keys
+    type: boolean
+  host_key_checking:
+    description: "Set this to V(false) if you want to avoid host key checking by the underlying tools Ansible uses to connect to the host."
+    type: boolean
+    default: true
+    env:
+      - name: ANSIBLE_HOST_KEY_CHECKING
+      - name: ANSIBLE_SSH_HOST_KEY_CHECKING
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_CHECKING
+    ini:
+      - section: defaults
+        key: host_key_checking
+      - section: paramiko_connection
+        key: host_key_checking
+    vars:
+      - name: ansible_host_key_checking
+      - name: ansible_ssh_host_key_checking
+      - name: ansible_paramiko_host_key_checking
+  use_persistent_connections:
+    description: "Toggles the use of persistence for connections."
+    type: boolean
+    default: False
+    env:
+      - name: ANSIBLE_USE_PERSISTENT_CONNECTIONS
+    ini:
+      - section: defaults
+        key: use_persistent_connections
+  banner_timeout:
+    type: float
+    default: 30
+    description:
+      - Configures, in seconds, the amount of time to wait for the SSH
+        banner to be presented. This option is supported by paramiko
+        version 1.15.0 or newer.
+    ini:
+      - section: paramiko_connection
+        key: banner_timeout
+    env:
+      - name: ANSIBLE_PARAMIKO_BANNER_TIMEOUT
+  timeout:
+    type: int
+    default: 10
+    description: Number of seconds until the plugin gives up on failing to establish a TCP connection.
+    ini:
+      - section: defaults
+        key: timeout
+      - section: ssh_connection
+        key: timeout
+      - section: paramiko_connection
+        key: timeout
+    env:
+      - name: ANSIBLE_TIMEOUT
+      - name: ANSIBLE_SSH_TIMEOUT
+      - name: ANSIBLE_PARAMIKO_TIMEOUT
+    vars:
+      - name: ansible_ssh_timeout
+      - name: ansible_paramiko_timeout
+    cli:
+      - name: timeout
+  lock_file_timeout:
+    type: int
+    default: 60
+    description: Number of seconds until the plugin gives up on trying to write a lock file when writing SSH known host keys.
+    vars:
+      - name: ansible_lock_file_timeout
+    env:
+      - name: ANSIBLE_LOCK_FILE_TIMEOUT
+  private_key_file:
+    description:
+      - Path to private key file to use for authentication.
+    type: string
+    ini:
+      - section: defaults
+        key: private_key_file
+      - section: paramiko_connection
+        key: private_key_file
+    env:
+      - name: ANSIBLE_PRIVATE_KEY_FILE
+      - name: ANSIBLE_PARAMIKO_PRIVATE_KEY_FILE
+    vars:
+      - name: ansible_private_key_file
+      - name: ansible_ssh_private_key_file
+      - name: ansible_paramiko_private_key_file
+    cli:
+      - name: private_key_file
+        option: "--private-key"
+  wsl_distribution:
+    description:
+      - WSL distribution name
+    type: string
+    vars:
+      - name: wsl_distribution
+  wsl_user:
+    description:
+      - WSL distribution user
+    type: string
+    vars:
+      - name: wsl_user
+  become_user:
+    description:
+      - WSL distribution user
+    type: string
+    default: root
+    vars:
+      - name: become_user
+      - name: ansible_become_user
+  become:
+    description:
+      - whether to use the user defined by ansible_become_user.
+    type: bool
+    default: false
+    vars:
+      - name: become
+      - name: ansible_become
+"""
+
+EXAMPLES = r"""
+# ------------------------
+# Inventory: inventory.yml
+# ------------------------
+# all:
+#   children:
+#     wsl:
+#       hosts:
+#         ubuntu:
+#           ansible_host: 10.0.0.10
+#           wsl_distribution: ubuntu
+#         debian:
+#           ansible_host: 10.0.0.10
+#           wsl_distribution: debian
+#       vars:
+#         ansible_connection: community.general.wsl
+#         ansible_user: vagrant
+#         wsl_user: wsl
+# ----------------------
+# Playbook: playbook.yml
+# ----------------------
+---
+- hosts: wsl
+  tasks:
+    - name: ping
+      ansible.builtin.ping:
+    - name: id
+      args:
+        executable: /bin/bash
+      ansible.builtin.shell: |
+        exec 2>&1
+        set -x
+        echo "$0"
+        pwd
+        id
+    - name: become id
+      become: true
+      args:
+        executable: /bin/bash
+      ansible.builtin.shell: |
+        exec 2>&1
+        set -x
+        echo "$0"
+        pwd
+        id
+"""
+
+import os
+import pathlib
+import shlex
+import socket
+import tempfile
+import typing as t
+
+from ansible.errors import (
+    AnsibleAuthenticationFailure,
+    AnsibleConnectionFailure,
+    AnsibleError,
+)
+from ansible_collections.community.general.plugins.module_utils._filelock import FileLock, LockTimeout
+from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
+from ansible.module_utils.compat.paramiko import PARAMIKO_IMPORT_ERR, paramiko
+from ansible.module_utils.compat.version import LooseVersion
+from ansible.plugins.connection import ConnectionBase
+from ansible.utils.display import Display
+from ansible.utils.path import makedirs_safe
+from binascii import hexlify
+from subprocess import list2cmdline
+
+
+display = Display()
+
+
+def authenticity_msg(hostname: str, ktype: str, fingerprint: str) -> str:
+    msg = f"""
+    paramiko: The authenticity of host '{hostname}' can't be established.
+    The {ktype} key fingerprint is {fingerprint}.
+    Are you sure you want to continue connecting (yes/no)?
+    """
+    return msg
+
+
+MissingHostKeyPolicy: type = object
+if paramiko:
+    MissingHostKeyPolicy = paramiko.MissingHostKeyPolicy
+
+
+class MyAddPolicy(MissingHostKeyPolicy):
+    """
+    Based on AutoAddPolicy in paramiko so we can determine when keys are added
+
+    and also prompt for input.
+
+    Policy for automatically adding the hostname and new host key to the
+    local L{HostKeys} object, and saving it. This is used by L{SSHClient}.
+    """
+
+    def __init__(self, connection: Connection) -> None:
+        self.connection = connection
+        self._options = connection._options
+
+    def missing_host_key(self, client, hostname, key) -> None:
+
+        if all((self.connection.get_option('host_key_checking'), not self.connection.get_option('host_key_auto_add'))):
+
+            fingerprint = hexlify(key.get_fingerprint())
+            ktype = key.get_name()
+
+            if self.connection.get_option('use_persistent_connections') or self.connection.force_persistence:
+                # don't print the prompt string since the user cannot respond
+                # to the question anyway
+                raise AnsibleError(authenticity_msg(hostname, ktype, fingerprint)[1:92])
+
+            inp = to_text(
+                display.prompt_until(authenticity_msg(hostname, ktype, fingerprint), private=False),
+                errors='surrogate_or_strict'
+            )
+
+            if inp.lower() not in ['yes', 'y', '']:
+                raise AnsibleError('host connection rejected by user')
+
+        key._added_by_ansible_this_time = True
+
+        # existing implementation below:
+        client._host_keys.add(hostname, key.get_name(), key)
+
+        # host keys are actually saved in close() function below
+        # in order to control ordering.
+
+
+class Connection(ConnectionBase):
+    """ SSH based connections (paramiko) to WSL """
+
+    transport = 'community.general.wsl'
+    _log_channel: str | None = None
+
+    def __init__(self, play_context, new_stdin, *args, **kwargs):
+        super(Connection, self).__init__(play_context, new_stdin, *args, **kwargs)
+
+    def _set_log_channel(self, name: str) -> None:
+        """ Mimic paramiko.SSHClient.set_log_channel """
+        self._log_channel = name
+
+    def _parse_proxy_command(self, port: int = 22) -> dict[str, t.Any]:
+        proxy_command = self.get_option('proxy_command') or None
+
+        sock_kwarg = {}
+        if proxy_command:
+            replacers = {
+                '%h': self.get_option('remote_addr'),
+                '%p': port,
+                '%r': self.get_option('remote_user')
+            }
+            for find, replace in replacers.items():
+                proxy_command = proxy_command.replace(find, str(replace))
+            try:
+                sock_kwarg = {'sock': paramiko.ProxyCommand(proxy_command)}
+                display.vvv(f'CONFIGURE PROXY COMMAND FOR CONNECTION: {proxy_command}', host=self.get_option('remote_addr'))
+            except AttributeError:
+                display.warning('Paramiko ProxyCommand support unavailable. '
+                                'Please upgrade to Paramiko 1.9.0 or newer. '
+                                'Not using configured ProxyCommand')
+
+        return sock_kwarg
+
+    def _connect(self) -> Connection:
+        """ activates the connection object """
+
+        if paramiko is None:
+            raise AnsibleError(f'paramiko is not installed: {to_native(PARAMIKO_IMPORT_ERR)}')
+
+        port = self.get_option('port')
+        display.vvv(f'ESTABLISH PARAMIKO SSH CONNECTION FOR USER: {self.get_option("remote_user")} on PORT {to_text(port)} TO {self.get_option("remote_addr")}',
+                    host=self.get_option('remote_addr'))
+
+        ssh = paramiko.SSHClient()
+
+        # Set pubkey and hostkey algorithms to disable, the only manipulation allowed currently
+        # is keeping or omitting rsa-sha2 algorithms
+        # default_keys: t.Tuple[str] = ()
+        paramiko_preferred_pubkeys = getattr(paramiko.Transport, '_preferred_pubkeys', ())
+        paramiko_preferred_hostkeys = getattr(paramiko.Transport, '_preferred_keys', ())
+        use_rsa_sha2_algorithms = self.get_option('use_rsa_sha2_algorithms')
+        disabled_algorithms: t.Dict[str, t.Iterable[str]] = {}
+        if not use_rsa_sha2_algorithms:
+            if paramiko_preferred_pubkeys:
+                disabled_algorithms['pubkeys'] = tuple(a for a in paramiko_preferred_pubkeys if 'rsa-sha2' in a)
+            if paramiko_preferred_hostkeys:
+                disabled_algorithms['keys'] = tuple(a for a in paramiko_preferred_hostkeys if 'rsa-sha2' in a)
+
+        # override paramiko's default logger name
+        if self._log_channel is not None:
+            ssh.set_log_channel(self._log_channel)
+
+        self.keyfile = os.path.expanduser('~/.ssh/known_hosts')
+
+        if self.get_option('host_key_checking'):
+            for ssh_known_hosts in ('/etc/ssh/ssh_known_hosts', '/etc/openssh/ssh_known_hosts'):
+                try:
+                    ssh.load_system_host_keys(ssh_known_hosts)
+                    break
+                except IOError:
+                    pass  # file was not found, but not required to function
+                except paramiko.hostkeys.InvalidHostKey as e:
+                    raise AnsibleConnectionFailure(f'Invalid host key: {to_text(e.line)}')
+            try:
+                ssh.load_system_host_keys()
+            except paramiko.hostkeys.InvalidHostKey as e:
+                raise AnsibleConnectionFailure(f'Invalid host key: {to_text(e.line)}')
+
+        ssh_connect_kwargs = self._parse_proxy_command(port)
+        ssh.set_missing_host_key_policy(MyAddPolicy(self))
+        conn_password = self.get_option('password')
+        allow_agent = True
+
+        if conn_password is not None:
+            allow_agent = False
+
+        try:
+            key_filename = None
+            if self.get_option('private_key_file'):
+                key_filename = os.path.expanduser(self.get_option('private_key_file'))
+
+            # paramiko 2.2 introduced auth_timeout parameter
+            if LooseVersion(paramiko.__version__) >= LooseVersion('2.2.0'):
+                ssh_connect_kwargs['auth_timeout'] = self.get_option('timeout')
+
+            # paramiko 1.15 introduced banner timeout parameter
+            if LooseVersion(paramiko.__version__) >= LooseVersion('1.15.0'):
+                ssh_connect_kwargs['banner_timeout'] = self.get_option('banner_timeout')
+
+            ssh.connect(
+                self.get_option('remote_addr').lower(),
+                username=self.get_option('remote_user'),
+                allow_agent=allow_agent,
+                look_for_keys=self.get_option('look_for_keys'),
+                key_filename=key_filename,
+                password=conn_password,
+                timeout=self.get_option('timeout'),
+                port=port,
+                disabled_algorithms=disabled_algorithms,
+                **ssh_connect_kwargs,
+            )
+        except paramiko.ssh_exception.BadHostKeyException as e:
+            raise AnsibleConnectionFailure(f'host key mismatch for {to_text(e.hostname)}')
+        except paramiko.ssh_exception.AuthenticationException as e:
+            msg = f'Failed to authenticate: {e}'
+            raise AnsibleAuthenticationFailure(msg)
+        except Exception as e:
+            msg = to_text(e)
+            if u'PID check failed' in msg:
+                raise AnsibleError('paramiko version issue, please upgrade paramiko on the machine running ansible')
+            elif u'Private key file is encrypted' in msg:
+                msg = f'ssh {self.get_option("remote_user")}@{self.get_options("remote_addr")}:{port} : ' + \
+                    f'{msg}\nTo connect as a different user, use -u <username>.'
+                raise AnsibleConnectionFailure(msg)
+            else:
+                raise AnsibleConnectionFailure(msg)
+        self.ssh = ssh
+        self._connected = True
+        return self
+
+    def _any_keys_added(self) -> bool:
+        for hostname, keys in self.ssh._host_keys.items():
+            for keytype, key in keys.items():
+                added_this_time = getattr(key, '_added_by_ansible_this_time', False)
+                if added_this_time:
+                    return True
+        return False
+
+    def _save_ssh_host_keys(self, filename: str) -> None:
+        """
+        not using the paramiko save_ssh_host_keys function as we want to add new SSH keys at the bottom so folks
+        don't complain about it :)
+        """
+
+        if not self._any_keys_added():
+            return
+
+        path = os.path.expanduser('~/.ssh')
+        makedirs_safe(path)
+
+        with open(filename, 'w') as f:
+            for hostname, keys in self.ssh._host_keys.items():
+                for keytype, key in keys.items():
+                    # was f.write
+                    added_this_time = getattr(key, '_added_by_ansible_this_time', False)
+                    if not added_this_time:
+                        f.write(f'{hostname} {keytype} {key.get_base64()}\n')
+
+            for hostname, keys in self.ssh._host_keys.items():
+                for keytype, key in keys.items():
+                    added_this_time = getattr(key, '_added_by_ansible_this_time', False)
+                    if added_this_time:
+                        f.write(f'{hostname} {keytype} {key.get_base64()}\n')
+
+    def _build_wsl_command(self, cmd: str) -> str:
+        wsl_distribution = self.get_option('wsl_distribution')
+        if not wsl_distribution:
+            raise AnsibleError('A wsl_distribution is required but none was supplied')
+        become = self.get_option('become')
+        become_user = self.get_option('become_user')
+        if become and become_user:
+            wsl_user = become_user
+        else:
+            wsl_user = self.get_option('wsl_user')
+        args = ['wsl.exe', '--distribution', wsl_distribution]
+        if wsl_user:
+            args.extend(['--user', wsl_user])
+        args.extend(['--'])
+        args.extend(shlex.split(cmd))
+        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 """
+
+        cmd = self._build_wsl_command(cmd)
+
+        super(Connection, self).exec_command(cmd, in_data=in_data, sudoable=sudoable)
+
+        bufsize = 4096
+
+        try:
+            self.ssh.get_transport().set_keepalive(5)
+            chan = self.ssh.get_transport().open_session()
+        except Exception as e:
+            text_e = to_text(e)
+            msg = 'Failed to open session'
+            if text_e:
+                msg += f': {text_e}'
+            raise AnsibleConnectionFailure(to_native(msg))
+
+        display.vvv(f'EXEC {cmd}', host=self.get_option('remote_addr'))
+
+        cmd = to_bytes(cmd, errors='surrogate_or_strict')
+
+        no_prompt_out = b''
+        no_prompt_err = b''
+        become_output = b''
+
+        try:
+            chan.exec_command(cmd)
+            if self.become and self.become.expect_prompt():
+                password_prompt = False
+                become_success = False
+                while not (become_success or password_prompt):
+                    display.debug('Waiting for Privilege Escalation input')
+
+                    chunk = chan.recv(bufsize)
+                    display.debug(f'chunk is: {to_text(chunk)}')
+                    if not chunk:
+                        if b'unknown user' in become_output:
+                            n_become_user = to_native(self.become.get_option('become_user'))
+                            raise AnsibleError(f'user {n_become_user} does not exist')
+                        else:
+                            break
+                            # raise AnsibleError('ssh connection closed waiting for password prompt')
+                    become_output += chunk
+
+                    # need to check every line because we might get lectured
+                    # and we might get the middle of a line in a chunk
+                    for line in become_output.splitlines(True):
+                        if self.become.check_success(line):
+                            become_success = True
+                            break
+                        elif self.become.check_password_prompt(line):
+                            password_prompt = True
+                            break
+
+                if password_prompt:
+                    if self.become:
+                        become_pass = self.become.get_option('become_pass')
+                        chan.sendall(to_bytes(become_pass, errors='surrogate_or_strict') + b'\n')
+                    else:
+                        raise AnsibleError('A password is required but none was supplied')
+                else:
+                    no_prompt_out += become_output
+                    no_prompt_err += become_output
+
+            if in_data:
+                for i in range(0, len(in_data), bufsize):
+                    chan.send(in_data[i:i + bufsize])
+                chan.shutdown_write()
+            elif in_data == b'':
+                chan.shutdown_write()
+
+        except socket.timeout:
+            raise AnsibleError('ssh timed out waiting for privilege escalation.\n' + to_text(become_output))
+
+        stdout = b''.join(chan.makefile('rb', bufsize))
+        stderr = b''.join(chan.makefile_stderr('rb', bufsize))
+        returncode = chan.recv_exit_status()
+
+        # NB the full english error message is:
path: plugins/connection/wsl.py
user: login: russoz
body: You should set the env vars `LANGUAGE` and  `LC_ALL` to `C`. Just watch out if the command spits out "funny" chars (as in emojis or non-English letters) there might be a little bit of a challenge to adapt this to `C.utf-8` or some other codepage. Some systems will complain about that.
created_at: 2025-04-13T21:44:09Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,14 @@
+# Copyright (c) 2025 Nils Stein (@mietzen) <github.nstein@mailbox.org>
path: tests/integration/targets/connection_wsl/test_connection.inventory
user: login: rgl
body: done. please check it again.
created_at: 2025-04-14T18:38:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,32 @@
+---
+# Copyright (c) Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+- hosts: "{{ target_hosts }}"
+  gather_facts: false
+  serial: 1
path: tests/integration/targets/connection_wsl/plugin-specific-tests.yml
user: login: rgl
body: I'm afraid I do not known. I've removed it, and will see whether the tests still succeed.
created_at: 2025-04-14T18:40:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,12 @@
+# Copyright (c) 2025 Nils Stein (@mietzen) <github.nstein@mailbox.org>
+# Copyright (c) 2025 Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+azp/posix/3
+destructive
+needs/root
+needs/target/connection
+skip/docker
+skip/alpine
+skip/macos
path: tests/integration/targets/connection_wsl/aliases
user: login: rgl
body: I'm afraid I do not known. I've removed it, and will see whether the tests still succeed.
created_at: 2025-04-14T18:40:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,589 @@
+# -*- coding: utf-8 -*-
+# Derived from test_proxmox_pct_remote.py (c) 2024 Nils Stein (@mietzen) <github.nstein@mailbox.org>
+# Copyright (c) 2025 Rui Lopes (@rgl) <ruilopes.com>
+# Copyright (c) 2025 Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import (annotations, absolute_import, division, print_function)
+__metaclass__ = type
path: tests/unit/plugins/connection/test_wsl.py
user: login: rgl
body: what is the minimum python version supported by this project? I think if its 3.7+ I can drop it.
created_at: 2025-04-14T18:42:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,589 @@
+# -*- coding: utf-8 -*-
+# Derived from test_proxmox_pct_remote.py (c) 2024 Nils Stein (@mietzen) <github.nstein@mailbox.org>
+# Copyright (c) 2025 Rui Lopes (@rgl) <ruilopes.com>
+# Copyright (c) 2025 Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import (annotations, absolute_import, division, print_function)
+__metaclass__ = type
path: tests/unit/plugins/connection/test_wsl.py
user: login: rgl
body: this repository tests still cover "CI (Units 2.16 Python 2.7)" and "CI (Units 2.16 Python 3.6)", so I guess, this cannot be removed?
created_at: 2025-04-14T18:47:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,12 @@
+# Copyright (c) 2025 Nils Stein (@mietzen) <github.nstein@mailbox.org>
+# Copyright (c) 2025 Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+azp/posix/3
+destructive
+needs/root
+needs/target/connection
+skip/docker
+skip/alpine
+skip/macos
path: tests/integration/targets/connection_wsl/aliases
user: login: rgl
body: the docker and alpine ones worked.

but, some (and not all), of macOS failed. first because they didn't have write permissions to /usr/sbin, then I've modified it to write to /usr/local/bin, which succeeded, but now /usr/local/bin does not seem to the on the PATH, which is something I'm not sure how to do in macOS. so please advise.
created_at: 2025-04-14T19:14:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,12 @@
+# Copyright (c) 2025 Nils Stein (@mietzen) <github.nstein@mailbox.org>
+# Copyright (c) 2025 Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+azp/posix/3
+destructive
+needs/root
+needs/target/connection
+skip/docker
+skip/alpine
+skip/macos
path: tests/integration/targets/connection_wsl/aliases
user: login: felixfontein
body: I think it's ok to skip macOS.
created_at: 2025-04-14T19:58:43Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,589 @@
+# -*- coding: utf-8 -*-
+# Derived from test_proxmox_pct_remote.py (c) 2024 Nils Stein (@mietzen) <github.nstein@mailbox.org>
+# Copyright (c) 2025 Rui Lopes (@rgl) <ruilopes.com>
+# Copyright (c) 2025 Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import (annotations, absolute_import, division, print_function)
+__metaclass__ = type
path: tests/unit/plugins/connection/test_wsl.py
user: login: felixfontein
body: Connection plugins run in the controller context, and ansible-core 2.15+ only support Python 3.9+ in that context ([ref](https://docs.ansible.com/ansible-core/devel/reference_appendices/release_and_maintenance.html)). Python 2.7 and 3.6 support is for modules and module utils only.
created_at: 2025-04-14T20:00:06Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,589 @@
+# -*- coding: utf-8 -*-
+# Derived from test_proxmox_pct_remote.py (c) 2024 Nils Stein (@mietzen) <github.nstein@mailbox.org>
+# Copyright (c) 2025 Rui Lopes (@rgl) <ruilopes.com>
+# Copyright (c) 2025 Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import (annotations, absolute_import, division, print_function)
+__metaclass__ = type
path: tests/unit/plugins/connection/test_wsl.py
user: login: rgl
body: Ah, nice. I've removed those lines then. Please check it again.
created_at: 2025-04-14T20:27:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,12 @@
+# Copyright (c) 2025 Nils Stein (@mietzen) <github.nstein@mailbox.org>
+# Copyright (c) 2025 Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+azp/posix/3
+destructive
+needs/root
+needs/target/connection
+skip/docker
+skip/alpine
+skip/macos
path: tests/integration/targets/connection_wsl/aliases
user: login: rgl
body: I've added the macOS skip again. Please check it now.
created_at: 2025-04-14T20:30:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,758 @@
+# -*- coding: utf-8 -*-
+# Derived from ansible/plugins/connection/proxmox_pct_remote.py (c) 2024 Nils Stein (@mietzen) <github.nstein@mailbox.org>
+# Derived from ansible/plugins/connection/paramiko_ssh.py (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
+# Copyright (c) 2025 Rui Lopes (@rgl) <ruilopes.com>
+# Copyright (c) 2025 Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import annotations
+
+DOCUMENTATION = r"""
+author: Rui Lopes (@rgl) <ruilopes.com>
+name: wsl
+short_description: Run tasks in WSL distribution using wsl.exe CLI via SSH
+requirements:
+  - paramiko
+description:
+  - Run commands or put/fetch files to an existing WSL distribution using wsl.exe CLI via SSH.
+  - Uses the Python SSH implementation (Paramiko) to connect to the WSL host.
+version_added: "10.3.0"
+options:
+  remote_addr:
+    description:
+      - Address of the remote target.
+    default: inventory_hostname
+    type: string
+    vars:
+      - name: inventory_hostname
+      - name: ansible_host
+      - name: ansible_ssh_host
+      - name: ansible_paramiko_host
+  port:
+    description: Remote port to connect to.
+    type: int
+    default: 22
+    ini:
+      - section: defaults
+        key: remote_port
+      - section: paramiko_connection
+        key: remote_port
+    env:
+      - name: ANSIBLE_REMOTE_PORT
+      - name: ANSIBLE_REMOTE_PARAMIKO_PORT
+    vars:
+      - name: ansible_port
+      - name: ansible_ssh_port
+      - name: ansible_paramiko_port
+    keyword:
+      - name: port
+  remote_user:
+    description:
+      - User to login/authenticate as.
+      - Can be set from the CLI via the C(--user) or C(-u) options.
+    type: string
+    vars:
+      - name: ansible_user
+      - name: ansible_ssh_user
+      - name: ansible_paramiko_user
+    env:
+      - name: ANSIBLE_REMOTE_USER
+      - name: ANSIBLE_PARAMIKO_REMOTE_USER
+    ini:
+      - section: defaults
+        key: remote_user
+      - section: paramiko_connection
+        key: remote_user
+    keyword:
+      - name: remote_user
+  password:
+    description:
+      - Secret used to either login the SSH server or as a passphrase for SSH keys that require it.
+      - Can be set from the CLI via the C(--ask-pass) option.
+    type: string
+    vars:
+      - name: ansible_password
+      - name: ansible_ssh_pass
+      - name: ansible_ssh_password
+      - name: ansible_paramiko_pass
+      - name: ansible_paramiko_password
+  use_rsa_sha2_algorithms:
+    description:
+      - Whether or not to enable RSA SHA2 algorithms for pubkeys and hostkeys.
+      - On paramiko versions older than 2.9, this only affects hostkeys.
+      - For behavior matching paramiko<2.9 set this to V(false).
+    vars:
+      - name: ansible_paramiko_use_rsa_sha2_algorithms
+    ini:
+      - {key: use_rsa_sha2_algorithms, section: paramiko_connection}
+    env:
+      - {name: ANSIBLE_PARAMIKO_USE_RSA_SHA2_ALGORITHMS}
+    default: true
+    type: boolean
+  host_key_auto_add:
+    description: "Automatically add host keys to C(~/.ssh/known_hosts)."
+    env:
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_AUTO_ADD
+    ini:
+      - key: host_key_auto_add
+        section: paramiko_connection
+    type: boolean
+  look_for_keys:
+    default: True
+    description: "Set to V(false) to disable searching for private key files in C(~/.ssh/)."
+    env:
+      - name: ANSIBLE_PARAMIKO_LOOK_FOR_KEYS
+    ini:
+      - {key: look_for_keys, section: paramiko_connection}
+    type: boolean
+  proxy_command:
+    default: ""
+    description:
+      - Proxy information for running the connection via a jumphost.
+    type: string
+    env:
+      - name: ANSIBLE_PARAMIKO_PROXY_COMMAND
+    ini:
+      - {key: proxy_command, section: paramiko_connection}
+    vars:
+      - name: ansible_paramiko_proxy_command
+  record_host_keys:
+    default: True
+    description: "Save the host keys to a file."
+    env:
+      - name: ANSIBLE_PARAMIKO_RECORD_HOST_KEYS
+    ini:
+      - section: paramiko_connection
+        key: record_host_keys
+    type: boolean
+  host_key_checking:
+    description: "Set this to V(false) if you want to avoid host key checking by the underlying tools Ansible uses to connect to the host."
+    type: boolean
+    default: true
+    env:
+      - name: ANSIBLE_HOST_KEY_CHECKING
+      - name: ANSIBLE_SSH_HOST_KEY_CHECKING
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_CHECKING
+    ini:
+      - section: defaults
+        key: host_key_checking
+      - section: paramiko_connection
+        key: host_key_checking
+    vars:
+      - name: ansible_host_key_checking
+      - name: ansible_ssh_host_key_checking
+      - name: ansible_paramiko_host_key_checking
+  use_persistent_connections:
+    description: "Toggles the use of persistence for connections."
+    type: boolean
+    default: False
+    env:
+      - name: ANSIBLE_USE_PERSISTENT_CONNECTIONS
+    ini:
+      - section: defaults
+        key: use_persistent_connections
+  banner_timeout:
+    type: float
+    default: 30
+    description:
+      - Configures, in seconds, the amount of time to wait for the SSH
+        banner to be presented. This option is supported by paramiko
+        version 1.15.0 or newer.
+    ini:
+      - section: paramiko_connection
+        key: banner_timeout
+    env:
+      - name: ANSIBLE_PARAMIKO_BANNER_TIMEOUT
+  timeout:
+    type: int
+    default: 10
+    description: Number of seconds until the plugin gives up on failing to establish a TCP connection.
+    ini:
+      - section: defaults
+        key: timeout
+      - section: ssh_connection
+        key: timeout
+      - section: paramiko_connection
+        key: timeout
+    env:
+      - name: ANSIBLE_TIMEOUT
+      - name: ANSIBLE_SSH_TIMEOUT
+      - name: ANSIBLE_PARAMIKO_TIMEOUT
+    vars:
+      - name: ansible_ssh_timeout
+      - name: ansible_paramiko_timeout
+    cli:
+      - name: timeout
+  lock_file_timeout:
+    type: int
+    default: 60
+    description: Number of seconds until the plugin gives up on trying to write a lock file when writing SSH known host keys.
+    vars:
+      - name: ansible_lock_file_timeout
+    env:
+      - name: ANSIBLE_LOCK_FILE_TIMEOUT
+  private_key_file:
+    description:
+      - Path to private key file to use for authentication.
+    type: string
+    ini:
+      - section: defaults
+        key: private_key_file
+      - section: paramiko_connection
+        key: private_key_file
+    env:
+      - name: ANSIBLE_PRIVATE_KEY_FILE
+      - name: ANSIBLE_PARAMIKO_PRIVATE_KEY_FILE
+    vars:
+      - name: ansible_private_key_file
+      - name: ansible_ssh_private_key_file
+      - name: ansible_paramiko_private_key_file
+    cli:
+      - name: private_key_file
+        option: "--private-key"
+  wsl_distribution:
+    description:
+      - WSL distribution name
+    type: string
+    vars:
+      - name: wsl_distribution
+  wsl_user:
+    description:
+      - WSL distribution user
+    type: string
+    vars:
+      - name: wsl_user
+  become_user:
+    description:
+      - WSL distribution user
+    type: string
+    default: root
+    vars:
+      - name: become_user
+      - name: ansible_become_user
+  become:
+    description:
+      - whether to use the user defined by ansible_become_user.
+    type: bool
+    default: false
+    vars:
+      - name: become
+      - name: ansible_become
+"""
+
+EXAMPLES = r"""
+# ------------------------
+# Inventory: inventory.yml
+# ------------------------
+# all:
+#   children:
+#     wsl:
+#       hosts:
+#         ubuntu:
+#           ansible_host: 10.0.0.10
+#           wsl_distribution: ubuntu
+#         debian:
+#           ansible_host: 10.0.0.10
+#           wsl_distribution: debian
+#       vars:
+#         ansible_connection: community.general.wsl
+#         ansible_user: vagrant
+#         wsl_user: wsl
+# ----------------------
+# Playbook: playbook.yml
+# ----------------------
+---
+- hosts: wsl
+  tasks:
+    - name: ping
+      ansible.builtin.ping:
+    - name: id
+      args:
+        executable: /bin/bash
+      ansible.builtin.shell: |
+        exec 2>&1
+        set -x
+        echo "$0"
+        pwd
+        id
+    - name: become id
+      become: true
+      args:
+        executable: /bin/bash
+      ansible.builtin.shell: |
+        exec 2>&1
+        set -x
+        echo "$0"
+        pwd
+        id
+"""
+
+import os
+import pathlib
+import shlex
+import socket
+import tempfile
+import typing as t
+
+from ansible.errors import (
+    AnsibleAuthenticationFailure,
+    AnsibleConnectionFailure,
+    AnsibleError,
+)
+from ansible_collections.community.general.plugins.module_utils._filelock import FileLock, LockTimeout
+from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
+from ansible.module_utils.compat.paramiko import PARAMIKO_IMPORT_ERR, paramiko
+from ansible.module_utils.compat.version import LooseVersion
+from ansible.plugins.connection import ConnectionBase
+from ansible.utils.display import Display
+from ansible.utils.path import makedirs_safe
+from binascii import hexlify
+from subprocess import list2cmdline
+
+
+display = Display()
+
+
+def authenticity_msg(hostname: str, ktype: str, fingerprint: str) -> str:
+    msg = f"""
+    paramiko: The authenticity of host '{hostname}' can't be established.
+    The {ktype} key fingerprint is {fingerprint}.
+    Are you sure you want to continue connecting (yes/no)?
+    """
+    return msg
+
+
+MissingHostKeyPolicy: type = object
+if paramiko:
+    MissingHostKeyPolicy = paramiko.MissingHostKeyPolicy
+
+
+class MyAddPolicy(MissingHostKeyPolicy):
+    """
+    Based on AutoAddPolicy in paramiko so we can determine when keys are added
+
+    and also prompt for input.
+
+    Policy for automatically adding the hostname and new host key to the
+    local L{HostKeys} object, and saving it. This is used by L{SSHClient}.
+    """
+
+    def __init__(self, connection: Connection) -> None:
+        self.connection = connection
+        self._options = connection._options
+
+    def missing_host_key(self, client, hostname, key) -> None:
+
+        if all((self.connection.get_option('host_key_checking'), not self.connection.get_option('host_key_auto_add'))):
+
+            fingerprint = hexlify(key.get_fingerprint())
+            ktype = key.get_name()
+
+            if self.connection.get_option('use_persistent_connections') or self.connection.force_persistence:
+                # don't print the prompt string since the user cannot respond
+                # to the question anyway
+                raise AnsibleError(authenticity_msg(hostname, ktype, fingerprint)[1:92])
+
+            inp = to_text(
+                display.prompt_until(authenticity_msg(hostname, ktype, fingerprint), private=False),
+                errors='surrogate_or_strict'
+            )
+
+            if inp.lower() not in ['yes', 'y', '']:
+                raise AnsibleError('host connection rejected by user')
+
+        key._added_by_ansible_this_time = True
+
+        # existing implementation below:
+        client._host_keys.add(hostname, key.get_name(), key)
+
+        # host keys are actually saved in close() function below
+        # in order to control ordering.
+
+
+class Connection(ConnectionBase):
+    """ SSH based connections (paramiko) to WSL """
+
+    transport = 'community.general.wsl'
+    _log_channel: str | None = None
+
+    def __init__(self, play_context, new_stdin, *args, **kwargs):
+        super(Connection, self).__init__(play_context, new_stdin, *args, **kwargs)
+
+    def _set_log_channel(self, name: str) -> None:
+        """ Mimic paramiko.SSHClient.set_log_channel """
+        self._log_channel = name
+
+    def _parse_proxy_command(self, port: int = 22) -> dict[str, t.Any]:
+        proxy_command = self.get_option('proxy_command') or None
+
+        sock_kwarg = {}
+        if proxy_command:
+            replacers = {
+                '%h': self.get_option('remote_addr'),
+                '%p': port,
+                '%r': self.get_option('remote_user')
+            }
+            for find, replace in replacers.items():
+                proxy_command = proxy_command.replace(find, str(replace))
+            try:
+                sock_kwarg = {'sock': paramiko.ProxyCommand(proxy_command)}
+                display.vvv(f'CONFIGURE PROXY COMMAND FOR CONNECTION: {proxy_command}', host=self.get_option('remote_addr'))
+            except AttributeError:
+                display.warning('Paramiko ProxyCommand support unavailable. '
+                                'Please upgrade to Paramiko 1.9.0 or newer. '
+                                'Not using configured ProxyCommand')
+
+        return sock_kwarg
+
+    def _connect(self) -> Connection:
+        """ activates the connection object """
+
+        if paramiko is None:
+            raise AnsibleError(f'paramiko is not installed: {to_native(PARAMIKO_IMPORT_ERR)}')
+
+        port = self.get_option('port')
+        display.vvv(f'ESTABLISH PARAMIKO SSH CONNECTION FOR USER: {self.get_option("remote_user")} on PORT {to_text(port)} TO {self.get_option("remote_addr")}',
+                    host=self.get_option('remote_addr'))
+
+        ssh = paramiko.SSHClient()
+
+        # Set pubkey and hostkey algorithms to disable, the only manipulation allowed currently
+        # is keeping or omitting rsa-sha2 algorithms
+        # default_keys: t.Tuple[str] = ()
+        paramiko_preferred_pubkeys = getattr(paramiko.Transport, '_preferred_pubkeys', ())
+        paramiko_preferred_hostkeys = getattr(paramiko.Transport, '_preferred_keys', ())
+        use_rsa_sha2_algorithms = self.get_option('use_rsa_sha2_algorithms')
+        disabled_algorithms: t.Dict[str, t.Iterable[str]] = {}
+        if not use_rsa_sha2_algorithms:
+            if paramiko_preferred_pubkeys:
+                disabled_algorithms['pubkeys'] = tuple(a for a in paramiko_preferred_pubkeys if 'rsa-sha2' in a)
+            if paramiko_preferred_hostkeys:
+                disabled_algorithms['keys'] = tuple(a for a in paramiko_preferred_hostkeys if 'rsa-sha2' in a)
+
+        # override paramiko's default logger name
+        if self._log_channel is not None:
+            ssh.set_log_channel(self._log_channel)
+
+        self.keyfile = os.path.expanduser('~/.ssh/known_hosts')
+
+        if self.get_option('host_key_checking'):
+            for ssh_known_hosts in ('/etc/ssh/ssh_known_hosts', '/etc/openssh/ssh_known_hosts'):
+                try:
+                    ssh.load_system_host_keys(ssh_known_hosts)
+                    break
+                except IOError:
+                    pass  # file was not found, but not required to function
+                except paramiko.hostkeys.InvalidHostKey as e:
+                    raise AnsibleConnectionFailure(f'Invalid host key: {to_text(e.line)}')
+            try:
+                ssh.load_system_host_keys()
+            except paramiko.hostkeys.InvalidHostKey as e:
+                raise AnsibleConnectionFailure(f'Invalid host key: {to_text(e.line)}')
+
+        ssh_connect_kwargs = self._parse_proxy_command(port)
+        ssh.set_missing_host_key_policy(MyAddPolicy(self))
+        conn_password = self.get_option('password')
+        allow_agent = True
+
+        if conn_password is not None:
+            allow_agent = False
+
+        try:
+            key_filename = None
+            if self.get_option('private_key_file'):
+                key_filename = os.path.expanduser(self.get_option('private_key_file'))
+
+            # paramiko 2.2 introduced auth_timeout parameter
+            if LooseVersion(paramiko.__version__) >= LooseVersion('2.2.0'):
+                ssh_connect_kwargs['auth_timeout'] = self.get_option('timeout')
+
+            # paramiko 1.15 introduced banner timeout parameter
+            if LooseVersion(paramiko.__version__) >= LooseVersion('1.15.0'):
+                ssh_connect_kwargs['banner_timeout'] = self.get_option('banner_timeout')
+
+            ssh.connect(
+                self.get_option('remote_addr').lower(),
+                username=self.get_option('remote_user'),
+                allow_agent=allow_agent,
+                look_for_keys=self.get_option('look_for_keys'),
+                key_filename=key_filename,
+                password=conn_password,
+                timeout=self.get_option('timeout'),
+                port=port,
+                disabled_algorithms=disabled_algorithms,
+                **ssh_connect_kwargs,
+            )
+        except paramiko.ssh_exception.BadHostKeyException as e:
+            raise AnsibleConnectionFailure(f'host key mismatch for {to_text(e.hostname)}')
+        except paramiko.ssh_exception.AuthenticationException as e:
+            msg = f'Failed to authenticate: {e}'
+            raise AnsibleAuthenticationFailure(msg)
+        except Exception as e:
+            msg = to_text(e)
+            if u'PID check failed' in msg:
+                raise AnsibleError('paramiko version issue, please upgrade paramiko on the machine running ansible')
+            elif u'Private key file is encrypted' in msg:
+                msg = f'ssh {self.get_option("remote_user")}@{self.get_options("remote_addr")}:{port} : ' + \
+                    f'{msg}\nTo connect as a different user, use -u <username>.'
+                raise AnsibleConnectionFailure(msg)
+            else:
+                raise AnsibleConnectionFailure(msg)
+        self.ssh = ssh
+        self._connected = True
+        return self
+
+    def _any_keys_added(self) -> bool:
+        for hostname, keys in self.ssh._host_keys.items():
+            for keytype, key in keys.items():
+                added_this_time = getattr(key, '_added_by_ansible_this_time', False)
+                if added_this_time:
+                    return True
+        return False
+
+    def _save_ssh_host_keys(self, filename: str) -> None:
+        """
+        not using the paramiko save_ssh_host_keys function as we want to add new SSH keys at the bottom so folks
+        don't complain about it :)
+        """
+
+        if not self._any_keys_added():
+            return
+
+        path = os.path.expanduser('~/.ssh')
+        makedirs_safe(path)
+
+        with open(filename, 'w') as f:
+            for hostname, keys in self.ssh._host_keys.items():
+                for keytype, key in keys.items():
+                    # was f.write
+                    added_this_time = getattr(key, '_added_by_ansible_this_time', False)
+                    if not added_this_time:
+                        f.write(f'{hostname} {keytype} {key.get_base64()}\n')
+
+            for hostname, keys in self.ssh._host_keys.items():
+                for keytype, key in keys.items():
+                    added_this_time = getattr(key, '_added_by_ansible_this_time', False)
+                    if added_this_time:
+                        f.write(f'{hostname} {keytype} {key.get_base64()}\n')
+
+    def _build_wsl_command(self, cmd: str) -> str:
+        wsl_distribution = self.get_option('wsl_distribution')
+        if not wsl_distribution:
+            raise AnsibleError('A wsl_distribution is required but none was supplied')
+        become = self.get_option('become')
+        become_user = self.get_option('become_user')
+        if become and become_user:
+            wsl_user = become_user
+        else:
+            wsl_user = self.get_option('wsl_user')
+        args = ['wsl.exe', '--distribution', wsl_distribution]
+        if wsl_user:
+            args.extend(['--user', wsl_user])
+        args.extend(['--'])
+        args.extend(shlex.split(cmd))
+        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 """
+
+        cmd = self._build_wsl_command(cmd)
+
+        super(Connection, self).exec_command(cmd, in_data=in_data, sudoable=sudoable)
+
+        bufsize = 4096
+
+        try:
+            self.ssh.get_transport().set_keepalive(5)
+            chan = self.ssh.get_transport().open_session()
+        except Exception as e:
+            text_e = to_text(e)
+            msg = 'Failed to open session'
+            if text_e:
+                msg += f': {text_e}'
+            raise AnsibleConnectionFailure(to_native(msg))
+
+        display.vvv(f'EXEC {cmd}', host=self.get_option('remote_addr'))
+
+        cmd = to_bytes(cmd, errors='surrogate_or_strict')
+
+        no_prompt_out = b''
+        no_prompt_err = b''
+        become_output = b''
+
+        try:
+            chan.exec_command(cmd)
+            if self.become and self.become.expect_prompt():
+                password_prompt = False
+                become_success = False
+                while not (become_success or password_prompt):
+                    display.debug('Waiting for Privilege Escalation input')
+
+                    chunk = chan.recv(bufsize)
+                    display.debug(f'chunk is: {to_text(chunk)}')
+                    if not chunk:
+                        if b'unknown user' in become_output:
+                            n_become_user = to_native(self.become.get_option('become_user'))
+                            raise AnsibleError(f'user {n_become_user} does not exist')
+                        else:
+                            break
+                            # raise AnsibleError('ssh connection closed waiting for password prompt')
+                    become_output += chunk
+
+                    # need to check every line because we might get lectured
+                    # and we might get the middle of a line in a chunk
+                    for line in become_output.splitlines(True):
+                        if self.become.check_success(line):
+                            become_success = True
+                            break
+                        elif self.become.check_password_prompt(line):
+                            password_prompt = True
+                            break
+
+                if password_prompt:
+                    if self.become:
+                        become_pass = self.become.get_option('become_pass')
+                        chan.sendall(to_bytes(become_pass, errors='surrogate_or_strict') + b'\n')
+                    else:
+                        raise AnsibleError('A password is required but none was supplied')
+                else:
+                    no_prompt_out += become_output
+                    no_prompt_err += become_output
+
+            if in_data:
+                for i in range(0, len(in_data), bufsize):
+                    chan.send(in_data[i:i + bufsize])
+                chan.shutdown_write()
+            elif in_data == b'':
+                chan.shutdown_write()
+
+        except socket.timeout:
+            raise AnsibleError('ssh timed out waiting for privilege escalation.\n' + to_text(become_output))
+
+        stdout = b''.join(chan.makefile('rb', bufsize))
+        stderr = b''.join(chan.makefile_stderr('rb', bufsize))
+        returncode = chan.recv_exit_status()
+
+        # NB the full english error message is:
path: plugins/connection/wsl.py
user: login: felixfontein
body: Is this actually possible over SSH to Windows?
created_at: 2025-04-15T18:54:09Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,786 @@
+# -*- coding: utf-8 -*-
+# Derived from ansible/plugins/connection/proxmox_pct_remote.py (c) 2024 Nils Stein (@mietzen) <github.nstein@mailbox.org>
+# Derived from ansible/plugins/connection/paramiko_ssh.py (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
+# Copyright (c) 2025 Rui Lopes (@rgl) <ruilopes.com>
+# Copyright (c) 2025 Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import annotations
+
+DOCUMENTATION = r"""
+author: Rui Lopes (@rgl) <ruilopes.com>
+name: wsl
+short_description: Run tasks in WSL distribution using wsl.exe CLI via SSH
+requirements:
+  - paramiko
+description:
+  - Run commands or put/fetch files to an existing WSL distribution using wsl.exe CLI via SSH.
+  - Uses the Python SSH implementation (Paramiko) to connect to the WSL host.
+version_added: "10.5.0"
path: plugins/connection/wsl.py
user: login: russoz
body: ```suggestion
version_added: "10.6.0"
```
created_at: 2025-04-16T21:07:26Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,786 @@
+# -*- coding: utf-8 -*-
+# Derived from ansible/plugins/connection/proxmox_pct_remote.py (c) 2024 Nils Stein (@mietzen) <github.nstein@mailbox.org>
+# Derived from ansible/plugins/connection/paramiko_ssh.py (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
+# Copyright (c) 2025 Rui Lopes (@rgl) <ruilopes.com>
+# Copyright (c) 2025 Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import annotations
+
+DOCUMENTATION = r"""
+author: Rui Lopes (@rgl) <ruilopes.com>
+name: wsl
+short_description: Run tasks in WSL distribution using wsl.exe CLI via SSH
+requirements:
+  - paramiko
+description:
+  - Run commands or put/fetch files to an existing WSL distribution using wsl.exe CLI via SSH.
+  - Uses the Python SSH implementation (Paramiko) to connect to the WSL host.
+version_added: "10.5.0"
+options:
+  remote_addr:
+    description:
+      - Address of the remote target.
+    default: inventory_hostname
+    type: string
+    vars:
+      - name: inventory_hostname
+      - name: ansible_host
+      - name: ansible_ssh_host
+      - name: ansible_paramiko_host
+  port:
+    description: Remote port to connect to.
+    type: int
+    default: 22
+    ini:
+      - section: defaults
+        key: remote_port
+      - section: paramiko_connection
+        key: remote_port
+    env:
+      - name: ANSIBLE_REMOTE_PORT
+      - name: ANSIBLE_REMOTE_PARAMIKO_PORT
+    vars:
+      - name: ansible_port
+      - name: ansible_ssh_port
+      - name: ansible_paramiko_port
+    keyword:
+      - name: port
+  remote_user:
+    description:
+      - User to login/authenticate as.
+      - Can be set from the CLI via the C(--user) or C(-u) options.
+    type: string
+    vars:
+      - name: ansible_user
+      - name: ansible_ssh_user
+      - name: ansible_paramiko_user
+    env:
+      - name: ANSIBLE_REMOTE_USER
+      - name: ANSIBLE_PARAMIKO_REMOTE_USER
+    ini:
+      - section: defaults
+        key: remote_user
+      - section: paramiko_connection
+        key: remote_user
+    keyword:
+      - name: remote_user
+  password:
+    description:
+      - Secret used to either login the SSH server or as a passphrase for SSH keys that require it.
+      - Can be set from the CLI via the C(--ask-pass) option.
+    type: string
+    vars:
+      - name: ansible_password
+      - name: ansible_ssh_pass
+      - name: ansible_ssh_password
+      - name: ansible_paramiko_pass
+      - name: ansible_paramiko_password
+  use_rsa_sha2_algorithms:
+    description:
+      - Whether or not to enable RSA SHA2 algorithms for pubkeys and hostkeys.
+      - On paramiko versions older than 2.9, this only affects hostkeys.
+      - For behavior matching paramiko<2.9 set this to V(false).
+    vars:
+      - name: ansible_paramiko_use_rsa_sha2_algorithms
+    ini:
+      - {key: use_rsa_sha2_algorithms, section: paramiko_connection}
+    env:
+      - {name: ANSIBLE_PARAMIKO_USE_RSA_SHA2_ALGORITHMS}
+    default: true
+    type: boolean
+  host_key_auto_add:
+    description: "Automatically add host keys to C(~/.ssh/known_hosts)."
+    env:
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_AUTO_ADD
+    ini:
+      - key: host_key_auto_add
+        section: paramiko_connection
+    type: boolean
+  look_for_keys:
+    default: True
+    description: "Set to V(false) to disable searching for private key files in C(~/.ssh/)."
+    env:
+      - name: ANSIBLE_PARAMIKO_LOOK_FOR_KEYS
+    ini:
+      - {key: look_for_keys, section: paramiko_connection}
+    type: boolean
+  proxy_command:
+    default: ""
+    description:
+      - Proxy information for running the connection via a jumphost.
+      - This option is supported by paramiko version 1.9.0 or newer.
+    type: string
+    env:
+      - name: ANSIBLE_PARAMIKO_PROXY_COMMAND
+    ini:
+      - {key: proxy_command, section: paramiko_connection}
+    vars:
+      - name: ansible_paramiko_proxy_command
+  record_host_keys:
+    default: True
+    description: "Save the host keys to a file."
+    env:
+      - name: ANSIBLE_PARAMIKO_RECORD_HOST_KEYS
+    ini:
+      - section: paramiko_connection
+        key: record_host_keys
+    type: boolean
+  host_key_checking:
+    description: "Set this to V(false) if you want to avoid host key checking by the underlying tools Ansible uses to connect to the host."
+    type: boolean
+    default: true
+    env:
+      - name: ANSIBLE_HOST_KEY_CHECKING
+      - name: ANSIBLE_SSH_HOST_KEY_CHECKING
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_CHECKING
+    ini:
+      - section: defaults
+        key: host_key_checking
+      - section: paramiko_connection
+        key: host_key_checking
+    vars:
+      - name: ansible_host_key_checking
+      - name: ansible_ssh_host_key_checking
+      - name: ansible_paramiko_host_key_checking
+  use_persistent_connections:
+    description: "Toggles the use of persistence for connections."
+    type: boolean
+    default: False
+    env:
+      - name: ANSIBLE_USE_PERSISTENT_CONNECTIONS
+    ini:
+      - section: defaults
+        key: use_persistent_connections
+  banner_timeout:
+    type: float
+    default: 30
+    description:
+      - Configures, in seconds, the amount of time to wait for the SSH
+        banner to be presented.
+      - This option is supported by paramiko version 1.15.0 or newer.
+    ini:
+      - section: paramiko_connection
+        key: banner_timeout
+    env:
+      - name: ANSIBLE_PARAMIKO_BANNER_TIMEOUT
+  timeout:
+    type: int
+    default: 10
+    description:
+      - Number of seconds until the plugin gives up on failing to establish a TCP connection.
+      - This option is supported by paramiko version 2.2.0 or newer.
+    ini:
+      - section: defaults
+        key: timeout
+      - section: ssh_connection
+        key: timeout
+      - section: paramiko_connection
+        key: timeout
+    env:
+      - name: ANSIBLE_TIMEOUT
+      - name: ANSIBLE_SSH_TIMEOUT
+      - name: ANSIBLE_PARAMIKO_TIMEOUT
+    vars:
+      - name: ansible_ssh_timeout
+      - name: ansible_paramiko_timeout
+    cli:
+      - name: timeout
+  lock_file_timeout:
+    type: int
+    default: 60
+    description: Number of seconds until the plugin gives up on trying to write a lock file when writing SSH known host keys.
+    vars:
+      - name: ansible_lock_file_timeout
+    env:
+      - name: ANSIBLE_LOCK_FILE_TIMEOUT
+  private_key_file:
+    description:
+      - Path to private key file to use for authentication.
+    type: string
+    ini:
+      - section: defaults
+        key: private_key_file
+      - section: paramiko_connection
+        key: private_key_file
+    env:
+      - name: ANSIBLE_PRIVATE_KEY_FILE
+      - name: ANSIBLE_PARAMIKO_PRIVATE_KEY_FILE
+    vars:
+      - name: ansible_private_key_file
+      - name: ansible_ssh_private_key_file
+      - name: ansible_paramiko_private_key_file
+    cli:
+      - name: private_key_file
+        option: "--private-key"
+  user_known_hosts_file:
+    description:
+      - Path to the user known hosts file.
+      - Used to verify the ssh hosts keys.
+    type: string
path: plugins/connection/wsl.py
user: login: russoz
body: Wouldn't be better for this to be
```suggestion
    type: path
```
?

Not sure how/whether that works in connection plugins - it would be a must-do in a module.

Whatever the outcome of this comment here, applies to other "path-like" options in the plugin.
created_at: 2025-04-16T21:10:42Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,758 @@
+# -*- coding: utf-8 -*-
+# Derived from ansible/plugins/connection/proxmox_pct_remote.py (c) 2024 Nils Stein (@mietzen) <github.nstein@mailbox.org>
+# Derived from ansible/plugins/connection/paramiko_ssh.py (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
+# Copyright (c) 2025 Rui Lopes (@rgl) <ruilopes.com>
+# Copyright (c) 2025 Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import annotations
+
+DOCUMENTATION = r"""
+author: Rui Lopes (@rgl) <ruilopes.com>
+name: wsl
+short_description: Run tasks in WSL distribution using wsl.exe CLI via SSH
+requirements:
+  - paramiko
+description:
+  - Run commands or put/fetch files to an existing WSL distribution using wsl.exe CLI via SSH.
+  - Uses the Python SSH implementation (Paramiko) to connect to the WSL host.
+version_added: "10.3.0"
+options:
+  remote_addr:
+    description:
+      - Address of the remote target.
+    default: inventory_hostname
+    type: string
+    vars:
+      - name: inventory_hostname
+      - name: ansible_host
+      - name: ansible_ssh_host
+      - name: ansible_paramiko_host
+  port:
+    description: Remote port to connect to.
+    type: int
+    default: 22
+    ini:
+      - section: defaults
+        key: remote_port
+      - section: paramiko_connection
+        key: remote_port
+    env:
+      - name: ANSIBLE_REMOTE_PORT
+      - name: ANSIBLE_REMOTE_PARAMIKO_PORT
+    vars:
+      - name: ansible_port
+      - name: ansible_ssh_port
+      - name: ansible_paramiko_port
+    keyword:
+      - name: port
+  remote_user:
+    description:
+      - User to login/authenticate as.
+      - Can be set from the CLI via the C(--user) or C(-u) options.
+    type: string
+    vars:
+      - name: ansible_user
+      - name: ansible_ssh_user
+      - name: ansible_paramiko_user
+    env:
+      - name: ANSIBLE_REMOTE_USER
+      - name: ANSIBLE_PARAMIKO_REMOTE_USER
+    ini:
+      - section: defaults
+        key: remote_user
+      - section: paramiko_connection
+        key: remote_user
+    keyword:
+      - name: remote_user
+  password:
+    description:
+      - Secret used to either login the SSH server or as a passphrase for SSH keys that require it.
+      - Can be set from the CLI via the C(--ask-pass) option.
+    type: string
+    vars:
+      - name: ansible_password
+      - name: ansible_ssh_pass
+      - name: ansible_ssh_password
+      - name: ansible_paramiko_pass
+      - name: ansible_paramiko_password
+  use_rsa_sha2_algorithms:
+    description:
+      - Whether or not to enable RSA SHA2 algorithms for pubkeys and hostkeys.
+      - On paramiko versions older than 2.9, this only affects hostkeys.
+      - For behavior matching paramiko<2.9 set this to V(false).
+    vars:
+      - name: ansible_paramiko_use_rsa_sha2_algorithms
+    ini:
+      - {key: use_rsa_sha2_algorithms, section: paramiko_connection}
+    env:
+      - {name: ANSIBLE_PARAMIKO_USE_RSA_SHA2_ALGORITHMS}
+    default: true
+    type: boolean
+  host_key_auto_add:
+    description: "Automatically add host keys to C(~/.ssh/known_hosts)."
+    env:
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_AUTO_ADD
+    ini:
+      - key: host_key_auto_add
+        section: paramiko_connection
+    type: boolean
+  look_for_keys:
+    default: True
+    description: "Set to V(false) to disable searching for private key files in C(~/.ssh/)."
+    env:
+      - name: ANSIBLE_PARAMIKO_LOOK_FOR_KEYS
+    ini:
+      - {key: look_for_keys, section: paramiko_connection}
+    type: boolean
+  proxy_command:
+    default: ""
+    description:
+      - Proxy information for running the connection via a jumphost.
+    type: string
+    env:
+      - name: ANSIBLE_PARAMIKO_PROXY_COMMAND
+    ini:
+      - {key: proxy_command, section: paramiko_connection}
+    vars:
+      - name: ansible_paramiko_proxy_command
+  record_host_keys:
+    default: True
+    description: "Save the host keys to a file."
+    env:
+      - name: ANSIBLE_PARAMIKO_RECORD_HOST_KEYS
+    ini:
+      - section: paramiko_connection
+        key: record_host_keys
+    type: boolean
+  host_key_checking:
+    description: "Set this to V(false) if you want to avoid host key checking by the underlying tools Ansible uses to connect to the host."
+    type: boolean
+    default: true
+    env:
+      - name: ANSIBLE_HOST_KEY_CHECKING
+      - name: ANSIBLE_SSH_HOST_KEY_CHECKING
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_CHECKING
+    ini:
+      - section: defaults
+        key: host_key_checking
+      - section: paramiko_connection
+        key: host_key_checking
+    vars:
+      - name: ansible_host_key_checking
+      - name: ansible_ssh_host_key_checking
+      - name: ansible_paramiko_host_key_checking
+  use_persistent_connections:
+    description: "Toggles the use of persistence for connections."
+    type: boolean
+    default: False
+    env:
+      - name: ANSIBLE_USE_PERSISTENT_CONNECTIONS
+    ini:
+      - section: defaults
+        key: use_persistent_connections
+  banner_timeout:
+    type: float
+    default: 30
+    description:
+      - Configures, in seconds, the amount of time to wait for the SSH
+        banner to be presented. This option is supported by paramiko
+        version 1.15.0 or newer.
+    ini:
+      - section: paramiko_connection
+        key: banner_timeout
+    env:
+      - name: ANSIBLE_PARAMIKO_BANNER_TIMEOUT
+  timeout:
+    type: int
+    default: 10
+    description: Number of seconds until the plugin gives up on failing to establish a TCP connection.
+    ini:
+      - section: defaults
+        key: timeout
+      - section: ssh_connection
+        key: timeout
+      - section: paramiko_connection
+        key: timeout
+    env:
+      - name: ANSIBLE_TIMEOUT
+      - name: ANSIBLE_SSH_TIMEOUT
+      - name: ANSIBLE_PARAMIKO_TIMEOUT
+    vars:
+      - name: ansible_ssh_timeout
+      - name: ansible_paramiko_timeout
+    cli:
+      - name: timeout
+  lock_file_timeout:
+    type: int
+    default: 60
+    description: Number of seconds until the plugin gives up on trying to write a lock file when writing SSH known host keys.
+    vars:
+      - name: ansible_lock_file_timeout
+    env:
+      - name: ANSIBLE_LOCK_FILE_TIMEOUT
+  private_key_file:
+    description:
+      - Path to private key file to use for authentication.
+    type: string
+    ini:
+      - section: defaults
+        key: private_key_file
+      - section: paramiko_connection
+        key: private_key_file
+    env:
+      - name: ANSIBLE_PRIVATE_KEY_FILE
+      - name: ANSIBLE_PARAMIKO_PRIVATE_KEY_FILE
+    vars:
+      - name: ansible_private_key_file
+      - name: ansible_ssh_private_key_file
+      - name: ansible_paramiko_private_key_file
+    cli:
+      - name: private_key_file
+        option: "--private-key"
+  wsl_distribution:
+    description:
+      - WSL distribution name
+    type: string
+    vars:
+      - name: wsl_distribution
+  wsl_user:
+    description:
+      - WSL distribution user
+    type: string
+    vars:
+      - name: wsl_user
+  become_user:
+    description:
+      - WSL distribution user
+    type: string
+    default: root
+    vars:
+      - name: become_user
+      - name: ansible_become_user
+  become:
+    description:
+      - whether to use the user defined by ansible_become_user.
+    type: bool
+    default: false
+    vars:
+      - name: become
+      - name: ansible_become
+"""
+
+EXAMPLES = r"""
+# ------------------------
+# Inventory: inventory.yml
+# ------------------------
+# all:
+#   children:
+#     wsl:
+#       hosts:
+#         ubuntu:
+#           ansible_host: 10.0.0.10
+#           wsl_distribution: ubuntu
+#         debian:
+#           ansible_host: 10.0.0.10
+#           wsl_distribution: debian
+#       vars:
+#         ansible_connection: community.general.wsl
+#         ansible_user: vagrant
+#         wsl_user: wsl
+# ----------------------
+# Playbook: playbook.yml
+# ----------------------
+---
+- hosts: wsl
+  tasks:
+    - name: ping
+      ansible.builtin.ping:
+    - name: id
+      args:
+        executable: /bin/bash
+      ansible.builtin.shell: |
+        exec 2>&1
+        set -x
+        echo "$0"
+        pwd
+        id
+    - name: become id
+      become: true
+      args:
+        executable: /bin/bash
+      ansible.builtin.shell: |
+        exec 2>&1
+        set -x
+        echo "$0"
+        pwd
+        id
+"""
+
+import os
+import pathlib
+import shlex
+import socket
+import tempfile
+import typing as t
+
+from ansible.errors import (
+    AnsibleAuthenticationFailure,
+    AnsibleConnectionFailure,
+    AnsibleError,
+)
+from ansible_collections.community.general.plugins.module_utils._filelock import FileLock, LockTimeout
+from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
+from ansible.module_utils.compat.paramiko import PARAMIKO_IMPORT_ERR, paramiko
+from ansible.module_utils.compat.version import LooseVersion
+from ansible.plugins.connection import ConnectionBase
+from ansible.utils.display import Display
+from ansible.utils.path import makedirs_safe
+from binascii import hexlify
+from subprocess import list2cmdline
+
+
+display = Display()
+
+
+def authenticity_msg(hostname: str, ktype: str, fingerprint: str) -> str:
+    msg = f"""
+    paramiko: The authenticity of host '{hostname}' can't be established.
+    The {ktype} key fingerprint is {fingerprint}.
+    Are you sure you want to continue connecting (yes/no)?
+    """
+    return msg
+
+
+MissingHostKeyPolicy: type = object
+if paramiko:
+    MissingHostKeyPolicy = paramiko.MissingHostKeyPolicy
+
+
+class MyAddPolicy(MissingHostKeyPolicy):
+    """
+    Based on AutoAddPolicy in paramiko so we can determine when keys are added
+
+    and also prompt for input.
+
+    Policy for automatically adding the hostname and new host key to the
+    local L{HostKeys} object, and saving it. This is used by L{SSHClient}.
+    """
+
+    def __init__(self, connection: Connection) -> None:
+        self.connection = connection
+        self._options = connection._options
+
+    def missing_host_key(self, client, hostname, key) -> None:
+
+        if all((self.connection.get_option('host_key_checking'), not self.connection.get_option('host_key_auto_add'))):
+
+            fingerprint = hexlify(key.get_fingerprint())
+            ktype = key.get_name()
+
+            if self.connection.get_option('use_persistent_connections') or self.connection.force_persistence:
+                # don't print the prompt string since the user cannot respond
+                # to the question anyway
+                raise AnsibleError(authenticity_msg(hostname, ktype, fingerprint)[1:92])
+
+            inp = to_text(
+                display.prompt_until(authenticity_msg(hostname, ktype, fingerprint), private=False),
+                errors='surrogate_or_strict'
+            )
+
+            if inp.lower() not in ['yes', 'y', '']:
+                raise AnsibleError('host connection rejected by user')
+
+        key._added_by_ansible_this_time = True
+
+        # existing implementation below:
+        client._host_keys.add(hostname, key.get_name(), key)
+
+        # host keys are actually saved in close() function below
+        # in order to control ordering.
+
+
+class Connection(ConnectionBase):
+    """ SSH based connections (paramiko) to WSL """
+
+    transport = 'community.general.wsl'
+    _log_channel: str | None = None
+
+    def __init__(self, play_context, new_stdin, *args, **kwargs):
+        super(Connection, self).__init__(play_context, new_stdin, *args, **kwargs)
+
+    def _set_log_channel(self, name: str) -> None:
+        """ Mimic paramiko.SSHClient.set_log_channel """
+        self._log_channel = name
+
+    def _parse_proxy_command(self, port: int = 22) -> dict[str, t.Any]:
+        proxy_command = self.get_option('proxy_command') or None
+
+        sock_kwarg = {}
+        if proxy_command:
+            replacers = {
+                '%h': self.get_option('remote_addr'),
+                '%p': port,
+                '%r': self.get_option('remote_user')
+            }
+            for find, replace in replacers.items():
+                proxy_command = proxy_command.replace(find, str(replace))
+            try:
+                sock_kwarg = {'sock': paramiko.ProxyCommand(proxy_command)}
+                display.vvv(f'CONFIGURE PROXY COMMAND FOR CONNECTION: {proxy_command}', host=self.get_option('remote_addr'))
+            except AttributeError:
+                display.warning('Paramiko ProxyCommand support unavailable. '
+                                'Please upgrade to Paramiko 1.9.0 or newer. '
+                                'Not using configured ProxyCommand')
+
+        return sock_kwarg
+
+    def _connect(self) -> Connection:
+        """ activates the connection object """
+
+        if paramiko is None:
+            raise AnsibleError(f'paramiko is not installed: {to_native(PARAMIKO_IMPORT_ERR)}')
+
+        port = self.get_option('port')
+        display.vvv(f'ESTABLISH PARAMIKO SSH CONNECTION FOR USER: {self.get_option("remote_user")} on PORT {to_text(port)} TO {self.get_option("remote_addr")}',
+                    host=self.get_option('remote_addr'))
+
+        ssh = paramiko.SSHClient()
+
+        # Set pubkey and hostkey algorithms to disable, the only manipulation allowed currently
+        # is keeping or omitting rsa-sha2 algorithms
+        # default_keys: t.Tuple[str] = ()
+        paramiko_preferred_pubkeys = getattr(paramiko.Transport, '_preferred_pubkeys', ())
+        paramiko_preferred_hostkeys = getattr(paramiko.Transport, '_preferred_keys', ())
+        use_rsa_sha2_algorithms = self.get_option('use_rsa_sha2_algorithms')
+        disabled_algorithms: t.Dict[str, t.Iterable[str]] = {}
+        if not use_rsa_sha2_algorithms:
+            if paramiko_preferred_pubkeys:
+                disabled_algorithms['pubkeys'] = tuple(a for a in paramiko_preferred_pubkeys if 'rsa-sha2' in a)
+            if paramiko_preferred_hostkeys:
+                disabled_algorithms['keys'] = tuple(a for a in paramiko_preferred_hostkeys if 'rsa-sha2' in a)
+
+        # override paramiko's default logger name
+        if self._log_channel is not None:
+            ssh.set_log_channel(self._log_channel)
+
+        self.keyfile = os.path.expanduser('~/.ssh/known_hosts')
+
+        if self.get_option('host_key_checking'):
+            for ssh_known_hosts in ('/etc/ssh/ssh_known_hosts', '/etc/openssh/ssh_known_hosts'):
+                try:
+                    ssh.load_system_host_keys(ssh_known_hosts)
+                    break
+                except IOError:
+                    pass  # file was not found, but not required to function
+                except paramiko.hostkeys.InvalidHostKey as e:
+                    raise AnsibleConnectionFailure(f'Invalid host key: {to_text(e.line)}')
+            try:
+                ssh.load_system_host_keys()
+            except paramiko.hostkeys.InvalidHostKey as e:
+                raise AnsibleConnectionFailure(f'Invalid host key: {to_text(e.line)}')
+
+        ssh_connect_kwargs = self._parse_proxy_command(port)
+        ssh.set_missing_host_key_policy(MyAddPolicy(self))
+        conn_password = self.get_option('password')
+        allow_agent = True
+
+        if conn_password is not None:
+            allow_agent = False
+
+        try:
+            key_filename = None
+            if self.get_option('private_key_file'):
+                key_filename = os.path.expanduser(self.get_option('private_key_file'))
+
+            # paramiko 2.2 introduced auth_timeout parameter
+            if LooseVersion(paramiko.__version__) >= LooseVersion('2.2.0'):
+                ssh_connect_kwargs['auth_timeout'] = self.get_option('timeout')
+
+            # paramiko 1.15 introduced banner timeout parameter
+            if LooseVersion(paramiko.__version__) >= LooseVersion('1.15.0'):
+                ssh_connect_kwargs['banner_timeout'] = self.get_option('banner_timeout')
+
+            ssh.connect(
+                self.get_option('remote_addr').lower(),
+                username=self.get_option('remote_user'),
+                allow_agent=allow_agent,
+                look_for_keys=self.get_option('look_for_keys'),
+                key_filename=key_filename,
+                password=conn_password,
+                timeout=self.get_option('timeout'),
+                port=port,
+                disabled_algorithms=disabled_algorithms,
+                **ssh_connect_kwargs,
+            )
+        except paramiko.ssh_exception.BadHostKeyException as e:
+            raise AnsibleConnectionFailure(f'host key mismatch for {to_text(e.hostname)}')
+        except paramiko.ssh_exception.AuthenticationException as e:
+            msg = f'Failed to authenticate: {e}'
+            raise AnsibleAuthenticationFailure(msg)
+        except Exception as e:
+            msg = to_text(e)
+            if u'PID check failed' in msg:
+                raise AnsibleError('paramiko version issue, please upgrade paramiko on the machine running ansible')
+            elif u'Private key file is encrypted' in msg:
+                msg = f'ssh {self.get_option("remote_user")}@{self.get_options("remote_addr")}:{port} : ' + \
+                    f'{msg}\nTo connect as a different user, use -u <username>.'
+                raise AnsibleConnectionFailure(msg)
+            else:
+                raise AnsibleConnectionFailure(msg)
+        self.ssh = ssh
+        self._connected = True
+        return self
+
+    def _any_keys_added(self) -> bool:
+        for hostname, keys in self.ssh._host_keys.items():
+            for keytype, key in keys.items():
+                added_this_time = getattr(key, '_added_by_ansible_this_time', False)
+                if added_this_time:
+                    return True
+        return False
+
+    def _save_ssh_host_keys(self, filename: str) -> None:
+        """
+        not using the paramiko save_ssh_host_keys function as we want to add new SSH keys at the bottom so folks
+        don't complain about it :)
+        """
+
+        if not self._any_keys_added():
+            return
+
+        path = os.path.expanduser('~/.ssh')
+        makedirs_safe(path)
+
+        with open(filename, 'w') as f:
+            for hostname, keys in self.ssh._host_keys.items():
+                for keytype, key in keys.items():
+                    # was f.write
+                    added_this_time = getattr(key, '_added_by_ansible_this_time', False)
+                    if not added_this_time:
+                        f.write(f'{hostname} {keytype} {key.get_base64()}\n')
+
+            for hostname, keys in self.ssh._host_keys.items():
+                for keytype, key in keys.items():
+                    added_this_time = getattr(key, '_added_by_ansible_this_time', False)
+                    if added_this_time:
+                        f.write(f'{hostname} {keytype} {key.get_base64()}\n')
+
+    def _build_wsl_command(self, cmd: str) -> str:
+        wsl_distribution = self.get_option('wsl_distribution')
+        if not wsl_distribution:
+            raise AnsibleError('A wsl_distribution is required but none was supplied')
+        become = self.get_option('become')
+        become_user = self.get_option('become_user')
+        if become and become_user:
+            wsl_user = become_user
+        else:
+            wsl_user = self.get_option('wsl_user')
+        args = ['wsl.exe', '--distribution', wsl_distribution]
+        if wsl_user:
+            args.extend(['--user', wsl_user])
+        args.extend(['--'])
+        args.extend(shlex.split(cmd))
+        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 """
+
+        cmd = self._build_wsl_command(cmd)
+
+        super(Connection, self).exec_command(cmd, in_data=in_data, sudoable=sudoable)
+
+        bufsize = 4096
+
+        try:
+            self.ssh.get_transport().set_keepalive(5)
+            chan = self.ssh.get_transport().open_session()
+        except Exception as e:
+            text_e = to_text(e)
+            msg = 'Failed to open session'
+            if text_e:
+                msg += f': {text_e}'
+            raise AnsibleConnectionFailure(to_native(msg))
+
+        display.vvv(f'EXEC {cmd}', host=self.get_option('remote_addr'))
+
+        cmd = to_bytes(cmd, errors='surrogate_or_strict')
+
+        no_prompt_out = b''
+        no_prompt_err = b''
+        become_output = b''
+
+        try:
+            chan.exec_command(cmd)
+            if self.become and self.become.expect_prompt():
+                password_prompt = False
+                become_success = False
+                while not (become_success or password_prompt):
+                    display.debug('Waiting for Privilege Escalation input')
+
+                    chunk = chan.recv(bufsize)
+                    display.debug(f'chunk is: {to_text(chunk)}')
+                    if not chunk:
+                        if b'unknown user' in become_output:
+                            n_become_user = to_native(self.become.get_option('become_user'))
+                            raise AnsibleError(f'user {n_become_user} does not exist')
+                        else:
+                            break
+                            # raise AnsibleError('ssh connection closed waiting for password prompt')
+                    become_output += chunk
+
+                    # need to check every line because we might get lectured
+                    # and we might get the middle of a line in a chunk
+                    for line in become_output.splitlines(True):
+                        if self.become.check_success(line):
+                            become_success = True
+                            break
+                        elif self.become.check_password_prompt(line):
+                            password_prompt = True
+                            break
+
+                if password_prompt:
+                    if self.become:
+                        become_pass = self.become.get_option('become_pass')
+                        chan.sendall(to_bytes(become_pass, errors='surrogate_or_strict') + b'\n')
+                    else:
+                        raise AnsibleError('A password is required but none was supplied')
+                else:
+                    no_prompt_out += become_output
+                    no_prompt_err += become_output
+
+            if in_data:
+                for i in range(0, len(in_data), bufsize):
+                    chan.send(in_data[i:i + bufsize])
+                chan.shutdown_write()
+            elif in_data == b'':
+                chan.shutdown_write()
+
+        except socket.timeout:
+            raise AnsibleError('ssh timed out waiting for privilege escalation.\n' + to_text(become_output))
+
+        stdout = b''.join(chan.makefile('rb', bufsize))
+        stderr = b''.join(chan.makefile_stderr('rb', bufsize))
+        returncode = chan.recv_exit_status()
+
+        # NB the full english error message is:
path: plugins/connection/wsl.py
user: login: russoz
body: Good question - I have no good answer to that one other than: we should test it.
created_at: 2025-04-16T21:21:24Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,786 @@
+# -*- coding: utf-8 -*-
+# Derived from ansible/plugins/connection/proxmox_pct_remote.py (c) 2024 Nils Stein (@mietzen) <github.nstein@mailbox.org>
+# Derived from ansible/plugins/connection/paramiko_ssh.py (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
+# Copyright (c) 2025 Rui Lopes (@rgl) <ruilopes.com>
+# Copyright (c) 2025 Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import annotations
+
+DOCUMENTATION = r"""
+author: Rui Lopes (@rgl) <ruilopes.com>
+name: wsl
+short_description: Run tasks in WSL distribution using wsl.exe CLI via SSH
+requirements:
+  - paramiko
+description:
+  - Run commands or put/fetch files to an existing WSL distribution using wsl.exe CLI via SSH.
+  - Uses the Python SSH implementation (Paramiko) to connect to the WSL host.
+version_added: "10.5.0"
+options:
+  remote_addr:
+    description:
+      - Address of the remote target.
+    default: inventory_hostname
+    type: string
+    vars:
+      - name: inventory_hostname
+      - name: ansible_host
+      - name: ansible_ssh_host
+      - name: ansible_paramiko_host
+  port:
+    description: Remote port to connect to.
+    type: int
+    default: 22
+    ini:
+      - section: defaults
+        key: remote_port
+      - section: paramiko_connection
+        key: remote_port
+    env:
+      - name: ANSIBLE_REMOTE_PORT
+      - name: ANSIBLE_REMOTE_PARAMIKO_PORT
+    vars:
+      - name: ansible_port
+      - name: ansible_ssh_port
+      - name: ansible_paramiko_port
+    keyword:
+      - name: port
+  remote_user:
+    description:
+      - User to login/authenticate as.
+      - Can be set from the CLI via the C(--user) or C(-u) options.
+    type: string
+    vars:
+      - name: ansible_user
+      - name: ansible_ssh_user
+      - name: ansible_paramiko_user
+    env:
+      - name: ANSIBLE_REMOTE_USER
+      - name: ANSIBLE_PARAMIKO_REMOTE_USER
+    ini:
+      - section: defaults
+        key: remote_user
+      - section: paramiko_connection
+        key: remote_user
+    keyword:
+      - name: remote_user
+  password:
+    description:
+      - Secret used to either login the SSH server or as a passphrase for SSH keys that require it.
+      - Can be set from the CLI via the C(--ask-pass) option.
+    type: string
+    vars:
+      - name: ansible_password
+      - name: ansible_ssh_pass
+      - name: ansible_ssh_password
+      - name: ansible_paramiko_pass
+      - name: ansible_paramiko_password
+  use_rsa_sha2_algorithms:
+    description:
+      - Whether or not to enable RSA SHA2 algorithms for pubkeys and hostkeys.
+      - On paramiko versions older than 2.9, this only affects hostkeys.
+      - For behavior matching paramiko<2.9 set this to V(false).
+    vars:
+      - name: ansible_paramiko_use_rsa_sha2_algorithms
+    ini:
+      - {key: use_rsa_sha2_algorithms, section: paramiko_connection}
+    env:
+      - {name: ANSIBLE_PARAMIKO_USE_RSA_SHA2_ALGORITHMS}
+    default: true
+    type: boolean
+  host_key_auto_add:
+    description: "Automatically add host keys to C(~/.ssh/known_hosts)."
+    env:
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_AUTO_ADD
+    ini:
+      - key: host_key_auto_add
+        section: paramiko_connection
+    type: boolean
+  look_for_keys:
+    default: True
+    description: "Set to V(false) to disable searching for private key files in C(~/.ssh/)."
+    env:
+      - name: ANSIBLE_PARAMIKO_LOOK_FOR_KEYS
+    ini:
+      - {key: look_for_keys, section: paramiko_connection}
+    type: boolean
+  proxy_command:
+    default: ""
+    description:
+      - Proxy information for running the connection via a jumphost.
+      - This option is supported by paramiko version 1.9.0 or newer.
+    type: string
+    env:
+      - name: ANSIBLE_PARAMIKO_PROXY_COMMAND
+    ini:
+      - {key: proxy_command, section: paramiko_connection}
+    vars:
+      - name: ansible_paramiko_proxy_command
+  record_host_keys:
+    default: True
+    description: "Save the host keys to a file."
+    env:
+      - name: ANSIBLE_PARAMIKO_RECORD_HOST_KEYS
+    ini:
+      - section: paramiko_connection
+        key: record_host_keys
+    type: boolean
+  host_key_checking:
+    description: "Set this to V(false) if you want to avoid host key checking by the underlying tools Ansible uses to connect to the host."
+    type: boolean
+    default: true
+    env:
+      - name: ANSIBLE_HOST_KEY_CHECKING
+      - name: ANSIBLE_SSH_HOST_KEY_CHECKING
+      - name: ANSIBLE_PARAMIKO_HOST_KEY_CHECKING
+    ini:
+      - section: defaults
+        key: host_key_checking
+      - section: paramiko_connection
+        key: host_key_checking
+    vars:
+      - name: ansible_host_key_checking
+      - name: ansible_ssh_host_key_checking
+      - name: ansible_paramiko_host_key_checking
+  use_persistent_connections:
+    description: "Toggles the use of persistence for connections."
+    type: boolean
+    default: False
+    env:
+      - name: ANSIBLE_USE_PERSISTENT_CONNECTIONS
+    ini:
+      - section: defaults
+        key: use_persistent_connections
+  banner_timeout:
+    type: float
+    default: 30
+    description:
+      - Configures, in seconds, the amount of time to wait for the SSH
+        banner to be presented.
+      - This option is supported by paramiko version 1.15.0 or newer.
+    ini:
+      - section: paramiko_connection
+        key: banner_timeout
+    env:
+      - name: ANSIBLE_PARAMIKO_BANNER_TIMEOUT
+  timeout:
+    type: int
+    default: 10
+    description:
+      - Number of seconds until the plugin gives up on failing to establish a TCP connection.
+      - This option is supported by paramiko version 2.2.0 or newer.
+    ini:
+      - section: defaults
+        key: timeout
+      - section: ssh_connection
+        key: timeout
+      - section: paramiko_connection
+        key: timeout
+    env:
+      - name: ANSIBLE_TIMEOUT
+      - name: ANSIBLE_SSH_TIMEOUT
+      - name: ANSIBLE_PARAMIKO_TIMEOUT
+    vars:
+      - name: ansible_ssh_timeout
+      - name: ansible_paramiko_timeout
+    cli:
+      - name: timeout
+  lock_file_timeout:
+    type: int
+    default: 60
+    description: Number of seconds until the plugin gives up on trying to write a lock file when writing SSH known host keys.
+    vars:
+      - name: ansible_lock_file_timeout
+    env:
+      - name: ANSIBLE_LOCK_FILE_TIMEOUT
+  private_key_file:
+    description:
+      - Path to private key file to use for authentication.
+    type: string
+    ini:
+      - section: defaults
+        key: private_key_file
+      - section: paramiko_connection
+        key: private_key_file
+    env:
+      - name: ANSIBLE_PRIVATE_KEY_FILE
+      - name: ANSIBLE_PARAMIKO_PRIVATE_KEY_FILE
+    vars:
+      - name: ansible_private_key_file
+      - name: ansible_ssh_private_key_file
+      - name: ansible_paramiko_private_key_file
+    cli:
+      - name: private_key_file
+        option: "--private-key"
+  user_known_hosts_file:
+    description:
+      - Path to the user known hosts file.
+      - Used to verify the ssh hosts keys.
+    type: string
path: plugins/connection/wsl.py
user: login: rgl
body: indeed! that worked. I've also changed the other place where a path was being used.
created_at: 2025-04-17T18:43:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,786 @@
+# -*- coding: utf-8 -*-
+# Derived from ansible/plugins/connection/proxmox_pct_remote.py (c) 2024 Nils Stein (@mietzen) <github.nstein@mailbox.org>
+# Derived from ansible/plugins/connection/paramiko_ssh.py (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
+# Copyright (c) 2025 Rui Lopes (@rgl) <ruilopes.com>
+# Copyright (c) 2025 Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import annotations
+
+DOCUMENTATION = r"""
+author: Rui Lopes (@rgl) <ruilopes.com>
+name: wsl
+short_description: Run tasks in WSL distribution using wsl.exe CLI via SSH
+requirements:
+  - paramiko
+description:
+  - Run commands or put/fetch files to an existing WSL distribution using wsl.exe CLI via SSH.
+  - Uses the Python SSH implementation (Paramiko) to connect to the WSL host.
+version_added: "10.5.0"
path: plugins/connection/wsl.py
user: login: rgl
body: Changed.
created_at: 2025-04-17T18:43:46Z
author_association: CONTRIBUTOR

PR comments:
user: login: ansibullbot
created_at: 2025-02-22T16:22:09Z
body: The test `botmeta` failed with 2 errors:
```
.github/BOTMETA.yml:0:0: Author rgl not mentioned as active or inactive maintainer for plugins/connection/wsl.py (mentioned are: )
.github/BOTMETA.yml:0:0: No (active or inactive) maintainer mentioned for plugins/connection/wsl.py
```
<!-- job_id: 138314 -->
<!-- job_id: a334457605ecbca7c6a61a98e22d3993 -->
<!-- job_id: None -->
The test `ansible-test sanity --test pep8` [[explain](https://docs.ansible.com/ansible-core/devel/dev_guide/testing/sanity/pep8.html)] failed with 5 errors:
```
plugins/connection/wsl.py:543:11: E111: indentation is not a multiple of 4
plugins/connection/wsl.py:547:11: E111: indentation is not a multiple of 4
plugins/connection/wsl.py:549:11: E111: indentation is not a multiple of 4
plugins/connection/wsl.py:552:11: E111: indentation is not a multiple of 4
plugins/connection/wsl.py:555:34: E261: at least two spaces before inline comment
```
<!-- job_id: 138314 -->
<!-- job_id: 41494b9fd982f87ae760fd977164e82d -->
<!-- job_id: None -->
The test `ansible-test sanity --test pep8` [[explain](https://docs.ansible.com/ansible-core/devel/dev_guide/testing/sanity/pep8.html)] failed with 5 errors:
```
plugins/connection/wsl.py:543:11: E111: indentation is not a multiple of 4
plugins/connection/wsl.py:547:11: E111: indentation is not a multiple of 4
plugins/connection/wsl.py:549:11: E111: indentation is not a multiple of 4
plugins/connection/wsl.py:552:11: E111: indentation is not a multiple of 4
plugins/connection/wsl.py:555:34: E261: at least two spaces before inline comment
```
<!-- job_id: 138314 -->
<!-- job_id: 41494b9fd982f87ae760fd977164e82d -->
<!-- job_id: None -->
The test `ansible-test sanity --test pep8` [[explain](https://docs.ansible.com/ansible-core/devel/dev_guide/testing/sanity/pep8.html)] failed with 5 errors:
```
plugins/connection/wsl.py:543:11: E111: indentation is not a multiple of 4
plugins/connection/wsl.py:547:11: E111: indentation is not a multiple of 4
plugins/connection/wsl.py:549:11: E111: indentation is not a multiple of 4
plugins/connection/wsl.py:552:11: E111: indentation is not a multiple of 4
plugins/connection/wsl.py:555:34: E261: at least two spaces before inline comment
```
<!-- job_id: 138314 -->
<!-- job_id: 41494b9fd982f87ae760fd977164e82d -->
<!-- job_id: None -->
The test `ansible-test sanity --test pep8` [[explain](https://docs.ansible.com/ansible-core/devel/dev_guide/testing/sanity/pep8.html)] failed with 5 errors:
```
plugins/connection/wsl.py:543:11: E111: indentation is not a multiple of 4
plugins/connection/wsl.py:547:11: E111: indentation is not a multiple of 4
plugins/connection/wsl.py:549:11: E111: indentation is not a multiple of 4
plugins/connection/wsl.py:552:11: E111: indentation is not a multiple of 4
plugins/connection/wsl.py:555:34: E261: at least two spaces before inline comment
```
<!-- job_id: 138314 -->
<!-- job_id: 41494b9fd982f87ae760fd977164e82d -->
<!-- job_id: None -->

[click here for bot help](https://github.com/ansible/ansibullbot/blob/devel/ISSUE_HELP.md)
<!--- boilerplate: shippable_test_result --->
author_association: COLLABORATOR
user: login: ansibullbot
created_at: 2025-02-23T12:12:59Z
body: The test `ansible-test sanity --test yamllint` [[explain](https://docs.ansible.com/ansible-core/devel/dev_guide/testing/sanity/yamllint.html)] failed with 1 error:
```
plugins/connection/wsl.py:266:1: unparsable-with-libyaml: expected a single document in the stream - but found another document
```
<!-- job_id: 138376 -->
<!-- job_id: c07c2bea455d8be55bbacbc7425698ec -->
<!-- job_id: None -->

[click here for bot help](https://github.com/ansible/ansibullbot/blob/devel/ISSUE_HELP.md)
<!--- boilerplate: shippable_test_result --->
author_association: COLLABORATOR
user: login: rgl
created_at: 2025-02-23T12:14:37Z
body: @russoz thank you for the review! I've added the suggested changes for the code that is wsl specific, the other ones, I think, should first be applied to [the existing `community.general.proxmox_pct_remote` connection plugin](https://github.com/ansible-collections/community.general/blob/main/plugins/connection/proxmox_pct_remote.py) from which this plugin is derived from. I'm also not sure how to share code among the two and the [original paramiko ssh plugin](https://github.com/ansible/ansible/blob/devel/lib/ansible/plugins/connection/paramiko_ssh.py) (this one seems to have changes that were not propagated to proxmox_pct_remote, and vice-versa).

what do you think?
author_association: CONTRIBUTOR
user: login: ansibullbot
created_at: 2025-02-23T12:53:28Z
body: The test `ansible-test sanity --test yamllint` [[explain](https://docs.ansible.com/ansible-core/devel/dev_guide/testing/sanity/yamllint.html)] failed with 1 error:
```
plugins/connection/wsl.py:267:1: unparsable-with-libyaml: expected a single document in the stream - but found another document
```
<!-- job_id: 138378 -->
<!-- job_id: 8f508183852fcb05e13fa8d3ac0d7eb2 -->
<!-- job_id: None -->

[click here for bot help](https://github.com/ansible/ansibullbot/blob/devel/ISSUE_HELP.md)
<!--- boilerplate: shippable_test_result --->
author_association: COLLABORATOR
user: login: jborean93
created_at: 2025-02-23T18:50:46Z
body: > Thanks for your contribution! I'm wondering a bit whether this plugin would be more appropriate to be added to community.windows

This would not be accepted there for a few reasons:

* We are trying to cut back on the `community.windows` collection and are not accepting any new contributions
* The collection is designed to target Windows hosts, while this is Windows related the ultimate target is the WSL distribution and Windows is just the middle man
* Testing would be a nightmare to setup WSL on the CI hosts and configuring the distributions

Maybe it would be better to look into proxy command support to proxy the initial Windows SSH connection to the WSL instance and take advantage of the builtin ssh connection plugin.

Personally I'm not sure it should even be in this collection and is better off suited towards keeping it in your own but I'll leave that for the maintainers here to decide.
author_association: CONTRIBUTOR
user: login: russoz
created_at: 2025-02-23T21:00:16Z
body: I am far from being knowledgeable in Windows so forgive me if I'm saying something silly here. But thinking of this again, I cannot help thinking: it looks like the plugin connects to a sshd daemon that is NOT running in the WSL, hence it needs to connect, starts the WSL and then run whatever it is that needs running in there.

Wouldn't it be easier to start the ssh service within the WSL (which is an Ubuntu or some other distro) directly? Then it would be a plain old Linux to Linux ssh connection. A quick googling found some posts indicating that is possible but not entirely straightforward, so YMMV.
author_association: COLLABORATOR
user: login: felixfontein
created_at: 2025-02-24T05:15:26Z
body: > Wouldn't it be easier to start the ssh service within the WSL (which is an Ubuntu or some other distro) directly? Then it would be a plain old Linux to Linux ssh connection. A quick googling found some posts indicating that is possible but not entirely straightforward, so YMMV.

It would be even simpler to install Linux right away instead of using WSL in Windows. But in most cases there are some external requirements not under your control that force you to use a specific setup, and I guess there will be some folks who need SSH to run under Windows itself, outside WSL, and who'd like Ansible to connect to WSL inside their Windows. They will have to jump through this extra hoop.

From that POV I'm ok with merging this here, since this seems like something that is useful for parts of the community.
author_association: COLLABORATOR
user: login: rgl
created_at: 2025-02-24T16:38:24Z
body: I also tried the install open ssh daemon inside the WSl distribution, but that has more moving parts, and has a major caveat for me, the WSl dist shutdowns after a while (although you can configure it, which is something more to look for), and if it is shutdown, you cannot access the sshd to manage it. The only thing that is always up, is the windows host itself, hence, this PR.

If the CI host has all the windows features for WSl requirements (no idea if that works in a github hosted runner; maybe not, as it requires nested virtualization), creating a Ubuntu dist is quite straightforward (thou, it requires downloading a large rootfs tarball, when not cached somewhere). 
author_association: CONTRIBUTOR
user: login: felixfontein
created_at: 2025-02-24T21:12:01Z
body: Regarding the failing sanity checks: you need to add
```
plugins/connection/wsl.py yamllint:unparsable-with-libyaml
```
to `tests/sanity/ignore-2.15.txt` and `tests/sanity/ignore-2.16.txt` (not to the others!). Please add it in the lexicographically correct position.
author_association: COLLABORATOR
user: login: russoz
created_at: 2025-03-01T04:06:43Z
body: Hi @rgl thanks for the updates made so far. Please note that there are still a number of comments in the code and, there should be some testing associated with new plugins. Writing integration tests for connection plugins is certainly a harder thing to do, so I would suggest you take a look at the existing tests for other connection plugins and try and mimic/adapt their logic to your plugin.
author_association: COLLABORATOR
user: login: rgl
created_at: 2025-03-01T14:04:58Z
body: I've copied and derived the unit and integration tests from the proxmox_pct_remote connection plugin.

But I'm in a pickle with the integration tests. Because the wsl connection plugin expects a cmd.exe shell, as such, it is constructing the command line using its rules (see `list2cmdline` at https://github.com/python/cpython/blob/3.11/Lib/subprocess.py#L576), but since the integration test is running against a linux/bash shell, the rules are not the same, hence, the integration test fails. Please advice!
author_association: CONTRIBUTOR
user: login: rgl
created_at: 2025-03-01T15:08:39Z
body: I think this is ready to be reviewed again.

I'm not sure about the etiquette here, who should close the open review threads? Whoever open them, or whoever fixed them? I prefer the former, because the reviewer has a chance to verify, and finally close the thread.
 
Please note that I didn't do anything about the review threads that were inherited/copied from the proxmox_pct_remote connection plugin code, as I think they should be fixed in the original plugin, and after that, I would merge the changes here.

But, please advise otherwise.
author_association: CONTRIBUTOR
user: login: felixfontein
created_at: 2025-03-01T15:28:21Z
body: > I'm not sure about the etiquette here, who should close the open review threads? Whoever open them, or whoever fixed them? I prefer the former, because the reviewer has a chance to verify, and finally close the thread.

There's no perfect solution here. Generally I prefer to let reviewers resolve them (for the same reasons as you do), but unfortunately only folks with commit access (and the PR's author) can resolve, so most reviewers are not able to do this.
author_association: COLLABORATOR
user: login: russoz
created_at: 2025-03-04T20:46:17Z
body: > I'm not sure about the etiquette here, who should close the open review threads? Whoever open them, or whoever fixed them? I prefer the former, because the reviewer has a chance to verify, and finally close the thread.

Ditto what @felixfontein wrote.

> Please note that I didn't do anything about the review threads that were inherited/copied from the proxmox_pct_remote connection plugin code, as I think they should be fixed in the original plugin, and after that, I would merge the changes here.

I think we need to evaluate those reviews based on their own merit - if we agree to adjust, then we go back to `proxmox_pct_remote` and retrofit them in there. Merit outweighs (temporary) consistency, IMHO.

author_association: COLLABORATOR
user: login: ansibullbot
created_at: 2025-03-05T19:00:56Z
body: The test `ansible-test sanity --test import --python 3.10` [[explain](https://docs.ansible.com/ansible-core/devel/dev_guide/testing/sanity/import.html)] failed with 1 error:
```
plugins/connection/wsl.py:324:0: traceback: ModuleNotFoundError: No module named 'paramiko'
```
<!-- job_id: 139205 -->
<!-- job_id: 00c2ddd680a8e6cfd8638c1ec3912925 -->
<!-- job_id: None -->
The test `ansible-test sanity --test import --python 3.11` [[explain](https://docs.ansible.com/ansible-core/devel/dev_guide/testing/sanity/import.html)] failed with 1 error:
```
plugins/connection/wsl.py:324:0: traceback: ModuleNotFoundError: No module named 'paramiko'
```
<!-- job_id: 139205 -->
<!-- job_id: e852c0937b64de09b37f417d83c20505 -->
<!-- job_id: None -->
The test `ansible-test sanity --test import --python 3.12` [[explain](https://docs.ansible.com/ansible-core/devel/dev_guide/testing/sanity/import.html)] failed with 1 error:
```
plugins/connection/wsl.py:324:0: traceback: ModuleNotFoundError: No module named 'paramiko'
```
<!-- job_id: 139205 -->
<!-- job_id: 59d83e181fe81840c509f7ad46c90392 -->
<!-- job_id: None -->
The test `ansible-test sanity --test import --python 3.10` [[explain](https://docs.ansible.com/ansible-core/devel/dev_guide/testing/sanity/import.html)] failed with 1 error:
```
plugins/connection/wsl.py:324:0: traceback: ModuleNotFoundError: No module named 'paramiko'
```
<!-- job_id: 139205 -->
<!-- job_id: 00c2ddd680a8e6cfd8638c1ec3912925 -->
<!-- job_id: None -->
The test `ansible-test sanity --test import --python 3.11` [[explain](https://docs.ansible.com/ansible-core/devel/dev_guide/testing/sanity/import.html)] failed with 1 error:
```
plugins/connection/wsl.py:324:0: traceback: ModuleNotFoundError: No module named 'paramiko'
```
<!-- job_id: 139205 -->
<!-- job_id: e852c0937b64de09b37f417d83c20505 -->
<!-- job_id: None -->
The test `ansible-test sanity --test import --python 3.12` [[explain](https://docs.ansible.com/ansible-core/devel/dev_guide/testing/sanity/import.html)] failed with 1 error:
```
plugins/connection/wsl.py:324:0: traceback: ModuleNotFoundError: No module named 'paramiko'
```
<!-- job_id: 139205 -->
<!-- job_id: 59d83e181fe81840c509f7ad46c90392 -->
<!-- job_id: None -->
The test `ansible-test sanity --test import --python 3.11` [[explain](https://docs.ansible.com/ansible-core/devel/dev_guide/testing/sanity/import.html)] failed with 1 error:
```
plugins/connection/wsl.py:324:0: traceback: ModuleNotFoundError: No module named 'paramiko'
```
<!-- job_id: 139205 -->
<!-- job_id: e852c0937b64de09b37f417d83c20505 -->
<!-- job_id: None -->
The test `ansible-test sanity --test import --python 3.12` [[explain](https://docs.ansible.com/ansible-core/devel/dev_guide/testing/sanity/import.html)] failed with 1 error:
```
plugins/connection/wsl.py:324:0: traceback: ModuleNotFoundError: No module named 'paramiko'
```
<!-- job_id: 139205 -->
<!-- job_id: 59d83e181fe81840c509f7ad46c90392 -->
<!-- job_id: None -->
The test `ansible-test sanity --test import --python 3.13` [[explain](https://docs.ansible.com/ansible-core/devel/dev_guide/testing/sanity/import.html)] failed with 1 error:
```
plugins/connection/wsl.py:324:0: traceback: ModuleNotFoundError: No module named 'paramiko'
```
<!-- job_id: 139205 -->
<!-- job_id: 36f625e9889e123b1e6298a1078817fb -->
<!-- job_id: None -->
The test `ansible-test sanity --test import --python 3.11` [[explain](https://docs.ansible.com/ansible-core/devel/dev_guide/testing/sanity/import.html)] failed with 1 error:
```
plugins/connection/wsl.py:324:0: traceback: ModuleNotFoundError: No module named 'paramiko'
```
<!-- job_id: 139205 -->
<!-- job_id: e852c0937b64de09b37f417d83c20505 -->
<!-- job_id: None -->
The test `ansible-test sanity --test import --python 3.12` [[explain](https://docs.ansible.com/ansible-core/devel/dev_guide/testing/sanity/import.html)] failed with 1 error:
```
plugins/connection/wsl.py:324:0: traceback: ModuleNotFoundError: No module named 'paramiko'
```
<!-- job_id: 139205 -->
<!-- job_id: 59d83e181fe81840c509f7ad46c90392 -->
<!-- job_id: None -->
The test `ansible-test sanity --test import --python 3.13` [[explain](https://docs.ansible.com/ansible-core/devel/dev_guide/testing/sanity/import.html)] failed with 1 error:
```
plugins/connection/wsl.py:324:0: traceback: ModuleNotFoundError: No module named 'paramiko'
```
<!-- job_id: 139205 -->
<!-- job_id: 36f625e9889e123b1e6298a1078817fb -->
<!-- job_id: None -->

[click here for bot help](https://github.com/ansible/ansibullbot/blob/devel/ISSUE_HELP.md)
<!--- boilerplate: shippable_test_result --->
author_association: COLLABORATOR
user: login: rgl
created_at: 2025-03-05T19:04:19Z
body: please review this again.

it seems the TODO about paramiko that I've added to the code, really had impact on the failed CI tests above, that means I'm not sure how to handle the paramiko imports and I'm not really sure how to force paramiko to be installed in the tests, please advise. :-)
author_association: CONTRIBUTOR
user: login: mietzen
created_at: 2025-03-06T12:41:06Z
body: @rgl, @felixfontein and @russoz, just my two cents. 

Now that there are two "remote connection" plugins that share a common codebase, it might be a good idea to migrate all common code into a base class, e.g., `remote_connection.py`, that implements most of the functionality. This would make it easier to maintain, switching from `paramiko` to regular `ssh` and to implement new "remote connection" plugins.

I wanted to do this for a `proxmox_qm_remote` plugin, but at the moment, personal matters have a higher priority.
author_association: CONTRIBUTOR
user: login: ansibullbot
created_at: 2025-03-11T22:03:55Z
body: The test `ansible-test sanity --test import --python 3.10` [[explain](https://docs.ansible.com/ansible-core/devel/dev_guide/testing/sanity/import.html)] failed with 1 error:
```
plugins/connection/wsl.py:335:0: traceback: ModuleNotFoundError: No module named 'paramiko'
```
<!-- job_id: 139807 -->
<!-- job_id: 3b429ed005d4b024b9bd8259d80590e9 -->
<!-- job_id: None -->
The test `ansible-test sanity --test import --python 3.11` [[explain](https://docs.ansible.com/ansible-core/devel/dev_guide/testing/sanity/import.html)] failed with 1 error:
```
plugins/connection/wsl.py:335:0: traceback: ModuleNotFoundError: No module named 'paramiko'
```
<!-- job_id: 139807 -->
<!-- job_id: 2e596e455d2b306048f61a614acaa94e -->
<!-- job_id: None -->
The test `ansible-test sanity --test import --python 3.12` [[explain](https://docs.ansible.com/ansible-core/devel/dev_guide/testing/sanity/import.html)] failed with 1 error:
```
plugins/connection/wsl.py:335:0: traceback: ModuleNotFoundError: No module named 'paramiko'
```
<!-- job_id: 139807 -->
<!-- job_id: ce840c6d8d68a09a5561d10809a56ec5 -->
<!-- job_id: None -->
The test `ansible-test sanity --test import --python 3.10` [[explain](https://docs.ansible.com/ansible-core/devel/dev_guide/testing/sanity/import.html)] failed with 1 error:
```
plugins/connection/wsl.py:335:0: traceback: ModuleNotFoundError: No module named 'paramiko'
```
<!-- job_id: 139807 -->
<!-- job_id: 3b429ed005d4b024b9bd8259d80590e9 -->
<!-- job_id: None -->
The test `ansible-test sanity --test import --python 3.11` [[explain](https://docs.ansible.com/ansible-core/devel/dev_guide/testing/sanity/import.html)] failed with 1 error:
```
plugins/connection/wsl.py:335:0: traceback: ModuleNotFoundError: No module named 'paramiko'
```
<!-- job_id: 139807 -->
<!-- job_id: 2e596e455d2b306048f61a614acaa94e -->
<!-- job_id: None -->
The test `ansible-test sanity --test import --python 3.12` [[explain](https://docs.ansible.com/ansible-core/devel/dev_guide/testing/sanity/import.html)] failed with 1 error:
```
plugins/connection/wsl.py:335:0: traceback: ModuleNotFoundError: No module named 'paramiko'
```
<!-- job_id: 139807 -->
<!-- job_id: ce840c6d8d68a09a5561d10809a56ec5 -->
<!-- job_id: None -->
The test `ansible-test sanity --test import --python 3.11` [[explain](https://docs.ansible.com/ansible-core/devel/dev_guide/testing/sanity/import.html)] failed with 1 error:
```
plugins/connection/wsl.py:335:0: traceback: ModuleNotFoundError: No module named 'paramiko'
```
<!-- job_id: 139807 -->
<!-- job_id: 2e596e455d2b306048f61a614acaa94e -->
<!-- job_id: None -->
The test `ansible-test sanity --test import --python 3.12` [[explain](https://docs.ansible.com/ansible-core/devel/dev_guide/testing/sanity/import.html)] failed with 1 error:
```
plugins/connection/wsl.py:335:0: traceback: ModuleNotFoundError: No module named 'paramiko'
```
<!-- job_id: 139807 -->
<!-- job_id: ce840c6d8d68a09a5561d10809a56ec5 -->
<!-- job_id: None -->
The test `ansible-test sanity --test import --python 3.13` [[explain](https://docs.ansible.com/ansible-core/devel/dev_guide/testing/sanity/import.html)] failed with 1 error:
```
plugins/connection/wsl.py:335:0: traceback: ModuleNotFoundError: No module named 'paramiko'
```
<!-- job_id: 139807 -->
<!-- job_id: ea2284d4ac5219dec4bd77aa48c2988a -->
<!-- job_id: None -->
The test `ansible-test sanity --test import --python 3.11` [[explain](https://docs.ansible.com/ansible-core/devel/dev_guide/testing/sanity/import.html)] failed with 1 error:
```
plugins/connection/wsl.py:335:0: traceback: ModuleNotFoundError: No module named 'paramiko'
```
<!-- job_id: 139807 -->
<!-- job_id: 2e596e455d2b306048f61a614acaa94e -->
<!-- job_id: None -->
The test `ansible-test sanity --test import --python 3.12` [[explain](https://docs.ansible.com/ansible-core/devel/dev_guide/testing/sanity/import.html)] failed with 1 error:
```
plugins/connection/wsl.py:335:0: traceback: ModuleNotFoundError: No module named 'paramiko'
```
<!-- job_id: 139807 -->
<!-- job_id: ce840c6d8d68a09a5561d10809a56ec5 -->
<!-- job_id: None -->
The test `ansible-test sanity --test import --python 3.13` [[explain](https://docs.ansible.com/ansible-core/devel/dev_guide/testing/sanity/import.html)] failed with 1 error:
```
plugins/connection/wsl.py:335:0: traceback: ModuleNotFoundError: No module named 'paramiko'
```
<!-- job_id: 139807 -->
<!-- job_id: ea2284d4ac5219dec4bd77aa48c2988a -->
<!-- job_id: None -->

[click here for bot help](https://github.com/ansible/ansibullbot/blob/devel/ISSUE_HELP.md)
<!--- boilerplate: shippable_test_result --->
author_association: COLLABORATOR
user: login: felixfontein
created_at: 2025-04-12T15:46:06Z
body: ping @rgl
author_association: COLLABORATOR
user: login: rgl
created_at: 2025-04-13T11:53:09Z
body: @felixfontein, I think all the comments are now addressed. please have another look.
author_association: CONTRIBUTOR
user: login: ansibullbot
created_at: 2025-04-14T20:28:45Z
body: cc @Ajpantuso @Thulium-Drake @UnderGreen @joshainglis @karmab @krauthosting
[click here for bot help](https://github.com/ansible/ansibullbot/blob/devel/ISSUE_HELP.md)
<!--- boilerplate: notify --->
author_association: COLLABORATOR
user: login: felixfontein
created_at: 2025-04-14T20:33:56Z
body: For some reason the diff for this PR now contains two changes that are already part of `main`. Maybe something went wrong during rebasing?
author_association: COLLABORATOR
user: login: rgl
created_at: 2025-04-14T20:49:56Z
body: I've rebased this branch over the current `main` branch.
author_association: CONTRIBUTOR
user: login: felixfontein
created_at: 2025-04-15T18:55:17Z
body: I think this looks good enough for a first version. If nobody objects, I'll merge this on the upcoming weekend.
author_association: COLLABORATOR
user: login: patchback[bot]
created_at: 2025-04-19T07:01:40Z
body: 
### Backport to stable-10: 💚 backport PR created

✅ Backport PR branch: `patchback/backports/stable-10/96b493002ceb892cf05a04a99bac84b2ad947432/pr-9795`

Backported as https://github.com/ansible-collections/community.general/pull/10019



<sub><sup>
--
🤖 @patchback
I'm built with [octomachinery](https://octomachinery.dev) and
my source is open — https://github.com/sanitizers/patchback-github-app.
</sup></sub>


author_association: NONE
user: login: felixfontein
created_at: 2025-04-19T07:02:05Z
body: @rgl thanks for your contribution!
@russoz @mietzen @jborean93 thanks for your comments and reviews!
author_association: COLLABORATOR