Remote mode and dangerous flag, other tweaks

* Add remote mode for harvesting a remote machine via a local workstation (no need to install enroll remotely)
   Optionally use `--no-sudo` if you don't want the remote user to have passwordless sudo when conducting the
   harvest, albeit you'll end up with less useful data (same as if running `enroll harvest` on a machine without
   sudo)
 * Add `--dangerous` flag to capture even sensitive data (use at your own risk!)
 * Do a better job at capturing other config files in `/etc/<package>/` even if that package doesn't normally
   ship or manage those files.
This commit is contained in:
Miguel Jacq 2025-12-17 17:02:16 +11:00
parent 026416d158
commit 6a36a9d2d5
Signed by: mig5
GPG key ID: 59B3F0C24135C6A9
13 changed files with 1083 additions and 155 deletions

View file

@ -6,8 +6,9 @@ import enroll.cli as cli
def test_cli_harvest_subcommand_calls_harvest(monkeypatch, capsys, tmp_path):
called = {}
def fake_harvest(out: str):
def fake_harvest(out: str, dangerous: bool = False):
called["out"] = out
called["dangerous"] = dangerous
return str(tmp_path / "state.json")
monkeypatch.setattr(cli, "harvest", fake_harvest)
@ -15,6 +16,7 @@ def test_cli_harvest_subcommand_calls_harvest(monkeypatch, capsys, tmp_path):
cli.main()
assert called["out"] == str(tmp_path)
assert called["dangerous"] is False
captured = capsys.readouterr()
assert str(tmp_path / "state.json") in captured.out
@ -53,8 +55,8 @@ def test_cli_manifest_subcommand_calls_manifest(monkeypatch, tmp_path):
def test_cli_enroll_subcommand_runs_harvest_then_manifest(monkeypatch, tmp_path):
calls = []
def fake_harvest(bundle_dir: str):
calls.append(("harvest", bundle_dir))
def fake_harvest(bundle_dir: str, dangerous: bool = False):
calls.append(("harvest", bundle_dir, dangerous))
return str(tmp_path / "bundle" / "state.json")
def fake_manifest(bundle_dir: str, out_dir: str, **kwargs):
@ -85,11 +87,142 @@ def test_cli_enroll_subcommand_runs_harvest_then_manifest(monkeypatch, tmp_path)
cli.main()
assert calls == [
("harvest", str(tmp_path / "bundle")),
("harvest", str(tmp_path / "bundle"), False),
("manifest", str(tmp_path / "bundle"), str(tmp_path / "ansible"), None, "auto"),
]
def test_cli_harvest_dangerous_flag_is_forwarded(monkeypatch, tmp_path):
called = {}
def fake_harvest(out: str, dangerous: bool = False):
called["out"] = out
called["dangerous"] = dangerous
return str(tmp_path / "state.json")
monkeypatch.setattr(cli, "harvest", fake_harvest)
monkeypatch.setattr(
sys, "argv", ["enroll", "harvest", "--out", str(tmp_path), "--dangerous"]
)
cli.main()
assert called["dangerous"] is True
def test_cli_harvest_remote_calls_remote_harvest_and_uses_cache_dir(
monkeypatch, capsys, tmp_path
):
from enroll.cache import HarvestCache
cache_dir = tmp_path / "cache"
cache_dir.mkdir()
called = {}
def fake_cache_dir(*, hint=None):
called["hint"] = hint
return HarvestCache(dir=cache_dir)
def fake_remote_harvest(
*,
local_out_dir,
remote_host,
remote_port,
remote_user,
dangerous,
no_sudo,
):
called.update(
{
"local_out_dir": local_out_dir,
"remote_host": remote_host,
"remote_port": remote_port,
"remote_user": remote_user,
"dangerous": dangerous,
"no_sudo": no_sudo,
}
)
return cache_dir / "state.json"
monkeypatch.setattr(cli, "new_harvest_cache_dir", fake_cache_dir)
monkeypatch.setattr(cli, "remote_harvest", fake_remote_harvest)
monkeypatch.setattr(
sys,
"argv",
[
"enroll",
"harvest",
"--remote-host",
"example.test",
"--remote-user",
"alice",
],
)
cli.main()
out = capsys.readouterr().out
assert str(cache_dir / "state.json") in out
assert called["hint"] == "example.test"
assert called["local_out_dir"] == cache_dir
assert called["remote_host"] == "example.test"
assert called["remote_port"] == 22
assert called["remote_user"] == "alice"
assert called["dangerous"] is False
assert called["no_sudo"] is False
def test_cli_single_shot_remote_without_harvest_prints_state_path(
monkeypatch, capsys, tmp_path
):
from enroll.cache import HarvestCache
cache_dir = tmp_path / "cache"
cache_dir.mkdir()
ansible_dir = tmp_path / "ansible"
calls = []
def fake_cache_dir(*, hint=None):
return HarvestCache(dir=cache_dir)
def fake_remote_harvest(**kwargs):
calls.append(("remote_harvest", kwargs))
return cache_dir / "state.json"
def fake_manifest(harvest_dir: str, out_dir: str, **kwargs):
calls.append(("manifest", harvest_dir, out_dir, kwargs.get("fqdn")))
monkeypatch.setattr(cli, "new_harvest_cache_dir", fake_cache_dir)
monkeypatch.setattr(cli, "remote_harvest", fake_remote_harvest)
monkeypatch.setattr(cli, "manifest", fake_manifest)
monkeypatch.setattr(
sys,
"argv",
[
"enroll",
"single-shot",
"--remote-host",
"example.test",
"--remote-user",
"alice",
"--out",
str(ansible_dir),
"--fqdn",
"example.test",
],
)
cli.main()
out = capsys.readouterr().out
# It should print the derived state.json path for usability when --harvest
# wasn't provided.
assert str(cache_dir / "state.json") in out
# And it should manifest using the cache dir.
assert ("manifest", str(cache_dir), str(ansible_dir), "example.test") in calls
def test_cli_manifest_common_args(monkeypatch, tmp_path):
"""Ensure --fqdn and jinjaturtle mode flags are forwarded correctly."""