Commit graph jw-pkg/src/python/jw/pkg/lib
Author SHA1 Message Date
aa7275f426 App.distro_xxx: Move properties to Distro.xxx
Commit a19679fec reverted the first attempt to make AsyncSSH reuse
one connection during an instance lifetime. That failed because a lot
of distribution-specific properties were filled in a new event loop
thread started by AsyncRunner, and AsyncSSH didn't like that.

This commit is the first part of the solution: Move those properties
from the App class to the Distro class, and load the Distro class
in an async loader. As soon as it's instantiated, it can provide all
its properties without cluttering the code with async keywords.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-04-19 21:00:21 +02:00
c238a0d2d0 lib.ec.ssh.AsyncSSH: Replace legacy Caps.Env
AsyncSSH uses the legacy Caps.Env, replace it by Caps.ModEnv.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-04-19 20:59:16 +02:00
f1898941e7 lib.ec.ssh.AsyncSSH: Code beautification
Apply some style changes:

  - Replace double by single quotes for consistency

  - Add spaces around equal signs in long parameter lists

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-04-19 14:36:50 +02:00
1359719f04 lib.ExecContext: Code beautification
Add spaces around equal signs in long parameter lists.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-04-19 14:36:50 +02:00
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
9f756222fe lib.ExecContext.username: Add property
Add the property .username, backed by the protected _username()
callback. It should return the user run()'s cmd parameter is executed
as.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-04-19 14:36:50 +02:00
5ea6ab0383 lib.ec.SSHClient._run(): Fix empty stderr output logging
If stderr is None, a bogus Exception is thrown in verbose mode, fix
that.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-04-19 14:36:50 +02:00
2851ef8f42 lib.ec.ssh.Paramiko: Support Caps.Env
Add support for modifying the execution environment via the env
parameter to Paramiko.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-04-18 12:23:26 +00:00
3cf5b2264e 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-18 10:43:31 +02:00
bc3ed1737f lib.base.StatResult: Add type
Add the basic type StatResult. It is something akin to
os.stat_result, but with user and group string members instead of
st_uid and st_gid. The latter can't be expected to be stable across
remote contexts.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-04-18 10:43:31 +02:00
154db1ebc4 lib.ExecContext.get(): Raise FileNotFoundError
Raise FileNotFoundError from ExecContext.get() if the _run() returns
"No such File" in stderr.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-04-18 10:43:31 +02:00
8a0770aec5 lib.ExecContext.run(): Pass LC_ALL=C by default
Pass LC_ALL="C" to _run() by default. This is necessary to be able to
parse error messages and raise FileNotFound if need be.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-04-18 10:43:31 +02:00
fe3508036e lib.FileContext.put(): Change param mode type str -> int
Don't pass mode as a string to put(). Given the multitunde of
possible string representations for numbers, some understood by
int(string, 0) and some not, there's too much room for passing
strings which are unparseable, or worse, prone to be parsed wrongly.

