Factor out the VFS since it's kinda neat
This commit is contained in:
parent
64e7de4321
commit
97c0a10e79
6 changed files with 111 additions and 86 deletions
|
@ -5,6 +5,7 @@ zapp_binary(
|
|||
"src/python"
|
||||
],
|
||||
deps = [
|
||||
"//projects/vfs",
|
||||
py_requirement("toposort"),
|
||||
]
|
||||
)
|
||||
|
|
|
@ -2,35 +2,32 @@
|
|||
|
||||
> To force (people or things) into a place or container that is or appears to be too small to contain them.
|
||||
|
||||
An alternative to GNU Stow, which critically supports jinja2 templating and some other niceties.
|
||||
An alternative to GNU Stow, more some notion of packages with dependencies and install scripts.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
$ cram.zapp [hostname]
|
||||
# cram [--dry-run | --execute] <configdir> <destdir>
|
||||
$ cram ~/conf ~/ # --dry-run is the default
|
||||
```
|
||||
|
||||
|
||||
Cram consumes a directory tree of the following structure:
|
||||
Cram operates in terms of packages, which are directories with the following structure -
|
||||
|
||||
```
|
||||
# Hosts
|
||||
./hosts.d/<hostname>/
|
||||
./hosts.d/<hostname>/REQUIRES
|
||||
./hosts.d/<hostname>/PRE_INSTALL
|
||||
./hosts.d/<hostname>/INSTALL
|
||||
./hosts.d/<hostname>/POST_INSTALL
|
||||
/REQUIRES # A list of other packages this one requires
|
||||
/BUILD # 1. Perform any compile or package management tasks
|
||||
/PRE_INSTALL # 2. Any other tasks required before installation occurs
|
||||
/INSTALL # 3. Do whatever constitutes installation
|
||||
/POST_INSTALL # 4. Any cleanup or other tasks following installation
|
||||
|
||||
# Profiles
|
||||
./profiles.d/<profilename>/
|
||||
./profiles.d/<profilename>/REQUIRES
|
||||
./profiles.d/<profilename>/PRE_INSTALL
|
||||
./profiles.d/<profilename>/INSTALL
|
||||
./profiles.d/<profilename>/POST_INSTALL
|
||||
... # Any other files are treated as package contents
|
||||
|
||||
# Packages
|
||||
./packages.d/<packagename>/
|
||||
./packages.d/<packagename>/REQUIRES
|
||||
./packages.d/<packagename>/PRE_INSTALL
|
||||
./packages.d/<packagename>/POST_INSTALL
|
||||
```
|
||||
|
||||
Cram reads a config dir with three groups of packages
|
||||
- `packages.d/<packagename>` contains a package that installs but probably shouldn't configure a given tool, package or group of files.
|
||||
Configuration should be left to profiles.
|
||||
- `profiles.d/<profilename>` contains a profile; a group of related profiles and packages that should be installed together.
|
||||
- `hosts.d/<hostname>` contains one package for each host, and should pull in a list of profiles.
|
||||
|
||||
The intent of this tool is to keep GNU Stow's intuitive model of deploying configs via symlinks, and augment it with a useful pattern for talking about "layers" / "packages" of related configs.
|
||||
|
|
|
@ -2,80 +2,25 @@
|
|||
|
||||
import argparse
|
||||
from itertools import chain
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
from subprocess import run
|
||||
import sys
|
||||
from typing import NamedTuple
|
||||
|
||||
from toposort import toposort_flatten
|
||||
from vfs import Vfs
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("-x", "--execute", dest="execute", action="store_true", default=False)
|
||||
parser.add_argument("-d", "--dry-run", dest="execute", action="store_false")
|
||||
parser.add_argument("confdir", default="~/conf", type=Path)
|
||||
parser.add_argument("destdir", default="~/", type=Path)
|
||||
parser.add_argument("confdir", type=Path)
|
||||
parser.add_argument("destdir", type=Path)
|
||||
|
||||
|
||||
class Fs(object):
|
||||
"""An abstract filesystem device which can accumulate changes, and apply them in a batch."""
|
||||
|
||||
def __init__(self):
|
||||
self._log = []
|
||||
|
||||
def execute(self, execute=False):
|
||||
for e in self._log:
|
||||
print(e)
|
||||
|
||||
if not execute:
|
||||
continue
|
||||
|
||||
elif e[0] == "exec":
|
||||
_, dir, cmd = e
|
||||
run(cmd, cwd=str(dir))
|
||||
|
||||
elif e[0] == "link":
|
||||
_, src, dest = e
|
||||
if dest.exists() and dest.is_symlink() and dest.readlink() == dest:
|
||||
continue
|
||||
else:
|
||||
if dest.exists():
|
||||
dest.unlink()
|
||||
dest.symlink_to(src)
|
||||
|
||||
elif e[0] == "copy":
|
||||
raise NotImplementedError()
|
||||
|
||||
elif e[0] == "chmod":
|
||||
_, dest, mode = e
|
||||
dest.chmod(mode)
|
||||
|
||||
elif e[0] == "mkdir":
|
||||
_, dest = e
|
||||
dest.mkdir(exist_ok=True)
|
||||
|
||||
|
||||
def _append(self, msg):
|
||||
self._log.append(msg)
|
||||
|
||||
def link(self, src, dest):
|
||||
self._append(("link", src, dest))
|
||||
|
||||
def copy(self, src, dest):
|
||||
self._append(("copy", src, dest))
|
||||
|
||||
def chmod(self, dest, mode):
|
||||
self._append(("chmod", dest, mode))
|
||||
|
||||
def mkdir(self, dest):
|
||||
self._append(("mkdir", dest))
|
||||
|
||||
def exec(self, dest, cmd):
|
||||
self._append(("exec", dest, cmd))
|
||||
|
||||
|
||||
def stow(fs: Fs, src_dir: Path, dest_dir: Path, skip=[]):
|
||||
def stow(fs: Vfs, src_dir: Path, dest_dir: Path, skip=[]):
|
||||
"""Recursively 'stow' (link) the contents of the source into the destination."""
|
||||
|
||||
dest_root = Path(dest_dir)
|
||||
|
@ -100,22 +45,26 @@ class PackageV0(NamedTuple):
|
|||
root: Path
|
||||
name: str
|
||||
|
||||
SPECIAL_FILES = ["BUILD", "INSTALL", "POST_INSTALL", "requires"]
|
||||
SPECIAL_FILES = ["BUILD", "PRE_INSTALL", "INSTALL", "POST_INSTALL", "REQUIRES"]
|
||||
|
||||
def requires(self):
|
||||
"""Get the dependencies of this package."""
|
||||
requiresf = self.root / "requires"
|
||||
requiresf = self.root / "REQUIRES"
|
||||
if requiresf.exists():
|
||||
with open(requiresf) as fp:
|
||||
return [l.strip() for l in fp]
|
||||
return []
|
||||
|
||||
def install(self, fs, dest):
|
||||
def install(self, fs: Vfs, dest):
|
||||
"""Install this package."""
|
||||
buildf = self.root / "BUILD"
|
||||
if buildf.exists():
|
||||
fs.exec(self.root, ["bash", str(buildf)])
|
||||
|
||||
pref = self.root / "PRE_INSTALL"
|
||||
if pref.exists():
|
||||
fs.exec(self.root, ["bash", str(pref)])
|
||||
|
||||
installf = self.root / "INSTALL"
|
||||
if installf.exists():
|
||||
fs.exec(self.root, ["bash", str(installf)])
|
||||
|
@ -132,6 +81,11 @@ def main():
|
|||
|
||||
opts, args = parser.parse_known_args()
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.DEBUG,
|
||||
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
||||
)
|
||||
|
||||
root = opts.confdir
|
||||
|
||||
packages = {str(p.relative_to(root)): PackageV0(p, str(p))
|
||||
|
@ -155,7 +109,7 @@ def main():
|
|||
|
||||
# Compute the topsort graph
|
||||
requirements = {r: packages[r].requires() for r in requirements}
|
||||
fs = Fs()
|
||||
fs = Vfs()
|
||||
|
||||
for r in toposort_flatten(requirements):
|
||||
r = packages[r]
|
||||
|
|
3
projects/vfs/BUILD
Normal file
3
projects/vfs/BUILD
Normal file
|
@ -0,0 +1,3 @@
|
|||
py_project(
|
||||
name = "vfs"
|
||||
)
|
5
projects/vfs/src/python/vfs/__init__.py
Normal file
5
projects/vfs/src/python/vfs/__init__.py
Normal file
|
@ -0,0 +1,5 @@
|
|||
"""
|
||||
The published interface of the VFS package.
|
||||
"""
|
||||
|
||||
from .impl import Vfs # noqa
|
65
projects/vfs/src/python/vfs/impl.py
Normal file
65
projects/vfs/src/python/vfs/impl.py
Normal file
|
@ -0,0 +1,65 @@
|
|||
"""
|
||||
The implementation.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from subprocess import run
|
||||
|
||||
|
||||
_log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Vfs(object):
|
||||
"""An abstract filesystem device which can accumulate changes, and apply them in a batch."""
|
||||
|
||||
def __init__(self):
|
||||
self._log = []
|
||||
|
||||
def execute(self, execute=False):
|
||||
for e in self._log:
|
||||
_log.debug(e)
|
||||
|
||||
if not execute:
|
||||
continue
|
||||
|
||||
elif e[0] == "exec":
|
||||
_, dir, cmd = e
|
||||
run(cmd, cwd=str(dir))
|
||||
|
||||
elif e[0] == "link":
|
||||
_, src, dest = e
|
||||
if dest.exists() and dest.is_symlink() and dest.readlink() == dest:
|
||||
continue
|
||||
else:
|
||||
if dest.exists():
|
||||
dest.unlink()
|
||||
dest.symlink_to(src)
|
||||
|
||||
elif e[0] == "copy":
|
||||
raise NotImplementedError()
|
||||
|
||||
elif e[0] == "chmod":
|
||||
_, dest, mode = e
|
||||
dest.chmod(mode)
|
||||
|
||||
elif e[0] == "mkdir":
|
||||
_, dest = e
|
||||
dest.mkdir(exist_ok=True)
|
||||
|
||||
def _append(self, msg):
|
||||
self._log.append(msg)
|
||||
|
||||
def link(self, src, dest):
|
||||
self._append(("link", src, dest))
|
||||
|
||||
def copy(self, src, dest):
|
||||
self._append(("copy", src, dest))
|
||||
|
||||
def chmod(self, dest, mode):
|
||||
self._append(("chmod", dest, mode))
|
||||
|
||||
def mkdir(self, dest):
|
||||
self._append(("mkdir", dest))
|
||||
|
||||
def exec(self, dest, cmd):
|
||||
self._append(("exec", dest, cmd))
|
Loading…
Reference in a new issue