Source code for duct.protocol.ssh

"""
.. module:: ssh
   :synopsis: Provides a simplified SSH client interface via asyncssh

.. moduleauthor:: Colin Alston <colin@tamvera.com>
"""
import os
import asyncio
import logging

import asyncssh

log = logging.getLogger(__name__)


[docs] class SSHClient(object): """Create an SSH connection and tunnel commands over it. :param hostname: Hostname to connect to :param username: Username to authenticate with :param port: Port to connect to (default: 22) :param password: Optional password for authentication :param knownhosts: Known hosts file path """ def __init__(self, hostname, username, port, password=None, knownhosts=None): self.hostname = hostname self.username = username self.port = int(port) self.password = password self.connection = None self.knownhosts = None if knownhosts: if os.path.exists(knownhosts): self.knownhosts = knownhosts self._client_keys = []
[docs] def addKeyFile(self, kfile, password=None): """Import a private key from a file""" if not os.path.exists(kfile): raise FileNotFoundError(f"Key file not found: {kfile}") key = asyncssh.read_private_key(kfile, passphrase=password) self._client_keys.append(key)
[docs] def addKeyString(self, kstring, password=None): """Import a private key from a string""" key = asyncssh.import_private_key(kstring, passphrase=password) self._client_keys.append(key)
[docs] async def connect(self): """Open connection to SSH server""" log.info("Opening SSH connection to %s@%s:%s", self.username, self.hostname, self.port) connect_kwargs = dict( host=self.hostname, port=self.port, username=self.username, known_hosts=self.knownhosts, ) if self._client_keys: connect_kwargs['client_keys'] = self._client_keys if self.password: connect_kwargs['password'] = self.password self.connection = await asyncssh.connect(**connect_kwargs) log.info("Established SSH connection to %s", self.hostname)
[docs] async def fork(self, command, args=(), env=None, path=None, _timeout=3600): """Execute a remote command. Returns (stdout, stderr, exit_code).""" if not self.connection: return (None, "SSH not ready", 255) full_cmd = command if path: full_cmd = f'PATH={path} {full_cmd}' if env: prefix = ' '.join(f'{k}={v}' for k, v in env.items()) full_cmd = prefix + ' ' + full_cmd if args: full_cmd += ' ' + ' '.join(args) try: result = await asyncio.wait_for( self.connection.run(full_cmd), timeout=_timeout ) return result.stdout, result.stderr, result.exit_status or 0 except asyncio.TimeoutError: return (None, "SSH command timed out", 1) except Exception as e: log.warning("SSH fork error: %s", e) return (None, str(e), 1)