Compare commits
No commits in common. "29b52d451d4d477ea2f9d05fdc5c85fe8f8ecd16" and "09438246ae0557185c3343c0db6e0101f2d75385" have entirely different histories.
29b52d451d
...
09438246ae
8 changed files with 9 additions and 186 deletions
|
|
@ -1,8 +1,3 @@
|
|||
# 0.2.1
|
||||
|
||||
* Don't accidentally add extra_paths role to usr_local_custom list, resulting in extra_paths appearing twice in manifested playbook
|
||||
* Ensure directories in the tree of anything included with --include are defined in the state and manifest so we make dirs before we try to create files
|
||||
|
||||
# 0.2.0
|
||||
|
||||
* Add version CLI arg
|
||||
|
|
|
|||
7
debian/changelog
vendored
7
debian/changelog
vendored
|
|
@ -1,10 +1,3 @@
|
|||
enroll (0.2.1) unstable; urgency=medium
|
||||
|
||||
* Don't accidentally add extra_paths role to usr_local_custom list, resulting in extra_paths appearing twice in manifested playbook
|
||||
* Ensure directories in the tree of anything included with --include are defined in the state and manifest so we make dirs before we try to create files
|
||||
|
||||
-- Miguel Jacq <mig@mig5.net> Fri, 01 Jan 2026 21:30:00 +1100
|
||||
|
||||
enroll (0.2.0) unstable; urgency=medium
|
||||
|
||||
* Add version CLI arg
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ def stat_triplet(path: str) -> Tuple[str, str, str]:
|
|||
mode is a zero-padded octal string (e.g. "0644").
|
||||
"""
|
||||
st = os.stat(path, follow_symlinks=True)
|
||||
mode = oct(st.st_mode & 0o7777)[2:].zfill(4)
|
||||
mode = oct(st.st_mode & 0o777)[2:].zfill(4)
|
||||
|
||||
import grp
|
||||
import pwd
|
||||
|
|
|
|||
|
|
@ -34,15 +34,6 @@ class ManagedFile:
|
|||
reason: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class ManagedDir:
|
||||
path: str
|
||||
owner: str
|
||||
group: str
|
||||
mode: str
|
||||
reason: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class ExcludedFile:
|
||||
path: str
|
||||
|
|
@ -118,7 +109,6 @@ class ExtraPathsSnapshot:
|
|||
role_name: str
|
||||
include_patterns: List[str]
|
||||
exclude_patterns: List[str]
|
||||
managed_dirs: List[ManagedDir]
|
||||
managed_files: List[ManagedFile]
|
||||
excluded: List[ExcludedFile]
|
||||
notes: List[str]
|
||||
|
|
@ -1494,78 +1484,12 @@ def harvest(
|
|||
extra_notes: List[str] = []
|
||||
extra_excluded: List[ExcludedFile] = []
|
||||
extra_managed: List[ManagedFile] = []
|
||||
extra_managed_dirs: List[ManagedDir] = []
|
||||
extra_dir_seen: Set[str] = set()
|
||||
|
||||
def _walk_and_capture_dirs(root: str) -> None:
|
||||
root = os.path.normpath(root)
|
||||
if not root.startswith("/"):
|
||||
root = "/" + root
|
||||
if not os.path.isdir(root) or os.path.islink(root):
|
||||
return
|
||||
for dirpath, dirnames, _ in os.walk(root, followlinks=False):
|
||||
if len(extra_managed_dirs) >= MAX_FILES_CAP:
|
||||
extra_notes.append(
|
||||
f"Reached directory cap ({MAX_FILES_CAP}) while scanning {root}."
|
||||
)
|
||||
return
|
||||
dirpath = os.path.normpath(dirpath)
|
||||
if not dirpath.startswith("/"):
|
||||
dirpath = "/" + dirpath
|
||||
if path_filter.is_excluded(dirpath):
|
||||
# Prune excluded subtrees.
|
||||
dirnames[:] = []
|
||||
continue
|
||||
if os.path.islink(dirpath) or not os.path.isdir(dirpath):
|
||||
dirnames[:] = []
|
||||
continue
|
||||
|
||||
if dirpath not in extra_dir_seen:
|
||||
deny = policy.deny_reason_dir(dirpath)
|
||||
if not deny:
|
||||
try:
|
||||
owner, group, mode = stat_triplet(dirpath)
|
||||
extra_managed_dirs.append(
|
||||
ManagedDir(
|
||||
path=dirpath,
|
||||
owner=owner,
|
||||
group=group,
|
||||
mode=mode,
|
||||
reason="user_include_dir",
|
||||
)
|
||||
)
|
||||
except OSError:
|
||||
pass
|
||||
extra_dir_seen.add(dirpath)
|
||||
|
||||
# Prune excluded dirs and symlinks early.
|
||||
pruned: List[str] = []
|
||||
for d in dirnames:
|
||||
p = os.path.join(dirpath, d)
|
||||
if os.path.islink(p) or path_filter.is_excluded(p):
|
||||
continue
|
||||
pruned.append(d)
|
||||
dirnames[:] = pruned
|
||||
|
||||
extra_role_name = "extra_paths"
|
||||
extra_role_seen = seen_by_role.setdefault(extra_role_name, set())
|
||||
|
||||
include_specs = list(include_paths or [])
|
||||
exclude_specs = list(exclude_paths or [])
|
||||
|
||||
# If any include pattern points at a directory, capture that directory tree's
|
||||
# ownership/mode so the manifest can recreate it accurately.
|
||||
include_pats = path_filter.iter_include_patterns()
|
||||
for pat in include_pats:
|
||||
if pat.kind == "prefix":
|
||||
p = pat.value
|
||||
if os.path.isdir(p) and not os.path.islink(p):
|
||||
_walk_and_capture_dirs(p)
|
||||
elif pat.kind == "glob":
|
||||
for h in glob.glob(pat.value, recursive=True):
|
||||
if os.path.isdir(h) and not os.path.islink(h):
|
||||
_walk_and_capture_dirs(h)
|
||||
|
||||
if include_specs:
|
||||
extra_notes.append("User include patterns:")
|
||||
extra_notes.extend([f"- {p}" for p in include_specs])
|
||||
|
|
@ -1605,7 +1529,6 @@ def harvest(
|
|||
role_name=extra_role_name,
|
||||
include_patterns=include_specs,
|
||||
exclude_patterns=exclude_specs,
|
||||
managed_dirs=extra_managed_dirs,
|
||||
managed_files=extra_managed,
|
||||
excluded=extra_excluded,
|
||||
notes=extra_notes,
|
||||
|
|
|
|||
|
|
@ -137,33 +137,3 @@ class IgnorePolicy:
|
|||
return "sensitive_content"
|
||||
|
||||
return None
|
||||
|
||||
def deny_reason_dir(self, path: str) -> Optional[str]:
|
||||
"""Directory-specific deny logic.
|
||||
|
||||
deny_reason() is file-oriented (it rejects directories as "not_regular_file").
|
||||
For directory metadata capture (so roles can recreate directory trees), we need
|
||||
a lighter-weight check:
|
||||
- apply deny_globs (unless dangerous)
|
||||
- require the path to be a real directory (no symlink)
|
||||
- ensure it's stat'able/readable
|
||||
|
||||
No size checks or content scanning are performed for directories.
|
||||
"""
|
||||
if not self.dangerous:
|
||||
for g in self.deny_globs or []:
|
||||
if fnmatch.fnmatch(path, g):
|
||||
return "denied_path"
|
||||
|
||||
try:
|
||||
os.stat(path, follow_symlinks=True)
|
||||
except OSError:
|
||||
return "unreadable"
|
||||
|
||||
if os.path.islink(path):
|
||||
return "symlink"
|
||||
|
||||
if not os.path.isdir(path):
|
||||
return "not_directory"
|
||||
|
||||
return None
|
||||
|
|
|
|||
|
|
@ -344,29 +344,6 @@ def _write_role_defaults(role_dir: str, mapping: Dict[str, Any]) -> None:
|
|||
f.write(out)
|
||||
|
||||
|
||||
def _build_managed_dirs_var(
|
||||
managed_dirs: List[Dict[str, Any]],
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Convert enroll managed_dirs into an Ansible-friendly list of dicts.
|
||||
|
||||
Each dict drives a role task loop and is safe across hosts.
|
||||
"""
|
||||
out: List[Dict[str, Any]] = []
|
||||
for d in managed_dirs:
|
||||
dest = d.get("path") or ""
|
||||
if not dest:
|
||||
continue
|
||||
out.append(
|
||||
{
|
||||
"dest": dest,
|
||||
"owner": d.get("owner") or "root",
|
||||
"group": d.get("group") or "root",
|
||||
"mode": d.get("mode") or "0755",
|
||||
}
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
def _build_managed_files_var(
|
||||
managed_files: List[Dict[str, Any]],
|
||||
templated_src_rels: Set[str],
|
||||
|
|
@ -413,22 +390,7 @@ def _render_generic_files_tasks(
|
|||
# Using first_found makes roles work in both modes:
|
||||
# - site-mode: inventory/host_vars/<host>/<role>/.files/...
|
||||
# - non-site: roles/<role>/files/...
|
||||
return f"""- name: Ensure managed directories exist (preserve owner/group/mode)
|
||||
ansible.builtin.file:
|
||||
path: "{{{{ item.dest }}}}"
|
||||
state: directory
|
||||
owner: "{{{{ item.owner }}}}"
|
||||
group: "{{{{ item.group }}}}"
|
||||
mode: "{{{{ item.mode }}}}"
|
||||
loop: "{{{{ {var_prefix}_managed_dirs | default([]) }}}}"
|
||||
|
||||
- name: Ensure destination directories exist
|
||||
ansible.builtin.file:
|
||||
path: "{{{{ item.dest | dirname }}}}"
|
||||
state: directory
|
||||
loop: "{{{{ {var_prefix}_managed_files | default([]) }}}}"
|
||||
|
||||
- name: Deploy any systemd unit files (templates)
|
||||
return f"""- name: Deploy any systemd unit files (templates)
|
||||
ansible.builtin.template:
|
||||
src: "{{{{ item.src_rel }}}}.j2"
|
||||
dest: "{{{{ item.dest }}}}"
|
||||
|
|
@ -1482,17 +1444,13 @@ Unowned /etc config files not attributed to packages or services.
|
|||
# -------------------------
|
||||
# extra_paths role (user-requested includes)
|
||||
# -------------------------
|
||||
if extra_paths_snapshot and (
|
||||
extra_paths_snapshot.get("managed_files")
|
||||
or extra_paths_snapshot.get("managed_dirs")
|
||||
):
|
||||
if extra_paths_snapshot and extra_paths_snapshot.get("managed_files"):
|
||||
role = extra_paths_snapshot.get("role_name", "extra_paths")
|
||||
role_dir = os.path.join(roles_root, role)
|
||||
_write_role_scaffold(role_dir)
|
||||
|
||||
var_prefix = role
|
||||
|
||||
managed_dirs = extra_paths_snapshot.get("managed_dirs", []) or []
|
||||
managed_files = extra_paths_snapshot.get("managed_files", [])
|
||||
excluded = extra_paths_snapshot.get("excluded", [])
|
||||
notes = extra_paths_snapshot.get("notes", [])
|
||||
|
|
@ -1531,23 +1489,12 @@ Unowned /etc config files not attributed to packages or services.
|
|||
notify_systemd=None,
|
||||
)
|
||||
|
||||
dirs_var = _build_managed_dirs_var(managed_dirs)
|
||||
|
||||
jt_map = _yaml_load_mapping(jt_vars) if jt_vars.strip() else {}
|
||||
vars_map: Dict[str, Any] = {
|
||||
f"{var_prefix}_managed_dirs": dirs_var,
|
||||
f"{var_prefix}_managed_files": files_var,
|
||||
}
|
||||
vars_map: Dict[str, Any] = {f"{var_prefix}_managed_files": files_var}
|
||||
vars_map = _merge_mappings_overwrite(vars_map, jt_map)
|
||||
|
||||
if site_mode:
|
||||
_write_role_defaults(
|
||||
role_dir,
|
||||
{
|
||||
f"{var_prefix}_managed_dirs": [],
|
||||
f"{var_prefix}_managed_files": [],
|
||||
},
|
||||
)
|
||||
_write_role_defaults(role_dir, {f"{var_prefix}_managed_files": []})
|
||||
_write_hostvars(out_dir, fqdn or "", role, vars_map)
|
||||
else:
|
||||
_write_role_defaults(role_dir, vars_map)
|
||||
|
|
@ -1583,10 +1530,6 @@ User-requested extra file harvesting.
|
|||
"""
|
||||
+ ("\n".join([f"- {p}" for p in exclude_pats]) or "- (none)")
|
||||
+ """\n
|
||||
## Managed directories
|
||||
"""
|
||||
+ ("\n".join([f"- {d.get('path')}" for d in managed_dirs]) or "- (none)")
|
||||
+ """\n
|
||||
## Managed files
|
||||
"""
|
||||
+ ("\n".join([f"- {mf.get('path')}" for mf in managed_files]) or "- (none)")
|
||||
|
|
@ -1608,6 +1551,8 @@ User-requested extra file harvesting.
|
|||
|
||||
manifested_extra_paths_roles.append(role)
|
||||
|
||||
manifested_usr_local_custom_roles.append(role)
|
||||
|
||||
# -------------------------
|
||||
# Service roles
|
||||
# -------------------------
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[tool.poetry]
|
||||
name = "enroll"
|
||||
version = "0.2.1"
|
||||
version = "0.2.0"
|
||||
description = "Enroll a server's running state retrospectively into Ansible"
|
||||
authors = ["Miguel Jacq <mig@mig5.net>"]
|
||||
license = "GPL-3.0-or-later"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
%global upstream_version 0.2.1
|
||||
%global upstream_version 0.2.0
|
||||
|
||||
Name: enroll
|
||||
Version: %{upstream_version}
|
||||
|
|
@ -43,9 +43,6 @@ Enroll a server's running state retrospectively into Ansible.
|
|||
%{_bindir}/enroll
|
||||
|
||||
%changelog
|
||||
* Fri Jan 01 2026 Miguel Jacq <mig@mig5.net> - %{version}-%{release}
|
||||
- Don't accidentally add extra_paths role to usr_local_custom list, resulting in extra_paths appearing twice in manifested playbook
|
||||
- Ensure directories in the tree of anything included with --include are defined in the state and manifest so we make dirs before we try to create files
|
||||
* Mon Dec 29 2025 Miguel Jacq <mig@mig5.net> - %{version}-%{release}
|
||||
- Add version CLI arg
|
||||
- Add ability to enroll RH-style systems (DNF5/DNF/RPM)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue