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
This commit is contained in:
Jonathan Lebon 2018-02-14 14:27:06 +00:00 committed by Atomic Bot
parent 45a3b53558
commit 0729487ae5
10 changed files with 822 additions and 22 deletions

View File

@ -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

View File

@ -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);

View File

@ -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);

View File

@ -87,6 +87,7 @@
'downgraded' (type 'a(us(ss)(ss))')
'removed' (type 'a(usss)')
'added' (type 'a(usss)')
'advisories' (type 'a(suuasa{sv})')
-->
<property name="CachedUpdate" type="a{sv}" access="read"/>
<property name="HasCachedUpdateRpmDiff" type="b" access="read"/>

View File

@ -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));

View File

@ -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:
*

View File

@ -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]

View File

@ -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

388
tests/utils/updateinfo Executable file
View File

@ -0,0 +1,388 @@
#!/usr/bin/env python
#
# Copyright (C) 2018 Jonathan Lebon <jlebon@redhat.com>
#
# 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())

View File

@ -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'