Support --remote-ssh-config [path-to-ssh-config] as an argument in case extra params are required beyond --remote-port or --remote-user.
All checks were successful
CI / test (push) Successful in 8m18s
Lint / test (push) Successful in 33s
Trivy / test (push) Successful in 25s

Note: `--remote-host` must still be set, but it can be an 'alias' represented by the 'Host' value in the ssh config.
This commit is contained in:
Miguel Jacq 2026-01-13 21:56:28 +11:00
parent 5754ef1aad
commit f5eaac9f75
Signed by: mig5
GPG key ID: 59B3F0C24135C6A9
6 changed files with 110 additions and 14 deletions

View file

@ -1,3 +1,7 @@
# 0.4.2
* Support `--remote-ssh-config [path-to-ssh-config]` as an argument in case extra params are required beyond `--remote-port` or `--remote-user`. Note: `--remote-host` must still be set, but it can be an 'alias' represented by the 'Host' value in the ssh config.
# 0.4.1 # 0.4.1
* Add interactive output when 'enroll diff --enforce' is invoking Ansible. * Add interactive output when 'enroll diff --enforce' is invoking Ansible.

6
debian/changelog vendored
View file

@ -1,3 +1,9 @@
enroll (0.4.2) unstable; urgency=medium
* Support `--remote-ssh-config [path-to-ssh-config]` as an argument in case extra params are required beyond `--remote-port` or `--remote-user`. Note: `--remote-host` must still be set, but it can be an 'alias' represented by the 'Host' value in the ssh config.
-- Miguel Jacq <mig@mig5.net> Tue, 13 Jan 2026 21:55:00 +1100
enroll (0.4.1) unstable; urgency=medium enroll (0.4.1) unstable; urgency=medium
* Add interactive output when 'enroll diff --enforce' is invoking Ansible. * Add interactive output when 'enroll diff --enforce' is invoking Ansible.

View file

