mirror of
ssh://git.janware.com/janware/proj/jw-pkg
synced 2026-04-24 17:23:36 +02:00
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>
153 lines
4.4 KiB
Python
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)
|