However, pass mode down to _put() as a string for convenience,
because that's what most _put() implementations will need to use. If
they don't, converting to int is easy from the one defined string
format.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-04-18 10:43:31 +02:00
64e9fbff59 lib.FileContext.put(): Swap params path and content
Swap the positions of the "path" and "content" parameters of put().
Path comes always first, in every path related function I know.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-04-18 10:43:31 +02:00
94eee5c4bb lib.FileContext.put(): Add parameter "atomic"
Add the parameter "atomic" to put() / _put(). If instructs the
implementation to take extra precautions to make sure the operation
either succeeds or fails entirely, i.e. doesn't leave a broken target
file behind.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-04-18 10:43:31 +02:00
bbb2d16956 lib.FileContext.put(): Simplify
.put() has some commands to _run(), and it uses its own CallContext
for them. Since that pattern only replicates what run() does anyway,
we could just as well use run() itself with less code, so do that.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-04-18 10:43:31 +02:00
0d6eeeadcf lib.FileContext.log_name: Beautify
Prepend the class name to .log_name. Not sure if that makes logs more
legible, but we'll try it out for a while.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-04-18 10:43:31 +02:00
4f98fd6c78 lib.FileTransfer: Rename to FileContext
Rename class FileTransfer to FileContext because that's the better
name. It's the base class of ExecContext and also a context.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-04-18 10:43:31 +02:00
2f1265b7b2 lib.ec.SSHClient: Fix return value type hints
Add proper type-hinting for port, hostname, username and password
return values.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-04-18 10:43:31 +02:00
64a5b5d429 lib.ec.Local._run(): Interpret env as mod_env
An env argument environment passed to Local._run() entirely replaces
the environment. Make it modify the enviroment instead.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-04-18 10:43:31 +02:00
9575339972 lib.ec.ssh.AsyncSSH: Declare Caps.Env
AsyncSSH's implementation already supports modifying the execution
environment via env, so declare it to the base class with Caps.Env.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-04-18 10:43:31 +02:00
f253466a3f lib.ec.ssh.Exec._run_ssh(): Fix: interactive ignored
Exec's _run_ssh() ignores its "interactive" parameter and uses the
instances' default instead, fix that.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-04-18 10:43:31 +02:00
4a8ccfb0a6 lib.ec.ssh.Exec: Honour username and port
Username and port of an Exec SSH client are not passed to the ssh
executable, fix that.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-04-18 10:43:31 +02:00
3574c0f1bf lib.ec.ssh.Exec: Support Caps.Env
Add support for modifying the execution environment via the env
parameter to Exec.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-04-18 10:43:31 +02:00
6cdcd23010 lib.ec.ssh.Exec._run_ssh(): Fix interactivity translation
cmd_input is passed as None to _run(), which is legal, but then used
in a call to cmd_run(), which is a public API and, hence, illegal.
InputMode.NonInteractive should be used instead, do that.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-04-18 10:43:31 +02:00
910f10b194 lib.util.run_cmd(): Remove parameter interactive
run_cmd() is a thin layer over the public ExecContext API, which
falls back to using a Local instance if not other ExecContext is
specified explicitly. Both the default Local context as the
subsequent call to run() should have the same idea about
interactivity, so allowing to specify it in two parameters
("interactive" and "cmd_input") is a bad idea. Remove "interactive".

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-04-18 10:43:29 +02:00
fd336ecdcf lib.ec.SSHClient: Support JW_DEFAULT_SSH_CLIENT
Allow to configure via the environment which class ssh_client()
picks. Can currently be exec, asyncssh, paramiko or a comma-separated
search list. The list will be tried through until a class is found
that can be instantiated.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-04-18 10:42:32 +02:00
193b8242d7 lib.Types.LoadTypes: Support JW_LOG_LEVEL_LOAD_TYPES
Allow to configure logging of LoadTypes' decisions whether or not a
class is elegible for loading. Currently, the respective log level is
"off", allow to set it via JW_LOG_LEVEL_LOAD_TYPES in the
environment.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-04-18 10:42:32 +02:00
f78d08f0d8 lib.ec.ssh.Exec: Fix cmd_input == None
cmd_input is passed as None to _run(), which is legal, but then used
in a call to cmd_run(), which is a public API and, hence, illegal.
Fix that.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-04-16 19:43:29 +02:00
a19679fecc lib.ec.ssh.AsyncSSH: Revert "Reuse connection"
This reverts commit 04fef1e67a.

Reusing AsyncSSH's connection is fine and fast, but only if it's not
combined with the AsyncRunner. See commit 67e51cf0 why it was
introduced in the first place, along with a reasoning why it may be a
bad idea. Looks like we're now reaping what we sowed.

