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:
parent
026416d158
commit
6a36a9d2d5
13 changed files with 1083 additions and 155 deletions
|
|
@ -3,6 +3,8 @@ from __future__ import annotations
|
|||
import json
|
||||
import os
|
||||
import shutil
|
||||
import stat
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Set, Tuple
|
||||
|
||||
|
|
@ -70,36 +72,6 @@ def _yaml_dump_mapping(obj: Dict[str, Any], *, sort_keys: bool = True) -> str:
|
|||
)
|
||||
|
||||
|
||||
def _merge_list_keep_order(existing: List[Any], new: List[Any]) -> List[Any]:
|
||||
out = list(existing)
|
||||
seen = set(existing)
|
||||
for item in new:
|
||||
if item not in seen:
|
||||
out.append(item)
|
||||
seen.add(item)
|
||||
return out
|
||||
|
||||
|
||||
def _merge_mappings_preserve(
|
||||
existing: Dict[str, Any], incoming: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""Merge incoming into existing:
|
||||
- lists: union (preserve existing order)
|
||||
- scalars/dicts: only set if missing (do not overwrite)
|
||||
"""
|
||||
merged = dict(existing)
|
||||
for k, v in incoming.items():
|
||||
if k in merged:
|
||||
if isinstance(merged[k], list) and isinstance(v, list):
|
||||
merged[k] = _merge_list_keep_order(merged[k], v)
|
||||
else:
|
||||
# keep existing value (non-overwriting)
|
||||
continue
|
||||
else:
|
||||
merged[k] = v
|
||||
return merged
|
||||
|
||||
|
||||
def _merge_mappings_overwrite(
|
||||
existing: Dict[str, Any], incoming: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
|
|
@ -113,33 +85,6 @@ def _merge_mappings_overwrite(
|
|||
return merged
|
||||
|
||||
|
||||
def _write_role_defaults_merge(role_dir: str, incoming: Dict[str, Any]) -> None:
|
||||
"""Write/merge role defaults without clobbering existing values.
|
||||
Used in site mode to keep roles reusable across hosts.
|
||||
"""
|
||||
defaults_path = os.path.join(role_dir, "defaults", "main.yml")
|
||||
existing: Dict[str, Any] = {}
|
||||
if os.path.exists(defaults_path):
|
||||
try:
|
||||
existing_text = Path(defaults_path).read_text(encoding="utf-8")
|
||||
existing = _yaml_load_mapping(existing_text)
|
||||
except Exception:
|
||||
existing = {}
|
||||
merged = _merge_mappings_preserve(existing, incoming)
|
||||
body = "---\n" + _yaml_dump_mapping(merged, sort_keys=True)
|
||||
with open(defaults_path, "w", encoding="utf-8") as f:
|
||||
f.write(body)
|
||||
|
||||
|
||||
def _extract_jinjaturtle_block(text: str) -> str:
|
||||
"""Return YAML text inside JINJATURTLE_BEGIN/END markers, or the whole text if no markers."""
|
||||
if JINJATURTLE_BEGIN in text and JINJATURTLE_END in text:
|
||||
start = text.split(JINJATURTLE_BEGIN, 1)[1]
|
||||
inner = start.split(JINJATURTLE_END, 1)[0]
|
||||
return inner.strip() + "\n"
|
||||
return text.strip() + "\n"
|
||||
|
||||
|
||||
def _normalise_jinjaturtle_vars_text(vars_text: str) -> str:
|
||||
"""Deduplicate keys in a vars fragment by parsing as YAML and dumping it back."""
|
||||
m = _yaml_load_mapping(vars_text)
|
||||
|
|
@ -158,6 +103,30 @@ def _yaml_list(items: List[str], indent: int = 2) -> str:
|
|||
return "\n".join(f"{pad}- {x}" for x in items)
|
||||
|
||||
|
||||
def _copy2_replace(src: str, dst: str) -> None:
|
||||
dst_dir = os.path.dirname(dst)
|
||||
os.makedirs(dst_dir, exist_ok=True)
|
||||
|
||||
# Copy to a temp file in the same directory, then atomically replace.
|
||||
fd, tmp = tempfile.mkstemp(prefix=".enroll-tmp-", dir=dst_dir)
|
||||
os.close(fd)
|
||||
try:
|
||||
shutil.copy2(src, tmp)
|
||||
|
||||
# Ensure the working tree stays mergeable: make the file user-writable.
|
||||
st = os.stat(tmp, follow_symlinks=False)
|
||||
mode = stat.S_IMODE(st.st_mode)
|
||||
if not (mode & stat.S_IWUSR):
|
||||
os.chmod(tmp, mode | stat.S_IWUSR)
|
||||
|
||||
os.replace(tmp, dst)
|
||||
finally:
|
||||
try:
|
||||
os.unlink(tmp)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
|
||||
def _copy_artifacts(
|
||||
bundle_dir: str,
|
||||
role: str,
|
||||
|
|
@ -195,7 +164,7 @@ def _copy_artifacts(
|
|||
if preserve_existing and os.path.exists(dst):
|
||||
continue
|
||||
os.makedirs(os.path.dirname(dst), exist_ok=True)
|
||||
shutil.copy2(src, dst)
|
||||
_copy2_replace(src, dst)
|
||||
|
||||
|
||||
def _write_role_scaffold(role_dir: str) -> None:
|
||||
|
|
@ -380,11 +349,6 @@ def _jinjify_managed_files(
|
|||
return templated, ""
|
||||
|
||||
|
||||
def _hostvars_only_jinjaturtle(vars_text: str) -> str:
|
||||
# keep as valid YAML file
|
||||
return _defaults_with_jinjaturtle("---\n", vars_text)
|
||||
|
||||
|
||||
def _defaults_with_jinjaturtle(base_defaults: str, vars_text: str) -> str:
|
||||
if not vars_text.strip():
|
||||
return base_defaults.rstrip() + "\n"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue