Hardening cram, stratify uninstalls

This commit is contained in:
Reid 'arrdem' McKenzie 2022-07-25 22:44:10 -06:00
parent 0fb3cfa595
commit 547396b76e
4 changed files with 60 additions and 11 deletions

View file

@ -5,7 +5,6 @@ import logging
import os import os
from pathlib import Path from pathlib import Path
import pickle import pickle
import sys
from typing import List from typing import List
from . import ( from . import (
@ -25,6 +24,11 @@ from vfs import Vfs
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
def _exit(val):
logging.shutdown()
exit(val)
def load(root: Path, name: str, clss): def load(root: Path, name: str, clss):
for c in clss: for c in clss:
i = c(root, name) i = c(root, name)
@ -33,10 +37,12 @@ def load(root: Path, name: str, clss):
def load_package(root, name): def load_package(root, name):
log.debug(f"Attempting to load package {name} from {root}")
return load(root, name, [PackageV1, PackageV0]) return load(root, name, [PackageV1, PackageV0])
def load_profile(root, name): def load_profile(root, name):
log.debug(f"Attempting to load profile {name} from {root}")
return load(root, name, [ProfileV1, ProfileV0]) return load(root, name, [ProfileV1, ProfileV0])
@ -44,6 +50,7 @@ def load_packages(root: Path) -> dict:
"""Load the configured packages.""" """Load the configured packages."""
packages = {} packages = {}
log.debug(f"Trying to load packages from {root}...")
for p in (root / "packages.d").glob("*"): for p in (root / "packages.d").glob("*"):
name = str(p.relative_to(root)) name = str(p.relative_to(root))
packages[name] = load_package(p, name) packages[name] = load_package(p, name)
@ -71,14 +78,20 @@ def build_fs(root: Path, dest: Path, prelude: List[str]) -> Vfs:
requirements = [] requirements = []
requirements.extend(prelude) 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: for r in requirements:
try: try:
for d in packages[r].requires(): for d in packages[r].requires():
if d not in requirements: if d not in requirements:
requirements.append(d) requirements.append(d)
except KeyError: except KeyError:
print(f"Error: Unable to load package {r}", file=sys.stderr) log.fatal(f"Error: Unable to load package {r}")
exit(1) _exit(1)
# Compute the topsort graph # Compute the topsort graph
requirements = {r: packages[r].requires() for r in requirements} 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() old_fs = old_fs.copy()
new_fs = new_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 # Look for files in the old log which are no longer present in the new log
for txn in old_fs._log: for txn in old_fs._log:
if txn[0] == "link" and txn not in new_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: 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() @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 # Resolve the two input paths to absolutes
root = confdir.resolve() root = confdir.resolve()
dest = destdir.resolve() dest = destdir.resolve()
if not root.is_dir():
log.fatal(f"{confdir} does not exist!")
_exit(1)
if not state_file.is_absolute(): if not state_file.is_absolute():
state_file = root / state_file state_file = root / state_file
new_fs = build_fs(root, dest, require)
old_fs = load_state(state_file) 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) # Middleware processing of the resulting filesystem(s)
executable_fs = scrub(old_fs, new_fs) 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) @click.argument("list_packages", nargs=-1)
def do_list(confdir, list_packages): def do_list(confdir, list_packages):
"""List out packages, profiles, hosts and subpackages in the <confdir>.""" """List out packages, profiles, hosts and subpackages in the <confdir>."""
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: if list_packages:
dest = Path("~/") dest = Path("~/")
@ -249,8 +281,14 @@ def do_list(confdir, list_packages):
def do_state(confdir, state_file): def do_state(confdir, state_file):
"""List out the last `apply` state in the <confdir>/.cram.log or --state-file.""" """List out the last `apply` state in the <confdir>/.cram.log or --state-file."""
root = confdir.resolve() root = confdir.resolve()
if not root.is_dir():
log.fatal(f"{confdir} does not exist!")
_exit(1)
if not state_file.is_absolute(): if not state_file.is_absolute():
state_file = root / state_file state_file = root / state_file
fs = load_state(state_file) fs = load_state(state_file)
for e in fs._log: for e in fs._log:
print(*e) print(*e)

View file

@ -38,7 +38,10 @@ def stow(fs: Vfs, src_dir: Path, dest_dir: Path, skip=[]):
continue continue
dest = dest_root / src.relative_to(src_root) 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.mkdir(dest)
fs.chmod(dest, src.stat().st_mode) fs.chmod(dest, src.stat().st_mode)

View file

@ -51,7 +51,12 @@ class PackageV1(Package):
return any(self.do_sh_or_script(c, fs, dest) for c in content) return any(self.do_sh_or_script(c, fs, dest) for c in content)
elif isinstance(content, dict): 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): elif isinstance(content, str):
sum = sha256() sum = sha256()

View file

@ -52,7 +52,7 @@ class Vfs(object):
elif e[0] == "unlink": elif e[0] == "unlink":
_, dest = e _, dest = e
if dest.is_dir(): if dest.is_dir():
rmtree(dest) rmtree(dest)
elif dest.is_file(): elif dest.is_file():
dest.unlink() dest.unlink()
@ -79,3 +79,6 @@ class Vfs(object):
def copy(self): def copy(self):
return Vfs(list(self._log)) return Vfs(list(self._log))
def merge(self, other: "Vfs"):
self._log.extend(other._log)