# -*- coding: utf-8 -*- from typing import Any import os, abc, sys, pwd from enum import Flag, auto from ..util import pretty_cmd from ..log import * from ..base import Result from ..ExecContext import ExecContext from urllib.parse import urlparse class SSHClient(ExecContext): class Caps(Flag): LogOutput = auto() Interactive = auto() ModEnv = auto() Wd = auto() def __init__(self, uri: str, caps: Caps=Caps(0), *args, **kwargs) -> None: super().__init__(uri=uri, *args, **kwargs) self.__caps = caps try: parsed = urlparse(uri) except Exception as e: log(ERR, f'Failed to parse SSH URI "{uri}"') raise self.__hostname = parsed.hostname if self.__hostname is None: raise Exception(f'Can\'t parse host name from SSH URI "{uri}"') self.__port = parsed.port self.__password = parsed.password self.__username = parsed.username @abc.abstractmethod async def _run_ssh( cmd: list[str], wd: str|None, verbose: bool, cmd_input: bytes|None, mod_env: dict[str, str]|None, interactive: bool, log_prefix: str ) -> Result: pass async def _run( self, cmd: list[str], wd: str|None, verbose: bool, cmd_input: bytes|None, mod_env: dict[str, str]|None, interactive: bool, log_prefix: str ) -> Result: def __log(prio: int, *args): log(prio, log_prefix, *args) def __log_block(prio: int, title: str, block: str): if self.__caps & self.Caps.LogOutput: return if not block: return encoding = sys.stdout.encoding or 'utf-8' block = block.decode(encoding).strip() if not block: return delim = f'---- {title} ----' __log(prio, f',{delim}') for line in block.splitlines(): __log(prio, '|', line) __log(prio, f'`{delim}') if wd is not None and not self.__caps & self.Caps.Wd: cmd = ['cd', wd, '&&', *cmd] if interactive and not self.__caps & self.Caps.Interactive: raise NotImplementedError('Interactive SSH is not yet implemented') if mod_env is not None and not self.__caps & self.Caps.ModEnv: raise NotImplementedError('Passing an environment to SSH commands is not yet implemented') ret = await self._run_ssh( cmd=cmd, wd=wd, verbose=verbose, cmd_input=cmd_input, mod_env=mod_env, interactive=interactive, log_prefix=log_prefix ) if verbose: __log_block(NOTICE, 'stdout', ret.stdout) __log_block(NOTICE, 'stderr', ret.stderr) if ret.status != 0: __log(WARNING, f'Exit code {ret.status}') return ret @property def hostname(self) -> str|None: return self.__hostname @property def port(self) -> int|None: return self.__port def set_password(self, password: str) -> None: self.__password = password @property def password(self) -> str|None: return self.__password def set_username(self, username: str) -> None: self.__username = username def _username(self) -> str: if self.__username is None: return pwd.getpwuid(os.getuid()).pw_name return self.__username def ssh_client(*args, type: str|list[str]|None=None, **kwargs) -> SSHClient: # export from importlib import import_module errors: list[str] = [] if type is None: val = os.getenv('JW_DEFAULT_SSH_CLIENT') if val is not None: type = val.split(',') else: type = ['AsyncSSH', 'Paramiko', 'Exec'] if isinstance(type, str): type = [type] for name in type: try: ret = getattr(import_module(f'jw.pkg.lib.ec.ssh.{name}'), name)(*args, **kwargs) log(INFO, f'Using SSH-client "{name}"') return ret except Exception as e: msg = f'Can\'t instantiate SSH client class {name} ({str(e)})' errors.append(msg) log(DEBUG, f'{msg}, trying next') msg = f'No working SSH clients for {" ".join(args)}' log(ERR, f'----- {msg}') for error in errors: log(ERR, error) raise Exception(msg)