diff options
-rw-r--r-- | COPYING | 14 | ||||
-rw-r--r--[l---------] | Makefile | 48 | ||||
-rw-r--r--[l---------] | README.md | 103 | ||||
-rw-r--r-- | config.mk.in | 198 | ||||
-rwxr-xr-x | drain | 128 | ||||
-rwxr-xr-x | fill | 44 | ||||
-rw-r--r-- | pristine-etc-keeper.service | 17 | ||||
-rw-r--r-- | zz-pristine-etc-keeper-post-install.hook | 14 |
8 files changed, 366 insertions, 200 deletions
@@ -0,0 +1,14 @@ + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
+ Version 2, December 2004
+
+ Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
+
+ Everyone is permitted to copy and distribute verbatim or modified
+ copies of this license document, and changing it is allowed as long
+ as the name is changed.
+
+ DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. You just DO WHAT THE FUCK YOU WANT TO.
+
@@ -1 +1,47 @@ -build-aux/Makefile.README.mk
\ No newline at end of file +# Copyright 2016 Luke Shumaker +# This work is free. You can redistribute it and/or modify it under the +# terms of the Do What The Fuck You Want To Public License, Version 2, +# as published by Sam Hocevar. See the COPYING file for more details. + +topsrcdir = . +topoutdir = . +include $(topsrcdir)/build-aux/Makefile.head.mk + +# The core of pristine-etc-keeper +std.sys_files += /etc/etckeeper/pristine/drain +std.sys_files += /etc/etckeeper/pristine/fill +std.sys_files += /var/lib/pristine-etc/chroot.lock +std.sys_files += /var/lib/pristine-etc/spool.lock + +$(DESTDIR)/etc/etckeeper/pristine/%: $(srcdir)/% + install -Dm755 $< $@ +$(DESTDIR)/var/lib/pristine-etc/%.lock: + mkdir -p $(@D) + touch $@ + +# Convenience symlinks in bindir +std.sys_files += /usr/bin/pristine-etc-keeper + +$(DESTDIR)/usr/bin/pristine-etc-keeper: $(DESTDIR)/etc/etckeeper/pristine/fill + mkdir -p $(@D) + ln -srfT $< $@ + +# pacman integration +std.sys_files += /usr/share/libalpm/hooks/zz-pristine-etc-keeper-post-install.hook + +$(DESTDIR)/usr/share/libalpm/hooks/%.hook: %.hook + install -Dm644 $< $@ + +# systemd integration +std.sys_files += /usr/lib/systemd/system/pristine-etc-keeper.service + +$(DESTDIR)/usr/lib/systemd/system/%: $(srcdir)/% + install -Dm644 $< $@ + +# Documentation +std.sys_files += /usr/share/doc/pristine-etc-keeper/README.md + +$(DESTDIR)/usr/share/doc/pristine-etc-keeper/%: % + install -Dm644 $< $@ + +include $(topsrcdir)/build-aux/Makefile.tail.mk diff --git a/README.md b/README.md index 5e5ea4a..df4ae55 120000..100644 --- a/README.md +++ b/README.md @@ -1 +1,102 @@ -build-aux/Makefile.README.txt
\ No newline at end of file +The main functionality +====================== + +`pristine-etc-keeper` is a program that hooks into `etckeeper` to +maintain a git branch that is a "pristine" version of `/etc`--just the +raw files provided by installed packages. + +It makes several assumptions that make it a bit less general than +etckeeper: + - the real directory being tracked by etckeeper is `/etc` + - the VCS using used is `git` + - the system package manager is `pacman` + +It works by asking `pacman` which packages are installed, and +extracting `/etc` files from the cached copies of the package +tarballs. A litteral pristine version of `/etc` lives at +`/var/lib/pristine-etc/etc`. + +Invocation +========== + +The normal operation of pristine-etc-keeper is for it to be called by +etckeeper's post-install hook. You usually don't need to worry about +it. + +However, sometimes it may be useful to call it from somewhere else. + +The `pristine-etc-keeper` program essentially has two modes of +operation: + + 1. With arguments: Run, and use "$*" for the commit message. + + 2. No arguments: If a run was previously requested, but never + fulfilled; do that now. + +Really though, if it has arguments, it requests a run with that commit +message; then, whether it requested a run or not, it launches another +process to fulfill any outstanding requests. + +If you're thinking "What!? Why request runs? Why not just run +directly? It's needless complexity!" Instinctively, I'd agree with +that line of reasoning; but see "The spool" below for justification of +the complexity. + +Implementation details +====================== + +The spool +--------- + +It would be really simple to just provide a program that you run when +you want to update the pristine-etc. Unfortunately, +pristine-etc-keeper is very slow, and this would make many tasks +painful. So we run it asyncronously. + +But that opens a whole other slew of problems. What happens if we try +to run it again while it is already running? The second instance +should wait until the first instance is finished. But only one +instance should be queued at a time; if a 3rd instance tries to start, +it should just be discarded/merged with the one already waiting +("coalescing"). + +A simple thing to do would have been to write a daemon that is always +running, waiting to receive a signal that it should run. I don't like +that because I don't really want the process to be long lived, nor for +it to be started at boot. + +But, let's take that idea, tweak it a bit. + +Let's conceptualize the "signal" as the invoker adding an item to a +filesystem spool; each time the daemon runs a job, it empties the +spool. + +Now, we modify this idea by saying that the daemon may exit when the +spool is empty (ie, it has no work to do, and would just idle-wait +until it is asked to run again). When the invoker adds an item to the +spool, we simply have it ensure that the daemon is running. + +To translate this into the source code names, the invoker is "fill" +because it adds an item to the spool, and the so-called-daemon that +runs the jobs is "drain" because it drains the spool. + +Systemd integration +------------------- + +In the above section, we conceptualized the "drain" program as a +daemon, even though it isn't truly one. This conceptualization is +useful in administration as well. + +So, if systemd is being used, we have it litterally show up as +pristine-etc-keeper.service. This has a number of benefits: + + - cgroup isolation + - uniform logging with other system services + - most importantly: systemd v230 and up won't kill a run in progress + when you log out + +But, if systemd wasn't used to boot the system, then it simply forks +to the background. + +If you'd like to see similar integration with another service manager, +patches welcome! diff --git a/config.mk.in b/config.mk.in deleted file mode 100644 index be95ab4..0000000 --- a/config.mk.in +++ /dev/null @@ -1,198 +0,0 @@ -# Copyright (C) 2016-2017 Luke Shumaker -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - -# This is based on §7.2 "Makefile Conventions" of the July 25, 2016 -# release of the GNU Coding Standards. -# -# Grep for '^##' in this file to see which Autoconf macros it depends -# on. - -dist.pkgname = @PACKAGE_TARNAME@ -gnuconf.pkgname = @PACKAGE_NAME@ - -# 7.2.2: Utilities in Makefiles -# ----------------------------- - -# It's ok to hard-code these commands in rules, but who wants to -# memorize the list of what's ok? - -## AC_PROG_AWK -## AC_PROG_GREP -## AC_PROG_EGREP -## AC_PROG_SED - -AWK = @AWK@ -CAT = cat -CMP = cmp -CP = cp -DIFF = diff -ECHO = echo -EGREP = @EGREP@ -EXPR = expr -FALSE = false -GREP = @GREP@ -INSTALL_INFO = install-info -LN = ln -LS = ls -MKDIR = mkdir -MV = mv -PRINTF = printf -PWD = pwd -RM = rm -RMDIR = rmdir -SED = @SED@ -SLEEP = sleep -SORT = sort -TAR = tar -TEST = test -TOUCH = touch -TR = tr -TRUE = true - -# 7.2.2: Utilities in Makefiles/7.2.3: Variables for Specifying Commands -# ---------------------------------------------------------------------- - -# Standard user-configurable programs. -# -# The list of programs here is specified in §7.2.2, but the associated FLAGS -# variables are specified in §7.2.3. I found it cleaner to list them together. - -## AC_PROG_INSTALL # @INSTALL@ @INSTALL_PROGRAM@ @INSTALL_SCRIPT@ @INSTALL_DATA@ -## AC_PROG_LEX # @LEX@ @LEXLIB@ -## AC_PROG_RANLIB -## AC_PROG_YACC -## AC_PROG_CC -# -# TODO: What causes Autoconf to define @AR@? - -AR = @AR@ -ARFLAGS = -BISON = bison -BISONFLAGS = -CC = @CC@ -CFLAGS = @CFLAGS@ # CFLAGS instead of CCFLAGS -FLEX = flex -FLEXFLAGS = -INSTALL = @INSTALL@ -# There is no INSTALLFLAGS[0] -LD = ld -LDFLAGS = @LDFLAGS@ -LDCONFIG = ldconfig # TODO[1] -LDCONFIGFLAGS = -LEX = @LEX@ -LFLAGS = #LFLAGS instead of LEXFLAGS -#MAKE -MAKEINFO = makeinfo -MAKEINFOFLAGS = -RANLIB = @RANLIB@ -RANLIBFLAGS = -TEXI2DVI = texi2dvi -TEXI2DVIFLAGS = -YACC = @YACC@ -YFLAGS = # YFLAGS instead of YACCFLAGS - -CPPFLAGS = @CPPFLAGS@ - -LN_S = @LN_S@ - -CHGRP = chgrp -CHGRPFLAGS = -CHMOD = chmod -CHMODFLAGS = -CHOWN = chown -CHOWNFLAGS = -MKNOD = mknod -MKNODFLAGS = - -# [0]: There is no INSTALLFLAGS because it would be inconsistent with how the -# standards otherwise recommend using $(INSTALL); with INSTALL_PROGRAM and -# INSTALL_DATA; which are specified in a way precluding the use of -# INSTALLFLAGS. To have the variable, but to ignore it in the common case -# would be confusing. -# -# [1]: The RANLIB and LDCONFIG variables need some extra smarts; §7.2.2 says: -# -# > When you use ranlib or ldconfig, you should make sure nothing bad -# > happens if the system does not have the program in question. Arrange -# > to ignore an error from that command, and print a message before the -# > command to tell the user that failure of this command does not mean a -# > problem. (The Autoconf ‘AC_PROG_RANLIB’ macro can help with this.) - -# 7.2.3: Variables for Specifying Commands -# ---------------------------------------- - -INSTALL_PROGRAM = @INSTALL_PROGRAM@ -INSTALL_DATA = @INSTALL_DATA@ - -# 7.2.5: Variables for Installation Directories -# --------------------------------------------- - -# Root for the installation -prefix = @prefix@ -exec_prefix = @exec_prefix@ -# Executable programs -bindir = @bindir@ -sbindir = @sbindir@ -libexecdir = @libexecdir@ -# Data files (Autoconf won't support runstatedir until version 2.70) -datarootdir = @datarootdir@ -datadir = @datadir@ -sysconfdir = @sysconfdir@ -sharedstatedir = @sharedstatedir@ -localstatedir = @localstatedir@ -runstatedir = $(localstatedir)/run -# Specific types of files -includedir = @includedir@ -oldincludedir = @oldincludedir@ -docdir = @docdir@ -infodir = @infodir@ -htmldir = @htmldir@ -dvidir = @dvidir@ -pdfdir = @pdfdir@ -psdir = @psdir@ -libdir = @libdir@ -lispdir = $(datarootdir)/emacs/site-lisp -localedir = @localedir@ - -mandir = @mandir@ -man1dir = $(mandir)/man1 -man2dir = $(mandir)/man2 -man3dir = $(mandir)/man3 -man4dir = $(mandir)/man4 -man5dir = $(mandir)/man5 -man6dir = $(mandir)/man6 -man7dir = $(mandir)/man7 -man8dir = $(mandir)/man8 - -manext = .1 -man1ext = .1 -man2ext = .2 -man3ext = .3 -man4ext = .4 -man5ext = .5 -man6ext = .6 -man7ext = .7 -man8ext = .8 - -# 7.2.7: Install Command Categories -# --------------------------------- - -PRE_INSTALL = -POST_INSTALL = -NORMAL_INSTALL = - -PRE_UNINSTALL = -POST_UNINSTALL = -NORMAL_UNINSTALL = @@ -0,0 +1,128 @@ +#!/usr/bin/env bash +# Copyright 2016-2017 Luke Shumaker +# This work is free. You can redistribute it and/or modify it under the +# terms of the Do What The Fuck You Want To Public License, Version 2, +# as published by Sam Hocevar. See the COPYING file for more details. + +# The user should not call this script directly. + +declare -r workdir=/var/lib/pristine-etc + +watchdirs=( + /etc + /usr/share/holo/files +) +readonly watchdirs + +pacman-watched-name-ver-dirs() { + local dir dirs + dirs=() + for dir in "${watchdirs[@]}"; do + if [[ -d "$dir" ]]; then + dirs+=("$dir") + fi + done + LC_ALL=C pacman -Qo "${dirs[@]}" | sed -r 's| is owned by | |' | + awk '{i=$2" "$3; a[i]=a[i]" "$1} END{for(i in a){print i " " a[i]}}' +} + +pacman-all-name-arch() { + LC_ALL=C pacman -Qni | tr $'\n' $'\r' | sed 's/\r\r/\n/g' | sed -r 's|(.*\r)?Name\s*:\s*(\S+)(\r.*)?\rArchitecture\s*:\s*(\S+)\r.*|\2 \4|' +} + +pacman-watched-name-arch-ver-dirs() { + join <(pacman-all-name-arch|sort) <(pacman-watched-name-ver-dirs|sort) +} + +commit() ( + msg="$1" + + cd "$workdir" + if ! [[ -d etc.git ]]; then + mkdir -p chroot/etc + (cd chroot/etc && etckeeper init -d "$PWD") + mv chroot/etc/.git etc.git + fi + rm -rf chroot + mkdir chroot + cd chroot + + err=false + files=() + while IFS=' ' read -r pkgname arch pkgver dirs; do + file=("/var/cache/pacman/pkg/$pkgname-$pkgver-$arch".pkg.tar.*) + if ! test -f "$file"; then + printf "ERROR: no cached package for %s %s %s\n" "$pkgname" "$pkgver" "$arch" + err=true + fi + files+=("$file $dirs") + done < <(pacman-watched-name-arch-ver-dirs) + if $err; then + return 1 + fi + for filespec in "${files[@]}"; do + read file dirs_str <<<"$filespec" + read -a dirs <<<"$dirs_str" + printf " -> %s\n" "$file" + bsdtar xpvf "$file" "${dirs[@]#/}" + done + + ln -srT ../etc.git etc/.git + + if type holo &>/dev/null; then + mkdir -p -- run usr/lib + ln -sT /usr/lib/holo usr/lib/holo + if [ -f /etc/os-release ]; then + ln -sT /etc/os-release usr/lib/os-release + else + ln -sT /usr/lib/os-release usr/lib/os-release + fi + HOLO_ROOT_DIR=. holo apply + fi + cd etc/ + etckeeper update-ignore -d "$PWD" + if etckeeper unclean -d "$PWD"; then + etckeeper commit -d "$PWD" "$msg" + fi +) + +pull() ( + cd /etc + git remote add pristine "${workdir}/chroot/etc" &>/dev/null || true + git fetch pristine +) + +lock() { + local fd=$1 + local file=$2 + eval "exec $fd>"'"$file"' + flock "${@:3}" "$fd" +} + +unlock() { + local fd=$1 + exec {fd}>&- +} + +main() { + set -e -o pipefail + umask 0022 + + if ! lock 7 "${workdir}/chroot.lock" -n; then + return 0 + fi + while true; do + lock 8 "${workdir}/spool.lock" + if ! [[ -f "${workdir}/spool" ]]; then + return 0 + fi + msg="$(cat "${workdir}/spool")" + rm -f "${workdir}/spool" + unlock 8 + + commit "$msg" + pull + done +} + +main "$@" @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +# Copyright 2016 Luke Shumaker +# This work is free. You can redistribute it and/or modify it under the +# terms of the Do What The Fuck You Want To Public License, Version 2, +# as published by Sam Hocevar. See the COPYING file for more details. + +declare -r workdir=/var/lib/pristine-etc +declare -r bindir=/etc/etckeeper/pristine + +lock() { + local fd=$1 + local file=$2 + eval "exec $fd>"'"$file"' + flock "${@:3}" "$fd" +} + +unlock() { + local fd=$1 + exec {fd}>&- +} + +drain() { + if [[ -d /run/systemd/system ]]; then + systemctl reset-failed pristine-etc-keeper.service &>/dev/null || true + systemctl start pristine-etc-keeper.service + else + nohup "${bindir}/drain" <&- &> /dev/null & + fi +} + +main() { + set -e + umask 0022 + + if [[ $# -gt 0 ]]; then + lock 8 "${workdir}/spool.lock" + printf '%s\n' "$*" >> "${workdir}/spool" + unlock 8 + fi + + drain +} + +main "$@" diff --git a/pristine-etc-keeper.service b/pristine-etc-keeper.service new file mode 100644 index 0000000..4cf4305 --- /dev/null +++ b/pristine-etc-keeper.service @@ -0,0 +1,17 @@ +# Copyright 2016 Luke Shumaker +# This work is free. You can redistribute it and/or modify it under the +# terms of the Do What The Fuck You Want To Public License, Version 2, +# as published by Sam Hocevar. See the COPYING file for more details. +[Unit] +Description=Update pristine /etc directory +Documentation=man:pristine-etc-keeper(8) +DefaultDependencies=no +Conflicts=shutdown.target +After=local-fs.target time-sync.target +Before=shutdown.target + +[Service] +# Can't use Type=oneshot because that would block when starting +Type=simple +ExecStart=/etc/etckeeper/pristine/drain +IOSchedulingClass=idle diff --git a/zz-pristine-etc-keeper-post-install.hook b/zz-pristine-etc-keeper-post-install.hook new file mode 100644 index 0000000..4e8b1c6 --- /dev/null +++ b/zz-pristine-etc-keeper-post-install.hook @@ -0,0 +1,14 @@ +# pristine-etc-keeper post-install hook for Pacman 5 and newer + +[Trigger] +Operation = Install +Operation = Upgrade +Operation = Remove +Type = File +Target = etc/* usr/share/holo/files/* + +[Action] +Description = pristine-etc-keeper: post-transaction commit +When = PostTransaction +Exec = /etc/etckeeper/pristine/fill 'pacman post-install' +Depends = pristine-etc-keeper |