Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add the wsl connection plugin #9795

Open
wants to merge 9 commits into
base: main
Choose a base branch
from

Conversation

rgl
Copy link

@rgl rgl commented Feb 22, 2025

SUMMARY

This adds the community.general.wsl connection plugin.

This allows Ansible to remotely manage a WSL Distribution 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.

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:

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:

- 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

@ansibullbot ansibullbot added connection connection plugin new_contributor Help guide this first time contributor plugins plugin (any type) labels Feb 22, 2025
@ansibullbot

This comment was marked as outdated.

@ansibullbot ansibullbot added ci_verified Push fixes to PR branch to re-run CI needs_revision This PR fails CI tests or a maintainer has requested a review/revision of the PR labels Feb 22, 2025
@rgl rgl force-pushed the rgl-add-wsl-connection-plugin branch from 69e5503 to 5ea8dfa Compare February 22, 2025 16:47
@ansibullbot ansibullbot removed the ci_verified Push fixes to PR branch to re-run CI label Feb 22, 2025
@rgl rgl force-pushed the rgl-add-wsl-connection-plugin branch from 5ea8dfa to 02c2e8a Compare February 22, 2025 16:51
@ansibullbot ansibullbot removed the needs_revision This PR fails CI tests or a maintainer has requested a review/revision of the PR label Feb 22, 2025
Copy link
Collaborator

@russoz russoz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hi @rgl thanks for your contribution!!

I have a couple of comments below. :-)

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"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
version_added: "10.3.0"
version_added: "10.4.0"

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done. please check it now.

Comment on lines +751 to +759
raise AnsibleError(
f'error occurred while writing SSH host keys!\n{to_text(e)}')
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
raise AnsibleError(
f'error occurred while writing SSH host keys!\n{to_text(e)}')
raise AnsibleError(f'error occurred while writing SSH host keys!\n{to_text(e)}')

Comment on lines +467 to +476
# paramiko 2.2 introduced auth_timeout parameter
if LooseVersion(paramiko.__version__) >= LooseVersion('2.2.0'):
ssh_connect_kwargs['auth_timeout'] = self.get_option('timeout')
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not mentioned in the docs for the user. It should have some note about it, like the "banner timeout" below has.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done. please verify.

Comment on lines +403 to +412
display.warning('Paramiko ProxyCommand support unavailable. '
'Please upgrade to Paramiko 1.9.0 or newer. '
'Not using configured ProxyCommand')
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be mentioned in the docs for proxy_command.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done. please verify.

Comment on lines 245 to 261
# ------------------------
# 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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah, much better! please check it now.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh, it seems that comment can only have a single document due to #9795 (comment)

should this change be reverted? or the CI be changed?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done. please verify.

Comment on lines +445 to +453
except IOError:
pass # file was not found, but not required to function
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure either.

This was introduced by @mietzen in #8424.

@mietzen, maybe you can tell us more about this?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also only copied it from: paramiko_ssh

I guessed this would cover some edge case, so I left it in.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW, that code change was introduced in 2015 by @bcoca in ansible/ansible@b9710b4 anda its still present in ansible 2.18.3.

stderr = b''.join(chan.makefile_stderr('rb', bufsize))
returncode = chan.recv_exit_status()

# NB the full english error message is:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Author

@rgl rgl Mar 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it probably is subject to localization settings, thou, not sure how to handle it, please advise.

Comment on lines +712 to +749
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)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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. :-)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 #8424.

@mietzen, maybe you can tell us more about this?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

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.

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')
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor style nit picking. It might be leaner as:

Suggested change
chan.sendall(to_bytes(become_pass, errors='surrogate_or_strict') + b'\n')
chan.sendall(to_bytes(become_pass + '\n', errors='surrogate_or_strict'))

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done. please verify.

@rgl rgl force-pushed the rgl-add-wsl-connection-plugin branch from 02c2e8a to d3bb34c Compare February 23, 2025 12:05
@ansibullbot

This comment was marked as outdated.

@ansibullbot ansibullbot added ci_verified Push fixes to PR branch to re-run CI needs_revision This PR fails CI tests or a maintainer has requested a review/revision of the PR labels Feb 23, 2025
@rgl
Copy link
Author

rgl commented Feb 23, 2025

@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 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 (this one seems to have changes that were not propagated to proxmox_pct_remote, and vice-versa).

what do you think?

Copy link
Collaborator

@felixfontein felixfontein left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@rgl rgl force-pushed the rgl-add-wsl-connection-plugin branch from d3bb34c to 923da85 Compare February 23, 2025 12:48
@ansibullbot
Copy link
Collaborator

The test ansible-test sanity --test yamllint [explain] failed with 1 error:

plugins/connection/wsl.py:267:1: unparsable-with-libyaml: expected a single document in the stream - but found another document

click here for bot help

@jborean93
Copy link
Contributor

jborean93 commented Feb 23, 2025

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.

@russoz
Copy link
Collaborator

russoz commented Feb 23, 2025

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.

@felixfontein
Copy link
Collaborator

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.

@rgl
Copy link
Author

rgl commented Feb 24, 2025

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).

@felixfontein
Copy link
Collaborator

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.

@rgl rgl force-pushed the rgl-add-wsl-connection-plugin branch from 923da85 to 76acdd0 Compare February 24, 2025 22:28
@ansibullbot ansibullbot added tests tests and removed ci_verified Push fixes to PR branch to re-run CI needs_revision This PR fails CI tests or a maintainer has requested a review/revision of the PR labels Feb 24, 2025
@russoz
Copy link
Collaborator

