X-Git-Url: https://mattmccutchen.net/utils/utils.git/blobdiff_plain/40ea9b7868f2b7746e7cbabfba6aba982096392a..273c390351c42303171c25215304d1cfd6ca02d4:/rpm-audit diff --git a/rpm-audit b/rpm-audit new file mode 100755 index 0000000..ca7431a --- /dev/null +++ b/rpm-audit @@ -0,0 +1,116 @@ +#!/bin/bash +# ~ 2017-11-12 + +WANTS_FILE=/usr/local/etc/rpm-wants + +set -e +set -o pipefail +# XXX: $BASH_COMMAND may be wrong with pipefail, but it's better than what we had before. +trap 'echo "Unexpected error in command: $BASH_COMMAND"; if [ -n "$tmpdir" ] && [ -r "$tmpdir/dnf.log" ]; then cat "$tmpdir/dnf.log"; fi ' ERR + +if [ $EUID != 0 ]; then + echo >&2 'Unfortunately, rpm-audit can only run as root because it calls' + echo >&2 '"dnf install --assumeno".' + exit 1 +fi + +tmpdir="$(mktemp -d --tmpdir rpm-audit.XXXXXX)" +trap 'rm -rf "$tmpdir"' EXIT + +uncommented_entries=($(<"$WANTS_FILE" sed -re 's/(^|[^[:space:]])[[:space:]]*#.*$/\1/' | egrep -v '^$')) +wants=() +modules=() +for e in "${uncommented_entries[@]}"; do + case "$e" in + (module:*) modules+=("${e#module:}");; + (*) wants+=("$e");; + esac +done + +function rpmq { + # Set query format to match "dnf repoquery". + # + # Looks like rpm exits with the number of unmatched arguments, so we + # have to swallow any exit code. :( ~ Matt 2018-06-01 + rpm -q --qf '%{NAME}-%|EPOCH?{%{EPOCH}}:{0}|:%{VERSION}-%{RELEASE}.%{ARCH}\n' "$@" || true +} +function strip_zero_epoch { + sed -e 's/-0:/-/' +} +function indent { + sed -e 's/^/ /' +} +function filter_packages { + # Exempt kernel-{core,devel,modules{,-extra}} from audit because of multiple + # installed versions: not worth trying to do something better. + grep -Ev '^(gpg-pubkey|kernel-core|kernel-devel|kernel-modules|kernel-modules-extra)-[^-]+-[^-]+$' +} + +# Check 1: installed want providers vs. userinstalled + +# "dnf repoquery --installed --whatprovides" doesn't seem to accept multiple +# arguments, and running it once per entry would be unacceptably slow. See how +# long we can get away with this before looking for another solution. +# +# "rpm -q --whatprovides glibc.i686" does not work. Take all wants that look +# like they have an architecture and use "rpm -q" instead. This may fail if +# someone writes an actual provide of a package-like name with an architecture. +# ~ Matt 2017-11-16 +pkg_arch_wants=() +normal_wants=() +for w in "${wants[@]}"; do + case "$w" in + (*.i686|*.x86_64) + pkg_arch_wants+=("$w");; + (*) + normal_wants+=("$w");; + esac +done +# Since F29, kernel packages keep getting unmarked as userinstalled. Not +# investigating; just exclude them here. ~ Matt 2019-01-22 +{ rpmq --whatprovides "${normal_wants[@]}" && rpmq "${pkg_arch_wants[@]}"; } | strip_zero_epoch | filter_packages | sort | uniq >"$tmpdir/wants-installed" +dnf repoquery --userinstalled | strip_zero_epoch | filter_packages | sort >"$tmpdir/userinstalled" + +if ! cmp -s "$tmpdir/wants-installed" "$tmpdir/userinstalled"; then + echo "Installed wants that are not marked as userinstalled:" + comm -2 -3 "$tmpdir/wants-installed" "$tmpdir/userinstalled" | indent + echo "Userinstalled packages that are not wants:" + comm -1 -3 "$tmpdir/wants-installed" "$tmpdir/userinstalled" | indent + echo "To correct, edit the wants file or use 'dnf mark {install|remove} PACKAGE_NAME'." + exit 1 +fi + +# Check 2: fresh solution of wants vs. installed (should catch different choice of provider, packages needing update, orphans, and problems) + +platform_id="$(sed -nre 's,^PLATFORM_ID="(.*)"$,\1,p' /etc/os-release)" + +SANDBOX_DNF=(dnf + --installroot="$tmpdir/installroot" + --setopt=cachedir=/var/cache/dnf # Share main cache to save time + --disableplugin=qubes-hooks # Qubes plugin takes unwanted actions when using an installroot + --releasever=/ --setopt=module_platform_id=$platform_id +) + +"${SANDBOX_DNF[@]}" --assumeyes module enable "${modules[@]}" &>"$tmpdir/dnf.log" + +# --verbose for "Package ... will be installed" output. The human-readable list +# of packages to install is harder to scrape because of line wrapping. +# +# "dnf --assumeno" exits 1. I don't see an obvious way to distinguish this from +# real errors. However, real errors are likely to generate a diff anyway. +{ "${SANDBOX_DNF[@]}" --verbose --assumeno install --best "${wants[@]}" 2>&1 || [ $? == 1 ]; } | tee -a "$tmpdir/dnf.log" | sed -nre 's/^---> Package ([^ ]+)\.([^ .]+) ([^ ]+) will be installed$/\1-\3.\2/p' | filter_packages | sort >"$tmpdir/solved" +# Looks like "dnf repoquery --installed" doesn't catch extras. +rpmq -a | filter_packages | strip_zero_epoch | sort >"$tmpdir/installed" + +if ! cmp -s "$tmpdir/solved" "$tmpdir/installed"; then + echo "Packages in fresh solution that are not installed:" + comm -2 -3 "$tmpdir/solved" "$tmpdir/installed" | indent + echo "Installed packages that are not in fresh solution:" + comm -1 -3 "$tmpdir/solved" "$tmpdir/installed" | indent + echo "To correct, install or remove packages to match the canonical solution," + echo "or override the default choice of providers by adding the desired" + echo "providers to the wants file." + exit 1 +fi + +echo "RPM package set audit passed!"