@ -350,16 +350,33 @@ def _add_remote_args(p: argparse.ArgumentParser) -> None:
"--remote-host", "--remote-host",
help="SSH host to run harvesting on (if set, harvest runs remotely and is pulled locally).", help="SSH host to run harvesting on (if set, harvest runs remotely and is pulled locally).",
) )
p.add_argument(
"--remote-ssh-config",
nargs="?",
const=str(Path.home() / ".ssh" / "config"),
default=None,
help=(
"Use OpenSSH-style ssh_config settings for --remote-host. "
"If provided without a value, defaults to ~/.ssh/config. "
"(Applies HostName/User/Port/IdentityFile/ProxyCommand/HostKeyAlias when supported.)"
),
)
p.add_argument( p.add_argument(
"--remote-port", "--remote-port",
type=int, type=int,
default=22, default=None,
help="SSH port for --remote-host (default: 22).", help=(
"SSH port for --remote-host. If omitted, defaults to 22, or a value from ssh_config when "
"--remote-ssh-config is set."
),
) )
p.add_argument( p.add_argument(
"--remote-user", "--remote-user",
default=os.environ.get("USER") or None, default=None,
help="SSH username for --remote-host (default: local $USER).", help=(
"SSH username for --remote-host. If omitted, defaults to local $USER, or a value from ssh_config when "
"--remote-ssh-config is set."
),
) )
# Align terminology with Ansible: "become" == sudo. # Align terminology with Ansible: "become" == sudo.
@ -728,6 +745,17 @@ def main() -> None:
) )
args = ap.parse_args(argv) args = ap.parse_args(argv)
# Preserve historical defaults for remote harvesting unless ssh_config lookup is enabled.
# This lets ssh_config values take effect when the user did not explicitly set
# --remote-user / --remote-port.
if hasattr(args, "remote_host"):
rsc = getattr(args, "remote_ssh_config", None)
if not rsc:
if getattr(args, "remote_port", None) is None:
setattr(args, "remote_port", 22)
if getattr(args, "remote_user", None) is None:
setattr(args, "remote_user", os.environ.get("USER") or None)
try: try:
if args.cmd == "harvest": if args.cmd == "harvest":
sops_fps = getattr(args, "sops", None) sops_fps = getattr(args, "sops", None)
@ -745,8 +773,9 @@ def main() -> None:
ask_become_pass=args.ask_become_pass, ask_become_pass=args.ask_become_pass,
local_out_dir=tmp_bundle, local_out_dir=tmp_bundle,
remote_host=args.remote_host, remote_host=args.remote_host,
remote_port=int(args.remote_port), remote_port=args.remote_port,
remote_user=args.remote_user, remote_user=args.remote_user,
remote_ssh_config=args.remote_ssh_config,
dangerous=bool(args.dangerous), dangerous=bool(args.dangerous),
no_sudo=bool(args.no_sudo), no_sudo=bool(args.no_sudo),
include_paths=list(getattr(args, "include_path", []) or []), include_paths=list(getattr(args, "include_path", []) or []),
@ -766,8 +795,9 @@ def main() -> None:
ask_become_pass=args.ask_become_pass, ask_become_pass=args.ask_become_pass,
local_out_dir=out_dir, local_out_dir=out_dir,
remote_host=args.remote_host, remote_host=args.remote_host,
remote_port=int(args.remote_port), remote_port=args.remote_port,
remote_user=args.remote_user, remote_user=args.remote_user,
remote_ssh_config=args.remote_ssh_config,
dangerous=bool(args.dangerous), dangerous=bool(args.dangerous),
no_sudo=bool(args.no_sudo), no_sudo=bool(args.no_sudo),
include_paths=list(getattr(args, "include_path", []) or []), include_paths=list(getattr(args, "include_path", []) or []),
@ -968,8 +998,9 @@ def main() -> None:
ask_become_pass=args.ask_become_pass, ask_become_pass=args.ask_become_pass,
local_out_dir=tmp_bundle, local_out_dir=tmp_bundle,
remote_host=args.remote_host, remote_host=args.remote_host,
remote_port=int(args.remote_port), remote_port=args.remote_port,
remote_user=args.remote_user, remote_user=args.remote_user,
remote_ssh_config=args.remote_ssh_config,
dangerous=bool(args.dangerous), dangerous=bool(args.dangerous),
no_sudo=bool(args.no_sudo), no_sudo=bool(args.no_sudo),
include_paths=list(getattr(args, "include_path", []) or []), include_paths=list(getattr(args, "include_path", []) or []),
@ -998,8 +1029,9 @@ def main() -> None:
ask_become_pass=args.ask_become_pass, ask_become_pass=args.ask_become_pass,
local_out_dir=harvest_dir, local_out_dir=harvest_dir,
remote_host=args.remote_host, remote_host=args.remote_host,
remote_port=int(args.remote_port), remote_port=args.remote_port,
remote_user=args.remote_user, remote_user=args.remote_user,
remote_ssh_config=args.remote_ssh_config,
dangerous=bool(args.dangerous), dangerous=bool(args.dangerous),
no_sudo=bool(args.no_sudo), no_sudo=bool(args.no_sudo),
include_paths=list(getattr(args, "include_path", []) or []), include_paths=list(getattr(args, "include_path", []) or []),

View file

