Commit | Line | Data |
---|---|---|
273c3903 MM |
1 | #!/bin/bash |
2 | # ~ 2017-11-12 | |
3 | ||
4 | WANTS_FILE=/usr/local/etc/rpm-wants | |
5 | ||
6 | set -e | |
7 | set -o pipefail | |
8 | # XXX: $BASH_COMMAND may be wrong with pipefail, but it's better than what we had before. | |
9 | trap 'echo "Unexpected error in command: $BASH_COMMAND"; if [ -n "$tmpdir" ] && [ -r "$tmpdir/dnf.log" ]; then cat "$tmpdir/dnf.log"; fi ' ERR | |
10 | ||
11 | if [ $EUID != 0 ]; then | |
12 | echo >&2 'Unfortunately, rpm-audit can only run as root because it calls' | |
13 | echo >&2 '"dnf install --assumeno".' | |
14 | exit 1 | |
15 | fi | |
16 | ||
17 | tmpdir="$(mktemp -d --tmpdir rpm-audit.XXXXXX)" | |
18 | trap 'rm -rf "$tmpdir"' EXIT | |
19 | ||
20 | uncommented_entries=($(<"$WANTS_FILE" sed -re 's/(^|[^[:space:]])[[:space:]]*#.*$/\1/' | egrep -v '^$')) | |
21 | wants=() | |
22 | modules=() | |
23 | for e in "${uncommented_entries[@]}"; do | |
24 | case "$e" in | |
25 | (module:*) modules+=("${e#module:}");; | |
26 | (*) wants+=("$e");; | |
27 | esac | |
28 | done | |
29 | ||
30 | function rpmq { | |
31 | # Set query format to match "dnf repoquery". | |
32 | # | |
33 | # Looks like rpm exits with the number of unmatched arguments, so we | |
34 | # have to swallow any exit code. :( ~ Matt 2018-06-01 | |
35 | rpm -q --qf '%{NAME}-%|EPOCH?{%{EPOCH}}:{0}|:%{VERSION}-%{RELEASE}.%{ARCH}\n' "$@" || true | |
36 | } | |
37 | function strip_zero_epoch { | |
38 | sed -e 's/-0:/-/' | |
39 | } | |
40 | function indent { | |
41 | sed -e 's/^/ /' | |
42 | } | |
43 | function filter_packages { | |
44 | # Exempt kernel-{core,devel,modules{,-extra}} from audit because of multiple | |
45 | # installed versions: not worth trying to do something better. | |
46 | grep -Ev '^(gpg-pubkey|kernel-core|kernel-devel|kernel-modules|kernel-modules-extra)-[^-]+-[^-]+$' | |
47 | } | |
48 | ||
49 | # Check 1: installed want providers vs. userinstalled | |
50 | ||
51 | # "dnf repoquery --installed --whatprovides" doesn't seem to accept multiple | |
52 | # arguments, and running it once per entry would be unacceptably slow. See how | |
53 | # long we can get away with this before looking for another solution. | |
54 | # | |
55 | # "rpm -q --whatprovides glibc.i686" does not work. Take all wants that look | |
56 | # like they have an architecture and use "rpm -q" instead. This may fail if | |
57 | # someone writes an actual provide of a package-like name with an architecture. | |
58 | # ~ Matt 2017-11-16 | |
59 | pkg_arch_wants=() | |
60 | normal_wants=() | |
61 | for w in "${wants[@]}"; do | |
62 | case "$w" in | |
63 | (*.i686|*.x86_64) | |
64 | pkg_arch_wants+=("$w");; | |
65 | (*) | |
66 | normal_wants+=("$w");; | |
67 | esac | |
68 | done | |
69 | # Since F29, kernel packages keep getting unmarked as userinstalled. Not | |
70 | # investigating; just exclude them here. ~ Matt 2019-01-22 | |
71 | { rpmq --whatprovides "${normal_wants[@]}" && rpmq "${pkg_arch_wants[@]}"; } | strip_zero_epoch | filter_packages | sort | uniq >"$tmpdir/wants-installed" | |
72 | dnf repoquery --userinstalled | strip_zero_epoch | filter_packages | sort >"$tmpdir/userinstalled" | |
73 | ||
74 | if ! cmp -s "$tmpdir/wants-installed" "$tmpdir/userinstalled"; then | |
75 | echo "Installed wants that are not marked as userinstalled:" | |
76 | comm -2 -3 "$tmpdir/wants-installed" "$tmpdir/userinstalled" | indent | |
77 | echo "Userinstalled packages that are not wants:" | |
78 | comm -1 -3 "$tmpdir/wants-installed" "$tmpdir/userinstalled" | indent | |
79 | echo "To correct, edit the wants file or use 'dnf mark {install|remove} PACKAGE_NAME'." | |
80 | exit 1 | |
81 | fi | |
82 | ||
83 | # Check 2: fresh solution of wants vs. installed (should catch different choice of provider, packages needing update, orphans, and problems) | |
84 | ||
85 | platform_id="$(sed -nre 's,^PLATFORM_ID="(.*)"$,\1,p' /etc/os-release)" | |
86 | ||
87 | SANDBOX_DNF=(dnf | |
88 | --installroot="$tmpdir/installroot" | |
89 | --setopt=cachedir=/var/cache/dnf # Share main cache to save time | |
90 | --disableplugin=qubes-hooks # Qubes plugin takes unwanted actions when using an installroot | |
91 | --releasever=/ --setopt=module_platform_id=$platform_id | |
92 | ) | |
93 | ||
94 | "${SANDBOX_DNF[@]}" --assumeyes module enable "${modules[@]}" &>"$tmpdir/dnf.log" | |
95 | ||
96 | # --verbose for "Package ... will be installed" output. The human-readable list | |
97 | # of packages to install is harder to scrape because of line wrapping. | |
98 | # | |
99 | # "dnf --assumeno" exits 1. I don't see an obvious way to distinguish this from | |
100 | # real errors. However, real errors are likely to generate a diff anyway. | |
101 | { "${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" | |
102 | # Looks like "dnf repoquery --installed" doesn't catch extras. | |
103 | rpmq -a | filter_packages | strip_zero_epoch | sort >"$tmpdir/installed" | |
104 | ||
105 | if ! cmp -s "$tmpdir/solved" "$tmpdir/installed"; then | |
106 | echo "Packages in fresh solution that are not installed:" | |
107 | comm -2 -3 "$tmpdir/solved" "$tmpdir/installed" | indent | |
108 | echo "Installed packages that are not in fresh solution:" | |
109 | comm -1 -3 "$tmpdir/solved" "$tmpdir/installed" | indent | |
110 | echo "To correct, install or remove packages to match the canonical solution," | |
111 | echo "or override the default choice of providers by adding the desired" | |
112 | echo "providers to the wants file." | |
113 | exit 1 | |
114 | fi | |
115 | ||
116 | echo "RPM package set audit passed!" |