Files
computer-playbook/utils/directory_batch.py
T
kevinveenbirkenbachandClaude Opus 5.5 73a16bef09 perf(sys-svc-compose): prepare the compose instance in one module call
Every app's compose pass ran three remote tasks before compose up: create the directories, compose ps for running containers, and stat the pending marker (53 apps x ~2.7 s in a production run). compose_instance_prepare does all three in one call from 02_stack_host_prep.yml, sharing the directory creation with directory_batch through utils/directory_batch.py; compose ps runs only in compose mode. up.yml evaluates the registered result, named compose_instance_before_files because it predates 04_files: the running check is skipped whenever the compose, env or override file changed in this run (those changes notify compose-up themselves).

Replay harness old vs new: rendered files and the compose-up, marker and swarm handlers identical in every scenario; the only difference is the skipped running check when only the override changed, where compose-up still runs. About 85 s per full production run.

Merge note: store-i18n's a4c7fda974 hoists the removed marker stat into a block; resolve up.yml with this side.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-25 21:22:32 +01:00

102 lines
2.9 KiB
Python

import os
from pathlib import Path
ATTRIBUTES = ("mode", "owner", "group")
ATTRIBUTE_SPEC = {
"mode": {"type": "raw"},
"owner": {"type": "str"},
"group": {"type": "str"},
}
class DirectoryError(Exception):
pass
def path_state(path):
target = Path(path)
if target.is_symlink():
return "link"
if target.is_dir():
return "directory"
if target.exists():
return "file"
return "absent"
def apply_attributes(module, path, entry, changed, diff):
file_args = module.load_file_common_arguments(
{"path": path, **{name: entry[name] for name in ATTRIBUTES}}
)
return module.set_fs_attributes_if_different(file_args, changed, diff, expand=False)
def create(module, path, entry, diff):
changed = False
current = ""
for part in path.strip("/").split("/"):
current = f"{current}/{part}"
candidate = Path(current)
if candidate.exists():
continue
try:
candidate.mkdir()
changed = True
except FileExistsError:
if not candidate.is_dir():
raise
changed = apply_attributes(module, current, entry, changed, diff)
return changed
def ensure(module, entry):
path = entry["path"]
if not Path(path).is_absolute():
raise DirectoryError(f"{path!r} is not an absolute path")
if Path(path).is_symlink():
path = os.path.realpath(path)
state = path_state(path)
diff = {"before": {"path": path}, "after": {"path": path}}
if state == "directory":
return apply_attributes(module, path, entry, False, diff), diff
if state != "absent":
raise DirectoryError(f"{path} already exists as a {state}")
diff["before"]["state"] = state
diff["after"]["state"] = "directory"
if module.check_mode:
return True, diff
try:
return create(module, path, entry, diff), diff
except OSError as error:
raise DirectoryError(f"Could not create {path}: {error}") from error
def ensure_all(module, entries):
"""Ensure every entry in order and fail the module on the first error.
Args:
module: the running ``AnsibleModule``.
entries: list of ``{path, mode, owner, group}``.
Returns:
``(changed_paths, diffs)``: the paths that were created or whose
attributes changed, and one ``before``/``after`` record per such path.
"""
changed_paths = []
diffs = []
for entry in entries:
try:
changed, diff = ensure(module, entry)
except DirectoryError as error:
module.fail_json(
msg=str(error),
path=entry["path"],
changed=bool(changed_paths),
changed_paths=changed_paths,
)
if changed:
changed_paths.append(entry["path"])
diffs.append(diff)
return changed_paths, diffs