mirror of
ssh://git.janware.com/janware/proj/jw-pkg
synced 2026-04-24 17:23:36 +02:00
Add lib.base to provide basic definitions. For now, move the definiions of Result, Input and InputMode from ExecContext into lib.base. Having to import them from the ExecContect module is too heavy-handed for those simple types. Signed-off-by: Jan Lindemann <jan@janware.com>
151 lines
4.4 KiB
Python
151 lines
4.4 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
from typing import Any
|
|
|
|
import os, abc, sys
|
|
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()
|
|
Env = 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,
|
|
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,
|
|
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
|
|
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 env is not None and not self.__caps & self.Caps.Env:
|
|
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,
|
|
env=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
|
|
|
|
async def _sudo(self, cmd: list[str], mod_env: dict[str, str], opts: list[str], *args, **kwargs) -> Result:
|
|
if self.username != 'root':
|
|
cmd = ['sudo', *opts, *cmd]
|
|
if mod_env:
|
|
log(WARNING, f'Modifying environment over SSH is not implemented, ignored')
|
|
return await self._run(cmd, *args, **kwargs)
|
|
|
|
@property
|
|
def hostname(self):
|
|
return self.__hostname
|
|
|
|
@property
|
|
def port(self):
|
|
return self.__port
|
|
|
|
def set_password(self, password: str) -> None:
|
|
self.__password = password
|
|
|
|
@property
|
|
def password(self) -> str:
|
|
return self.__password
|
|
|
|
def set_username(self, username: str) -> None:
|
|
self.__username = username
|
|
|
|
@property
|
|
def username(self) -> str:
|
|
return self.__username
|
|
|
|
def ssh_client(*args, type=['AsyncSSH', 'Paramiko', 'Exec'], **kwargs) -> SSHClient: # export
|
|
from importlib import import_module
|
|
errors: list[str] = []
|
|
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)
|