@ -330,8 +330,9 @@ def _remote_harvest(
*, *,
local_out_dir: Path, local_out_dir: Path,
remote_host: str, remote_host: str,
remote_port: int = 22, remote_port: Optional[int] = None,
remote_user: Optional[str] = None, remote_user: Optional[str] = None,
remote_ssh_config: Optional[str] = None,
remote_python: str = "python3", remote_python: str = "python3",
dangerous: bool = False, dangerous: bool = False,
no_sudo: bool = False, no_sudo: bool = False,
@ -370,10 +371,60 @@ def _remote_harvest(
# Users should add the key to known_hosts. # Users should add the key to known_hosts.
ssh.set_missing_host_key_policy(paramiko.RejectPolicy()) ssh.set_missing_host_key_policy(paramiko.RejectPolicy())
# Resolve SSH connection parameters.
connect_host = remote_host
connect_port = int(remote_port) if remote_port is not None else 22
connect_user = remote_user
key_filename = None
sock = None
hostkey_name = connect_host
if remote_ssh_config:
from paramiko.config import SSHConfig # type: ignore
from paramiko.proxy import ProxyCommand # type: ignore
import socket as _socket
cfg_path = Path(str(remote_ssh_config)).expanduser()
if not cfg_path.exists():
raise RuntimeError(f"SSH config file not found: {cfg_path}")
cfg = SSHConfig()
with cfg_path.open("r", encoding="utf-8") as _fp:
cfg.parse(_fp)
hcfg = cfg.lookup(remote_host)
connect_host = str(hcfg.get("hostname") or remote_host)
hostkey_name = str(hcfg.get("hostkeyalias") or connect_host)
if remote_port is None and hcfg.get("port"):
try:
connect_port = int(str(hcfg.get("port")))
except ValueError:
pass
if connect_user is None and hcfg.get("user"):
connect_user = str(hcfg.get("user"))
ident = hcfg.get("identityfile")
if ident:
if isinstance(ident, (list, tuple)):
key_filename = [str(Path(p).expanduser()) for p in ident]
else:
key_filename = str(Path(str(ident)).expanduser())
proxycmd = hcfg.get("proxycommand")
if proxycmd:
sock = ProxyCommand(str(proxycmd))
elif hostkey_name != connect_host:
# If HostKeyAlias is used, connect to HostName via a socket but
# use HostKeyAlias for known_hosts lookups.
sock = _socket.create_connection((connect_host, connect_port))
ssh.connect( ssh.connect(
hostname=remote_host, hostname=hostkey_name if sock is not None else connect_host,
port=int(remote_port), port=connect_port,
username=remote_user, username=connect_user,
key_filename=key_filename,
sock=sock,
allow_agent=True, allow_agent=True,
look_for_keys=True, look_for_keys=True,
) )

View file

@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "enroll" name = "enroll"
version = "0.4.1" version = "0.4.2"
description = "Enroll a server's running state retrospectively into Ansible" description = "Enroll a server's running state retrospectively into Ansible"
authors = ["Miguel Jacq <mig@mig5.net>"] authors = ["Miguel Jacq <mig@mig5.net>"]
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"

View file

@ -1,4 +1,4 @@
%global upstream_version 0.4.1 %global upstream_version 0.4.2
Name: enroll Name: enroll
Version: %{upstream_version} Version: %{upstream_version}
@ -43,6 +43,9 @@ Enroll a server's running state retrospectively into Ansible.
%{_bindir}/enroll %{_bindir}/enroll
%changelog %changelog
* Tue Jan 13 2026 Miguel Jacq <mig@mig5.net> - %{version}-%{release}
- Support `--remote-ssh-config [path-to-ssh-config]` as an argument in case extra params are required beyond `--remote-port` or `--remote-user`. Note: `--remote-host` must still be s
et, but it can be an 'alias' represented by the 'Host' value in the ssh config.
* Sun Jan 11 2026 Miguel Jacq <mig@mig5.net> - %{version}-%{release} * Sun Jan 11 2026 Miguel Jacq <mig@mig5.net> - %{version}-%{release}
- Add interactive output when 'enroll diff --enforce' is invoking Ansible. - Add interactive output when 'enroll diff --enforce' is invoking Ansible.
* Sat Jan 10 2026 Miguel Jacq <mig@mig5.net> - %{version}-%{release} * Sat Jan 10 2026 Miguel Jacq <mig@mig5.net> - %{version}-%{release}