jw-pkg/src/python/jw/pkg/lib/ec/SSHClient.py
Jan Lindemann 54aecff8e4 lib.ExecContext.run(), .sudo(): Rename env
The name of the env parameter to ExecContext.run() and .sudo() is not
descriptive enough for which environment is supposed to be modified
and how, so rename and split it up as follows:

  - .run(): env -> mod_env

  - .sudo(): env -> mod_env_sudo and mod_env_cmd

The parameters have the following meaning:

  - "mod_env*" means that the environment is modified, not replaced

  - "mod_env" and "mod_env_cmd" modify the environment "cmd" runs in

  - "mod_env_sudo" modifies the environment sudo runs in

Fix the fallout of the API change all over jw-pkg.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-04-19 14:36:50 +02:00

153 lines
4.4 KiB
Python

# -*- 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)