ssh: Introduce LocalHost vs. Remote via Host interface
Motivation: local builds and deployments without ssh Add a new interface `Host` which is implemented bei either `Remote` or `Localhost` This simplifies all interactions with hosts. THe caller does ot need to know if the Host is remote or local in mot cases anymore
This commit is contained in:
111
pkgs/clan-cli/clan_lib/ssh/localhost.py
Normal file
111
pkgs/clan-cli/clan_lib/ssh/localhost.py
Normal file
@@ -0,0 +1,111 @@
|
||||
import logging
|
||||
import os
|
||||
from collections.abc import Iterator
|
||||
from contextlib import contextmanager
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from clan_lib.cmd import CmdOut, RunOpts, run
|
||||
from clan_lib.colors import AnsiColor
|
||||
from clan_lib.ssh.host import Host
|
||||
|
||||
cmdlog = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class LocalHost(Host):
|
||||
"""
|
||||
A Host implementation that executes commands locally without SSH.
|
||||
"""
|
||||
|
||||
command_prefix: str = "localhost"
|
||||
_user: str = field(default_factory=lambda: os.environ.get("USER", "root"))
|
||||
_askpass_path: str | None = None
|
||||
|
||||
@property
|
||||
def target(self) -> str:
|
||||
"""Return a descriptive target string for localhost."""
|
||||
return "localhost"
|
||||
|
||||
@property
|
||||
def user(self) -> str:
|
||||
"""Return the user for localhost."""
|
||||
return self._user
|
||||
|
||||
def run(
|
||||
self,
|
||||
cmd: list[str],
|
||||
opts: RunOpts | None = None,
|
||||
extra_env: dict[str, str] | None = None,
|
||||
tty: bool = False,
|
||||
verbose_ssh: bool = False,
|
||||
quiet: bool = False,
|
||||
control_master: bool = True,
|
||||
) -> CmdOut:
|
||||
"""
|
||||
Run a command locally instead of via SSH.
|
||||
"""
|
||||
if opts is None:
|
||||
opts = RunOpts()
|
||||
|
||||
# Set up environment
|
||||
env = opts.env or os.environ.copy()
|
||||
if extra_env:
|
||||
env.update(extra_env)
|
||||
|
||||
# Handle sudo if needed
|
||||
if self._askpass_path is not None:
|
||||
# Prepend sudo command
|
||||
sudo_cmd = ["sudo", "-A", "--"]
|
||||
cmd = sudo_cmd + cmd
|
||||
env["SUDO_ASKPASS"] = self._askpass_path
|
||||
|
||||
# Set options
|
||||
opts.env = env
|
||||
opts.prefix = opts.prefix or self.command_prefix
|
||||
|
||||
# Log the command
|
||||
displayed_cmd = " ".join(cmd)
|
||||
if not quiet:
|
||||
cmdlog.info(
|
||||
f"$ {displayed_cmd}",
|
||||
extra={
|
||||
"command_prefix": self.command_prefix,
|
||||
"color": AnsiColor.GREEN.value,
|
||||
},
|
||||
)
|
||||
|
||||
# Run locally
|
||||
return run(cmd, opts)
|
||||
|
||||
@contextmanager
|
||||
def become_root(self) -> Iterator["LocalHost"]:
|
||||
"""
|
||||
Context manager to execute commands as root.
|
||||
"""
|
||||
if self._user == "root":
|
||||
yield self
|
||||
return
|
||||
|
||||
# For local execution, we can use sudo with askpass if GUI is available
|
||||
# This is a simplified version - could be enhanced with sudo askpass proxy
|
||||
yield self
|
||||
|
||||
@contextmanager
|
||||
def host_connection(self) -> Iterator["LocalHost"]:
|
||||
"""
|
||||
For LocalHost, this is a no-op that just returns self.
|
||||
"""
|
||||
yield self
|
||||
|
||||
def nix_ssh_env(
|
||||
self,
|
||||
env: dict[str, str] | None = None,
|
||||
control_master: bool = True,
|
||||
) -> dict[str, str]:
|
||||
"""
|
||||
LocalHost doesn't need SSH environment variables.
|
||||
"""
|
||||
if env is None:
|
||||
env = {}
|
||||
# Don't set NIX_SSHOPTS for localhost
|
||||
return env
|
||||
Reference in New Issue
Block a user