The current plan to get this to fly is to sprinkle async / await all
over the code paths to App.os_release(). That is a lot of churn, so
postpone and revert for now to keep CI working.

  File "~/local/src/jw.dev/proj/jw-pkg/scripts/jw/pkg/lib/ec/ssh/AsyncSSH.py", line 463, in _run_ssh
    return await self._run_on_conn(
           ^^^^^^^^^^^^^^^^^^^^^^^^
    ...<7 lines>...
    )
    ^
  File "~/local/src/jw.dev/proj/jw-pkg/scripts/jw/pkg/lib/ec/ssh/AsyncSSH.py", line 403, in _run_on_conn
    proc = await conn.create_process(
           ^^^^^^^^^^^^^^^^^^^^^^^^^^
    ...<7 lines>...
    )
    ^
  File "/usr/lib/python3.13/site-packages/asyncssh/connection.py", line 4492, in create_process
    chan, process = await self.create_session(
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^
        SSHClientProcess, *args, **kwargs) # type: ignore
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.13/site-packages/asyncssh/connection.py", line 4385, in create_session
    session = await chan.create(session_factory, command, subsystem,
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    ...<4 lines>...
                                bool(self._agent_forward_path))
                                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.13/site-packages/asyncssh/channel.py", line 1149, in create
    packet = await self._open(b'session')
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.13/site-packages/asyncssh/channel.py", line 717, in _open
    return await self._open_waiter

^^^^^^^^^^^^^^^^^^^^^^^

RuntimeError: Task <Task pending name='Task-1' coro=<App.__run() running at ~/local/src/jw.dev/proj/jw-pkg/scripts/jw/pkg/lib/App.py:137> cb=[_run_until_complete_cb() at /usr/lib64/python3.13/asyncio/base_events.py:181]> got Future <Future pending> attached to a different loop

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-04-16 16:07:11 +02:00
d1df9f0ac7 lib.Distro.install(): Treat URL arguments specially
As of now, install() passes the "names" parameter on to _install(),
which is expected to pass the list of package names on to the package
manager for having it handle package download and installation. This
commit makes it easier for Distro instances to support directly
installing packages via an URL instead by providing a few callback
methods to be overridden, in case the package manager doesn't handle
package URLs the same way as package names.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-04-16 13:02:29 +02:00
45001144d7 lib.ec.Curl: Add class
Add class Curl as the first pure FileTransfer class without _run()
/ _sudo(). It doesn't use any PycURL / libcurl-like binding, but runs
the curl binary in a subprocess. That looks the most portable still.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-04-16 12:59:42 +02:00
825dd1449c lib.FileTransfer: Split from ExecContext
ExecContext has get() / _get() and put() / _put(), which make a fine
API for a file transfer class. A class supporting file transfer
should not, however, be forced to implement _run() and _sudo(), so
place this functionality in a new class FileTransfer, and derive
ExecContext from it.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-04-16 12:57:32 +02:00
888c0495ec lib.base: Add module
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>
2026-04-16 12:57:04 +02:00
04fef1e67a lib.ec.ssh.AsyncSSH: Reuse connection
With CmdCopy as test case and ExecContext.close() in place, we can
actually implement connection reuse, so do it for AsyncSSH.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-04-16 12:56:55 +02:00
63383cb683 lib.util.copy(): Add function
Add copy(src_uri, dst_uri), instatiating two ExecContext instances,
and doing the obvious with them - copying from src_uri to dst_uri.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-04-16 12:55:47 +02:00
3d27cc09d9 lib.App.run_sub_commands(): Instantiate as context manager
App currently has no hook to close async resources. Call it as
context manager, so that __aexit__() gets invoked if
run_sub_commands() exits.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-04-16 10:21:25 +02:00
4f17a9cc93 lib.ExecContext.log_name: Add property
Add a .log_name property to be used in log messages.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-04-16 10:21:25 +02:00
1214451c15 lib.ExecContext.close(): Add method
Add ExecContext.close() as a hook to clean up async resources living
longer than an ExecContext method call.

