2025-11-18 12:07:02 +01:00
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
|
2026-03-06 11:45:15 +01:00
|
|
|
from __future__ import annotations
|
2026-01-27 16:20:57 +01:00
|
|
|
|
2026-03-06 11:45:15 +01:00
|
|
|
from typing import TYPE_CHECKING, Iterable
|
|
|
|
|
|
|
|
|
|
if TYPE_CHECKING:
|
|
|
|
|
from typing import Sequence
|
2026-04-15 14:06:35 +02:00
|
|
|
from .ExecContext import ExecContext
|
2026-03-06 11:45:15 +01:00
|
|
|
|
|
|
|
|
import os, sys, json
|
2026-01-27 16:20:57 +01:00
|
|
|
|
2025-11-18 12:07:02 +01:00
|
|
|
from argparse import Namespace
|
2025-11-20 11:29:50 +01:00
|
|
|
from enum import Enum, auto
|
|
|
|
|
|
2026-01-27 16:20:57 +01:00
|
|
|
from .log import *
|
2026-04-16 11:23:05 +02:00
|
|
|
from .base import InputMode
|
2026-04-27 09:37:28 +02:00
|
|
|
from .Uri import Uri
|
2026-01-27 16:20:57 +01:00
|
|
|
|
2025-11-20 11:29:50 +01:00
|
|
|
class AskpassKey(Enum):
|
|
|
|
|
Username = auto()
|
|
|
|
|
Password = auto()
|
2025-11-18 12:07:02 +01:00
|
|
|
|
2025-11-20 10:44:14 +01:00
|
|
|
def pretty_cmd(cmd: list[str], wd=None):
|
2025-11-18 12:07:02 +01:00
|
|
|
tokens = [cmd[0]]
|
|
|
|
|
for token in cmd[1:]:
|
|
|
|
|
if token.find(' ') != -1:
|
|
|
|
|
token = '"' + token + '"'
|
|
|
|
|
tokens.append(token)
|
2026-02-23 10:26:59 +01:00
|
|
|
ret = ' '.join(tokens)
|
2025-11-18 12:07:02 +01:00
|
|
|
if wd is not None:
|
2025-11-20 10:44:14 +01:00
|
|
|
ret += f' in {wd}'
|
|
|
|
|
return ret
|
|
|
|
|
|
2026-03-06 16:50:27 +01:00
|
|
|
# See ExecContext.run() for what this function does
|
2026-04-17 18:11:43 +02:00
|
|
|
async def run_cmd(*args, ec: ExecContext|None=None, verbose: bool|None=None, cmd_input: Input=InputMode.NonInteractive, **kwargs) -> Result:
|
2026-03-06 16:50:27 +01:00
|
|
|
if verbose is None:
|
|
|
|
|
verbose = False if ec is None else ec.verbose_default
|
2026-03-06 11:54:27 +01:00
|
|
|
if ec is None:
|
|
|
|
|
from .ec.Local import Local
|
2026-04-17 18:11:43 +02:00
|
|
|
interactive = cmd_input == InputMode.Interactive
|
2026-04-10 15:30:48 +02:00
|
|
|
ec = Local(verbose_default=verbose, interactive=interactive)
|
2026-03-06 16:50:27 +01:00
|
|
|
return await ec.run(verbose=verbose, *args, **kwargs)
|
2026-01-27 16:20:57 +01:00
|
|
|
|
2026-04-15 14:02:44 +02:00
|
|
|
async def run_curl(args: list[str], parse_json: bool=False, wd=None, throw=None, verbose=None, cmd_input=InputMode.NonInteractive, ec: ExecContext|None=None, decode=False) -> dict|str: # export
|
2026-03-06 16:50:27 +01:00
|
|
|
if verbose is None:
|
|
|
|
|
verbose = False if ec is None else ec.verbose_default
|
2025-11-18 12:07:02 +01:00
|
|
|
cmd = ['curl']
|
|
|
|
|
if not verbose:
|
|
|
|
|
cmd.append('-s')
|
|
|
|
|
cmd.extend(args)
|
|
|
|
|
if parse_json:
|
2026-04-07 13:04:11 +02:00
|
|
|
decode = True
|
|
|
|
|
output = await run_cmd(cmd, wd=wd, throw=throw, verbose=verbose, cmd_input=cmd_input, ec=ec)
|
|
|
|
|
stdout, stderr, status = output.decode() if decode else output
|
|
|
|
|
if not parse_json:
|
|
|
|
|
ret = stdout
|
|
|
|
|
else:
|
2025-11-20 10:44:14 +01:00
|
|
|
try:
|
2026-04-07 13:04:11 +02:00
|
|
|
ret = json.loads(stdout)
|
2025-11-20 10:44:14 +01:00
|
|
|
except Exception as e:
|
2026-02-27 15:45:08 +01:00
|
|
|
size = 'unknown number of'
|
2026-01-27 16:20:57 +01:00
|
|
|
try:
|
2026-04-07 13:04:11 +02:00
|
|
|
size = len(stdout)
|
2026-01-27 16:20:57 +01:00
|
|
|
except:
|
|
|
|
|
pass
|
2026-03-25 11:38:29 +01:00
|
|
|
log(ERR, f'Failed to parse {size} bytes output of command '
|
2026-04-07 13:04:11 +02:00
|
|
|
+ f'>{pretty_cmd(cmd, wd)}< ({str(e)}): "{stdout}"', file=sys.stderr)
|
2025-11-20 10:44:14 +01:00
|
|
|
raise
|
2026-03-03 08:34:27 +01:00
|
|
|
return ret, stderr, status
|
2025-11-18 12:07:02 +01:00
|
|
|
|
2026-03-06 11:54:27 +01:00
|
|
|
async def run_askpass(askpass_env: list[str], key: AskpassKey, host: str|None=None, ec: ExecContext|None=None):
|
2026-04-25 07:45:14 +02:00
|
|
|
if host is not None: # Currently unsupported
|
|
|
|
|
raise NotImplementedError(f'Tried to run askpass with host "{host}"')
|
2025-11-20 11:29:50 +01:00
|
|
|
for var in askpass_env:
|
|
|
|
|
exe = os.getenv(var)
|
|
|
|
|
if exe is None:
|
|
|
|
|
continue
|
|
|
|
|
exe_arg = ''
|
|
|
|
|
match var:
|
|
|
|
|
case 'GIT_ASKPASS':
|
|
|
|
|
match key:
|
|
|
|
|
case AskpassKey.Username:
|
|
|
|
|
exe_arg += 'Username'
|
|
|
|
|
case AskpassKey.Password:
|
|
|
|
|
exe_arg += 'Password'
|
|
|
|
|
case 'SSH_ASKPASS':
|
|
|
|
|
match key:
|
|
|
|
|
case AskpassKey.Username:
|
|
|
|
|
continue # Can't get user name from SSH_ASKPASS
|
|
|
|
|
case AskpassKey.Password:
|
|
|
|
|
exe_arg += 'Password'
|
2026-03-23 13:13:55 +01:00
|
|
|
ret, stderr, status = await run_cmd([exe, exe_arg], throw=False, ec=ec).decode()
|
2025-11-20 11:29:50 +01:00
|
|
|
if ret is not None:
|
|
|
|
|
return ret
|
|
|
|
|
return None
|
|
|
|
|
|
2026-03-25 08:28:10 +01:00
|
|
|
async def run_sudo(cmd: list[str], *args, interactive: bool=True, ec: ExecContext|None=None, **kwargs):
|
2026-03-06 11:54:27 +01:00
|
|
|
if ec is None:
|
|
|
|
|
from .ec.Local import Local
|
|
|
|
|
ec = Local(interactive=interactive)
|
2026-03-25 08:28:10 +01:00
|
|
|
return await ec.sudo(cmd, *args, **kwargs)
|
2026-02-17 10:19:57 +01:00
|
|
|
|
2026-04-17 15:05:42 +02:00
|
|
|
async def copy(src_uri: str, dst_uri: str, owner: str|None=None, group: str|None=None, mode: int|None=None, throw=True) -> Exception|str:
|
2026-04-15 14:06:35 +02:00
|
|
|
from .ExecContext import ExecContext
|
|
|
|
|
src: ExecContext|None = None
|
|
|
|
|
dst: ExecContext|None = None
|
|
|
|
|
def __ec(uri: str) -> tuple[ExecContext, str]:
|
|
|
|
|
return ExecContext.create(uri), urlparse(uri).path
|
|
|
|
|
try:
|
|
|
|
|
src, src_path = __ec(src_uri)
|
|
|
|
|
content = (await src.get(src_path, throw=True)).stdout
|
|
|
|
|
dst, dst_path = __ec(dst_uri)
|
lib.FileContext: Add file methods
Add the following methods, meant to do the obvious:
unlink(self, path: str) -> None
erase(self, path: str) -> None
rename(self, src: str, dst: str) -> None
mktemp(self, tmpl: str, directory: bool=False) -> None
chown(self, path: str, owner: str|None=None, group: str|None=None) -> None
chmod(self, path: str, mode: int) -> None
stat(self, path: str, follow_symlinks: bool=True) -> StatResult
file_exists(self, path: str) -> bool
is_dir(self, path: str) -> bool
All methods are async and call their protected counterpart, which is
designed to be overridden. If possible, default implementations do
something meaningful, if not, they just raise plain
NotImplementedError.
Signed-off-by: Jan Lindemann <jan@janware.com>
2026-04-17 09:15:06 +02:00
|
|
|
if await dst.is_dir(path):
|
2026-04-15 14:06:35 +02:00
|
|
|
dst_path += os.path.basename(src_path)
|
|
|
|
|
await dst.put(path=dst_path, content=content, owner=owner, group=group, mode=mode, throw=True)
|
|
|
|
|
except Exception as e:
|
|
|
|
|
if throw:
|
|
|
|
|
raise
|
|
|
|
|
log(ERR, f'Failed to copy {src_uri} -> {dst_uri} ({str(e)})')
|
|
|
|
|
return e
|
|
|
|
|
finally:
|
|
|
|
|
for ec in [src, dst]:
|
|
|
|
|
if ec is not None:
|
|
|
|
|
await ec.close()
|
|
|
|
|
return dst_path
|
|
|
|
|
|
2026-03-06 11:54:27 +01:00
|
|
|
async def get_username(args: Namespace|None=None, url: str|None=None, askpass_env: list[str]=[], ec: ExecContext|None=None) -> str: # export
|
2026-04-27 09:37:28 +02:00
|
|
|
url_user = None if url is None else Uri(url).username
|
2025-11-18 12:07:02 +01:00
|
|
|
if args is not None:
|
|
|
|
|
if args.username is not None:
|
|
|
|
|
if url_user is not None and url_user != args.username:
|
|
|
|
|
raise Exception(f'Username mismatch: called with --username="{args.username}", URL has user name "{url_user}"')
|
|
|
|
|
return args.username
|
2025-11-20 11:29:50 +01:00
|
|
|
if url_user is not None:
|
2025-11-18 12:07:02 +01:00
|
|
|
return url_user
|
2026-03-06 11:54:27 +01:00
|
|
|
return await run_askpass(askpass_env, AskpassKey.Username, ec=ec)
|
2025-11-18 12:07:02 +01:00
|
|
|
|
2026-03-06 11:54:27 +01:00
|
|
|
async def get_password(args: Namespace|None=None, url: str|None=None, askpass_env: list[str]=[], ec: ExecContext|None=None) -> str: # export
|
2025-11-18 12:07:02 +01:00
|
|
|
if args is None and url is None and not askpass_env:
|
|
|
|
|
raise Exception(f'Neither URL nor command-line arguments nor askpass environment variable available, can\'t get password')
|
|
|
|
|
if args is not None and hasattr(args, 'password'): # use getattr(), because we don't necessarily want to have insecure --password among options
|
|
|
|
|
ret = getattr(args, 'password')
|
|
|
|
|
if ret is not None:
|
|
|
|
|
return ret
|
|
|
|
|
if url is not None:
|
2026-04-27 09:37:28 +02:00
|
|
|
ret = Uri(url).password
|
|
|
|
|
if ret is not None:
|
|
|
|
|
return ret
|
2026-03-06 11:54:27 +01:00
|
|
|
return await run_askpass(askpass_env, AskpassKey.Password, ec=ec)
|
2026-02-18 14:18:26 +01:00
|
|
|
|
2026-03-06 11:54:27 +01:00
|
|
|
async def get_profile_env(throw: bool=True, keep: Iterable[str]|bool=False, ec: ExecContext|None=None) -> dict[str, str]: # export
|
2026-02-19 07:33:59 +01:00
|
|
|
"""
|
|
|
|
|
Get a fresh environment from /etc/profile
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
keep:
|
|
|
|
|
- False -> Don't keep anything
|
|
|
|
|
- True -> Keep what's in the current environment
|
|
|
|
|
- List of strings -> Keep those variables
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
Dictionary with fresh environment
|
|
|
|
|
"""
|
2026-04-19 14:04:35 +02:00
|
|
|
mod_env: dict[str,str]|None = None
|
2026-02-19 07:33:59 +01:00
|
|
|
if keep == False or isinstance(keep, Iterable):
|
2026-04-19 14:04:35 +02:00
|
|
|
mod_env = {
|
2026-02-19 07:33:59 +01:00
|
|
|
'HOME': os.environ.get('HOME', '/'),
|
|
|
|
|
'USER': os.environ.get('USER', ''),
|
|
|
|
|
'PATH': '/usr/bin:/bin',
|
|
|
|
|
}
|
|
|
|
|
# Run bash as a login shell, which sources /etc/profile, then print environment as NUL-separated key=value pairs
|
|
|
|
|
cmd = ['/usr/bin/env', '-i', '/bin/bash', '-lc', 'env -0']
|
2026-04-19 14:04:35 +02:00
|
|
|
result = await run_cmd(cmd, throw=throw, verbose=True, mod_env=mod_env, ec=ec)
|
2026-02-18 14:18:26 +01:00
|
|
|
ret: dict[str, str] = {}
|
2026-03-23 13:13:55 +01:00
|
|
|
for entry in result.stdout.rstrip(b"\0").split(b"\0"):
|
2026-02-18 14:18:26 +01:00
|
|
|
if not entry:
|
|
|
|
|
continue
|
|
|
|
|
key, val = entry.split(b"=", 1)
|
|
|
|
|
ret[key.decode()] = val.decode()
|
2026-02-19 07:33:59 +01:00
|
|
|
if isinstance(keep, Iterable):
|
|
|
|
|
for key in keep:
|
|
|
|
|
val = os.getenv(key)
|
|
|
|
|
if val is not None:
|
|
|
|
|
ret[key] = val
|
2026-02-18 14:18:26 +01:00
|
|
|
return ret
|