From f151e57a12f802c9b93fe687b56c075a1b315059 Mon Sep 17 00:00:00 2001 From: Reid 'arrdem' McKenzie Date: Mon, 25 Jul 2022 22:44:10 -0600 Subject: [PATCH] Hardening cram, stratify uninstalls --- projects/cram/src/python/cram/__main__.py | 54 +++++++++++++++++++---- projects/cram/src/python/cram/common.py | 5 ++- projects/cram/src/python/cram/v1.py | 7 ++- projects/vfs/src/python/vfs/impl.py | 5 ++- 4 files changed, 60 insertions(+), 11 deletions(-) diff --git a/projects/cram/src/python/cram/__main__.py b/projects/cram/src/python/cram/__main__.py index 033cb66..ec8621a 100644 --- a/projects/cram/src/python/cram/__main__.py +++ b/projects/cram/src/python/cram/__main__.py @@ -5,7 +5,6 @@ import logging import os from pathlib import Path import pickle -import sys from typing import List from . import ( @@ -25,6 +24,11 @@ from vfs import Vfs log = logging.getLogger(__name__) +def _exit(val): + logging.shutdown() + exit(val) + + def load(root: Path, name: str, clss): for c in clss: i = c(root, name) @@ -33,10 +37,12 @@ def load(root: Path, name: str, clss): def load_package(root, name): + log.debug(f"Attempting to load package {name} from {root}") return load(root, name, [PackageV1, PackageV0]) def load_profile(root, name): + log.debug(f"Attempting to load profile {name} from {root}") return load(root, name, [ProfileV1, ProfileV0]) @@ -44,6 +50,7 @@ def load_packages(root: Path) -> dict: """Load the configured packages.""" packages = {} + log.debug(f"Trying to load packages from {root}...") for p in (root / "packages.d").glob("*"): name = str(p.relative_to(root)) packages[name] = load_package(p, name) @@ -71,14 +78,20 @@ def build_fs(root: Path, dest: Path, prelude: List[str]) -> Vfs: requirements = [] requirements.extend(prelude) + if packages: + for p in packages: + log.debug(f"Loaded package {p}") + else: + log.warning("Loaded no packages!") + for r in requirements: try: for d in packages[r].requires(): if d not in requirements: requirements.append(d) except KeyError: - print(f"Error: Unable to load package {r}", file=sys.stderr) - exit(1) + log.fatal(f"Error: Unable to load package {r}") + _exit(1) # Compute the topsort graph requirements = {r: packages[r].requires() for r in requirements} @@ -140,16 +153,21 @@ def scrub(old_fs: Vfs, new_fs: Vfs) -> Vfs: old_fs = old_fs.copy() new_fs = new_fs.copy() + cleanup_fs = Vfs([]) # Look for files in the old log which are no longer present in the new log for txn in old_fs._log: if txn[0] == "link" and txn not in new_fs._log: - new_fs.unlink(txn[2]) + cleanup_fs.unlink(txn[2]) elif txn[0] == "mkdir" and txn not in new_fs._log: - new_fs.unlink(txn[1]) + cleanup_fs.unlink(txn[1]) - return new_fs + # Do unlink operations before we do install operations. + # This works around being unable to finely straify uninstall operations over their source packages. + cleanup_fs.merge(new_fs) + + return cleanup_fs @click.group() @@ -186,11 +204,19 @@ def do_apply(confdir, destdir, state_file, execute, optimize, require, exec_idem # Resolve the two input paths to absolutes root = confdir.resolve() dest = destdir.resolve() + + if not root.is_dir(): + log.fatal(f"{confdir} does not exist!") + _exit(1) + if not state_file.is_absolute(): state_file = root / state_file - new_fs = build_fs(root, dest, require) old_fs = load_state(state_file) + log.debug(f"Loaded old state consisting of {len(old_fs._log)} steps") + + new_fs = build_fs(root, dest, require) + log.debug(f"Built new state consisting of {len(new_fs._log)} steps") # Middleware processing of the resulting filesystem(s) executable_fs = scrub(old_fs, new_fs) @@ -219,7 +245,13 @@ def do_apply(confdir, destdir, state_file, execute, optimize, require, exec_idem @click.argument("list_packages", nargs=-1) def do_list(confdir, list_packages): """List out packages, profiles, hosts and subpackages in the .""" - packages = load_packages(confdir) + root = confdir.resolve() + + if not root.is_dir(): + log.fatal(f"{confdir} does not exist!") + _exit(1) + + packages = load_packages(root) if list_packages: dest = Path("~/") @@ -249,8 +281,14 @@ def do_list(confdir, list_packages): def do_state(confdir, state_file): """List out the last `apply` state in the /.cram.log or --state-file.""" root = confdir.resolve() + + if not root.is_dir(): + log.fatal(f"{confdir} does not exist!") + _exit(1) + if not state_file.is_absolute(): state_file = root / state_file + fs = load_state(state_file) for e in fs._log: print(*e) diff --git a/projects/cram/src/python/cram/common.py b/projects/cram/src/python/cram/common.py index e970813..6e952f7 100644 --- a/projects/cram/src/python/cram/common.py +++ b/projects/cram/src/python/cram/common.py @@ -38,7 +38,10 @@ def stow(fs: Vfs, src_dir: Path, dest_dir: Path, skip=[]): continue dest = dest_root / src.relative_to(src_root) - if src.is_dir(): + if src.is_symlink(): + + fs.link(src.readlink().resolve(), dest) + elif src.is_dir(): fs.mkdir(dest) fs.chmod(dest, src.stat().st_mode) diff --git a/projects/cram/src/python/cram/v1.py b/projects/cram/src/python/cram/v1.py index 76c6ff1..a6efaa1 100644 --- a/projects/cram/src/python/cram/v1.py +++ b/projects/cram/src/python/cram/v1.py @@ -51,7 +51,12 @@ class PackageV1(Package): return any(self.do_sh_or_script(c, fs, dest) for c in content) elif isinstance(content, dict): - return self.do_sh_or_script(content["run"], fs, dest, {"cwd": self.root}.get(content.get("root"), "/tmp")) + return self.do_sh_or_script( + content["run"], + fs, + dest, + {"cwd": self.root}.get(content.get("root"), "/tmp") + ) elif isinstance(content, str): sum = sha256() diff --git a/projects/vfs/src/python/vfs/impl.py b/projects/vfs/src/python/vfs/impl.py index 1dbce87..6fd84c1 100644 --- a/projects/vfs/src/python/vfs/impl.py +++ b/projects/vfs/src/python/vfs/impl.py @@ -52,7 +52,7 @@ class Vfs(object): elif e[0] == "unlink": _, dest = e if dest.is_dir(): - rmtree(dest) + rmtree(dest) elif dest.is_file(): dest.unlink() @@ -79,3 +79,6 @@ class Vfs(object): def copy(self): return Vfs(list(self._log)) + + def merge(self, other: "Vfs"): + self._log.extend(other._log)