#!/bin/bash # lfs-backup -- back up the configuration files you changed, and put them on # another system without overwriting anything there. # # lfs-backup list what the config names, and what exists # lfs-backup create [--run] [ARCHIVE] write the archive (root; secrets inside) # lfs-backup show ARCHIVE what an archive holds # lfs-backup restore ARCHIVE [--run] put it on THIS system: # missing file -> created # same content -> left alone # different -> .new, listed # lfs-backup clean ARCHIVE [--run] remove the .new files restore left, # unless you edited one (kept, listed) # lfs-backup merge ARCHIVE go through those .new files one by # one: diff, then keep live / take new / # edit both / skip # lfs-backup sections [ARCHIVE] the section names to pick from # lfs-backup plan ARCHIVE [FILE] write a restore plan to edit: one # line per file, restore or skip, and # where it goes # lfs-backup restore ARCHIVE --plan FILE [--run] restore by that plan # # --only a,b restore/create/clean just these sections # --skip a,b everything except these # # Without --run, create, restore and clean only print what they would do. # # THE CONFIG: /etc/pkgusr/backup.conf (LFS_BACKUP_CONF overrides). Sections, # one path per line, shell globs allowed: # [user] or [user:NAME] ~ is the main user's home (PKGUSR_SYS_MAIN_USER) # [root] or [root:NAME] ~ is root's home # [system] or [system:NAME] absolute paths # NAME is what --only/--skip select ("accounts", "ssh", "vim" ...); a section # without one is named after its kind. A directory entry takes every file # below it. A line starting with ! excludes what matches it, anywhere in the # config (* crosses /): !~/.vim/pack/*/.git/* !*~ # # WHAT IS STORED IS THE FILE, NOT THE LINK. A path that is a symlink is # backed up as the file it points to, under the path you named. # # OWNERSHIP GOES BY NAME, NOT NUMBER. Ids differ between two systems; the # archive records owner and group names and the mode. The old main user's # name becomes this system's main user on restore; a name this system does # not have becomes root, and is reported. # # NOTHING IS REPLACED. An existing file with different content gets a .new # beside it, with the backed-up mode and owner, and the end of the run lists # every one of them to merge by hand. /etc/passwd, group, shadow and gshadow # ALWAYS go to .new, even when the file is missing -- a restored account # database that does not match this system locks you out. # # THE ARCHIVE HOLDS SECRETS (shadow, wpa_supplicant keys): created 0600, root. set -u VERSION="1.3.0" PROG="lfs-backup" CONF="${LFS_BACKUP_CONF:-/etc/pkgusr/backup.conf}" # ROOT: the tree to read from (create) and write to (restore). / on a # running system; a mounted new system's root when restoring into it. ROOT="${LFS_BACKUP_ROOT:-/}" ROOT="${ROOT%/}" RUN=0 PLAN="" ONLY="" SKIP="" TMPD="" trap '[ -n "$TMPD" ] && rm -rf "$TMPD"' EXIT # SECRETS ARE RESTORED FOR THEIR OWNER ONLY, whatever mode they had. The # first real backup recorded /etc/gshadow 644 and most Wi-Fi keys 664; a # restore that copies those modes spreads the mistake to the new system. # Group and other bits are dropped (a 664 key becomes 600). SECRET_RE='^/etc/(shadow|gshadow)$|^/etc/sysconfig/wpa_supplicant/|^/etc/ssh/ssh_host_.*_key$|^~/\.ssh/id_[^./]*$|^~/\.ssh/authorized_keys$' # always .new: account databases are merged, never dropped in ALWAYS_NEW='^/etc/(passwd|group|shadow|gshadow)$' say() { printf '%s\n' "$*"; } note() { printf '# %s\n' "$*"; } warn() { printf '!! %s\n' "$*" >&2; } die() { warn "$*"; exit 1; } usage() { sed -n '2,/^set -u/p' "$0" | sed '/^set -u/d; s/^# \{0,1\}//'; } need_root() { [ "$(id -u)" = 0 ] || die "$PROG $1 needs root (root's files, /etc/shadow)"; } # THE MAIN USER, FROM WHAT THE SYSTEM ALREADY SAYS. packagemanager writes # PKGUSR_SYS_MAIN_USER to /etc/profile.d/pkgusr-system.sh; a root shell that # did not source it still gets the answer from the file itself. main_user() { local u="${PKGUSR_SYS_MAIN_USER:-}" if [ -z "$u" ] && [ -r "$ROOT/etc/profile.d/pkgusr-system.sh" ]; then u="$(sed -n "s/^export PKGUSR_SYS_MAIN_USER=['\"]\{0,1\}\([^'\"]*\).*/\1/p" \ "$ROOT/etc/profile.d/pkgusr-system.sh" | head -n1)" fi printf '%s' "$u" } home_of() { # home_of -- from THIS tree's passwd awk -F: -v u="$1" '$1 == u { print $6; exit }' "$ROOT/etc/passwd" 2>/dev/null } # NAMES AND IDS FROM THE TREE'S OWN FILES. With LFS_BACKUP_ROOT pointing at a # mounted system, the host's accounts are the wrong ones to ask. _name_in() { # _name_in -- the name, or the id itself when unknown if [ -r "$1" ]; then awk -F: -v i="$2" '$3 == i { print $1; f = 1; exit } END { if (!f) print i }' "$1" else printf '%s\n' "$2" fi } uname_of() { _name_in "$ROOT/etc/passwd" "$1"; } gname_of() { _name_in "$ROOT/etc/group" "$1"; } uid_of() { awk -F: -v u="$1" '$1 == u { print $3; exit }' "$ROOT/etc/passwd" 2>/dev/null; } gid_of() { awk -F: -v u="$1" '$1 == u { print $3; exit }' "$ROOT/etc/group" 2>/dev/null; } # SELECTION. A section matches --only/--skip by its name or its kind, so # `--skip accounts` and `--only user` both do what they read like. selected() { # selected