russoz commented Mar 1, 2025

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.

vars:
- name: become
- name: ansible_become
_wsl_command_use_bash_quoting:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah, indeed, much better, its now using an environment variable, please check it out :)

@ansibullbot ansibullbot removed the needs_revision This PR fails CI tests or a maintainer has requested a review/revision of the PR label Mar 1, 2025
@rgl rgl force-pushed the rgl-add-wsl-connection-plugin branch 2 times, most recently from 0ad7826 to 4eebc5d Compare March 1, 2025 14:55
@rgl
Copy link
Author

rgl commented Mar 1, 2025

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.

@rgl rgl force-pushed the rgl-add-wsl-connection-plugin branch from 4eebc5d to e0e3f5e Compare March 1, 2025 15:27
@felixfontein
Copy link
Collaborator

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.

@rgl rgl force-pushed the rgl-add-wsl-connection-plugin branch 2 times, most recently from 1b8890e to f12f2ac Compare March 1, 2025 18:46
@rgl rgl force-pushed the rgl-add-wsl-connection-plugin branch from f12f2ac to 71a2482 Compare March 1, 2025 18:55
@russoz
Copy link
Collaborator

russoz commented Mar 4, 2025

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.

@ansibullbot

This comment was marked as outdated.

@ansibullbot ansibullbot added ci_verified Push fixes to PR branch to re-run CI needs_revision This PR fails CI tests or a maintainer has requested a review/revision of the PR labels Mar 5, 2025
@rgl
Copy link
Author

rgl commented Mar 5, 2025

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. :-)

@mietzen
Copy link
Contributor

mietzen commented Mar 6, 2025

@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.

@ansibullbot ansibullbot removed the ci_verified Push fixes to PR branch to re-run CI label Mar 11, 2025
@ansibullbot
Copy link
Collaborator

The test ansible-test sanity --test import --python 3.10 [explain] failed with 1 error:

plugins/connection/wsl.py:335:0: traceback: ModuleNotFoundError: No module named 'paramiko'

The test ansible-test sanity --test import --python 3.11 [explain] failed with 1 error:

plugins/connection/wsl.py:335:0: traceback: ModuleNotFoundError: No module named 'paramiko'

The test ansible-test sanity --test import --python 3.12 [explain] failed with 1 error:

plugins/connection/wsl.py:335:0: traceback: ModuleNotFoundError: No module named 'paramiko'

The test ansible-test sanity --test import --python 3.10 [explain] failed with 1 error:

plugins/connection/wsl.py:335:0: traceback: ModuleNotFoundError: No module named 'paramiko'

The test ansible-test sanity --test import --python 3.11 [explain] failed with 1 error:

plugins/connection/wsl.py:335:0: traceback: ModuleNotFoundError: No module named 'paramiko'

The test ansible-test sanity --test import --python 3.12 [explain] failed with 1 error:

plugins/connection/wsl.py:335:0: traceback: ModuleNotFoundError: No module named 'paramiko'

The test ansible-test sanity --test import --python 3.11 [explain] failed with 1 error:

plugins/connection/wsl.py:335:0: traceback: ModuleNotFoundError: No module named 'paramiko'

The test ansible-test sanity --test import --python 3.12 [explain] failed with 1 error:

plugins/connection/wsl.py:335:0: traceback: ModuleNotFoundError: No module named 'paramiko'

The test ansible-test sanity --test import --python 3.13 [explain] failed with 1 error:

plugins/connection/wsl.py:335:0: traceback: ModuleNotFoundError: No module named 'paramiko'

The test ansible-test sanity --test import --python 3.11 [explain] failed with 1 error:

plugins/connection/wsl.py:335:0: traceback: ModuleNotFoundError: No module named 'paramiko'

The test ansible-test sanity --test import --python 3.12 [explain] failed with 1 error:

plugins/connection/wsl.py:335:0: traceback: ModuleNotFoundError: No module named 'paramiko'

The test ansible-test sanity --test import --python 3.13 [explain] failed with 1 error:

plugins/connection/wsl.py:335:0: traceback: ModuleNotFoundError: No module named 'paramiko'

click here for bot help

@ansibullbot ansibullbot added the ci_verified Push fixes to PR branch to re-run CI label Mar 11, 2025
@felixfontein felixfontein added the check-before-release PR will be looked at again shortly before release and merged if possible. label Mar 12, 2025
Comment on lines +335 to +336
from paramiko.client import SSHClient
from paramiko.pkey import PKey
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These paramiko imports will not work if paramiko is not present. You must only do them if paramiko is truish:

Suggested change
from paramiko.client import SSHClient
from paramiko.pkey import PKey
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:

Suggested change
from paramiko.client import SSHClient
from paramiko.pkey import PKey
if t.TYPE_CHECKING and paramiko:
from paramiko.client import SSHClient
from paramiko.pkey import PKey

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
check-before-release PR will be looked at again shortly before release and merged if possible. ci_verified Push fixes to PR branch to re-run CI connection connection plugin integration tests/integration needs_revision This PR fails CI tests or a maintainer has requested a review/revision of the PR new_contributor Help guide this first time contributor new_plugin New plugin plugins plugin (any type) tests tests unit tests/unit
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants