Commit graph

44 commits

Author SHA1 Message Date
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
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
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
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
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
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
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
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
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
3c9ce19deb lib.ec.ssh.Paramiko: Fix exception logging

The catch-block around Paramiko's connect code throws another exception, fix that.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-04-03 17:29:44 +02:00
b21d2d1c21 lib.ec.ssh.AsyncSSH: Add interactivity

Request a remote PTY from AsyncSSH, and wire the local terminal's stdin up with it if interactive == True. This gives a real interactive session if local stdin belongs to a terminal. Also, thanks to AsyncSSH understanding that, forward terminal size changes to the remote end.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-03-25 07:32:46 +01:00
737cbc3e24 lib.ec.ssh.AsyncSSH: Add class

Add a SSHClient implementation using AsyncSSH. This is the first and currently only class derived from SSHClient which implements SSHClient.Cap.LogOutput, designed to consume and log command output as it streams in. It felt like the lower hanging fruit not to do that with Paramiko: Paramiko doesn't provide a native async API, so it would need to spawn additional worker threads. I think.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-03-25 07:32:46 +01:00
279b7789e2 lib.ec.SSHClient: Add property port

Add a port property to SSHClient, parsed from the ctor's URL, to supply the obvious information.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-03-25 07:32:46 +01:00
3a84408436 lib.ec.SSHClient.__init__(): Add parameter caps

Add an optional caps ("capabilities") argument to the constructor of SSHClient. It is meant to be used by derived classes in order to declare that they don't want the base class to handle a default behaviour for a certain capability, but that they want to implement it themselves instead.

Also, give the _run_ssh() callbacks the necessary info as parameters, so that the derived classes have the means to do so.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-03-25 07:32:46 +01:00
21e67291b5 Fix: Decode run_cmd() result

Since commit 02697af5, ExecContext.run() returns bytes for stdout and stderr and fixes that in calling code. The thing it did not fix was the code calling run_cmd(), which also made return bytes. This commit catches up on that.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-03-25 07:32:45 +01:00
f4c76ebab9 lib.ec.SSHClientInternal|SSHClientCmd: Own .py

Move the code of SSHClientInternal and SSCClientCmd into lib.ec.ssh, as "Paramiko" and "Exec", respectively. This makes the class layout a little more modular, and along the way fixes a bug where SSHClientInternal could be instantiated but was unusable (if the Paramiko is not installed).

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-03-20 13:35:50 +01:00
f37f025b17 lib.SSHClient: Move to lib.ec

SSHClient in an ExecContext, hence it's better off in lib.ec, move it there and adapt the references.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-03-20 13:35:11 +01:00
02697af568 lib.ExecContext: Align .sudo() prototype to .run()

ExecContext's .sudo() omits many of run()'s parameters, and this commit adds them. To avoid redundancy around repeating and massaging the long parameter list of both functions and their return values, it also adds some deeper changes:

- Make run(), _run(), sudo() and _sudo() always return instances of Result. Before it was allowed to return a triplet of stdout, stderr, and exit status.
- Have ExecContext stay out of the business of decoding the result entirely. Result provides a convenience method .decode() operating on stdout and stderr and leaves the decision to the caller.
This entails miniscule adaptations in calling code, namely in App.os_release, util.get_profile_env() and CmdListRepos._run().
- Wrap the _run() and _sudo() callbacks in a context manager object of type CallContext to avoid code duplication.
- Consistently name the first argument to run(), _run(), sudo() and _sudo() "cmd", not "args". The latter suggests that the caller is omitting the executable, which is not the case.
Signed-off-by: Jan Lindemann <jan@janware.com>
2026-03-20 10:30:25 +01:00
52dd3b8f21 lib.ExecContext.run(): Push code up into base class

Take implementation burden from the derived classes _run() callback by moving the respective code into the run() wrapper methods of the base class.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-03-20 10:30:25 +01:00
888c1e7f16 lib.ExecContext.__init__(): Add parameter uri

Take a positional uri argument to the constructor of ExecContext, forcing SSHClient to follow suit. The latter was instantiated with a hostname as only argument up to now, which still works as a special case of an uri.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-03-20 10:30:25 +01:00
72bd5e3555 lib.Local.run(): Be less dramatic about exit != 0

Don't mention "error" in log message for exit codes > 0 from spawned processes, because sometimes they don't mean an error.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-03-09 20:03:01 +01:00
1325222fbd lib.ExecContext,Local: Remove callback default params

Remove defaults from protected callback function parameters. They have to be decided by the base class's public API.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-03-06 19:02:22 +01:00
fadf1bca49 lib.util.run_cmd(): Add parameter ec: ExecContext

Allow to specify the ExecContext in a call to run_cmd(). This effectively makes run_cmd() an thin wrapper around ExecContext.run(), which is what's going to be used in the future. The wrapper is for backwards-compatibility.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-03-06 15:14:13 +01:00
3e897f4df8 lib.Distro, ExecContext: Add classes, refactor lib.distro

The code below lib.distro, as left behind by the previous commit, is geared towards being directly used as a command-line API. This commit introduces the abstract base class Distro, a proxy for distribution-specific interactions. The proxy abstracts distro specifics into an API with proper method prototypes, not argparse.Namespace contents, and can thus be more easily driven by arbitrary code.

The Distro class is initialized with a member variable of type ExecContext, another new class introduced by this commit. It is designed to abstract the communication channel to the distribution instance. Currently only one specialization exists, Local, which interacts with the distribution and root file system it is running in, but is planned to be subclassed to support interaction via SSH, serial, chroot, or chains thereof.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-03-06 14:56:46 +01:00