Also, implement __aenter__() and __aexit__(), to allow using
ExecContext as context manager.  close() is invoked it goes out of
scope.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-04-16 10:21:25 +02:00
f158ab76a8 lib.ExecContext: Add get(), _get(), put() and _put()
Add wrapper methods get() and put(), plus their wrapped methods
_get() and _put(). The wrapped methods have default implementations,
using POSIX utilities on the target machine over _run().

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-04-16 10:21:25 +02:00
dbf41d7b48 lib.ExecContext.create(): Support path-only URIs
ExecContext.create() relies on properly formed URLs with a schema for
deciding which backend gets created. Create a Local instance if an
URL doesn't have schema.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-04-16 10:21:25 +02:00
04b294917f lib.ExecContext: Support bytes-typed cmd_input
The Input instance passed as cmd_input to ExecContext.run() and
.sudo() currently may be of type str. Allow to pass bytes, too.

At the same time, disallow None to be passed as cmd_input. Force the
caller to be more explicit how it wants input to be handled, notably
with respect to interactivity.

Along the way fix a bug: Content in cmd_input should result in
CallContext.interactive == False but doesn't. Fix that.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-04-16 10:21:25 +02:00
8ef478e63f lib.ExecContext: Code beautification
- Move _sudo() above sudo()

    To have a pattern in lib.ExecContext and avoid future churn: If a
    public wrapper calls a protected method, define the protected
    method above the respective wrapper.

  - sudo(): Make cmd_input default equal to run(): InputMode.OptInteractive

  - CallContext: Expose parameters throw, wd, cmd as properties for
    later use

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-04-16 10:21:16 +02:00
1b2ebab33a lib.App.__run(): Beautify error logging
Without --backtrace, the outmost try-catch block logs exceptions
plainly as their text. If it catches a key error, the exception text
only consists of the key itself, which can be easily mistaken for a
normal program output, so prefix it with a "Failed:".

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-04-13 12:14:03 +02:00
d680d6c5ed lib.ec.ssh.Exec._run_ssh(): run_cmd(throw=False)
_run_ssh() of ssh.Exec doesn't pass throw=False to run_cmd(), which
makes it throw exceptions, and effectively strips the caller of any
chance to get hold of stdout and stderr. Pass throw=False and let
run() decide according the the caller-provided throw parameter
whether or not a problem should propagate up as exception or return
value.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-04-11 14:55:30 +02:00
a58220a131 lib.ec.SSHClient.ssh_client(): Add type parameter
ssh_client() tries a predefined order of client class implementations
until it finds a workable candidate. For testing all, it's desirable
to be able to target the exact class. Add a "type" parameter to
achieve that.

I'm aware that type is also a function. But the semantics look so
compelling to me that I'm using the variable name anyway.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-04-11 14:53:12 +02:00
bafc7fed2a lib.ec.ssh.Exec: Honour self.interactive
The Exec SSHClient ignores the "interactive" argument passed to its
constructor, fix that.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-04-11 14:53:07 +02:00
84375cd482 lib.ec.ssh: Don't quote shell operators
Naively join()ing a command list to be executed remotely via SSH also
quotes shell operators which doesn't work, of course. Work around
that. The workaround will not always work but covers lots of cases.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-04-11 14:52:15 +02:00
61c1a628a1 lib.ec.SSHClient: Exception for empty host name
Instantiating a SSHClient-derived class with an invalid or missing
uri parameter is accepted and fails later down the road. Raise an
Exception early on to make the error log more comprehensible.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-04-11 14:51:50 +02:00
0e18d4abac lib.ec.ssh.Exec|Paramiko: Don't # export
The SSHClient classes Paramiko and Exec are exported via # export.
This is a bad idea, because if Paramiko is not installed, none of the
other's can be instantiated either: On the attempt to load them,
__init__.py is loaded first and fails. SSHClient.ssh_client() knows
what to do, no need to auto-import them into the lib.ec.ssh module.

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