From 0729487ae5ab43b71cbd041bf64d3c559dd32de7 Mon Sep 17 00:00:00 2001 From: Jonathan Lebon Date: Wed, 14 Feb 2018 14:27:06 +0000 Subject: [PATCH] Check and display pending security advisories Pick up security advisories when checking for pending updates and include them in the `cached-update` property. On the client-side, display them in the output of `status`. This was part of the original vision for how useful a smart `check` mode could be. It directly impacts how one manages their individual system (e.g. when to reboot), and paves the way for integration into higher-level apps that act at the cluster level. Closes: #1249 Approved by: cgwalters --- ci/build.sh | 3 +- src/app/rpmostree-builtin-status.c | 2 +- src/app/rpmostree-dbus-helpers.c | 153 +++++++- src/daemon/org.projectatomic.rpmostree1.xml | 1 + src/daemon/rpmostreed-deployment-utils.c | 169 +++++++++ src/libpriv/rpmostree-types.h | 9 + tests/common/libtest.sh | 18 +- tests/common/libvm.sh | 6 + tests/utils/updateinfo | 388 ++++++++++++++++++++ tests/vmcheck/test-autoupdate.sh | 95 ++++- 10 files changed, 822 insertions(+), 22 deletions(-) create mode 100755 tests/utils/updateinfo diff --git a/ci/build.sh b/ci/build.sh index 1482589b..084db9a5 100755 --- a/ci/build.sh +++ b/ci/build.sh @@ -22,7 +22,8 @@ pkg_install_builddeps rpm-ostree # Mostly dependencies for tests pkg_install ostree{,-devel,-grub2} createrepo_c /usr/bin/jq PyYAML \ libubsan libasan libtsan elfutils fuse sudo python-gobject-base \ - selinux-policy-devel selinux-policy-targeted + selinux-policy-devel selinux-policy-targeted python2-createrepo_c \ + rpm-python # provided by python2-rpm on Fedora # For ex-container tests and clang build pkg_install_if_os fedora parallel clang diff --git a/src/app/rpmostree-builtin-status.c b/src/app/rpmostree-builtin-status.c index 3ec588d5..474b41d3 100644 --- a/src/app/rpmostree-builtin-status.c +++ b/src/app/rpmostree-builtin-status.c @@ -315,7 +315,7 @@ print_daemon_state (RPMOSTreeSysroot *sysroot_proxy, case AUTO_UPDATE_SDSTATE_OK: { if (last_run) - /* e.g. "last check 4h 32min ago" */ + /* e.g. "last run 4h 32min ago" */ g_print ("(%s; last run %s)\n", policy, last_run); else g_print ("(%s; no runs since boot)\n", policy); diff --git a/src/app/rpmostree-dbus-helpers.c b/src/app/rpmostree-dbus-helpers.c index ce748fb8..13998c1a 100644 --- a/src/app/rpmostree-dbus-helpers.c +++ b/src/app/rpmostree-dbus-helpers.c @@ -1177,6 +1177,151 @@ append_to_summary (GString *summary, g_string_append_printf (summary, "%u %s", n, type); } +static int +compare_sec_advisories (gconstpointer ap, + gconstpointer bp) +{ + GVariant *a = *((GVariant**)ap); + GVariant *b = *((GVariant**)bp); + + RpmOstreeAdvisorySeverity asev; + g_variant_get_child (a, 2, "u", &asev); + + RpmOstreeAdvisorySeverity bsev; + g_variant_get_child (b, 2, "u", &bsev); + + if (asev != bsev) + return asev - bsev; + + const char *aid; + g_variant_get_child (a, 0, "&s", &aid); + + const char *bid; + g_variant_get_child (b, 0, "&s", &bid); + + return strcmp (aid, bid); +} + +static const char* +severity_to_str (RpmOstreeAdvisorySeverity severity) +{ + switch (severity) + { + case RPM_OSTREE_ADVISORY_SEVERITY_LOW: + return "Low"; + case RPM_OSTREE_ADVISORY_SEVERITY_MODERATE: + return "Moderate"; + case RPM_OSTREE_ADVISORY_SEVERITY_IMPORTANT: + return "Important"; + case RPM_OSTREE_ADVISORY_SEVERITY_CRITICAL: + return "Critical"; + default: /* including NONE */ + return "Unknown"; + } +} + +static void +print_advisories (GVariant *advisories, + gboolean verbose, + guint max_key_len) +{ + /* counters for none/unknown, low, moderate, important, critical advisories */ + guint n_sev[RPM_OSTREE_ADVISORY_SEVERITY_LAST] = {0,}; + + /* we only display security advisories for now */ + g_autoptr(GPtrArray) sec_advisories = + g_ptr_array_new_with_free_func ((GDestroyNotify)g_variant_unref); + + guint max_id_len = 0; + + GVariantIter iter; + g_variant_iter_init (&iter, advisories); + while (TRUE) + { + g_autoptr(GVariant) child = g_variant_iter_next_value (&iter); + if (!child) + break; + + DnfAdvisoryKind kind; + g_variant_get_child (child, 1, "u", &kind); + + /* we only display security advisories for now */ + if (kind != DNF_ADVISORY_KIND_SECURITY) + continue; + + const char *id; + g_variant_get_child (child, 0, "&s", &id); + max_id_len = MAX (max_id_len, strlen (id)); + + RpmOstreeAdvisorySeverity severity; + g_variant_get_child (child, 2, "u", &severity); + /* just make sure it's capped at LAST */ + if (severity < RPM_OSTREE_ADVISORY_SEVERITY_LAST) + n_sev[severity]++; + else /* bad val; count as unknown */ + n_sev[0]++; + + g_ptr_array_add (sec_advisories, g_variant_ref (child)); + } + + if (sec_advisories->len == 0) + return; + + g_print ("%s%s", get_red_start (), get_bold_start ()); + rpmostree_print_kv_no_newline ("SecAdvisories", max_key_len, ""); + + if (!verbose) + { + /* just spell out "severity" for the unknown case, because e.g. + * "SecAdvisories: 1 unknown" on its own is cryptic and scary */ + g_autoptr(GString) advisory_summary = g_string_new (NULL); + const char *sev_str[] = {"unknown severity", "low", "moderate", "important", "critical"}; + g_assert_cmpint (G_N_ELEMENTS (n_sev), ==, G_N_ELEMENTS (sev_str)); + for (guint i = 0; i < G_N_ELEMENTS (sev_str); i++) + append_to_summary (advisory_summary, sev_str[i], n_sev[i]); + g_print ("%s\n", advisory_summary->str); + } + + g_print ("%s%s", get_bold_end (), get_red_end ()); + if (!verbose) + return; + + const guint max_sev_len = strlen ("Important"); + + /* sort by severity */ + g_ptr_array_sort (sec_advisories, compare_sec_advisories); + + for (guint i = 0; i < sec_advisories->len; i++) + { + GVariant *advisory = sec_advisories->pdata[i]; + + const char *id; + g_variant_get_child (advisory, 0, "&s", &id); + + DnfAdvisoryKind kind; + g_variant_get_child (advisory, 1, "u", &kind); + + RpmOstreeAdvisorySeverity severity; + g_variant_get_child (advisory, 2, "u", &severity); + + g_autoptr(GVariant) pkgs = g_variant_get_child_value (advisory, 3); + + const char *severity_str = severity_to_str (severity); + const guint n_pkgs = g_variant_n_children (pkgs); + for (guint j = 0; j < n_pkgs; j++) + { + const char *nevra; + g_variant_get_child (pkgs, j, "&s", &nevra); + + if (i == 0 && j == 0) /* we're on the same line as SecInfo */ + g_print ("%-*s %-*s %s\n", max_id_len, id, max_sev_len, severity_str, nevra); + else + g_print (" %*s %-*s %-*s %s\n", max_key_len, "", max_id_len, id, + max_sev_len, severity_str, nevra); + } + } +} + /* this is used by both `status` and `upgrade --check/--preview` */ gboolean rpmostree_print_cached_update (GVariant *cached_update, @@ -1219,12 +1364,15 @@ rpmostree_print_cached_update (GVariant *cached_update, g_autoptr(GVariant) rpm_diff = g_variant_dict_lookup_value (&dict, "rpm-diff", G_VARIANT_TYPE ("a{sv}")); + g_autoptr(GVariant) advisories = + g_variant_dict_lookup_value (&dict, "advisories", G_VARIANT_TYPE ("a(suuasa{sv})")); + /* and now we can print 🖨️ things! */ g_print ("Available update:\n"); /* add the long keys here */ - const guint max_key_len = MAX (strlen ("Downgraded"), + const guint max_key_len = MAX (strlen ("SecAdvisories"), strlen ("GPGSignature")); if (is_new_checksum) @@ -1237,6 +1385,9 @@ rpmostree_print_cached_update (GVariant *cached_update, if (rpm_diff) { + if (advisories) + print_advisories (advisories, verbose, max_key_len); + g_auto(GVariantDict) rpm_diff_dict; g_variant_dict_init (&rpm_diff_dict, rpm_diff); diff --git a/src/daemon/org.projectatomic.rpmostree1.xml b/src/daemon/org.projectatomic.rpmostree1.xml index c74ef96d..5e961170 100644 --- a/src/daemon/org.projectatomic.rpmostree1.xml +++ b/src/daemon/org.projectatomic.rpmostree1.xml @@ -87,6 +87,7 @@ 'downgraded' (type 'a(us(ss)(ss))') 'removed' (type 'a(usss)') 'added' (type 'a(usss)') + 'advisories' (type 'a(suuasa{sv})') --> diff --git a/src/daemon/rpmostreed-deployment-utils.c b/src/daemon/rpmostreed-deployment-utils.c index 327efb4b..88ee223f 100644 --- a/src/daemon/rpmostreed-deployment-utils.c +++ b/src/daemon/rpmostreed-deployment-utils.c @@ -727,6 +727,147 @@ rpmmd_diff (OstreeSysroot *sysroot, return TRUE; } +/* convert those now to make the D-Bus API nicer and easier for clients */ +static RpmOstreeAdvisorySeverity +str2severity (const char *str) +{ + if (str == NULL) + return RPM_OSTREE_ADVISORY_SEVERITY_NONE; + + /* these expect RHEL naming conventions; Fedora hopefully should follow soon, see: + * https://github.com/fedora-infra/bodhi/pull/2099 */ + g_autofree char *str_up = g_ascii_strup (str, -1); + if (g_str_equal (str_up, "LOW")) + return RPM_OSTREE_ADVISORY_SEVERITY_LOW; + if (g_str_equal (str_up, "MODERATE")) + return RPM_OSTREE_ADVISORY_SEVERITY_MODERATE; + if (g_str_equal (str_up, "IMPORTANT")) + return RPM_OSTREE_ADVISORY_SEVERITY_IMPORTANT; + if (g_str_equal (str_up, "CRITICAL")) + return RPM_OSTREE_ADVISORY_SEVERITY_CRITICAL; + return RPM_OSTREE_ADVISORY_SEVERITY_NONE; +} + +/* Returns a *floating* variant ref representing the advisory */ +static GVariant* +advisory_variant_new (DnfAdvisory *adv, + GPtrArray *pkgs) +{ + g_auto(GVariantBuilder) builder; + g_variant_builder_init (&builder, G_VARIANT_TYPE_TUPLE); + g_variant_builder_add (&builder, "s", dnf_advisory_get_id (adv)); + g_variant_builder_add (&builder, "u", dnf_advisory_get_kind (adv)); + g_variant_builder_add (&builder, "u", str2severity (dnf_advisory_get_severity (adv))); + + { g_auto(GVariantBuilder) pkgs_array; + g_variant_builder_init (&pkgs_array, G_VARIANT_TYPE_ARRAY); + for (guint i = 0; i < pkgs->len; i++) + g_variant_builder_add (&pkgs_array, "s", dnf_package_get_nevra (pkgs->pdata[i])); + g_variant_builder_add_value (&builder, g_variant_builder_end (&pkgs_array)); + } + + /* for now we don't ship any extra info about the errata (e.g. title, date, desc, refs) */ + g_variant_builder_add_value (&builder, g_variant_new ("a{sv}", NULL)); + + return g_variant_builder_end (&builder); +} + +/* libdnf creates new DnfAdvisory objects on request */ + +static guint +advisory_hash (gconstpointer v) +{ + return g_str_hash (dnf_advisory_get_id ((DnfAdvisory*)v)); +} + +static gboolean +advisory_equal (gconstpointer v1, + gconstpointer v2) +{ + return g_str_equal (dnf_advisory_get_id ((DnfAdvisory*)v1), + dnf_advisory_get_id ((DnfAdvisory*)v2)); +} + +/* Go through the list of @pkgs and check if there are any advisories open for them. If + * no advisories are found, returns %NULL. Otherwise, returns a GVariant of the following + * type: + 'a(suuasa{sv})' + s advisory id (e.g. FEDORA-2018-a1b2c3d4e5f6) + u advisory kind (enum DnfAdvisoryKind) + u advisory severity (enum RpmOstreeAdvisorySeverity) + as list of packages (NEVRAs) contained in the advisory + a{sv} additional info about advisory (none so far) + */ +static GVariant* +advisories_variant (DnfSack *sack, + GPtrArray *pkgs) +{ + g_autoptr(GHashTable) advisories = + g_hash_table_new_full (advisory_hash, advisory_equal, g_object_unref, + (GDestroyNotify)g_ptr_array_unref); + + /* libdnf provides pkg -> set of advisories, but we want advisory -> set of pkgs; + * making sure we only keep the pkgs we actually care about */ + for (guint i = 0; i < pkgs->len; i++) + { + DnfPackage *pkg = pkgs->pdata[i]; + g_autoptr(GPtrArray) advisories_with_pkg = dnf_package_get_advisories (pkg, HY_EQ); + for (guint j = 0; j < advisories_with_pkg->len; j++) + { + DnfAdvisory *advisory = advisories_with_pkg->pdata[j]; + + /* for now we're only interested in security erratas */ + if (dnf_advisory_get_kind (advisory) != DNF_ADVISORY_KIND_SECURITY) + continue; + + /* reverse mapping */ + GPtrArray *pkgs_in_advisory = g_hash_table_lookup (advisories, advisory); + if (!pkgs_in_advisory) + { + pkgs_in_advisory = + g_ptr_array_new_with_free_func ((GDestroyNotify)g_object_unref); + g_hash_table_insert (advisories, g_object_ref (advisory), pkgs_in_advisory); + } + g_ptr_array_add (pkgs_in_advisory, g_object_ref (pkg)); + } + } + + if (g_hash_table_size (advisories) == 0) + return NULL; + + g_auto(GVariantBuilder) builder; + g_variant_builder_init (&builder, G_VARIANT_TYPE_ARRAY); + GLNX_HASH_TABLE_FOREACH_KV (advisories, DnfAdvisory*, advisory, GPtrArray*, pkgs) + g_variant_builder_add_value (&builder, advisory_variant_new (advisory, pkgs)); + return g_variant_ref_sink (g_variant_builder_end (&builder)); +} + +/* try to find the exact same RpmOstreePackage pkgs in the sack */ +static GPtrArray* +rpm_ostree_pkgs_to_dnf (DnfSack *sack, + GPtrArray *rpm_ostree_pkgs) +{ + g_autoptr(GPtrArray) dnf_pkgs = + g_ptr_array_new_with_free_func ((GDestroyNotify)g_object_unref); + + const guint n = rpm_ostree_pkgs->len; + for (guint i = 0; i < n; i++) + { + RpmOstreePackage *pkg = rpm_ostree_pkgs->pdata[i]; + hy_autoquery HyQuery query = hy_query_create (sack); + hy_query_filter (query, HY_PKG_NAME, HY_EQ, rpm_ostree_package_get_name (pkg)); + hy_query_filter (query, HY_PKG_EVR, HY_EQ, rpm_ostree_package_get_evr (pkg)); + hy_query_filter (query, HY_PKG_ARCH, HY_EQ, rpm_ostree_package_get_arch (pkg)); + g_autoptr(GPtrArray) pkgs = hy_query_run (query); + + /* 0 --> ostree stream is out of sync with rpmmd repos probably? */ + if (pkgs->len > 0) + g_ptr_array_add (dnf_pkgs, g_object_ref (pkgs->pdata[0])); + } + + return g_steal_pointer (&dnf_pkgs); +} + static gboolean get_cached_rpmmd_sack (OstreeSysroot *sysroot, OstreeRepo *repo, @@ -931,6 +1072,34 @@ rpmostreed_update_generate_variant (OstreeSysroot *sysroot, if (!rpm_diff_is_empty (&rpm_diff)) g_variant_dict_insert (&dict, "rpm-diff", "@a{sv}", rpm_diff_variant_new (&rpm_diff)); + /* now we look for advisories */ + + if (sack && (ostree_modified_new || rpmmd_modified_new)) + { + /* let's just merge the two now for convenience */ + g_autoptr(GPtrArray) new_packages = + g_ptr_array_new_with_free_func ((GDestroyNotify)g_object_unref); + + if (ostree_modified_new) + { + /* recall that @ostree_modified_new is an array of RpmOstreePackage; try to find + * the same pkg in the rpmmd so that we can search for advisories afterwards */ + g_autoptr(GPtrArray) pkgs = rpm_ostree_pkgs_to_dnf (sack, ostree_modified_new); + for (guint i = 0; i < pkgs->len; i++) + g_ptr_array_add (new_packages, g_object_ref (pkgs->pdata[i])); + } + + if (rpmmd_modified_new) + { + for (guint i = 0; i < rpmmd_modified_new->len; i++) + g_ptr_array_add (new_packages, g_object_ref (rpmmd_modified_new->pdata[i])); + } + + g_autoptr(GVariant) advisories = advisories_variant (sack, new_packages); + if (advisories) + g_variant_dict_insert (&dict, "advisories", "@a(suuasa{sv})", advisories); + } + /* but if there are no updates, then just ditch the whole thing and return NULL */ if (is_new_checksum || rpmmd_modified_new) *out_update = g_variant_ref_sink (g_variant_dict_end (&dict)); diff --git a/src/libpriv/rpmostree-types.h b/src/libpriv/rpmostree-types.h index f13fe775..7a1c360a 100644 --- a/src/libpriv/rpmostree-types.h +++ b/src/libpriv/rpmostree-types.h @@ -31,6 +31,15 @@ typedef enum { RPMOSTREED_AUTOMATIC_UPDATE_POLICY_CHECK, } RpmostreedAutomaticUpdatePolicy; +typedef enum { + RPM_OSTREE_ADVISORY_SEVERITY_NONE, + RPM_OSTREE_ADVISORY_SEVERITY_LOW, + RPM_OSTREE_ADVISORY_SEVERITY_MODERATE, + RPM_OSTREE_ADVISORY_SEVERITY_IMPORTANT, + RPM_OSTREE_ADVISORY_SEVERITY_CRITICAL, + RPM_OSTREE_ADVISORY_SEVERITY_LAST, +} RpmOstreeAdvisorySeverity; + /** * RPMOSTREE_DIFF_SINGLE_GVARIANT_FORMAT: * diff --git a/tests/common/libtest.sh b/tests/common/libtest.sh index e8fbf006..cf6415a6 100644 --- a/tests/common/libtest.sh +++ b/tests/common/libtest.sh @@ -31,6 +31,8 @@ if test -z "${SRCDIR:-}"; then fi . ${SRCDIR}/common/libtest-core.sh +UPDATEINFO=${SRCDIR}/utils/updateinfo + for bin in jq; do if ! command -v $bin >/dev/null; then (echo ${bin} is required to execute tests 1>&2; exit 1) @@ -380,6 +382,10 @@ get_obj_path() { echo "${repo}/objects/${csum:0:2}/${csum:2}.${objtype}" } +uinfo_cmd() { + $UPDATEINFO --repo "${test_tmpdir}/yumrepo" "$@" +} + # builds a new RPM and adds it to the testdir's repo # $1 - name # $2+ - optional, treated as directive/value pairs @@ -402,7 +408,7 @@ License: GPLv2+ EOF local build= install= files= pretrans= pre= post= posttrans= post_args= - local verifyscript= + local verifyscript= uinfo= local transfiletriggerin= transfiletriggerin_patterns= local transfiletriggerin2= transfiletriggerin2_patterns= local transfiletriggerun= transfiletriggerun_patterns= @@ -418,7 +424,7 @@ EOF echo "Conflicts: $arg" >> $spec;; post_args) post_args="$arg";; - version|release|epoch|arch|build|install|files|pretrans|pre|post|posttrans|verifyscript) + version|release|epoch|arch|build|install|files|pretrans|pre|post|posttrans|verifyscript|uinfo) declare $section="$arg";; transfiletriggerin) transfiletriggerin_patterns="$arg"; @@ -502,7 +508,13 @@ EOF --define "_srcrpmdir $PWD" \ --define "_rpmdir $test_tmpdir/yumrepo/packages" \ --define "_buildrootdir $PWD") - (cd $test_tmpdir/yumrepo && createrepo_c --no-database .) + # use --keep-all-metadata to retain previous updateinfo + (cd $test_tmpdir/yumrepo && + createrepo_c --no-database --update --keep-all-metadata .) + # convenience function to avoid follow-up add-pkg + if [ -n "$uinfo" ]; then + uinfo_cmd add-pkg $uinfo $name 0 $version $release $arch + fi if test '!' -f $test_tmpdir/yumrepo.repo; then cat > $test_tmpdir/yumrepo.repo.tmp << EOF [test-repo] diff --git a/tests/common/libvm.sh b/tests/common/libvm.sh index 99c00f0a..ee373ddd 100644 --- a/tests/common/libvm.sh +++ b/tests/common/libvm.sh @@ -351,6 +351,12 @@ vm_build_rpm() { vm_send_test_repo } +# Like uinfo_cmd, but also sends it to the VM +vm_uinfo() { + uinfo_cmd "$@" + vm_send_test_repo +} + # Like vm_build_rpm but takes a yumrepo mode vm_build_rpm_repo_mode() { mode=$1; shift diff --git a/tests/utils/updateinfo b/tests/utils/updateinfo new file mode 100755 index 00000000..27834b40 --- /dev/null +++ b/tests/utils/updateinfo @@ -0,0 +1,388 @@ +#!/usr/bin/env python +# +# Copyright (C) 2018 Jonathan Lebon +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. + +""" + This is a small helper CLI tool to create and modify updateinfo.xml data. + Note that the original createrepo_c API is geared towards creating the XML + in a single pass (e.g. Bodhi), so there are inefficiencies in the way we + modify data below (e.g. removing data involves copying). Our goal here is + to make it *really* easy to use from the command-line for testing purposes. +""" + +import os +import sys +import argparse + +from collections import namedtuple + +import rpm +import createrepo_c as cr + + +def main(): + args = parse_args() + args.func(args) + + +def parse_args(): + + parser = argparse.ArgumentParser() + parser.add_argument('--repo', help='rpmmd repo path', default=os.getcwd()) + subparsers = parser.add_subparsers(dest='cmd', title='subcommands') + subparsers.required = True + + add = subparsers.add_parser('add') + add.set_defaults(func=cmd_add) + add.add_argument('--if-not-exists', action='store_true') + add.add_argument('id') + add.add_argument('type', default='security', nargs='?', + choices=['bugfix', 'enhancement', 'security']) + add.add_argument('severity', default='none', nargs='?', + choices=['none', 'low', 'moderate', + 'important', 'critical']) + + delete = subparsers.add_parser('delete') + delete.set_defaults(func=cmd_delete) + add.add_argument('--if-exists', action='store_true') + delete.add_argument('id') + + show = subparsers.add_parser('show') + show.set_defaults(func=cmd_show) + show.add_argument('id', nargs='?') + + add_pkg = subparsers.add_parser('add-pkg') + add_pkg.set_defaults(func=cmd_add_pkg) + add_pkg.add_argument('id') + add_pkg.add_argument('name') + add_pkg.add_argument('epoch', nargs='?', type=int) + add_pkg.add_argument('version', nargs='?') + add_pkg.add_argument('release', nargs='?') + add_pkg.add_argument('arch', nargs='?') + + delete_pkg = subparsers.add_parser('delete-pkg') + delete_pkg.set_defaults(func=cmd_delete_pkg) + delete_pkg.add_argument('id') + delete_pkg.add_argument('name_or_nevra') + + return parser.parse_args() + + +def cmd_add(args): + + uinfo = get_updateinfo(args.repo) + + try: + uinfo = add_update(uinfo, args.id, args.type, args.severity) + except UpdateExistsError: + if args.if_not_exists: + return + raise + + set_updateinfo(args.repo, uinfo) + + +def cmd_show(args): + uinfo = get_updateinfo(args.repo) + show_updates(uinfo, args.id) + + +def cmd_delete(args): + + uinfo = get_updateinfo(args.repo) + + try: + uinfo = delete_update(uinfo, args.id) + except UpdateNotExistsError: + if args.if_exists: + return + raise + + set_updateinfo(args.repo, uinfo) + + +class Nevra(namedtuple('Nevra', 'name epoch version release arch')): + + def __str__(self): + if self.epoch is not None and self.epoch > 0: + return '%s-%u:%s-%s.%s' % (self.name, self.epoch, self.version, + self.release, self.arch) + return '%s-%s-%s.%s' % (self.name, self.version, + self.release, self.arch) + + # generic equals check for cr.UpdateCollectionPackage/cr.Package objects + def equals_pkg(self, pkg): + for attr in ['name', 'epoch', 'version', 'release', 'arch']: + if getattr(self, attr) != getattr(pkg, attr): + return False + return True + + +def nevra_from_rpm(rpm_filename): + + ts = rpm.TransactionSet() + with open(rpm_filename) as f: + hdr = ts.hdrFromFdno(f) + + return Nevra(hdr[rpm.RPMTAG_NAME], + hdr[rpm.RPMTAG_EPOCH], + hdr[rpm.RPMTAG_VERSION], + hdr[rpm.RPMTAG_RELEASE], + hdr[rpm.RPMTAG_ARCH]) + + +def nevra_from_obj(obj): + epoch = 0 + if obj.epoch is not None: + epoch = int(obj.epoch) + return Nevra(obj.name, epoch, obj.version, obj.release, obj.arch) + + +def nevra_from_primary(repo, name): + repomd = cr.Repomd(repomd_xml(repo)) + + pkgs = [] + def pkgcb(pkg): + if pkg.name == name: + pkgs.append(pkg) + + for record in repomd.records: + if record.type == 'primary': + primary_xml = os.path.join(repo, record.location_href) + cr.xml_parse_primary(primary_xml, do_files=False, pkgcb=pkgcb) + break + + if len(pkgs) == 0: + raise Exception("Package '%s' not found" % name) + if len(pkgs) > 1: + raise Exception("Multiple packages found for '%s'" % name) + return nevra_from_obj(pkgs[0]) + + +def cmd_add_pkg(args): + uinfo = get_updateinfo(args.repo) + if args.epoch is None: # user only passed the 'name' param + # we support passing an RPM file from which to extract fields + if os.path.isfile(args.name): + nevra = nevra_from_rpm(args.name) + else: + # try to find pkg in primary + nevra = nevra_from_primary(args.repo, args.name) + else: + nevra = nevra_from_obj(args) + uinfo = add_pkg_to_update(uinfo, args.id, nevra) + set_updateinfo(args.repo, uinfo) + + +def cmd_delete_pkg(args): + uinfo = get_updateinfo(args.repo) + uinfo = delete_pkg_from_update(uinfo, args.id, args.name_or_nevra) + set_updateinfo(args.repo, uinfo) + + +def get_updateinfo(repo): + ''' + Parse existing repo updateinfo.xml or create a new one. + ''' + repomd = cr.Repomd(repomd_xml(repo)) + for record in repomd.records: + if record.type == 'updateinfo': + return cr.UpdateInfo(os.path.join(repo, record.location_href)) + return cr.UpdateInfo() + + +def repomd_xml(repo): + return os.path.join(repo, "repodata/repomd.xml") + + +def sev2xml(sev): + # important -> Important, which is what yum/dnf expects + return sev[0].upper() + sev[1:] + + +class UpdateExistsError(Exception): + pass + +class UpdateNotExistsError(Exception): + pass + +def add_update(uinfo, uid, utype, severity): + + # check that the target id doesn't already exist + for update in uinfo.updates: + if update.id == id: + raise UpdateExistsError("Update '%s' already exists" % id) + + rec = cr.UpdateRecord() + rec.id = uid + rec.type = utype + rec.severity = sev2xml(severity) + uinfo.append(rec) + return uinfo + + +def modify_update(uinfo, uid, func, func_data=None): + + # createrepo_c doesn't allow modifying the original object + new_uinfo = cr.UpdateInfo() + + found = False + for update in uinfo.updates: + if update.id != uid: + new_uinfo.append(update) + else: + found = True + if func is not None: + new_update = func(uid, update, func_data) + if new_update is not None: + new_uinfo.append(new_update) + if not found: + raise Exception("Update '%s' does not exist" % uid) + return new_uinfo + + +def delete_update(uinfo, uid): + return modify_update(uinfo, uid, None) + + +def show_updates(uinfo, uid): + for update in uinfo.updates: + if uid is not None and update.id != uid: + continue + print update.id, update.type, update.severity + for col in update.collections: + for pkg in col.packages: + print " ", pkg.filename + + +def copy_update_no_cols(update): + # the default copy() also copies collections + new_update = cr.UpdateRecord() + new_update.id = update.id + new_update.type = update.type + new_update.severity = update.severity + return new_update + + +def add_pkg_to_update_cb(uid, update, nevra): + + if len(update.collections) > 1: + # let's just pretend that never happens for our purposes + raise Exception("Update '%s' has more than one collection" % uid) + elif len(update.collections) == 1: + col = update.collections[0] + else: + col = cr.UpdateCollection() + + new_update = copy_update_no_cols(update) + new_col = cr.UpdateCollection() + + for pkg in col.packages: + if nevra.equals_pkg(pkg): + raise Exception("Update '%s' already contains pkg '%s'" % + (uid, nevra)) + new_col.append(pkg) + + pkg = cr.UpdateCollectionPackage() + pkg.name = nevra.name + pkg.epoch = '0' + if nevra.epoch is not None: + pkg.epoch = str(nevra.epoch) + pkg.version = nevra.version + pkg.release = nevra.release + pkg.arch = nevra.arch + pkg.filename = str(nevra) + ".rpm" + new_col.append(pkg) + + new_update.append_collection(new_col) + + return new_update + + +def add_pkg_to_update(uinfo, uid, nevra): + return modify_update(uinfo, uid, add_pkg_to_update_cb, nevra) + + +def delete_pkg_from_update_cb(uid, update, name_or_nevra): + + if len(update.collections) > 1: + # let's just pretend that never happens for our purposes + raise Exception("Update '%s' has more than one collection" % uid) + elif len(update.collections) == 1: + col = update.collections[0] + else: + col = cr.UpdateCollection() + + new_update = copy_update_no_cols(update) + new_col = cr.UpdateCollection() + + found = False + # just compare by filename to make it easier + rpm_name = name_or_nevra + '.rpm' + for pkg in col.packages: + if pkg.filename != rpm_name and pkg.name != name_or_nevra: + new_col.append(pkg) + else: + found = True + if not found: + raise Exception("Update '%s' does not have package '%s'" % + (uid, name_or_nevra)) + + if len(new_col.packages) > 0: + new_update.append_collection(new_col) + + return new_update + + +def delete_pkg_from_update(uinfo, uid, name_or_nevra): + return modify_update(uinfo, uid, delete_pkg_from_update_cb, name_or_nevra) + + +def new_updateinfo_record(repo, uinfo): + + xml = os.path.join(repo, "repodata/updateinfo.xml.gz") + with open(xml, 'w') as f: + f.write(uinfo.xml_dump()) + + # calculate SHA256 and rename + ui_rec = cr.RepomdRecord('updateinfo', xml) + ui_rec.fill(cr.SHA256) + ui_rec.rename_file() + return ui_rec + + +def set_updateinfo(repo, uinfo): + repomd = cr.Repomd(repomd_xml(repo)) + + # clone + new_repomd = cr.Repomd() + for record in repomd.records: + if record.type != 'updateinfo': + new_repomd.set_record(record) + else: + os.unlink(os.path.join(repo, record.location_href)) + + uinfo_rec = new_updateinfo_record(repo, uinfo) + new_repomd.set_record(uinfo_rec) + + with open(repomd_xml(repo), 'w') as f: + f.write(new_repomd.xml_dump()) + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/tests/vmcheck/test-autoupdate.sh b/tests/vmcheck/test-autoupdate.sh index 4d33ce76..fb619126 100755 --- a/tests/vmcheck/test-autoupdate.sh +++ b/tests/vmcheck/test-autoupdate.sh @@ -63,34 +63,62 @@ create_update() { # (delete ref but don't prune for easier debugging) vm_cmd ostree refs --repo=$REMOTE_OSTREE vmcheck --delete +# this is split out for the sole purpose of making iterating easier when hacking +# (see below for more details) +init_updated_rpmmd_repo() { + vm_build_rpm base-pkg-foo version 1.4 release 8 # upgraded + vm_build_rpm base-pkg-bar version 0.9 release 3 # downgraded + vm_build_rpm base-pkg-boo version 3.7 release 2.11 # added + vm_uinfo add VMCHECK-ENH enhancement + vm_uinfo add VMCHECK-SEC-NONE security none + vm_uinfo add VMCHECK-SEC-LOW security low + vm_uinfo add VMCHECK-SEC-CRIT security critical + vm_build_rpm base-pkg-enh version 2.0 uinfo VMCHECK-ENH + vm_build_rpm base-pkg-sec-none version 2.0 uinfo VMCHECK-SEC-NONE + vm_build_rpm base-pkg-sec-low version 2.0 uinfo VMCHECK-SEC-LOW + vm_build_rpm base-pkg-sec-crit version 2.0 uinfo VMCHECK-SEC-CRIT +} + # now let's build some pkgs that we'll jury-rig into a base update -# this whole block can be commented out for a speed-up when iterating locally +# this whole block can be commented out (except the init_updated_rpmmd_repo +# call) after the first run for a speed-up when iterating locally vm_build_rpm base-pkg-foo version 1.4 release 7 vm_build_rpm base-pkg-bar vm_build_rpm base-pkg-baz version 1.1 release 1 -vm_rpmostree install base-pkg-{foo,bar,baz} +vm_build_rpm base-pkg-enh +vm_build_rpm base-pkg-sec-none +vm_build_rpm base-pkg-sec-low +vm_build_rpm base-pkg-sec-crit +vm_rpmostree install base-pkg-{foo,bar,baz,enh,sec-{none,low,crit}} lift_commit $(vm_get_pending_csum) v1 vm_rpmostree cleanup -p rm -rf $test_tmpdir/yumrepo -vm_build_rpm base-pkg-foo version 1.4 release 8 # upgraded -vm_build_rpm base-pkg-bar version 0.9 release 3 # downgraded -vm_build_rpm base-pkg-boo version 3.7 release 2.11 # added -vm_rpmostree install base-pkg-{foo,bar,boo} +init_updated_rpmmd_repo +vm_rpmostree install base-pkg-{foo,bar,boo,enh,sec-{none,low,crit}} lift_commit $(vm_get_pending_csum) v2 vm_rpmostree cleanup -p # ok, we're done with prep, now let's rebase on the first revision and install a -# layered package +# bunch of layered packages create_update v1 vm_cmd ostree remote add vmcheckmote --no-gpg-verify http://localhost:8888/ vm_build_rpm layered-cake version 2.1 release 3 -vm_rpmostree rebase vmcheckmote:vmcheck --install layered-cake +vm_build_rpm layered-enh +vm_build_rpm layered-sec-none +vm_build_rpm layered-sec-low +vm_build_rpm layered-sec-crit +vm_rpmostree rebase vmcheckmote:vmcheck \ + --install layered-cake \ + --install layered-enh \ + --install layered-sec-none \ + --install layered-sec-low \ + --install layered-sec-crit vm_reboot vm_rpmostree status -v vm_assert_status_jq \ ".deployments[0][\"origin\"] == \"vmcheckmote:vmcheck\"" \ ".deployments[0][\"version\"] == \"v1\"" \ - '.deployments[0]["packages"]|length == 1' \ + '.deployments[0]["packages"]|length == 5' \ '.deployments[0]["packages"]|index("layered-cake") >= 0' echo "ok prep" @@ -130,6 +158,7 @@ vm_rpmostree upgrade --trigger-automatic-update-policy vm_rpmostree status > out.txt assert_file_has_content out.txt "Available update" assert_file_has_content out.txt "Diff: 1 upgraded" +assert_not_file_has_content out.txt "SecAdvisories" vm_rpmostree status -v > out.txt assert_file_has_content out.txt "Upgraded: layered-cake 2.1-3 -> 2.1-4" # make sure we don't report ostree-based stuff somehow @@ -138,6 +167,31 @@ assert_file_has_content out.txt "Upgraded: layered-cake 2.1-3 -> 2.1-4" ! grep -A999 'Available update' out.txt | grep "Commit" echo "ok check mode layered only" +# now add some advisory updates +vm_build_rpm layered-enh version 2.0 uinfo VMCHECK-ENH +vm_build_rpm layered-sec-none version 2.0 uinfo VMCHECK-SEC-NONE +vm_rpmostree upgrade --trigger-automatic-update-policy +vm_rpmostree status > out.txt +assert_file_has_content out.txt "SecAdvisories: 1 unknown severity" +vm_rpmostree status -v > out.txt +assert_file_has_content out.txt \ + "SecAdvisories: VMCHECK-SEC-NONE Unknown layered-sec-none-2.0-1.x86_64" +assert_not_file_has_content out.txt "VMCHECK-ENH" + +# now add all of them +vm_build_rpm layered-sec-low version 2.0 uinfo VMCHECK-SEC-LOW +vm_build_rpm layered-sec-crit version 2.0 uinfo VMCHECK-SEC-CRIT +vm_rpmostree upgrade --trigger-automatic-update-policy +vm_rpmostree status > out.txt +assert_file_has_content out.txt \ + "SecAdvisories: 1 unknown severity, 1 low, 1 critical" +vm_rpmostree status -v > out.txt +assert_file_has_content out.txt \ + "SecAdvisories: VMCHECK-SEC-NONE Unknown layered-sec-none-2.0-1.x86_64" \ + " VMCHECK-SEC-LOW Low layered-sec-low-2.0-1.x86_64" \ + " VMCHECK-SEC-CRIT Critical layered-sec-crit-2.0-1.x86_64" +echo "ok check mode layered only with advisories" + # ok now let's add ostree updates in the picture create_update v2 vm_rpmostree upgrade --trigger-automatic-update-policy @@ -157,14 +211,23 @@ assert_update() { # we could assert more json here, though how it's presented to users is # important, and implicitly tests the json vm_rpmostree status > out.txt - assert_file_has_content out.txt 'Diff: 2 upgraded, 1 downgraded, 1 removed, 1 added' + assert_file_has_content out.txt \ + "SecAdvisories: 1 unknown severity, 1 low, 1 critical" \ + 'Diff: 10 upgraded, 1 downgraded, 1 removed, 1 added' vm_rpmostree status -v > out.txt - assert_file_has_content out.txt 'Upgraded: base-pkg-foo 1.4-7 -> 1.4-8' - assert_file_has_content out.txt " layered-cake 2.1-3 -> 2.1-4" - assert_file_has_content out.txt 'Downgraded: base-pkg-bar 1.0-1 -> 0.9-3' - assert_file_has_content out.txt 'Removed: base-pkg-baz-1.1-1.x86_64' - assert_file_has_content out.txt 'Added: base-pkg-boo-3.7-2.11.x86_64' + assert_file_has_content out.txt \ + "VMCHECK-SEC-NONE Unknown base-pkg-sec-none-2.0-1.x86_64" \ + "VMCHECK-SEC-NONE Unknown layered-sec-none-2.0-1.x86_64" \ + "VMCHECK-SEC-LOW Low base-pkg-sec-low-2.0-1.x86_64" \ + "VMCHECK-SEC-LOW Low layered-sec-low-2.0-1.x86_64" \ + "VMCHECK-SEC-CRIT Critical base-pkg-sec-crit-2.0-1.x86_64" \ + "VMCHECK-SEC-CRIT Critical layered-sec-crit-2.0-1.x86_64" \ + 'Upgraded: base-pkg-enh 1.0-1 -> 2.0-1' \ + ' base-pkg-foo 1.4-7 -> 1.4-8' \ + 'Downgraded: base-pkg-bar 1.0-1 -> 0.9-3' \ + 'Removed: base-pkg-baz-1.1-1.x86_64' \ + 'Added: base-pkg-boo-3.7-2.11.x86_64' } assert_update @@ -174,7 +237,7 @@ assert_default_deployment_is_update() { vm_assert_status_jq \ '.deployments[0]["origin"] == "vmcheckmote:vmcheck"' \ '.deployments[0]["version"] == "v2"' \ - '.deployments[0]["packages"]|length == 1' \ + '.deployments[0]["packages"]|length == 5' \ '.deployments[0]["packages"]|index("layered-cake") >= 0' vm_rpmostree db list $(vm_get_pending_csum) > list.txt assert_file_has_content list.txt 'layered-cake-2.1-4.x86_64'